import {Tab} from "../Tab"; import {Model} from "../Model"; import {Tree} from "../../util/Tree"; import {getInstanceById, setPanelFocus} from "../util"; import {getDockByType} from "../tabUtil"; import {fetchPost} from "../../util/fetch"; import {getAllModels} from "../getAll"; import {hasClosestBlock, hasClosestByClassName, hasTopClosestByClassName} from "../../protyle/util/hasClosest"; import {setStorageVal, updateHotkeyTip} from "../../protyle/util/compatibility"; import {openFileById} from "../../editor/util"; import {Constants} from "../../constants"; import {escapeHtml} from "../../util/escape"; import {unicode2Emoji} from "../../emoji"; import {getPreviousBlock} from "../../protyle/wysiwyg/getBlock"; import {App} from "../../index"; import {checkFold} from "../../util/noRelyPCFunction"; import {transaction} from "../../protyle/wysiwyg/transaction"; import {goHome} from "../../protyle/wysiwyg/commonHotkey"; import {Editor} from "../../editor"; export class Outline extends Model { public tree: Tree; public element: HTMLElement; public headerElement: HTMLElement; public type: "pin" | "local"; public blockId: string; public isPreview: boolean; private openNodes: { [key: string]: string[] } = {}; constructor(options: { app: App, tab: Tab, blockId: string, type: "pin" | "local", isPreview: boolean }) { super({ app: options.app, 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 "savedoc": this.onTransaction(data); break; case "rename": if (this.type === "local" && this.blockId === data.data.id) { this.parent.updateTitle(data.data.title); } else { this.updateDocTitle({ title: data.data.title, icon: Constants.ZWSP }); } break; case "unmount": if (this.type === "local") { fetchPost("/api/block/checkBlockExist", {id: this.blockId}, existResponse => { if (!existResponse.data) { this.parent.parent.removeTab(this.parent.id); } }); } break; case "removeDoc": if (data.data.ids.includes(this.blockId) && this.type === "local") { this.parent.parent.removeTab(this.parent.id); } break; } } } }); this.isPreview = options.isPreview; this.blockId = options.blockId; this.type = options.type; options.tab.panelElement.classList.add("fn__flex-column", "file-tree", "sy__outline"); options.tab.panelElement.innerHTML = `
`; this.element = options.tab.panelElement.lastElementChild as HTMLElement; this.headerElement = options.tab.panelElement.firstElementChild as HTMLElement; this.tree = new Tree({ element: options.tab.panelElement.lastElementChild as HTMLElement, data: null, click: (element: HTMLElement) => { const id = element.getAttribute("data-node-id"); if (this.isPreview) { const headElement = document.getElementById(id); if (headElement) { const tabElement = hasTopClosestByClassName(headElement, "protyle"); if (tabElement) { const tab = getInstanceById(tabElement.getAttribute("data-id")) as Tab; tab.parent.switchTab(tab.headElement); } headElement.scrollIntoView(); } else { openFileById({ app: options.app, id: this.blockId, mode: "preview", }); } } else { checkFold(id, (zoomIn) => { openFileById({ app: options.app, id, action: zoomIn ? [Constants.CB_GET_FOCUS, Constants.CB_GET_ALL, Constants.CB_GET_HTML] : [Constants.CB_GET_FOCUS, Constants.CB_GET_SETID, Constants.CB_GET_CONTEXT, Constants.CB_GET_HTML], }); }); } }, ctrlClick(element: HTMLElement) { const id = element.getAttribute("data-node-id"); openFileById({ app: options.app, id, action: [Constants.CB_GET_FOCUS, Constants.CB_GET_ALL, Constants.CB_GET_HTML], zoomIn: true, }); } }); // 为了快捷键的 dispatch options.tab.panelElement.querySelector('[data-type="collapse"]').addEventListener("click", () => { this.tree.collapseAll(); }); options.tab.panelElement.querySelector('[data-type="expand"]').addEventListener("click", (event: MouseEvent & { target: Element }) => { const iconElement = hasClosestByClassName(event.target, "block__icon"); if (!iconElement) { return; } if (iconElement.classList.contains("block__icon--active")) { iconElement.classList.remove("block__icon--active"); window.siyuan.storage[Constants.LOCAL_OUTLINE].keepExpand = false; } else { iconElement.classList.add("block__icon--active"); window.siyuan.storage[Constants.LOCAL_OUTLINE].keepExpand = true; this.tree.expandAll(); } setStorageVal(Constants.LOCAL_OUTLINE, window.siyuan.storage[Constants.LOCAL_OUTLINE]); }); options.tab.panelElement.addEventListener("click", (event: MouseEvent & { target: HTMLElement }) => { let target = event.target as HTMLElement; let isFocus = true; while (target && !target.isEqualNode(options.tab.panelElement)) { if (target.classList.contains("block__icon")) { const type = target.getAttribute("data-type"); switch (type) { case "min": getDockByType("outline").toggleModel("outline"); break; } break; } else if (target.isSameNode(this.headerElement.nextElementSibling) || target.classList.contains("block__icons")) { openFileById({ app: options.app, id: this.blockId, afterOpen: (model: Editor) => { if (model) { if (this.isPreview) { model.editor.protyle.preview.element.querySelector(".b3-typography").scrollTop = 0; } else { goHome(model.editor.protyle); } } } }); isFocus = false; break; } target = target.parentElement; } if (isFocus) { if (this.type === "local") { setPanelFocus(options.tab.panelElement.parentElement.parentElement); } else { setPanelFocus(options.tab.panelElement); } } }); this.bindSort(); fetchPost("/api/outline/getDocOutline", { id: this.blockId, preview: this.isPreview }, response => { this.update(response); }); } private bindSort() { this.element.addEventListener("mousedown", (event: MouseEvent) => { const item = hasClosestByClassName(event.target as HTMLElement, "b3-list-item"); if (!item || item.tagName !== "LI" || this.element.getAttribute("data-loading") === "true") { return; } const documentSelf = document; documentSelf.ondragstart = () => false; let ghostElement: HTMLElement; let selectItem: HTMLElement; let editor: IProtyle; getAllModels().editor.find(editItem => { if (editItem.editor.protyle.block.rootID === this.blockId) { editor = editItem.editor.protyle; return true; } }); documentSelf.onmousemove = (moveEvent: MouseEvent) => { if (!editor || editor.disabled || Math.abs(moveEvent.clientY - event.clientY) < 3 && Math.abs(moveEvent.clientX - event.clientX) < 3) { return; } moveEvent.preventDefault(); moveEvent.stopPropagation(); if (!ghostElement) { item.style.opacity = "0.38"; ghostElement = item.cloneNode(true) as HTMLElement; this.element.append(ghostElement); ghostElement.setAttribute("id", "dragGhost"); ghostElement.firstElementChild.setAttribute("style", "padding-left:4px"); ghostElement.setAttribute("style", `border-radius: var(--b3-border-radius);background-color: var(--b3-list-hover);position: fixed; top: ${event.clientY}px; left: ${event.clientX}px; z-index:999997;`); } ghostElement.style.top = moveEvent.clientY + "px"; ghostElement.style.left = moveEvent.clientX + "px"; selectItem = hasClosestByClassName(moveEvent.target as HTMLElement, "b3-list-item") as HTMLElement; if (!selectItem || selectItem.tagName !== "LI" || selectItem.isSameNode(item) || selectItem.style.position === "fixed" || !this.element.contains(selectItem)) { return; } this.element.querySelectorAll(".dragover__top, .dragover__bottom, .dragover").forEach(item => { item.classList.remove("dragover__top", "dragover__bottom", "dragover"); }); const selectRect = selectItem.getBoundingClientRect(); if (moveEvent.clientY > selectRect.bottom - 10) { selectItem.classList.add("dragover__bottom"); } else if (moveEvent.clientY < selectRect.top + 10) { selectItem.classList.add("dragover__top"); } else { selectItem.classList.add("dragover"); } }; documentSelf.onmouseup = () => { documentSelf.onmousemove = null; documentSelf.onmouseup = null; documentSelf.ondragstart = null; documentSelf.onselectstart = null; documentSelf.onselect = null; ghostElement?.remove(); item.style.opacity = ""; if (!selectItem) { selectItem = this.element.querySelector(".dragover__top, .dragover__bottom, .dragover"); } let hasChange = true; if (selectItem && selectItem.className.indexOf("dragover") > -1 && editor) { let previousID; let parentID; const undoPreviousID = (item.previousElementSibling && item.previousElementSibling.tagName === "UL") ? item.previousElementSibling.previousElementSibling.getAttribute("data-node-id") : item.previousElementSibling?.getAttribute("data-node-id"); const undoParentID = item.parentElement.previousElementSibling?.getAttribute("data-node-id"); if (selectItem.classList.contains("dragover")) { parentID = selectItem.getAttribute("data-node-id"); if (selectItem.nextElementSibling && selectItem.nextElementSibling.tagName === "UL") { selectItem.nextElementSibling.insertAdjacentElement("afterbegin", item); } else { selectItem.insertAdjacentHTML("afterend", ``); item.remove(); } } else if (selectItem.classList.contains("dragover__top")) { parentID = selectItem.parentElement.previousElementSibling?.getAttribute("data-node-id"); if (selectItem.previousElementSibling && selectItem.previousElementSibling.tagName === "UL") { previousID = selectItem.previousElementSibling.previousElementSibling.getAttribute("data-node-id"); } else { previousID = selectItem.previousElementSibling?.getAttribute("data-node-id"); } if (previousID === item.dataset.nodeId || parentID === item.dataset.nodeId) { hasChange = false; } else { selectItem.before(item); } } else if (selectItem.classList.contains("dragover__bottom")) { previousID = selectItem.getAttribute("data-node-id"); if (previousID === item.previousElementSibling?.getAttribute("data-node-id")) { hasChange = false; } else { selectItem.after(item); } } if (hasChange) { this.element.setAttribute("data-loading", "true"); transaction(editor, [{ action: "moveOutlineHeading", id: item.dataset.nodeId, previousID, parentID, }], [{ action: "moveOutlineHeading", id: item.dataset.nodeId, previousID: undoPreviousID, parentID: undoParentID, }]); // https://github.com/siyuan-note/siyuan/issues/10828#issuecomment-2044099675 editor.wysiwyg.element.querySelectorAll('[data-type="NodeHeading"] [contenteditable="true"][spellcheck]').forEach(item => { item.setAttribute("contenteditable", "false"); }); return true; } } this.element.querySelectorAll(".dragover__top, .dragover__bottom, .dragover").forEach(item => { item.classList.remove("dragover__top", "dragover__bottom", "dragover"); }); }; }); } public updateDocTitle(ial?: IObject) { const docTitleElement = this.headerElement.nextElementSibling as HTMLElement; if (this.type === "pin") { if (ial) { let iconHTML = `${unicode2Emoji(ial.icon || window.siyuan.storage[Constants.LOCAL_IMAGES].file, "b3-list-item__graphic", true)}`; if (ial.icon === Constants.ZWSP && docTitleElement.firstElementChild) { iconHTML = docTitleElement.firstElementChild.outerHTML; } docTitleElement.innerHTML = `${iconHTML} ${escapeHtml(ial.title)}`; docTitleElement.setAttribute("title", ial.title); docTitleElement.classList.remove("fn__none"); } else { docTitleElement.classList.add("fn__none"); } } else { docTitleElement.classList.add("fn__none"); } } private onTransaction(data: IWebSocketData) { if (data.data.rootID !== this.blockId) { return; } let needReload = false; const ops = data.data.sources[0]; ops.doOperations.find((item: IOperation) => { if (item.action === "update" && (this.element.querySelector(`.b3-list-item[data-node-id="${item.id}"]`) || item.data.indexOf('data-type="NodeHeading"') > -1)) { needReload = true; return true; } else if (item.action === "insert" && item.data.indexOf('data-type="NodeHeading"') > -1) { needReload = true; return true; } else if (item.action === "delete" || item.action === "move") { needReload = true; return true; } }); if (!needReload && ops.undoOperations) { ops.undoOperations.find((item: IOperation) => { if (item.action === "update" && item.data.indexOf('data-type="NodeHeading"') > -1) { needReload = true; return true; } }); } if (needReload) { fetchPost("/api/outline/getDocOutline", { id: this.blockId, preview: this.isPreview }, response => { // 文档切换后不再更新原有推送 https://github.com/siyuan-note/siyuan/issues/13409 if (data.data.rootID !== this.blockId) { return; } this.update(response); // https://github.com/siyuan-note/siyuan/issues/8372 if (getSelection().rangeCount > 0) { const blockElement = hasClosestBlock(getSelection().getRangeAt(0).startContainer); if (blockElement && blockElement.getAttribute("data-type") === "NodeHeading") { this.setCurrent(blockElement); } } }); } } public setCurrent(nodeElement: HTMLElement) { if (!nodeElement) { return; } if (nodeElement.getAttribute("data-type") === "NodeHeading") { this.setCurrentById(nodeElement.getAttribute("data-node-id")); } else { let previousElement = getPreviousBlock(nodeElement); while (previousElement) { if (previousElement.getAttribute("data-type") === "NodeHeading") { break; } else { previousElement = getPreviousBlock(previousElement); } } if (previousElement) { this.setCurrentById(previousElement.getAttribute("data-node-id")); } else { fetchPost("/api/block/getBlockBreadcrumb", { id: nodeElement.getAttribute("data-node-id"), excludeTypes: [] }, (response) => { response.data.reverse().find((item: IBreadcrumb) => { if (item.type === "NodeHeading") { this.setCurrentById(item.id); return true; } }); }); } } } public setCurrentByPreview(nodeElement: Element) { if (!nodeElement) { return; } let previousElement = nodeElement; while (previousElement && !previousElement.classList.contains("b3-typography")) { if (["H1", "H2", "H3", "H4", "H5", "H6"].includes(previousElement.tagName)) { break; } else { previousElement = previousElement.previousElementSibling || previousElement.parentElement; } } if (previousElement && previousElement.id) { this.setCurrentById(previousElement.id); } } private setCurrentById(id: string) { this.element.querySelectorAll(".b3-list-item.b3-list-item--focus").forEach(item => { item.classList.remove("b3-list-item--focus"); }); let currentElement = this.element.querySelector(`.b3-list-item[data-node-id="${id}"]`) as HTMLElement; while (currentElement && currentElement.clientHeight === 0) { currentElement = currentElement.parentElement.previousElementSibling as HTMLElement; } if (currentElement) { currentElement.classList.add("b3-list-item--focus"); this.element.scrollTop = currentElement.offsetTop - this.element.clientHeight / 2 - 30; } } public update(data: IWebSocketData, callbackId?: string) { let currentElement = this.element.querySelector(".b3-list-item--focus"); let currentId; if (currentElement) { currentId = currentElement.getAttribute("data-node-id"); } if (!this.isPreview && this.openNodes[this.blockId]) { this.openNodes[this.blockId] = this.tree.getExpandIds(); } if (typeof callbackId !== "undefined") { this.blockId = callbackId; } this.tree.updateData(data.data); if (!this.isPreview && this.openNodes[this.blockId] && !this.headerElement.querySelector('[data-type="expand"]').classList.contains("block__icon--active")) { this.tree.setExpandIds(this.openNodes[this.blockId]); } else { this.tree.expandAll(); if (!this.isPreview) { this.openNodes[this.blockId] = this.tree.getExpandIds(); } } if (this.isPreview) { this.tree.element.querySelectorAll(".popover__block").forEach(item => { item.classList.remove("popover__block"); }); } if (currentId) { currentElement = this.element.querySelector(`[data-node-id="${currentId}"]`); if (currentElement) { currentElement.classList.add("b3-list-item--focus"); } } this.element.removeAttribute("data-loading"); } }