diff --git a/src/js/src/bookmark/app.html b/src/js/src/bookmark/app.html new file mode 100644 index 0000000..0058229 --- /dev/null +++ b/src/js/src/bookmark/app.html @@ -0,0 +1,129 @@ + + + + + + + Solitude + + + +
+
+ +
+ +
+
+

ヘッダー部分

+
+
+

グラフ画像部分

+

+ +

+
+ +

メニュー

+ +
    +
  • +
  • +
  • +
  • +
+
+
+

編集部分

+
+
    +
  • id:
  • +
  • parent:
  • +
  • type:
  • +
  • label:
  • +
  • url:
  • +
+

+

    +
  • +
+

+
+
+
    +
  • id:
  • +
  • parent:
  • +
  • type:
  • +
  • label:
  • +
  • url:
  • +
+

+

    +
  • +
  • +
+

+
+
+
+

DOTソースコード部分

+ +
+
+

CSV部分

+ +
+
+
+
+
+ +
+
+ + + + + + \ No newline at end of file diff --git a/src/js/src/bookmark/main.css b/src/js/src/bookmark/main.css new file mode 100644 index 0000000..275da42 --- /dev/null +++ b/src/js/src/bookmark/main.css @@ -0,0 +1,131 @@ +:root { + --main-border-color: brown; + font-size: 16px; + } + +p { + margin: 0px; +} + +ul { + padding-left: 1em; + padding-right: 1em; + margin: 5px; +} + +div.app-body { + /* + display: grid; + grid-template-columns: 1fr; + */ + display: block flex; + min-width: 640px; + min-height: 480px; +} + +div.app-body div.left-pane { + min-width: 200px; + max-width: 250px; + overflow-x: scroll; + width: 30%; + + height: 100%; + background-color: green; +} + +div.app-body div.right-pane { + min-width: 400px; + min-height: 100px; + overflow-x: scroll; + + height: 100%; + +} + +div.top-bar, div.bottom-bar { + height: 1em; ; + border: 0.1em solid var(--main-border-color); +} + +#mainView { + height: 30vh; + overflow-x: scroll; + overflow-y: scroll; +} + +#mainView p { + width: fit-content; +} + +#ref-map { + width: 70vw; + overflow: scroll; + object-fit: scale-down; +} + +#editMenu { + display: flex; + list-style: none; +} + +div.editor.shrinked { + /* expanded -> shrinked*/ + animation: 1s linear 0.2s reverse editor-expanding ; + max-height: 0vh; + overflow: hidden; +} + +div.editor.expanded { + /* shrinked -> expanded */ + animation: 1s linear 0.2s normal editor-expanding ; + max-height: 60vh; + overflow: scroll; + +} + +div.editor input[readonly=""] { + background-color: lightsteelblue; +} + +div.editor div.ref-card { + border-style: solid; + border-color: orange; + border-width: 0.1em; +} + + +div.editor div.ref-card li{ + display: flex; +} + +div.editor div.ref-card li span{ + width:20%; +} + +div.editor div.ref-card input{ + width:80%; +} + +div.editor div.ref-card ul.card-menu { + display:flex; +} + +#dot-src-viewer, #csv-src-viewer { + display: block; + min-width: 400px; + min-height: 20vh; + width: 70vw; +} + +/* +@keyframes editor-expanding { + 0% { + max-height: 0vh; + overflow: hidden; + } + 100% { + max-height: 100vh; + overflow: hidden; + } +} +*/ \ No newline at end of file diff --git a/src/js/src/bookmark/main.js b/src/js/src/bookmark/main.js new file mode 100644 index 0000000..40756e9 --- /dev/null +++ b/src/js/src/bookmark/main.js @@ -0,0 +1,278 @@ +import { Graphviz } from "https://cdn.jsdelivr.net/npm/@hpcc-js/wasm/dist/index.js"; + +let counter = 0; + +// Graphviz --- + +(async function() { + document.querySelector("#ref-map").setAttribute("data", await dot2svg()); + document.querySelector("#dot-src-viewer").value = card2dot(); + const dataUri = await dot2svg(document.querySelector("#dot-src-viewer").value); + document.querySelector("#ref-map").setAttribute("data", dataUri); +})(); + +window.addEventListener("load", (event) => { + const editor = document.querySelector("#menuView"); + editor.addEventListener("click", handleAppendCardButton) + editor.addEventListener("click", handleDeleteCardButton) + editor.addEventListener("click", handleLoadCsvButton) + editor.addEventListener("click", handleSaveButton) + editor.addEventListener("click", handleExportButton) + + const geneBtn = document.querySelector("#generate"); + geneBtn.addEventListener("click",handleGenerateButton); + + function handleAppendCardButton(event) { + if(event.target.classList.contains("card-append")){ + const bigBrotherCard = event.target.closest("div.ref-card"); + appendCard(bigBrotherCard, { + id: counter++ + "", + parent: "root", + type: "folder", + label: "nonelabel", + url: "https://www.exmple.com/" + }) + } + } + + function handleGenerateButton(event) { + document.querySelector("#dot-src-viewer").value = card2dot(); + dot2svg(document.querySelector("#dot-src-viewer").value) + .then(dataUri => document.querySelector("#ref-map").setAttribute("data", dataUri)); + } + + function handleDeleteCardButton(event) { + if(event.target.classList.contains("card-delete")) { + setTimeout(() => { + deleteCard(event.target.closest("div.ref-card")); + } ,50); + } + } + + function handleLoadCsvButton(event) { + if(event.target.id != "loadCsv") { + return; + } + + for (const elem of document.querySelectorAll("button.card-delete")) { + + setTimeout(() => { + deleteCard(elem.closest("div.ref-card")); + }, 50); + }; + + csv2card(document.querySelector("#csv-src-viewer").value); + } + + async function handleSaveButton(event) { + if(event.target.id !="save") { + return; + } + const save = document.querySelector("#savelink"); + const data = await dot2svg(card2dot()); + + save.setAttribute("href", data); + save.setAttribute("download", "result.svg"); + save.click(); + } + + function handleExportButton(event) { + if (event.target.id != "export") { + return; + } + + download(card2csv(),"text/csv", "favorite.csv",); + } + + function download(text, mimeType, name,) { + const save = document.querySelector("#savelink"); + const encoded = window.btoa(unescape(encodeURIComponent(text))) + const dataUri = `data:${mimeType};charset=UTF-8;base64,${encoded}` + + save.setAttribute("href", dataUri); + save.setAttribute("download", name); + setTimeout(() => { + save.click(); + } ,50); + } +}); + + +function appendCard(bigbrother, node = {}) { + const newCard = document.querySelector("#tmpl-ref-card").content.cloneNode(true); + bigbrother.parentNode.insertBefore(newCard, bigbrother.nextElementSibling); + + const addedCard = bigbrother.nextElementSibling; + + const inputs = addedCard.querySelectorAll("input"); + const {id, parent, type, label, url} = node; + if (id && id.length > 0) { + inputs[0].value = id + } + + if (parent && parent.length > 0) { + inputs[1].value = parent + } + + if (type && type.length > 0) { + inputs[2].value = type + } + + if (label && label.length > 0) { + inputs[3].value = label + } + + + if (url && url.length > 0) { + inputs[4].value = url + } +} + +function deleteCard(card) { + card.parentNode.removeChild(card); +} + +function csv2card(csv) { + //改行をLFだけにしたい + const text = csv.replaceAll("\r", ""); + const textRows = text.split("\n") + const rows = []; + //当該要素はrootの一つしか残っていない想定 + let bigBrother = document.querySelector("div.ref-card") ; + for(const text of textRows) { + //ヘッダは読み飛ばす + if("id,parent,type,label,url" == text || "" == text.trim()) { + continue; + } + + const row = text.split(",") + + if(row[0] == "root") { + continue; + } + + appendCard(bigBrother, { + id: row[0], + parent: row[1], + type: row[2], + label: row[3], + url: row[4] + }) + + let cards = document.querySelectorAll("div.ref-card"); + bigBrother = cards[cards.length - 1] + } + +} +function card2dot() { + const nodes = []; + const edges = []; + + + function digraph(nodes, edges) { + const dot = `digraph G { + graph [overlap="true", bgcolor="#ffffdd", mindist="0.01"]; + node [fontname="BIZUDP Gothic" target="_blank", shape="tab", style="filled", color="#cc6666", fontcolor="#333333", fillcolor="#ffcc90"]; + edge [fontname="meiryo", color="#cc6666"]; + + + ${nodes} + + ${edges} +} +`; + return dot; + } + + const nodeList = []; + const edgeList = []; + + const cards = document.querySelectorAll("div.ref-card"); + + const id = 0, parent=1, type=2, label=3, url=4; + for(const card of cards) { + const values = card.querySelectorAll("input"); + const refNode = {}; + + refNode.id = values[id].value; + refNode.parent = values[parent].value; + refNode.type = values[type].value; + refNode.label = values[label].value; + refNode.url = values[url].value; + + nodeList.push(refNode); + } + for (const refNode of nodeList) { + const id = refNode.id, + parent = refNode.parent, + type = refNode.type, + label = refNode.label, + url = refNode.url; + + let shapeStmt; + if(type == "folder") { + shapeStmt = `, shape="ellipse"`; + } else { + shapeStmt = "" + } + + let labelStmt; + if(label == "") { + labelStmt = `label="${id}"`; + } else { + labelStmt = `label="${label}"`; + } + + let hrefStmt; + if(url == "") { + hrefStmt = ""; + } else { + //label属性が必ずあるため区切りの, を決め打ちで入れて問題なし + hrefStmt = `, href="${url}"`; + } + + const nodeStmt = `${id} [${labelStmt}${hrefStmt}${shapeStmt}];`; + nodes.push(nodeStmt); + + if(id == "root") { + continue; + } + + const edgeStmt = `${parent} -> ${id};`; + edges.push(edgeStmt); + } + return digraph(nodes.join("\n "),edges.join("\n ")); +} + +async function dot2svg(dot = "") { + const graphviz = await Graphviz.load(); + const src = dot == "" ? 'digraph G { Hello -> World }' : dot; + const graph = graphviz.circo(src); + //const graph = graphviz.neato(src); + const encoded = window.btoa(unescape(encodeURIComponent(graph))) + const dataUri = `data:image/svg+xml;charset=UTF-8;base64,${encoded}` + return dataUri +} + +function card2csv() { + const cards = document.querySelectorAll("div.ref-card"); + const csvText = [ + "id,parent,type,label,url" + ]; + + const id = 0, parent=1, type=2, label=3, url=4; + for(const card of cards) { + const values = card.querySelectorAll("input"); + const refNode = [ + values[id].value, + values[parent].value, + values[type].value, + values[label].value, + values[url].value + ]; + + csvText.push(refNode.join(",")); + } + + return csvText.join("\n") +} \ No newline at end of file