diff --git a/app/src/editor/util.ts b/app/src/editor/util.ts index 6a5c09936..06b45c665 100644 --- a/app/src/editor/util.ts +++ b/app/src/editor/util.ts @@ -429,7 +429,7 @@ export const updateBacklinkGraph = (models: IModels, protyle: IProtyle) => { item.searchGraph(true, blockId); } }); - models.backlinks.forEach(item => { + models.backlink.forEach(item => { if (item.type === "local" && item.rootId !== protyle?.block?.rootID) { return; } @@ -443,7 +443,7 @@ export const updateBacklinkGraph = (models: IModels, protyle: IProtyle) => { item.element.querySelector('.block__icon[data-type="refresh"] svg').classList.add("fn__rotate"); fetchPost("/api/ref/getBacklink", { id: blockId || "", - beforeLen: item.element.querySelector('.block__icon[data-type="more"]').classList.contains("ft__primary") ? item.beforeLen * 20 : item.beforeLen, + beforeLen: 12, k: item.inputsElement[0].value, mk: item.inputsElement[1].value, }, response => { diff --git a/app/src/layout/dock/Backlink.ts b/app/src/layout/dock/Backlink.ts new file mode 100644 index 000000000..93dda6837 --- /dev/null +++ b/app/src/layout/dock/Backlink.ts @@ -0,0 +1,400 @@ +import {Tab} from "../Tab"; +import {Model} from "../Model"; +import {getDisplayName} from "../../util/pathName"; +import {Tree} from "../../util/Tree"; +import {hasClosestByClassName} from "../../protyle/util/hasClosest"; +import {getDockByType, setPanelFocus} from "../util"; +import {fetchPost} from "../../util/fetch"; +import {Constants} from "../../constants"; +import {getAllModels} from "../getAll"; +import {onGet} from "../../protyle/util/onGet"; +import {updateHotkeyTip} from "../../protyle/util/compatibility"; +import {openFileById} from "../../editor/util"; +import {MenuItem} from "../../menus/Menu"; + +export class Backlink extends Model { + public element: HTMLElement; + public inputsElement: NodeListOf; + public type: "pin" | "local"; + public blockId: string; + public rootId: string; // "local" 必传 + private tree: Tree; + private notebookId: string; + private mTree: Tree; + + constructor(options: { + tab: Tab, + blockId: string, + rootId?: string, + type: "pin" | "local" + }) { + 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 && this.type === "local") { + switch (data.cmd) { + case "rename": + if (this.blockId === data.data.id) { + this.parent.updateTitle(data.data.title); + } + break; + case "unmount": + if (this.notebookId === data.data.box) { + this.parent.parent.removeTab(this.parent.id); + } + break; + case "remove": + if (this.path?.indexOf(getDisplayName(data.data.path, false, true)) === 0) { + this.parent.parent.removeTab(this.parent.id); + } + break; + } + } + } + }); + this.blockId = options.blockId; + this.rootId = options.rootId; + this.type = options.type; + this.element = options.tab.panelElement; + this.element.classList.add("fn__flex-column", "file-tree", "sy__backlink"); + this.element.innerHTML = `
+ + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + +
+
`; + + this.inputsElement = this.element.querySelectorAll("input"); + this.inputsElement.forEach((item) => { + item.addEventListener("keydown", (event: KeyboardEvent) => { + if (!event.isComposing && event.key === "Enter") { + this.searchBacklinks(); + } + }); + item.addEventListener("input", (event: KeyboardEvent) => { + const inputElement = event.target as HTMLInputElement; + if (inputElement.value === "") { + inputElement.classList.remove("search__input--block"); + } else { + inputElement.classList.add("search__input--block"); + } + }); + }); + + this.tree = new Tree({ + element: this.element.querySelector(".backlinkList") as HTMLElement, + data: null, + click(element: HTMLElement) { + openFileById({ + id: element.getAttribute("data-node-id"), + action: [Constants.CB_GET_FOCUS, Constants.CB_GET_CONTEXT] + }); + }, + ctrlClick(element: HTMLElement) { + openFileById({ + id: element.getAttribute("data-node-id"), + keepCursor: true, + action: [Constants.CB_GET_CONTEXT] + }); + }, + altClick(element: HTMLElement) { + openFileById({ + id: element.getAttribute("data-node-id"), + position: "right", + action: [Constants.CB_GET_FOCUS, Constants.CB_GET_CONTEXT] + }); + }, + shiftClick(element: HTMLElement) { + openFileById({ + id: element.getAttribute("data-node-id"), + position: "bottom", + action: [Constants.CB_GET_FOCUS, Constants.CB_GET_CONTEXT] + }); + } + }); + this.mTree = new Tree({ + element: this.element.querySelector(".backlinkMList") as HTMLElement, + data: null, + click: (element, event) => { + const actionElement = hasClosestByClassName(event.target as HTMLElement, "b3-list-item__action"); + if (actionElement) { + if (actionElement.firstElementChild.classList.contains("fn__rotate")) { + return; + } + window.siyuan.menus.menu.remove(); + window.siyuan.menus.menu.append(new MenuItem({ + label: window.siyuan.languages.turnInto + " " + window.siyuan.languages.turnToStaticRef, + click: () => { + this.turnToRef(element, false); + } + }).element); + window.siyuan.menus.menu.append(new MenuItem({ + label: window.siyuan.languages.turnInto + " " + window.siyuan.languages.turnToDynamicRef, + click: () => { + this.turnToRef(element, true); + } + }).element); + window.siyuan.menus.menu.popup({x: event.clientX, y: event.clientY}); + } else { + openFileById({ + id: element.getAttribute("data-node-id"), + action: [Constants.CB_GET_FOCUS, Constants.CB_GET_CONTEXT] + }); + } + }, + ctrlClick(element: HTMLElement) { + openFileById({ + id: element.getAttribute("data-node-id"), + keepCursor: true, + action: [Constants.CB_GET_CONTEXT] + }); + }, + altClick(element: HTMLElement) { + openFileById({ + id: element.getAttribute("data-node-id"), + position: "right", + action: [Constants.CB_GET_FOCUS, Constants.CB_GET_CONTEXT] + }); + }, + shiftClick(element: HTMLElement) { + openFileById({ + id: element.getAttribute("data-node-id"), + position: "bottom", + action: [Constants.CB_GET_FOCUS, Constants.CB_GET_CONTEXT] + }); + }, + blockExtHTML: `` + }); + // 为了快捷键的 dispatch + this.element.querySelector('[data-type="collapse"]').addEventListener("click", () => { + this.tree.collapseAll(); + }); + this.element.querySelector('[data-type="expand"]').addEventListener("click", () => { + this.tree.expandAll(); + }); + 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("block__icon")) { + const type = target.getAttribute("data-type"); + switch (type) { + case "refresh": + this.refresh(); + break; + case "mExpand": + this.mTree.expandAll(); + break; + case "mCollapse": + this.mTree.collapseAll(); + break; + case "min": + getDockByType("backlink").toggleModel("backlink"); + break; + case "layout": + if (this.mTree.element.style.flex) { + if (this.mTree.element.style.height === "0px") { + this.tree.element.classList.remove("fn__none"); + this.mTree.element.removeAttribute("style"); + target.setAttribute("aria-label", window.siyuan.languages.up); + target.querySelector("use").setAttribute("xlink:href", "#iconUp"); + } else { + this.tree.element.classList.remove("fn__none"); + this.mTree.element.removeAttribute("style"); + target.setAttribute("aria-label", window.siyuan.languages.down); + target.querySelector("use").setAttribute("xlink:href", "#iconDown"); + } + } else { + if (target.getAttribute("aria-label") === window.siyuan.languages.down) { + this.tree.element.classList.remove("fn__none"); + this.mTree.element.setAttribute("style", "flex:none;height:0px"); + target.setAttribute("aria-label", window.siyuan.languages.up); + target.querySelector("use").setAttribute("xlink:href", "#iconUp"); + } else { + this.tree.element.classList.add("fn__none"); + this.mTree.element.setAttribute("style", `flex:none;height:${this.element.clientHeight - this.tree.element.previousElementSibling.clientHeight * 2}px`); + target.setAttribute("aria-label", window.siyuan.languages.down); + target.querySelector("use").setAttribute("xlink:href", "#iconDown"); + } + } + target.setAttribute("data-clicked", "true"); + break; + } + } + target = target.parentElement; + } + }); + + this.searchBacklinks(); + + if (this.type === "pin") { + setPanelFocus(this.element.firstElementChild); + } + } + + private turnToRef(element: HTMLElement, isDynamic: boolean) { + element.querySelector(".b3-list-item__action").innerHTML = ''; + this.element.querySelector('.block__icon[data-type="refresh"] svg').classList.add("fn__rotate"); + fetchPost("/api/ref/createBacklink", { + refID: element.getAttribute("data-node-id"), + refText: decodeURIComponent(element.getAttribute("data-ref-text")), + defID: this.blockId, + pushMode: 0, + isDynamic + }, response => { + if (response.data.defID === this.blockId) { + this.searchBacklinks(true); + } + getAllModels().editor.forEach(item => { + if (response.data.refRootID === item.editor.protyle.block.rootID) { + fetchPost("/api/filetree/getDoc", { + id: item.editor.protyle.block.id, + size: Constants.SIZE_GET, + }, getResponse => { + onGet(getResponse, item.editor.protyle); + }); + } + }); + }); + } + + public refresh() { + fetchPost("/api/ref/refreshBacklink", { + id: this.blockId, + }, () => { + this.searchBacklinks(); + }); + } + + private searchBacklinks(reload = false) { + const element = this.element.querySelector('.block__icon[data-type="refresh"] svg'); + if (element.classList.contains("fn__rotate") && !reload) { + return; + } + element.classList.add("fn__rotate"); + fetchPost("/api/ref/getBacklink", { + k: this.inputsElement[0].value, + mk: this.inputsElement[1].value, + beforeLen: 12, + id: this.blockId, + }, response => { + this.render(response.data); + }); + } + + public render(data: { box: string, backlinks: IBlockTree[], backmentions: IBlockTree[], linkRefsCount: number, mentionsCount: number, k: string, mk: string }) { + if (!data) { + data = { + box: "", + backlinks: [], + backmentions: [], + linkRefsCount: 0, + mentionsCount: 0, + k: "", + mk: "" + }; + } + this.element.querySelector('.block__icon[data-type="refresh"] svg').classList.remove("fn__rotate"); + this.notebookId = data.box; + this.inputsElement[0].value = data.k; + this.inputsElement[1].value = data.mk; + + this.tree.updateData(data.backlinks); + this.mTree.updateData(data.backmentions); + + const countElement = this.element.querySelector(".listCount"); + if (data.linkRefsCount === 0) { + countElement.classList.add("fn__none"); + } else { + countElement.classList.remove("fn__none"); + countElement.textContent = data.linkRefsCount.toString(); + } + const mCountElement = this.element.querySelector(".listMCount"); + if (data.mentionsCount === 0) { + mCountElement.classList.add("fn__none"); + } else { + mCountElement.classList.remove("fn__none"); + mCountElement.textContent = data.mentionsCount.toString(); + } + + const layoutElement = this.element.querySelector("[data-type='layout']"); + if (layoutElement.getAttribute("data-clicked")) { + return; + } + if (data.mentionsCount === 0) { + this.tree.element.classList.remove("fn__none"); + this.mTree.element.setAttribute("style", "flex:none;height:0px"); + layoutElement.setAttribute("aria-label", window.siyuan.languages.up); + layoutElement.querySelector("use").setAttribute("xlink:href", "#iconUp"); + return; + } + if (data.linkRefsCount === 0) { + this.tree.element.classList.add("fn__none"); + this.mTree.element.setAttribute("style", `flex:none;height:${this.element.clientHeight - this.tree.element.previousElementSibling.clientHeight * 2}px`); + layoutElement.setAttribute("aria-label", window.siyuan.languages.down); + layoutElement.querySelector("use").setAttribute("xlink:href", "#iconDown"); + } else { + this.tree.element.classList.remove("fn__none"); + this.mTree.element.removeAttribute("style"); + layoutElement.setAttribute("aria-label", window.siyuan.languages.down); + layoutElement.querySelector("use").setAttribute("xlink:href", "#iconDown"); + } + } +} diff --git a/app/src/layout/dock/index.ts b/app/src/layout/dock/index.ts index 25b8a026a..6731292fc 100644 --- a/app/src/layout/dock/index.ts +++ b/app/src/layout/dock/index.ts @@ -8,11 +8,11 @@ import {getAllModels} from "../getAll"; import {Bookmark} from "./Bookmark"; import {Tag} from "./Tag"; import {Graph} from "./Graph"; -import {Backlinks} from "./Backlinks"; import {Model} from "../Model"; import {getDockByType, resizeTabs, setPanelFocus} from "../util"; import {Inbox} from "./Inbox"; import Protyle from "../../protyle"; +import {Backlink} from "./Backlink"; export class Dock { public element: HTMLElement; @@ -199,10 +199,22 @@ export class Dock { } }); break; + // TODO: remove + // case "backlink": + // tab = new Tab({ + // callback(tab: Tab) { + // tab.addModel(new Backlinks({ + // type: "pin", + // tab, + // blockId: editor?.protyle?.block?.rootID, + // })); + // } + // }); + // break; case "backlink": tab = new Tab({ callback(tab: Tab) { - tab.addModel(new Backlinks({ + tab.addModel(new Backlink({ type: "pin", tab, blockId: editor?.protyle?.block?.rootID, diff --git a/app/src/layout/dock/util.ts b/app/src/layout/dock/util.ts index d4646baef..05d8be066 100644 --- a/app/src/layout/dock/util.ts +++ b/app/src/layout/dock/util.ts @@ -1,12 +1,12 @@ import {getAllModels} from "../getAll"; import {Tab} from "../Tab"; -import {Backlinks} from "./Backlinks"; import {Graph} from "./Graph"; import {Outline} from "./Outline"; import {switchWnd} from "../util"; +import {Backlink} from "./Backlink"; export const openBacklink = (protyle: IProtyle) => { - const backlink = getAllModels().backlinks.find(item => { + const backlink = getAllModels().backlink.find(item => { if (item.blockId === protyle.block.id && item.type === "local") { item.parent.parent.removeTab(item.parent.id); return true; @@ -20,7 +20,7 @@ export const openBacklink = (protyle: IProtyle) => { icon: "iconLink", title: protyle.title.editElement.textContent, callback(tab: Tab) { - tab.addModel(new Backlinks({ + tab.addModel(new Backlink({ type: "local", tab, blockId: protyle.block.id, diff --git a/app/src/layout/getAll.ts b/app/src/layout/getAll.ts index 99da7a820..f83788424 100644 --- a/app/src/layout/getAll.ts +++ b/app/src/layout/getAll.ts @@ -3,7 +3,7 @@ import {Tab} from "./Tab"; import {Editor} from "../editor"; import {Graph} from "./dock/Graph"; import {Outline} from "./dock/Outline"; -import {Backlinks} from "./dock/Backlinks"; +import {Backlink} from "./dock/Backlink"; import {Asset} from "../asset"; import {Search} from "../search"; @@ -13,7 +13,7 @@ export const getAllModels = () => { graph: [], asset: [], outline: [], - backlinks: [], + backlink: [], search: [] }; const getTabs = (layout: Layout) => { @@ -27,8 +27,8 @@ export const getAllModels = () => { models.graph.push(model); } else if (model instanceof Outline) { models.outline.push(model); - } else if (model instanceof Backlinks) { - models.backlinks.push(model); + } else if (model instanceof Backlink) { + models.backlink.push(model); } else if (model instanceof Asset) { models.asset.push(model); } else if (model instanceof Search) { diff --git a/app/src/layout/util.ts b/app/src/layout/util.ts index 9360bb9a4..b01d53ab1 100644 --- a/app/src/layout/util.ts +++ b/app/src/layout/util.ts @@ -11,7 +11,6 @@ import {newFile} from "../util/newFile"; import {Outline} from "./dock/Outline"; import {Bookmark} from "./dock/Bookmark"; import {updateHotkeyTip} from "../protyle/util/compatibility"; -import {Backlinks} from "./dock/Backlinks"; import {Tag} from "./dock/Tag"; import {getAllModels, getAllTabs} from "./getAll"; import {Asset} from "../asset"; @@ -26,6 +25,7 @@ import {Constants} from "../constants"; import {openSearch} from "../search/spread"; import {saveScroll} from "../protyle/scroll/saveScroll"; import {pdfResize} from "../asset/renderAssets"; +import {Backlink} from "./dock/Backlink"; export const setPanelFocus = (element: Element) => { if (element.classList.contains("block__icons--active") || element.classList.contains("layout__wnd--active")) { @@ -214,8 +214,8 @@ const JSONToCenter = (json: any, layout?: Layout | Wnd | Tab | Model) => { tab: (layout as Tab), path: json.path, })); - } else if (json.instance === "Backlinks") { - (layout as Tab).addModel(new Backlinks({ + } else if (json.instance === "Backlink") { + (layout as Tab).addModel(new Backlink({ tab: (layout as Tab), blockId: json.blockId, rootId: json.rootId, @@ -305,7 +305,7 @@ export const layoutToJSON = (layout: Layout | Wnd | Tab | Model, json: any) => { json.pin = layout.headElement.classList.contains("item--pin"); if (layout.model instanceof Files) { json.lang = "fileTree"; - } else if (layout.model instanceof Backlinks && layout.model.type === "pin") { + } else if (layout.model instanceof Backlink && layout.model.type === "pin") { json.lang = "backlinks"; } else if (layout.model instanceof Bookmark) { json.lang = "bookmark"; @@ -331,11 +331,11 @@ export const layoutToJSON = (layout: Layout | Wnd | Tab | Model, json: any) => { } else if (layout instanceof Asset) { json.path = layout.path; json.instance = "Asset"; - } else if (layout instanceof Backlinks) { + } else if (layout instanceof Backlink) { json.blockId = layout.blockId; json.rootId = layout.rootId; json.type = layout.type; - json.instance = "Backlinks"; + json.instance = "Backlink"; } else if (layout instanceof Bookmark) { json.instance = "Bookmark"; } else if (layout instanceof Files) { @@ -471,8 +471,8 @@ export const copyTab = (tab: Tab) => { blockId: tab.model.blockId, type: tab.model.type }); - } else if (tab.model instanceof Backlinks) { - model = new Backlinks({ + } else if (tab.model instanceof Backlink) { + model = new Backlink({ tab: newTab, blockId: tab.model.blockId, rootId: tab.model.rootId, diff --git a/app/src/protyle/util/editorCommonEvent.ts b/app/src/protyle/util/editorCommonEvent.ts index 3bf51624d..b7be8dcdc 100644 --- a/app/src/protyle/util/editorCommonEvent.ts +++ b/app/src/protyle/util/editorCommonEvent.ts @@ -665,7 +665,7 @@ export const dropEvent = (protyle: IProtyle, editorElement: HTMLElement) => { if (isBacklink) { setTimeout(() => { // 等待 drag transaction - getAllModels().backlinks.forEach(item => { + getAllModels().backlink.forEach(item => { item.refresh(); }); }, 200); diff --git a/app/src/protyle/util/onGet.ts b/app/src/protyle/util/onGet.ts index 0ab1de110..f497ac018 100644 --- a/app/src/protyle/util/onGet.ts +++ b/app/src/protyle/util/onGet.ts @@ -76,7 +76,6 @@ export const onGet = (data: IWebSocketData, protyle: IProtyle, action: string[] protyle.block.id = data.data.id; protyle.scroll.lastScrollTop = 0; protyle.contentElement.scrollTop = 0; - preventScroll(protyle); // 聚焦返回时 protyle.contentElement.scrollTop 不等于 0,会导致滚动加载,从而导致 showAll 为 false protyle.wysiwyg.element.setAttribute("data-doc-type", data.data.type); } diff --git a/app/src/types/index.d.ts b/app/src/types/index.d.ts index eaf4bc282..1be882a18 100644 --- a/app/src/types/index.d.ts +++ b/app/src/types/index.d.ts @@ -1,6 +1,15 @@ type TLayout = "normal" | "top" | "bottom" | "left" | "right" | "center" type TDirection = "lr" | "tb" -type TDockType = "file" | "outline" | "bookmark" | "tag" | "graph" | "globalGraph" | "backlink" | "inbox" +type TDockType = + "file" + | "outline" + | "bookmark" + | "tag" + | "graph" + | "globalGraph" + | "backlink" + | "backlinkOld" + | "inbox" type TDockPosition = "Left" | "Right" | "Top" | "Bottom" type TWS = "main" | "filetree" | "protyle" type TEditorMode = "preview" | "wysiwyg" @@ -475,7 +484,7 @@ declare interface IModels { editor: import("../editor").Editor [], graph: import("../layout/dock/Graph").Graph[], outline: import("../layout/dock/Outline").Outline[] - backlinks: import("../layout/dock/Backlinks").Backlinks[] + backlink: import("../layout/dock/Backlink").Backlink[] asset: import("../asset").Asset[] search: import("../search").Search[] }