mirror of
https://github.com/siyuan-note/siyuan.git
synced 2025-05-17 01:21:14 +08:00
510 lines
24 KiB
TypeScript
510 lines
24 KiB
TypeScript
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 = `<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="padding: 3px 0 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) {
|
|
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", `<ul>${item.outerHTML}</ul>`);
|
|
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}
|
|
<span class="b3-list-item__text">${escapeHtml(ial.title)}</span>`;
|
|
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");
|
|
}
|
|
}
|