import {Tab} from "../Tab"; import {getDockByType, setPanelFocus} from "../util"; import {Model} from "../Model"; import {Constants} from "../../constants"; import {getDisplayName} from "../../util/pathName"; import {addScript} from "../../protyle/util/addScript"; import {BlockPanel} from "../../block/Panel"; import {fullscreen} from "../../protyle/breadcrumb/action"; import {fetchPost} from "../../util/fetch"; import {isCurrentEditor, openFileById} from "../../editor/util"; import {updateHotkeyTip} from "../../protyle/util/compatibility"; declare const vis: any; export class Graph extends Model { public inputElement: HTMLInputElement; private graphElement: HTMLDivElement; private panelElement: HTMLElement; private element: HTMLElement; private network: any; public blockId: string; // "local" / "pin" 必填 public rootId: string; // "local" 必填 private timeout: number; public graphData: { nodes: { box: string, id: string, path: string }[], links: Record[], box: string }; public type: "local" | "pin" | "global"; constructor(options: { tab: Tab blockId?: string rootId?: string type: "local" | "pin" | "global" }) { super({ id: options.tab.id, callback() { if (this.type === "local") { fetchPost("/api/block/checkBlockExist", {id: this.blockId}, existResponse => { if (!existResponse.data) { this.parent.parent.removeTab(this.parent.id); } }); } }, msgCallback(data) { if (data) { switch (data.cmd) { case "mount": if (this.type === "global" && data.code !== 1) { this.searchGraph(false); } break; case "rename": if (this.graphData && data.data.box === this.graphData.box && this.path === data.data.path) { this.searchGraph(false); if (this.type === "local") { this.parent.updateTitle(data.data.title); } } if (this.type === "global") { this.searchGraph(false); } break; case "moveDoc": if (this.type === "global") { this.searchGraph(false); } else if (this.graphData && (data.data.fromNotebook === this.graphData.box || data.data.toNotebook === this.graphData.box) && this.path === data.data.fromPath) { this.path = data.data.newPath; this.graphData.box = data.data.toNotebook; this.searchGraph(false); } break; case "unmount": case "remove": if (this.type === "global") { this.searchGraph(false); } else if (this.graphData && this.graphData.box === data.data.box && (!data.data.path || this.path.indexOf(getDisplayName(data.data.path, false, true)) === 0)) { this.parent.parent.removeTab(this.parent.id); } break; } } } }); this.element = options.tab.panelElement; this.blockId = options.blockId; this.rootId = options.rootId; this.type = options.type; this.element.classList.add("graph", "file-tree", this.type === "global" ? "sy__globalGraph" : "sy__graph"); let panelHTML; if (this.type === "global") { panelHTML = `
`; } else { panelHTML = `
`; } this.element.innerHTML = `
${panelHTML}
`; this.graphElement = this.element.querySelector(".graph__svg"); this.inputElement = this.element.querySelector("input"); this.panelElement = this.element.querySelector(".graph__panel") as HTMLElement; this.element.addEventListener("click", (event) => { if (this.type === "local") { setPanelFocus(this.element.parentElement.parentElement); } else { setPanelFocus(this.element.firstElementChild); } let target = event.target as HTMLElement; while (target && !target.isEqualNode(this.element)) { if (target.classList.contains("b3-button")) { if (this.type === "global") { fetchPost("/api/graph/resetGraph", {}, (data) => { this.reset(data.data.conf); }); } else { fetchPost("/api/graph/resetLocalGraph", {}, (data) => { this.reset(data.data.conf); }); } break; } else if (target.classList.contains("block__icon")) { const dataType = target.getAttribute("data-type"); if (dataType === "min") { getDockByType(this.type === "global" ? "globalGraph" : "graph").toggleModel(this.type === "global" ? "globalGraph" : "graph"); } else if (dataType === "menu") { if (target.classList.contains("ft__primary")) { target.classList.remove("ft__primary"); this.panelElement.style.right = ""; } else { target.classList.add("ft__primary"); this.panelElement.style.right = "0"; } } else if (dataType === "refresh") { this.searchGraph(false); } else if (dataType === "fullscreen") { fullscreen(this.element, target); } break; } else if (target.classList.contains("graph__svg")) { this.element.querySelectorAll(".block__icon.ft__primary").forEach(item => { item.classList.remove("ft__primary"); }); this.panelElement.style.right = ""; break; } target = target.parentElement; } }); this.inputElement.addEventListener("compositionend", () => { this.searchGraph(false); }); this.inputElement.addEventListener("input", (event: InputEvent) => { if (event.isComposing) { return; } this.searchGraph(false); }); this.element.querySelectorAll(".b3-slider").forEach((item: HTMLInputElement) => { item.addEventListener("input", () => { item.setAttribute("aria-label", item.value); this.searchGraph(false); }); }); this.element.querySelectorAll(".b3-switch").forEach((item) => { item.addEventListener("change", () => { this.searchGraph(false); }); }); this.searchGraph(options.type !== "global"); if (this.type !== "local") { setPanelFocus(this.element.firstElementChild); } } private reset(conf: IGraphCommon & ({ dailyNote: boolean } | { minRefs: number, dailyNote: boolean })) { if (this.type === "global") { window.siyuan.config.graph.global = conf as IGraphCommon & { minRefs: number, dailyNote: boolean }; this.panelElement.querySelector("[data-type='minRefs']").setAttribute("aria-label", window.siyuan.config.graph.global.minRefs.toString()); (this.panelElement.querySelector("[data-type='minRefs']") as HTMLInputElement).value = window.siyuan.config.graph.global.minRefs.toString(); } else { window.siyuan.config.graph.local = conf as IGraphCommon & { dailyNote: boolean }; } this.inputElement.value = ""; this.panelElement.querySelector("[data-type='nodeSize']").setAttribute("aria-label", conf.d3.nodeSize.toString()); this.panelElement.querySelector("[data-type='centerStrength']").setAttribute("aria-label", conf.d3.centerStrength.toString()); this.panelElement.querySelector("[data-type='collideRadius']").setAttribute("aria-label", conf.d3.collideRadius.toString()); this.panelElement.querySelector("[data-type='collideStrength']").setAttribute("aria-label", conf.d3.collideStrength.toString()); this.panelElement.querySelector("[data-type='lineOpacity']").setAttribute("aria-label", conf.d3.lineOpacity.toString()); this.panelElement.querySelector("[data-type='linkDistance']").setAttribute("aria-label", conf.d3.linkDistance.toString()); this.panelElement.querySelector("[data-type='linkWidth']").setAttribute("aria-label", conf.d3.linkWidth.toString()); (this.panelElement.querySelector("[data-type='nodeSize']") as HTMLInputElement).value = conf.d3.nodeSize.toString(); (this.panelElement.querySelector("[data-type='centerStrength']") as HTMLInputElement).value = conf.d3.centerStrength.toString(); (this.panelElement.querySelector("[data-type='collideRadius']") as HTMLInputElement).value = conf.d3.collideRadius.toString(); (this.panelElement.querySelector("[data-type='collideStrength']") as HTMLInputElement).value = conf.d3.collideStrength.toString(); (this.panelElement.querySelector("[data-type='lineOpacity']") as HTMLInputElement).value = conf.d3.lineOpacity.toString(); (this.panelElement.querySelector("[data-type='linkDistance']") as HTMLInputElement).value = conf.d3.linkDistance.toString(); (this.panelElement.querySelector("[data-type='linkWidth']") as HTMLInputElement).value = conf.d3.linkWidth.toString(); (this.panelElement.querySelector("[data-type='list']") as HTMLInputElement).checked = conf.type.list; (this.panelElement.querySelector("[data-type='listItem']") as HTMLInputElement).checked = conf.type.listItem; (this.panelElement.querySelector("[data-type='math']") as HTMLInputElement).checked = conf.type.math; (this.panelElement.querySelector("[data-type='paragraph']") as HTMLInputElement).checked = conf.type.paragraph; (this.panelElement.querySelector("[data-type='super']") as HTMLInputElement).checked = conf.type.super; (this.panelElement.querySelector("[data-type='table']") as HTMLInputElement).checked = conf.type.table; (this.panelElement.querySelector("[data-type='tag']") as HTMLInputElement).checked = conf.type.tag; (this.panelElement.querySelector("[data-type='dailyNote']") as HTMLInputElement).checked = conf.dailyNote; (this.panelElement.querySelector("[data-type='heading']") as HTMLInputElement).checked = conf.type.heading; (this.panelElement.querySelector("[data-type='arrow']") as HTMLInputElement).checked = conf.d3.arrow; (this.panelElement.querySelector("[data-type='blockquote']") as HTMLInputElement).checked = conf.type.blockquote; (this.panelElement.querySelector("[data-type='code']") as HTMLInputElement).checked = conf.type.code; this.searchGraph(false); } public searchGraph(focus: boolean, id?: string) { const element = this.element.querySelector('.block__icon[data-type="refresh"] svg'); if (element.classList.contains("fn__rotate") && !id) { return; } element.classList.add("fn__rotate"); const type = { list: (this.panelElement.querySelector("[data-type='list']") as HTMLInputElement).checked, listItem: (this.panelElement.querySelector("[data-type='listItem']") as HTMLInputElement).checked, math: (this.panelElement.querySelector("[data-type='math']") as HTMLInputElement).checked, paragraph: (this.panelElement.querySelector("[data-type='paragraph']") as HTMLInputElement).checked, super: (this.panelElement.querySelector("[data-type='super']") as HTMLInputElement).checked, table: (this.panelElement.querySelector("[data-type='table']") as HTMLInputElement).checked, tag: (this.panelElement.querySelector("[data-type='tag']") as HTMLInputElement).checked, heading: (this.panelElement.querySelector("[data-type='heading']") as HTMLInputElement).checked, blockquote: (this.panelElement.querySelector("[data-type='blockquote']") as HTMLInputElement).checked, code: (this.panelElement.querySelector("[data-type='code']") as HTMLInputElement).checked, }; const d3 = { arrow: (this.panelElement.querySelector("[data-type='arrow']") as HTMLInputElement).checked, nodeSize: parseFloat((this.panelElement.querySelector("[data-type='nodeSize']") as HTMLInputElement).value), centerStrength: parseFloat((this.panelElement.querySelector("[data-type='centerStrength']") as HTMLInputElement).value), collideRadius: parseFloat((this.panelElement.querySelector("[data-type='collideRadius']") as HTMLInputElement).value), collideStrength: parseFloat((this.panelElement.querySelector("[data-type='collideStrength']") as HTMLInputElement).value), lineOpacity: parseFloat((this.panelElement.querySelector("[data-type='lineOpacity']") as HTMLInputElement).value), linkDistance: parseFloat((this.panelElement.querySelector("[data-type='linkDistance']") as HTMLInputElement).value), linkWidth: parseFloat((this.panelElement.querySelector("[data-type='linkWidth']") as HTMLInputElement).value), }; if (this.type === "global") { // 全局 fetchPost("/api/graph/getGraph", { k: this.inputElement.value, conf: { type, d3, dailyNote: (this.panelElement.querySelector("[data-type='dailyNote']") as HTMLInputElement).checked, minRefs: parseFloat((this.panelElement.querySelector("[data-type='minRefs']") as HTMLInputElement).value) } }, response => { this.graphData = response.data; window.siyuan.config.graph.global = response.data.conf; this.onGraph(false); element.classList.remove("fn__rotate"); }); } else { fetchPost("/api/graph/getLocalGraph", { k: this.inputElement.value, id: id || this.blockId, conf: { type, d3, dailyNote: (this.panelElement.querySelector("[data-type='dailyNote']") as HTMLInputElement).checked, }, }, response => { element.classList.remove("fn__rotate"); if (id) { this.blockId = id; } if (!isCurrentEditor(this.blockId)) { return; } this.graphData = response.data; window.siyuan.config.graph.local = response.data.conf; this.onGraph(focus); }); } } public hlNode(id: string) { if (this.graphElement.clientHeight === 0 || !this.network || this.network.findNode(id).length === 0) { return; } this.network.focus(id, { animation: { duration: 1000, easingFunction: "easeInOutQuad", }, }); this.network.selectNodes([id]); } public onGraph(hl: boolean) { if (this.graphElement.clientHeight === 0) { // 界面没有渲染时不能进行渲染 return; } if (!this.graphData || !this.graphData.nodes || this.graphData.nodes.length === 0) { if (this.network) { this.network.destroy(); } this.graphElement.firstElementChild.classList.add("fn__none"); return; } clearTimeout(this.timeout); addScript(`${Constants.PROTYLE_CDN}/js/vis/vis-network.min.js?v=9.0.4`, "protyleVisScript").then(() => { this.timeout = window.setTimeout(() => { this.graphElement.firstElementChild.classList.remove("fn__none"); this.graphElement.firstElementChild.firstElementChild.setAttribute("style", "width:3%"); const config = window.siyuan.config.graph[this.type === "global" ? "global" : "local"]; const data = { nodes: this.graphData.nodes, edges: this.graphData.links, }; const rootStyle = getComputedStyle(document.body); const options = { autoResize: true, interaction: { hover: true, }, nodes: { borderWidth: 0, borderWidthSelected: 5, shape: "dot", font: { face: rootStyle.getPropertyValue("--b3-font-family-graph").trim(), size: 32, color: rootStyle.getPropertyValue("--b3-theme-on-background").trim(), }, color: { hover: { border: rootStyle.getPropertyValue("--b3-graph-hl-point").trim(), background: rootStyle.getPropertyValue("--b3-graph-hl-point").trim() }, highlight: { border: rootStyle.getPropertyValue("--b3-graph-hl-point").trim(), background: rootStyle.getPropertyValue("--b3-graph-hl-point").trim() }, } }, edges: { width: config.d3.linkWidth, arrowStrikethrough: false, smooth: false, color: { opacity: config.d3.lineOpacity, hover: rootStyle.getPropertyValue("--b3-graph-hl-line").trim(), highlight: rootStyle.getPropertyValue("--b3-graph-hl-line").trim(), } }, layout: { improvedLayout: false }, physics: { enabled: true, forceAtlas2Based: { theta: 0.5, gravitationalConstant: -config.d3.collideRadius, centralGravity: config.d3.centerStrength, springConstant: config.d3.collideStrength, springLength: config.d3.linkDistance, damping: 0.4, avoidOverlap: 0.5 }, maxVelocity: 50, minVelocity: 0.1, solver: "forceAtlas2Based", stabilization: { enabled: true, iterations: 256, updateInterval: 25, onlyDynamicEdges: false, fit: true }, timestep: 0.5, adaptiveTimestep: true, wind: {x: 0, y: 0} }, }; const network = new vis.Network(this.graphElement.lastElementChild, data, options); this.network = network; network.on("stabilizationIterationsDone", () => { network.physics.stopSimulation(); this.graphElement.firstElementChild.classList.add("fn__none"); if (hl) { this.hlNode(this.blockId); } }); network.on("dragEnd", () => { setTimeout(() => { network.physics.stopSimulation(); }, 5000); }); network.on("stabilizationProgress", (data: any) => { this.graphElement.firstElementChild.firstElementChild.setAttribute("style", `width:${Math.max(5, data.iterations) / data.total * 100}%`); }); network.on("click", (params: any) => { if (params.nodes.length !== 1) { return; } const node = this.graphData.nodes.find((item) => item.id === params.nodes[0]); if (!node) { return; } if (window.siyuan.shiftIsPressed) { openFileById({ id: node.id, position: "bottom", action: [Constants.CB_GET_FOCUS] }); } else if (window.siyuan.altIsPressed) { openFileById({ id: node.id, position: "right", action: [Constants.CB_GET_FOCUS] }); } else if (window.siyuan.ctrlIsPressed) { window.siyuan.blockPanels.push(new BlockPanel({ targetElement: this.inputElement, nodeIds: [node.id], })); } else { openFileById({id: node.id, action: [Constants.CB_GET_FOCUS]}); } }); }, 1000); }); } }