siyuan/app/src/layout/dock/Outline.ts

420 lines
19 KiB
TypeScript
Raw Normal View History

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";
2023-06-18 21:18:14 +08:00
import {hasClosestBlock, hasClosestByClassName, hasTopClosestByClassName} from "../../protyle/util/hasClosest";
import {setStorageVal, updateHotkeyTip} from "../../protyle/util/compatibility";
2023-12-12 12:09:31 +08:00
import {openFileById} from "../../editor/util";
import {Constants} from "../../constants";
import {escapeHtml} from "../../util/escape";
2022-05-31 17:51:34 +08:00
import {unicode2Emoji} from "../../emoji";
import {onGet} from "../../protyle/util/onGet";
import {getPreviousBlock} from "../../protyle/wysiwyg/getBlock";
import {App} from "../../index";
2023-12-12 12:09:31 +08:00
import {checkFold} from "../../util/noRelyPCFunction";
import {transaction} from "../../protyle/wysiwyg/transaction";
export class Outline extends Model {
public tree: Tree;
public element: HTMLElement;
2022-05-31 17:51:34 +08:00
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,
2023-06-18 22:40:44 +08:00
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 "transactions":
this.onTransaction(data);
break;
case "rename":
if (this.type === "local" && this.blockId === data.data.id) {
this.parent.updateTitle(data.data.title);
} else {
2022-05-31 17:51:34 +08:00
this.updateDocTitle({
title: data.data.title,
icon: Constants.ZWSP
2022-05-31 17:51:34 +08:00
});
}
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;
}
}
}
});
2023-06-18 22:40:44 +08:00
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 = `<div class="block__icons">
<div class="block__logo">
<svg class="block__logoicon"><use xlink:href="#iconAlignCenter"></use></svg>${window.siyuan.languages.outline}
</div>
<span class="fn__flex-1 fn__space"></span>
<span data-type="expand" class="block__icon b3-tooltips b3-tooltips__sw${window.siyuan.storage[Constants.LOCAL_OUTLINE].keepExpand ? " block__icon--active" : ""}" aria-label="${window.siyuan.languages.stickOpen} ${updateHotkeyTip(window.siyuan.config.keymap.editor.general.expand.custom)}">
<svg><use xlink:href="#iconExpand"></use></svg>
</span>
<span class="fn__space"></span>
<span data-type="collapse" class="block__icon b3-tooltips b3-tooltips__sw" aria-label="${window.siyuan.languages.collapse} ${updateHotkeyTip(window.siyuan.config.keymap.editor.general.collapse.custom)}">
<svg><use xlink:href="#iconContract"></use></svg>
</span>
<span class="${this.type === "local" ? "fn__none " : ""}fn__space"></span>
<span data-type="min" class="${this.type === "local" ? "fn__none " : ""}block__icon b3-tooltips b3-tooltips__sw" aria-label="${window.siyuan.languages.min} ${updateHotkeyTip(window.siyuan.config.keymap.general.closeTab.custom)}"><svg><use xlink:href='#iconMin'></use></svg></span>
</div>
<div class="b3-list-item fn__none"></div>
<div class="fn__flex-1" style="margin-bottom: 8px"></div>`;
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) {
2023-06-19 21:04:56 +08:00
const headElement = document.getElementById(id);
2023-06-18 22:40:44 +08:00
if (headElement) {
2023-06-19 21:04:56 +08:00
const tabElement = hasTopClosestByClassName(headElement, "protyle");
2023-06-18 22:40:44 +08:00
if (tabElement) {
2023-06-19 21:04:56 +08:00
const tab = getInstanceById(tabElement.getAttribute("data-id")) as Tab;
tab.parent.switchTab(tab.headElement);
2023-06-18 22:40:44 +08:00
}
headElement.scrollIntoView();
} else {
openFileById({
app: options.app,
2023-07-05 11:44:39 +08:00
id: this.blockId,
2023-06-18 22:40:44 +08:00
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],
});
2023-12-09 23:33:06 +08:00
});
}
},
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;
}
setStorageVal(Constants.LOCAL_OUTLINE, window.siyuan.storage[Constants.LOCAL_OUTLINE]);
this.tree.expandAll();
});
options.tab.panelElement.addEventListener("click", (event: MouseEvent & { target: HTMLElement }) => {
if (this.type === "local") {
setPanelFocus(options.tab.panelElement.parentElement.parentElement);
} else {
setPanelFocus(options.tab.panelElement);
}
let target = event.target as HTMLElement;
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")) {
getAllModels().editor.find(item => {
if (this.blockId === item.editor.protyle.block.rootID) {
if (item.editor.protyle.scroll.element.classList.contains("fn__none")) {
item.editor.protyle.contentElement.scrollTop = 0;
} else {
fetchPost("/api/filetree/getDoc", {
id: item.editor.protyle.block.rootID,
mode: 0,
size: window.siyuan.config.editor.dynamicLoadBlocks,
}, getResponse => {
onGet({
data: getResponse,
protyle: item.editor.protyle,
action: [Constants.CB_GET_FOCUS],
});
});
}
return true;
}
});
break;
}
target = target.parentElement;
}
});
this.bindSort();
2023-06-18 22:40:44 +08:00
if (this.isPreview) {
2023-07-05 11:44:39 +08:00
if (this.blockId) {
fetchPost("/api/export/preview", {
id: this.blockId,
}, response => {
response.data = response.data.outline;
this.update(response);
});
}
2023-06-18 22:40:44 +08:00
} else {
fetchPost("/api/outline/getDocOutline", {
id: this.blockId,
}, 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") {
return;
}
const documentSelf = document;
const ghostElement = item.cloneNode(true) as HTMLElement;
document.body.append(ghostElement);
ghostElement.firstElementChild.setAttribute("style", "padding-left:4px");
ghostElement.setAttribute("style", `opacity:.38;position: fixed; top: ${event.clientY}px; left: ${event.clientX}px; z-index:999997;`);
documentSelf.ondragstart = () => false;
documentSelf.onmousemove = (moveEvent: MouseEvent) => {
moveEvent.preventDefault();
moveEvent.stopPropagation();
ghostElement.style.top = moveEvent.clientY + "px";
ghostElement.style.left = moveEvent.clientX + "px";
};
documentSelf.onmouseup = () => {
documentSelf.onmousemove = null;
documentSelf.onmouseup = null;
documentSelf.ondragstart = null;
documentSelf.onselectstart = null;
documentSelf.onselect = null;
ghostElement.remove();
const selectItem = hasClosestByClassName(event.target as HTMLElement, "b3-list-item");
if (!selectItem || selectItem.tagName !== "LI") {
return;
}
getAllModels().editor.find(editItem => {
if (editItem.editor.protyle.block.rootID === this.blockId) {
transaction(editItem.editor.protyle, [{
action: "moveOutlineHeading",
id: item.dataset.blockId,
previousID: selectItem.previousElementSibling?.getAttribute("data-node-id"),
parentID: selectItem.parentElement.previousElementSibling?.getAttribute("data-node-id"),
}], [{
action: "moveOutlineHeading",
id: item.dataset.blockId,
previousID: item.previousElementSibling?.getAttribute("data-node-id"),
parentID: item.parentElement.previousElementSibling?.getAttribute("data-node-id"),
}]);
return true;
}
})
};
});
}
public updateDocTitle(ial?: IObject) {
2023-04-19 11:28:26 +08:00
const docTitleElement = this.headerElement.nextElementSibling as HTMLElement;
if (this.type === "pin") {
2022-05-31 17:51:34 +08:00
if (ial) {
let iconHTML = `${unicode2Emoji(ial.icon || Constants.SIYUAN_IMAGE_FILE, "b3-list-item__graphic", true)}`;
if (ial.icon === Constants.ZWSP && docTitleElement.firstElementChild) {
iconHTML = docTitleElement.firstElementChild.outerHTML;
2022-05-31 17:51:34 +08:00
}
docTitleElement.innerHTML = `${iconHTML}
2022-05-31 17:51:34 +08:00
<span class="b3-list-item__text">${escapeHtml(ial.title)}</span>`;
docTitleElement.setAttribute("title", ial.title);
2023-04-19 11:28:26 +08:00
docTitleElement.classList.remove("fn__none");
2022-05-31 17:51:34 +08:00
} else {
2023-04-19 11:28:26 +08:00
docTitleElement.classList.add("fn__none");
2022-05-31 17:51:34 +08:00
}
} else {
2023-04-19 11:28:26 +08:00
docTitleElement.classList.add("fn__none");
}
}
private onTransaction(data: IWebSocketData) {
if (this.isPreview) {
return;
}
let needReload = false;
data.data[0].doOperations.forEach((item: IOperation) => {
if ((item.action === "update" || item.action === "insert") &&
(item.data.indexOf('data-type="NodeHeading"') > -1 || item.data.indexOf(`<div contenteditable="true" spellcheck="${window.siyuan.config.editor.spellcheck}"><wbr></div>`) > -1)) {
needReload = true;
} else if (item.action === "delete" || item.action === "move") {
needReload = true;
}
});
if (data.data[0].undoOperations) {
data.data[0].undoOperations.forEach((item: IOperation) => {
if (item.action === "update" && item.data.indexOf('data-type="NodeHeading"') > -1) {
needReload = true;
}
});
}
if (needReload) {
fetchPost("/api/outline/getDocOutline", {
id: this.blockId,
}, response => {
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") {
2023-03-03 10:39:58 +08:00
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) {
2023-03-03 10:39:58 +08:00
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;
}
});
});
}
}
}
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");
2023-06-19 21:04:56 +08:00
});
}
if (currentId) {
currentElement = this.element.querySelector(`[data-node-id="${currentId}"]`);
if (currentElement) {
currentElement.classList.add("b3-list-item--focus");
}
}
}
}