2025-10-15 20:32:13 +08:00
|
|
|
|
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, updateHotkeyAfterTip, updateHotkeyTip} from "../../protyle/util/compatibility";
|
|
|
|
|
|
import {openFileById} from "../../editor/util";
|
|
|
|
|
|
import {Constants} from "../../constants";
|
|
|
|
|
|
import {MenuItem} from "../../menus/Menu";
|
|
|
|
|
|
import {escapeAttr, 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, turnsIntoTransaction} from "../../protyle/wysiwyg/transaction";
|
|
|
|
|
|
import {goHome} from "../../protyle/wysiwyg/commonHotkey";
|
|
|
|
|
|
import {Editor} from "../../editor";
|
|
|
|
|
|
import {writeText, isInAndroid, isInHarmony} from "../../protyle/util/compatibility";
|
|
|
|
|
|
import {mathRender} from "../../protyle/render/mathRender";
|
2025-10-16 11:09:45 +08:00
|
|
|
|
import {genEmptyElement} from "../../block/util";
|
|
|
|
|
|
import {focusBlock} from "../../protyle/util/selection";
|
2022-05-26 15:18:53 +08:00
|
|
|
|
|
|
|
|
|
|
export class Outline extends Model {
|
2022-12-07 18:27:24 +08:00
|
|
|
|
public tree: Tree;
|
2022-05-26 15:18:53 +08:00
|
|
|
|
public element: HTMLElement;
|
2022-05-31 17:51:34 +08:00
|
|
|
|
public headerElement: HTMLElement;
|
2022-05-26 15:18:53 +08:00
|
|
|
|
public type: "pin" | "local";
|
|
|
|
|
|
public blockId: string;
|
2023-06-18 00:36:51 +08:00
|
|
|
|
public isPreview: boolean;
|
2025-10-15 10:01:47 +08:00
|
|
|
|
private preFilterExpandIds: string[] | null = null;
|
2022-05-26 15:18:53 +08:00
|
|
|
|
|
|
|
|
|
|
constructor(options: {
|
2023-05-18 19:27:21 +08:00
|
|
|
|
app: App,
|
2022-05-26 15:18:53 +08:00
|
|
|
|
tab: Tab,
|
|
|
|
|
|
blockId: string,
|
2023-06-18 22:40:44 +08:00
|
|
|
|
type: "pin" | "local",
|
|
|
|
|
|
isPreview: boolean
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}) {
|
|
|
|
|
|
super({
|
2023-05-18 19:27:21 +08:00
|
|
|
|
app: options.app,
|
2022-05-26 15:18:53 +08:00
|
|
|
|
id: options.tab.id,
|
|
|
|
|
|
callback() {
|
|
|
|
|
|
if (this.type === "local") {
|
2025-10-15 20:32:13 +08:00
|
|
|
|
fetchPost("/api/block/checkBlockExist", {id: this.blockId}, existResponse => {
|
2022-05-26 15:18:53 +08:00
|
|
|
|
if (!existResponse.data) {
|
|
|
|
|
|
this.parent.parent.removeTab(this.parent.id);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
msgCallback(data) {
|
|
|
|
|
|
if (data) {
|
|
|
|
|
|
switch (data.cmd) {
|
2024-04-08 12:32:47 +08:00
|
|
|
|
case "savedoc":
|
2022-05-26 15:18:53 +08:00
|
|
|
|
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({
|
2022-06-06 15:20:01 +08:00
|
|
|
|
title: data.data.title,
|
|
|
|
|
|
icon: Constants.ZWSP
|
2022-05-31 17:51:34 +08:00
|
|
|
|
});
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
case "unmount":
|
|
|
|
|
|
if (this.type === "local") {
|
2025-10-15 20:32:13 +08:00
|
|
|
|
fetchPost("/api/block/checkBlockExist", {id: this.blockId}, existResponse => {
|
2022-05-26 15:18:53 +08:00
|
|
|
|
if (!existResponse.data) {
|
|
|
|
|
|
this.parent.parent.removeTab(this.parent.id);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
2022-11-03 22:47:19 +08:00
|
|
|
|
case "removeDoc":
|
|
|
|
|
|
if (data.data.ids.includes(this.blockId) && this.type === "local") {
|
|
|
|
|
|
this.parent.parent.removeTab(this.parent.id);
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2023-06-18 22:40:44 +08:00
|
|
|
|
this.isPreview = options.isPreview;
|
2022-05-26 15:18:53 +08:00
|
|
|
|
this.blockId = options.blockId;
|
|
|
|
|
|
this.type = options.type;
|
|
|
|
|
|
options.tab.panelElement.classList.add("fn__flex-column", "file-tree", "sy__outline");
|
2025-10-15 20:32:13 +08:00
|
|
|
|
options.tab.panelElement.innerHTML = `<div class="block__icons fn__hidescrollbar">
|
2022-05-26 15:18:53 +08:00
|
|
|
|
<div class="block__logo">
|
2024-01-19 23:59:34 +08:00
|
|
|
|
<svg class="block__logoicon"><use xlink:href="#iconAlignCenter"></use></svg>${window.siyuan.languages.outline}
|
2022-05-26 15:18:53 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<span class="fn__flex-1 fn__space"></span>
|
2025-10-15 10:01:47 +08:00
|
|
|
|
<input class="b3-text-field search__label fn__none fn__size200" placeholder="${window.siyuan.languages.filterKeywordEnter}" />
|
2025-10-15 20:32:13 +08:00
|
|
|
|
<span data-type="search" class="block__icon ariaLabel" aria-label="${window.siyuan.languages.filter}">
|
|
|
|
|
|
<svg><use xlink:href='#iconFilter'></use></svg>
|
2022-05-26 15:18:53 +08:00
|
|
|
|
</span>
|
|
|
|
|
|
<span class="fn__space"></span>
|
2025-10-15 20:32:13 +08:00
|
|
|
|
<span data-type="keepCurrentExpand" class="block__icon ariaLabel${window.siyuan.storage[Constants.LOCAL_OUTLINE].keepCurrentExpand ? " block__icon--active" : ""}" aria-label="${window.siyuan.languages.outlineKeepCurrentExpand}">
|
2025-10-15 10:01:47 +08:00
|
|
|
|
<svg><use xlink:href="#iconFocus"></use></svg>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span class="fn__space"></span>
|
2025-10-15 20:32:13 +08:00
|
|
|
|
<span data-type="expandLevel" class="block__icon ariaLabel" aria-label="${window.siyuan.languages.expandLevel}">
|
2025-10-15 10:01:47 +08:00
|
|
|
|
<svg><use xlink:href="#iconList"></use></svg>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span class="fn__space"></span>
|
2025-10-15 20:32:13 +08:00
|
|
|
|
<span data-type="expand" class="block__icon ariaLabel" aria-label="${window.siyuan.languages.expandAll}${updateHotkeyAfterTip(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 ariaLabel" aria-label="${window.siyuan.languages.foldAll}${updateHotkeyAfterTip(window.siyuan.config.keymap.editor.general.collapse.custom)}">
|
2022-05-26 15:18:53 +08:00
|
|
|
|
<svg><use xlink:href="#iconContract"></use></svg>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span class="${this.type === "local" ? "fn__none " : ""}fn__space"></span>
|
2025-10-15 20:32:13 +08:00
|
|
|
|
<span data-type="min" class="${this.type === "local" ? "fn__none " : ""}block__icon ariaLabel" aria-label="${window.siyuan.languages.min}${updateHotkeyAfterTip(window.siyuan.config.keymap.general.closeTab.custom)}">
|
|
|
|
|
|
<svg><use xlink:href='#iconMin'></use></svg>
|
|
|
|
|
|
</span>
|
2022-05-26 15:18:53 +08:00
|
|
|
|
</div>
|
2023-04-18 18:57:20 +08:00
|
|
|
|
<div class="b3-list-item fn__none"></div>
|
2024-03-29 11:03:53 +08:00
|
|
|
|
<div class="fn__flex-1" style="padding: 3px 0 8px"></div>`;
|
2025-10-15 20:32:13 +08:00
|
|
|
|
this.element = options.tab.panelElement.lastElementChild as HTMLElement;
|
2022-05-26 15:18:53 +08:00
|
|
|
|
this.headerElement = options.tab.panelElement.firstElementChild as HTMLElement;
|
2025-10-15 20:32:13 +08:00
|
|
|
|
const inputElement = this.headerElement.querySelector("input.b3-text-field.search__label") as HTMLInputElement;
|
|
|
|
|
|
inputElement.addEventListener("blur", () => {
|
|
|
|
|
|
inputElement.classList.add("fn__none");
|
|
|
|
|
|
const filterIconElement = inputElement.nextElementSibling as HTMLElement; // search 图标
|
|
|
|
|
|
const value = inputElement.value;
|
|
|
|
|
|
if (value) {
|
|
|
|
|
|
filterIconElement.classList.add("block__icon--active");
|
|
|
|
|
|
filterIconElement.setAttribute("aria-label", window.siyuan.languages.filter + " " + escapeAttr(value));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
filterIconElement.classList.remove("block__icon--active");
|
|
|
|
|
|
filterIconElement.setAttribute("aria-label", window.siyuan.languages.filter);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (inputElement.dataset.value !== value) {
|
|
|
|
|
|
this.setFilter();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
inputElement.addEventListener("keydown", (event: KeyboardEvent) => {
|
|
|
|
|
|
if (!event.isComposing && event.key === "Enter") {
|
|
|
|
|
|
inputElement.dataset.value = inputElement.value;
|
|
|
|
|
|
this.setFilter();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2022-05-26 15:18:53 +08:00
|
|
|
|
this.tree = new Tree({
|
2025-10-15 20:32:13 +08:00
|
|
|
|
element: this.element,
|
2022-05-26 15:18:53 +08:00
|
|
|
|
data: null,
|
|
|
|
|
|
click: (element: HTMLElement) => {
|
2022-08-04 00:11:11 +08:00
|
|
|
|
const id = element.getAttribute("data-node-id");
|
2023-06-18 00:36:51 +08:00
|
|
|
|
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",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2023-06-18 00:36:51 +08:00
|
|
|
|
} else {
|
2023-12-09 23:32:33 +08:00
|
|
|
|
checkFold(id, (zoomIn) => {
|
2023-06-18 00:36:51 +08:00
|
|
|
|
openFileById({
|
|
|
|
|
|
app: options.app,
|
|
|
|
|
|
id,
|
2025-05-06 13:20:58 +08:00
|
|
|
|
action: zoomIn ? [Constants.CB_GET_FOCUS, Constants.CB_GET_ALL, Constants.CB_GET_HTML, Constants.CB_GET_OUTLINE] : [Constants.CB_GET_FOCUS, Constants.CB_GET_OUTLINE, Constants.CB_GET_SETID, Constants.CB_GET_CONTEXT, Constants.CB_GET_HTML],
|
2023-06-18 00:36:51 +08:00
|
|
|
|
});
|
2023-12-09 23:33:06 +08:00
|
|
|
|
});
|
2023-06-18 00:36:51 +08:00
|
|
|
|
}
|
2023-10-15 22:13:55 +08:00
|
|
|
|
},
|
2025-10-15 20:32:13 +08:00
|
|
|
|
ctrlClick: (element: HTMLElement, event) => {
|
|
|
|
|
|
const arrowElement = hasClosestByClassName(event.target as Element, "b3-list-item__toggle");
|
|
|
|
|
|
if (arrowElement && !arrowElement.classList.contains("fn__hidden")) {
|
|
|
|
|
|
this.collapseChildren(element);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2023-10-15 22:13:55 +08:00
|
|
|
|
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,
|
|
|
|
|
|
});
|
2025-10-15 10:01:47 +08:00
|
|
|
|
},
|
2025-10-15 20:32:13 +08:00
|
|
|
|
altClick: (element: HTMLElement, event: MouseEvent) => {
|
|
|
|
|
|
// alt 点击箭头,切换同层级的所有标题的展开/折叠状态
|
|
|
|
|
|
const arrowElement = hasClosestByClassName(event.target as HTMLElement, "b3-list-item__toggle");
|
|
|
|
|
|
if (arrowElement) {
|
|
|
|
|
|
this.collapseSameLevel(element);
|
2025-10-15 10:01:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
rightClick: (element: HTMLElement, event: MouseEvent) => {
|
|
|
|
|
|
this.showContextMenu(element, event);
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
// 为了快捷键的 dispatch
|
|
|
|
|
|
options.tab.panelElement.querySelector('[data-type="collapse"]').addEventListener("click", () => {
|
2022-05-28 22:43:27 +08:00
|
|
|
|
this.tree.collapseAll();
|
2022-05-26 15:18:53 +08:00
|
|
|
|
});
|
2025-10-15 10:01:47 +08:00
|
|
|
|
|
|
|
|
|
|
// 普通的全部展开按钮
|
|
|
|
|
|
options.tab.panelElement.querySelector('[data-type="expand"]').addEventListener("click", () => {
|
|
|
|
|
|
this.tree.expandAll();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 保持当前标题展开功能
|
|
|
|
|
|
options.tab.panelElement.querySelector('[data-type="keepCurrentExpand"]').addEventListener("click", (event: MouseEvent & {
|
2023-03-01 15:17:45 +08:00
|
|
|
|
target: Element
|
|
|
|
|
|
}) => {
|
2022-05-26 15:18:53 +08:00
|
|
|
|
const iconElement = hasClosestByClassName(event.target, "block__icon");
|
|
|
|
|
|
if (!iconElement) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (iconElement.classList.contains("block__icon--active")) {
|
|
|
|
|
|
iconElement.classList.remove("block__icon--active");
|
2025-10-15 10:01:47 +08:00
|
|
|
|
window.siyuan.storage[Constants.LOCAL_OUTLINE].keepCurrentExpand = false;
|
2022-05-26 15:18:53 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
iconElement.classList.add("block__icon--active");
|
2025-10-15 10:01:47 +08:00
|
|
|
|
window.siyuan.storage[Constants.LOCAL_OUTLINE].keepCurrentExpand = true;
|
2025-10-15 20:32:13 +08:00
|
|
|
|
let focusElement;
|
|
|
|
|
|
getAllModels().editor.find(editItem => {
|
|
|
|
|
|
if (editItem.editor.protyle.block.rootID === this.blockId) {
|
|
|
|
|
|
const selection = getSelection();
|
|
|
|
|
|
if (selection.rangeCount > 0) {
|
|
|
|
|
|
const blockElement = hasClosestBlock(selection.getRangeAt(0).startContainer);
|
|
|
|
|
|
if (blockElement) {
|
|
|
|
|
|
focusElement = blockElement;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
if (focusElement) {
|
|
|
|
|
|
this.setCurrent(focusElement);
|
|
|
|
|
|
}
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
2025-10-15 10:01:47 +08:00
|
|
|
|
// 保存keepCurrentExpand状态到localStorage
|
2024-02-19 17:12:59 +08:00
|
|
|
|
setStorageVal(Constants.LOCAL_OUTLINE, window.siyuan.storage[Constants.LOCAL_OUTLINE]);
|
2022-05-26 15:18:53 +08:00
|
|
|
|
});
|
|
|
|
|
|
options.tab.panelElement.addEventListener("click", (event: MouseEvent & { target: HTMLElement }) => {
|
|
|
|
|
|
let target = event.target as HTMLElement;
|
2025-10-15 20:32:13 +08:00
|
|
|
|
if (target.tagName === "INPUT") {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2024-09-14 17:05:06 +08:00
|
|
|
|
let isFocus = true;
|
2022-05-26 15:18:53 +08:00
|
|
|
|
while (target && !target.isEqualNode(options.tab.panelElement)) {
|
|
|
|
|
|
if (target.classList.contains("block__icon")) {
|
|
|
|
|
|
const type = target.getAttribute("data-type");
|
|
|
|
|
|
switch (type) {
|
|
|
|
|
|
case "min":
|
2025-01-11 12:30:21 +08:00
|
|
|
|
getDockByType("outline").toggleModel("outline", false, true);
|
2022-05-26 15:18:53 +08:00
|
|
|
|
break;
|
2025-10-15 10:01:47 +08:00
|
|
|
|
case "search":
|
2025-10-15 20:32:13 +08:00
|
|
|
|
inputElement.classList.remove("fn__none");
|
|
|
|
|
|
inputElement.select();
|
2025-10-15 10:01:47 +08:00
|
|
|
|
break;
|
|
|
|
|
|
case "expandLevel":
|
2025-10-15 20:32:13 +08:00
|
|
|
|
this.showExpandLevelMenu(target);
|
2025-10-15 10:01:47 +08:00
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
|
break;
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
2022-06-03 11:11:33 +08:00
|
|
|
|
break;
|
2025-07-23 13:08:38 +08:00
|
|
|
|
} else if (this.blockId && (target === this.headerElement.nextElementSibling || target.classList.contains("block__icons"))) {
|
2024-09-14 17:05:06 +08:00
|
|
|
|
openFileById({
|
|
|
|
|
|
app: options.app,
|
|
|
|
|
|
id: this.blockId,
|
2024-09-18 11:34:41 +08:00
|
|
|
|
afterOpen: (model: Editor) => {
|
2024-09-14 17:05:06 +08:00
|
|
|
|
if (model) {
|
2024-09-15 17:14:19 +08:00
|
|
|
|
if (this.isPreview) {
|
|
|
|
|
|
model.editor.protyle.preview.element.querySelector(".b3-typography").scrollTop = 0;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
goHome(model.editor.protyle);
|
|
|
|
|
|
}
|
2022-06-28 18:03:03 +08:00
|
|
|
|
}
|
2022-06-03 11:11:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
2024-09-14 17:05:06 +08:00
|
|
|
|
isFocus = false;
|
2022-06-03 11:11:33 +08:00
|
|
|
|
break;
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
target = target.parentElement;
|
|
|
|
|
|
}
|
2024-09-14 17:05:06 +08:00
|
|
|
|
if (isFocus) {
|
|
|
|
|
|
if (this.type === "local") {
|
|
|
|
|
|
setPanelFocus(options.tab.panelElement.parentElement.parentElement);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setPanelFocus(options.tab.panelElement);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2022-05-26 15:18:53 +08:00
|
|
|
|
});
|
2024-03-29 10:08:58 +08:00
|
|
|
|
this.bindSort();
|
2024-09-18 11:34:41 +08:00
|
|
|
|
|
|
|
|
|
|
fetchPost("/api/outline/getDocOutline", {
|
|
|
|
|
|
id: this.blockId,
|
|
|
|
|
|
preview: this.isPreview
|
|
|
|
|
|
}, response => {
|
|
|
|
|
|
this.update(response);
|
2025-10-15 10:01:47 +08:00
|
|
|
|
// 初始化时从新的存储恢复折叠状态
|
|
|
|
|
|
if (!this.isPreview) {
|
|
|
|
|
|
fetchPost("/api/storage/getOutlineStorage", {
|
|
|
|
|
|
docID: this.blockId
|
|
|
|
|
|
}, storageResponse => {
|
|
|
|
|
|
const storageData = storageResponse.data;
|
|
|
|
|
|
if (storageData && storageData.expandIds) {
|
|
|
|
|
|
this.tree.setExpandIds(storageData.expandIds);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2024-09-18 11:34:41 +08:00
|
|
|
|
});
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-03-29 10:08:58 +08:00
|
|
|
|
private bindSort() {
|
|
|
|
|
|
this.element.addEventListener("mousedown", (event: MouseEvent) => {
|
|
|
|
|
|
const item = hasClosestByClassName(event.target as HTMLElement, "b3-list-item");
|
2024-04-10 18:35:45 +08:00
|
|
|
|
if (!item || item.tagName !== "LI" || this.element.getAttribute("data-loading") === "true") {
|
2024-03-29 10:08:58 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const documentSelf = document;
|
|
|
|
|
|
documentSelf.ondragstart = () => false;
|
2024-04-03 16:04:13 +08:00
|
|
|
|
let ghostElement: HTMLElement;
|
2024-03-29 11:03:53 +08:00
|
|
|
|
let selectItem: HTMLElement;
|
2024-04-09 08:24:34 +08:00
|
|
|
|
let editor: IProtyle;
|
2024-04-08 17:28:56 +08:00
|
|
|
|
getAllModels().editor.find(editItem => {
|
|
|
|
|
|
if (editItem.editor.protyle.block.rootID === this.blockId) {
|
|
|
|
|
|
editor = editItem.editor.protyle;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-01-11 12:22:44 +08:00
|
|
|
|
const contentRect = this.element.getBoundingClientRect();
|
2024-03-29 10:08:58 +08:00
|
|
|
|
documentSelf.onmousemove = (moveEvent: MouseEvent) => {
|
2024-05-25 17:29:21 +08:00
|
|
|
|
if (!editor || editor.disabled || Math.abs(moveEvent.clientY - event.clientY) < 3 &&
|
|
|
|
|
|
Math.abs(moveEvent.clientX - event.clientX) < 3) {
|
2024-04-02 23:34:30 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2024-03-29 10:08:58 +08:00
|
|
|
|
moveEvent.preventDefault();
|
|
|
|
|
|
moveEvent.stopPropagation();
|
2024-04-02 23:50:22 +08:00
|
|
|
|
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;`);
|
|
|
|
|
|
}
|
2024-03-29 10:08:58 +08:00
|
|
|
|
ghostElement.style.top = moveEvent.clientY + "px";
|
|
|
|
|
|
ghostElement.style.left = moveEvent.clientX + "px";
|
2025-01-11 12:22:44 +08:00
|
|
|
|
if (!this.element.contains(moveEvent.target as Element)) {
|
|
|
|
|
|
this.element.querySelectorAll(".dragover__top, .dragover__bottom, .dragover, .dragover__current").forEach(item => {
|
|
|
|
|
|
item.classList.remove("dragover__top", "dragover__bottom", "dragover", "dragover__current");
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (moveEvent.clientY < contentRect.top + Constants.SIZE_SCROLL_TB || moveEvent.clientY > contentRect.bottom - Constants.SIZE_SCROLL_TB) {
|
|
|
|
|
|
this.element.scroll({
|
|
|
|
|
|
top: this.element.scrollTop + (moveEvent.clientY < contentRect.top + Constants.SIZE_SCROLL_TB ? -Constants.SIZE_SCROLL_STEP : Constants.SIZE_SCROLL_STEP),
|
|
|
|
|
|
behavior: "smooth"
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2024-03-29 11:03:53 +08:00
|
|
|
|
selectItem = hasClosestByClassName(moveEvent.target as HTMLElement, "b3-list-item") as HTMLElement;
|
2025-01-11 12:22:44 +08:00
|
|
|
|
if (!selectItem || selectItem.tagName !== "LI" || selectItem.style.position === "fixed") {
|
2024-03-29 11:03:53 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-01-11 12:22:44 +08:00
|
|
|
|
this.element.querySelectorAll(".dragover__top, .dragover__bottom, .dragover, .dragover__current").forEach(item => {
|
|
|
|
|
|
item.classList.remove("dragover__top", "dragover__bottom", "dragover", "dragover__current");
|
2024-03-29 11:03:53 +08:00
|
|
|
|
});
|
2025-07-23 12:21:59 +08:00
|
|
|
|
if (selectItem === item) {
|
2025-01-11 12:22:44 +08:00
|
|
|
|
selectItem.classList.add("dragover__current");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2024-03-29 11:03:53 +08:00
|
|
|
|
const selectRect = selectItem.getBoundingClientRect();
|
2025-02-08 19:23:58 +08:00
|
|
|
|
const dragHeight = selectRect.height * .2;
|
2025-02-05 10:57:43 +08:00
|
|
|
|
if (moveEvent.clientY > selectRect.bottom - dragHeight) {
|
2024-03-29 11:03:53 +08:00
|
|
|
|
selectItem.classList.add("dragover__bottom");
|
2025-02-05 10:57:43 +08:00
|
|
|
|
} else if (moveEvent.clientY < selectRect.top + dragHeight) {
|
2024-03-29 11:03:53 +08:00
|
|
|
|
selectItem.classList.add("dragover__top");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
selectItem.classList.add("dragover");
|
|
|
|
|
|
}
|
2024-03-29 10:08:58 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
documentSelf.onmouseup = () => {
|
|
|
|
|
|
documentSelf.onmousemove = null;
|
|
|
|
|
|
documentSelf.onmouseup = null;
|
|
|
|
|
|
documentSelf.ondragstart = null;
|
|
|
|
|
|
documentSelf.onselectstart = null;
|
|
|
|
|
|
documentSelf.onselect = null;
|
2024-04-08 17:28:56 +08:00
|
|
|
|
ghostElement?.remove();
|
2024-03-29 11:03:53 +08:00
|
|
|
|
item.style.opacity = "";
|
2024-03-29 21:29:57 +08:00
|
|
|
|
if (!selectItem) {
|
|
|
|
|
|
selectItem = this.element.querySelector(".dragover__top, .dragover__bottom, .dragover");
|
|
|
|
|
|
}
|
2024-05-25 17:29:21 +08:00
|
|
|
|
let hasChange = true;
|
2025-01-11 12:22:44 +08:00
|
|
|
|
if (selectItem && editor &&
|
|
|
|
|
|
(selectItem.classList.contains("dragover__top") || selectItem.classList.contains("dragover__bottom") || selectItem.classList.contains("dragover"))) {
|
2024-04-08 17:28:56 +08:00
|
|
|
|
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) {
|
2024-05-25 17:29:21 +08:00
|
|
|
|
hasChange = false;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
selectItem.before(item);
|
2024-03-29 20:48:02 +08:00
|
|
|
|
}
|
2024-04-08 17:28:56 +08:00
|
|
|
|
} else if (selectItem.classList.contains("dragover__bottom")) {
|
|
|
|
|
|
previousID = selectItem.getAttribute("data-node-id");
|
2024-05-25 17:29:21 +08:00
|
|
|
|
if (previousID === item.previousElementSibling?.getAttribute("data-node-id")) {
|
|
|
|
|
|
hasChange = false;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
selectItem.after(item);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (hasChange) {
|
|
|
|
|
|
this.element.setAttribute("data-loading", "true");
|
2025-10-15 10:01:47 +08:00
|
|
|
|
|
2024-05-25 17:29:21 +08:00
|
|
|
|
transaction(editor, [{
|
|
|
|
|
|
action: "moveOutlineHeading",
|
|
|
|
|
|
id: item.dataset.nodeId,
|
|
|
|
|
|
previousID,
|
|
|
|
|
|
parentID,
|
|
|
|
|
|
}], [{
|
|
|
|
|
|
action: "moveOutlineHeading",
|
|
|
|
|
|
id: item.dataset.nodeId,
|
|
|
|
|
|
previousID: undoPreviousID,
|
|
|
|
|
|
parentID: undoParentID,
|
|
|
|
|
|
}]);
|
2025-10-15 10:01:47 +08:00
|
|
|
|
|
2024-05-25 17:29:21 +08:00
|
|
|
|
// 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;
|
2024-04-08 17:28:56 +08:00
|
|
|
|
}
|
2024-03-29 20:48:02 +08:00
|
|
|
|
}
|
2025-01-11 12:22:44 +08:00
|
|
|
|
this.element.querySelectorAll(".dragover__top, .dragover__bottom, .dragover, .dragover__current").forEach(item => {
|
|
|
|
|
|
item.classList.remove("dragover__top", "dragover__bottom", "dragover", "dragover__current");
|
2024-03-29 11:03:53 +08:00
|
|
|
|
});
|
2024-03-29 10:08:58 +08:00
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2022-06-03 11:11:33 +08:00
|
|
|
|
public updateDocTitle(ial?: IObject) {
|
2023-04-19 11:28:26 +08:00
|
|
|
|
const docTitleElement = this.headerElement.nextElementSibling as HTMLElement;
|
2022-05-28 22:43:27 +08:00
|
|
|
|
if (this.type === "pin") {
|
2022-05-31 17:51:34 +08:00
|
|
|
|
if (ial) {
|
2024-10-25 16:58:06 +08:00
|
|
|
|
let iconHTML = `${unicode2Emoji(ial.icon || window.siyuan.storage[Constants.LOCAL_IMAGES].file, "b3-list-item__graphic", true)}`;
|
2023-04-18 18:57:20 +08:00
|
|
|
|
if (ial.icon === Constants.ZWSP && docTitleElement.firstElementChild) {
|
|
|
|
|
|
iconHTML = docTitleElement.firstElementChild.outerHTML;
|
2022-05-31 17:51:34 +08:00
|
|
|
|
}
|
2023-04-18 18:57:20 +08:00
|
|
|
|
docTitleElement.innerHTML = `${iconHTML}
|
2022-05-31 17:51:34 +08:00
|
|
|
|
<span class="b3-list-item__text">${escapeHtml(ial.title)}</span>`;
|
2023-04-18 18:57:20 +08:00
|
|
|
|
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
|
|
|
|
}
|
2023-04-18 18:57:20 +08:00
|
|
|
|
} else {
|
2023-04-19 11:28:26 +08:00
|
|
|
|
docTitleElement.classList.add("fn__none");
|
2022-05-28 22:43:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2022-05-26 15:18:53 +08:00
|
|
|
|
private onTransaction(data: IWebSocketData) {
|
2024-09-18 11:34:41 +08:00
|
|
|
|
if (data.data.rootID !== this.blockId) {
|
2023-06-18 00:36:51 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2022-05-26 15:18:53 +08:00
|
|
|
|
let needReload = false;
|
2024-04-08 20:45:31 +08:00
|
|
|
|
const ops = data.data.sources[0];
|
2024-07-04 23:29:59 +08:00
|
|
|
|
ops.doOperations.find((item: IOperation) => {
|
2024-09-12 21:47:05 +08:00
|
|
|
|
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) {
|
2022-05-26 15:18:53 +08:00
|
|
|
|
needReload = true;
|
2024-07-06 09:57:00 +08:00
|
|
|
|
return true;
|
2022-05-26 15:18:53 +08:00
|
|
|
|
} else if (item.action === "delete" || item.action === "move") {
|
|
|
|
|
|
needReload = true;
|
2024-07-04 23:29:59 +08:00
|
|
|
|
return true;
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
2024-07-04 23:29:59 +08:00
|
|
|
|
if (!needReload && ops.undoOperations) {
|
|
|
|
|
|
ops.undoOperations.find((item: IOperation) => {
|
2025-06-22 18:11:04 +08:00
|
|
|
|
if (item.action === "update" && item.data?.indexOf('data-type="NodeHeading"') > -1) {
|
2022-05-26 15:18:53 +08:00
|
|
|
|
needReload = true;
|
2024-07-04 23:29:59 +08:00
|
|
|
|
return true;
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
if (needReload) {
|
|
|
|
|
|
fetchPost("/api/outline/getDocOutline", {
|
|
|
|
|
|
id: this.blockId,
|
2024-09-18 11:34:41 +08:00
|
|
|
|
preview: this.isPreview
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}, response => {
|
2024-12-10 11:44:39 +08:00
|
|
|
|
// 文档切换后不再更新原有推送 https://github.com/siyuan-note/siyuan/issues/13409
|
|
|
|
|
|
if (data.data.rootID !== this.blockId) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2022-05-26 15:18:53 +08:00
|
|
|
|
this.update(response);
|
2023-05-26 22:31:19 +08:00
|
|
|
|
// 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") {
|
2023-05-29 11:20:08 +08:00
|
|
|
|
this.setCurrent(blockElement);
|
2023-05-26 22:31:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2022-05-26 15:18:53 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-03-01 15:17:45 +08:00
|
|
|
|
public setCurrent(nodeElement: HTMLElement) {
|
2023-05-15 11:46:51 +08:00
|
|
|
|
if (!nodeElement) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2023-03-01 15:17:45 +08:00
|
|
|
|
if (nodeElement.getAttribute("data-type") === "NodeHeading") {
|
2023-03-03 10:39:58 +08:00
|
|
|
|
this.setCurrentById(nodeElement.getAttribute("data-node-id"));
|
2023-03-01 15:17:45 +08:00
|
|
|
|
} 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"));
|
2023-03-01 15:17:45 +08:00
|
|
|
|
} 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-05-13 23:32:56 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-03-01 15:17:45 +08:00
|
|
|
|
private setCurrentById(id: string) {
|
2022-05-26 15:18:53 +08:00
|
|
|
|
this.element.querySelectorAll(".b3-list-item.b3-list-item--focus").forEach(item => {
|
|
|
|
|
|
item.classList.remove("b3-list-item--focus");
|
|
|
|
|
|
});
|
2025-10-15 10:01:47 +08:00
|
|
|
|
let currentElement = this.element.querySelector(`.b3-list-item[data-node-id="${id}"]`) as HTMLElement;
|
2025-10-15 20:32:13 +08:00
|
|
|
|
if (window.siyuan.storage[Constants.LOCAL_OUTLINE].keepCurrentExpand) {
|
|
|
|
|
|
let ulElement = currentElement.parentElement;
|
|
|
|
|
|
while (ulElement && !ulElement.classList.contains("b3-list") && ulElement.tagName === "UL") {
|
|
|
|
|
|
ulElement.classList.remove("fn__none");
|
|
|
|
|
|
ulElement.previousElementSibling.querySelector(".b3-list-item__arrow").classList.add("b3-list-item__arrow--open");
|
|
|
|
|
|
ulElement = ulElement.parentElement;
|
2025-10-15 10:01:47 +08:00
|
|
|
|
}
|
2025-10-15 20:32:13 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
while (currentElement && currentElement.clientHeight === 0) {
|
|
|
|
|
|
currentElement = currentElement.parentElement.previousElementSibling as HTMLElement;
|
2025-10-15 10:01:47 +08:00
|
|
|
|
}
|
2025-10-15 20:32:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (currentElement) {
|
|
|
|
|
|
currentElement.classList.add("b3-list-item--focus");
|
|
|
|
|
|
const elementRect = this.element.getBoundingClientRect();
|
|
|
|
|
|
this.element.scrollTop = this.element.scrollTop + (currentElement.getBoundingClientRect().top - (elementRect.top + elementRect.height / 2));
|
|
|
|
|
|
}
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-15 10:01:47 +08:00
|
|
|
|
// 保存当前文档的折叠状态到新的持久化存储
|
|
|
|
|
|
if (!this.isPreview) {
|
|
|
|
|
|
const currentExpandIds = this.tree.getExpandIds();
|
|
|
|
|
|
fetchPost("/api/storage/setOutlineStorage", {
|
|
|
|
|
|
docID: this.blockId,
|
|
|
|
|
|
val: {
|
|
|
|
|
|
expandIds: currentExpandIds
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
2025-10-15 10:01:47 +08:00
|
|
|
|
|
2022-05-26 15:18:53 +08:00
|
|
|
|
if (typeof callbackId !== "undefined") {
|
|
|
|
|
|
this.blockId = callbackId;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.tree.updateData(data.data);
|
2025-10-15 10:01:47 +08:00
|
|
|
|
|
|
|
|
|
|
// 从新的持久化存储恢复折叠状态
|
|
|
|
|
|
if (!this.isPreview) {
|
|
|
|
|
|
fetchPost("/api/storage/getOutlineStorage", {
|
|
|
|
|
|
docID: this.blockId
|
|
|
|
|
|
}, storageResponse => {
|
|
|
|
|
|
const storageData = storageResponse.data;
|
|
|
|
|
|
if (storageData && storageData.expandIds) {
|
|
|
|
|
|
this.tree.setExpandIds(storageData.expandIds);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.tree.expandAll();
|
|
|
|
|
|
}
|
2025-10-15 20:32:13 +08:00
|
|
|
|
if ((this.headerElement.querySelector("input.b3-text-field.search__label") as HTMLInputElement).value) {
|
|
|
|
|
|
this.setFilter();
|
2025-10-15 10:01:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
2023-06-18 00:36:51 +08:00
|
|
|
|
}
|
2025-10-15 10:01:47 +08:00
|
|
|
|
|
2023-06-18 00:36:51 +08:00
|
|
|
|
if (this.isPreview) {
|
|
|
|
|
|
this.tree.element.querySelectorAll(".popover__block").forEach(item => {
|
|
|
|
|
|
item.classList.remove("popover__block");
|
2023-06-19 21:04:56 +08:00
|
|
|
|
});
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (currentId) {
|
|
|
|
|
|
currentElement = this.element.querySelector(`[data-node-id="${currentId}"]`);
|
|
|
|
|
|
if (currentElement) {
|
|
|
|
|
|
currentElement.classList.add("b3-list-item--focus");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-04-07 23:58:24 +08:00
|
|
|
|
this.element.removeAttribute("data-loading");
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
2025-10-15 10:01:47 +08:00
|
|
|
|
|
2025-10-15 20:32:13 +08:00
|
|
|
|
public saveExpendIds() {
|
|
|
|
|
|
if (!this.isPreview) {
|
|
|
|
|
|
fetchPost("/api/storage/setOutlineStorage", {
|
|
|
|
|
|
docID: this.blockId,
|
|
|
|
|
|
val: {
|
|
|
|
|
|
expandIds: this.tree.getExpandIds()
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-15 10:01:47 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 应用大纲筛选
|
|
|
|
|
|
*/
|
2025-10-15 20:32:13 +08:00
|
|
|
|
private setFilter() {
|
|
|
|
|
|
// 还原 display
|
|
|
|
|
|
this.element.querySelectorAll("li.b3-list-item").forEach((item: HTMLElement) => {
|
|
|
|
|
|
item.style.display = "";
|
|
|
|
|
|
});
|
|
|
|
|
|
this.element.querySelectorAll("ul.fn__none").forEach((item) => {
|
|
|
|
|
|
item.classList.remove("fn__none");
|
|
|
|
|
|
});
|
|
|
|
|
|
const keyword = (this.headerElement.querySelector("input.b3-text-field.search__label") as HTMLInputElement).value.toLowerCase();
|
|
|
|
|
|
if (keyword) {
|
|
|
|
|
|
// 首次筛选时记录折叠状态
|
|
|
|
|
|
if (!this.preFilterExpandIds) {
|
2025-10-15 10:01:47 +08:00
|
|
|
|
this.preFilterExpandIds = this.tree.getExpandIds();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-15 20:32:13 +08:00
|
|
|
|
const processUL = (ul: Element) => {
|
|
|
|
|
|
let hasMatch = false;
|
|
|
|
|
|
let hasChildMatch = false;
|
|
|
|
|
|
const children = ul.querySelectorAll(":scope > li.b3-list-item");
|
2025-10-15 10:01:47 +08:00
|
|
|
|
|
2025-10-15 20:32:13 +08:00
|
|
|
|
children.forEach((liItem: HTMLElement) => {
|
|
|
|
|
|
const nextUlElement = (liItem.nextElementSibling && liItem.nextElementSibling.tagName === "UL") ? liItem.nextElementSibling as HTMLElement : undefined;
|
2025-10-15 10:01:47 +08:00
|
|
|
|
|
2025-10-15 20:32:13 +08:00
|
|
|
|
let childResult = {hasMatch: false, hasChildMatch: false};
|
|
|
|
|
|
if (nextUlElement) {
|
|
|
|
|
|
childResult = processUL(nextUlElement);
|
|
|
|
|
|
}
|
2025-10-15 10:01:47 +08:00
|
|
|
|
|
2025-10-15 20:32:13 +08:00
|
|
|
|
const arrowElement = liItem.querySelector(".b3-list-item__arrow");
|
|
|
|
|
|
if ((liItem.querySelector(".b3-list-item__text")?.textContent || "").trim().toLowerCase().includes(keyword)) {
|
|
|
|
|
|
// 当前标题命中
|
|
|
|
|
|
liItem.style.display = "";
|
|
|
|
|
|
hasMatch = true;
|
|
|
|
|
|
|
|
|
|
|
|
if (nextUlElement) {
|
|
|
|
|
|
nextUlElement.classList.remove("fn__none");
|
|
|
|
|
|
if (childResult.hasMatch || childResult.hasChildMatch) {
|
|
|
|
|
|
// 子项也有命中,保持展开状态,但隐藏未命中的子项由子级处理
|
|
|
|
|
|
arrowElement.classList.add("b3-list-item__arrow--open");
|
|
|
|
|
|
nextUlElement.classList.remove("fn__none");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 子项无命中,折叠所有子项但保持可展开
|
|
|
|
|
|
arrowElement.classList.remove("b3-list-item__arrow--open");
|
|
|
|
|
|
// 折叠但不完全隐藏,保持子项可访问性
|
|
|
|
|
|
nextUlElement.classList.add("fn__none");
|
2025-10-15 10:01:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-15 20:32:13 +08:00
|
|
|
|
} else if (childResult.hasMatch || childResult.hasChildMatch) {
|
|
|
|
|
|
// 当前标题未命中,但子级有命中
|
|
|
|
|
|
liItem.style.display = "";
|
|
|
|
|
|
hasChildMatch = true;
|
|
|
|
|
|
|
|
|
|
|
|
if (nextUlElement) {
|
|
|
|
|
|
nextUlElement.classList.remove("fn__none");
|
|
|
|
|
|
arrowElement.classList.add("b3-list-item__arrow--open");
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 当前标题和子级都未命中,隐藏
|
|
|
|
|
|
liItem.style.display = "none";
|
|
|
|
|
|
if (nextUlElement) {
|
|
|
|
|
|
nextUlElement.classList.add("fn__none");
|
2025-10-15 10:01:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-15 20:32:13 +08:00
|
|
|
|
});
|
|
|
|
|
|
return {hasMatch, hasChildMatch};
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
processUL(this.element.firstElementChild);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 恢复折叠状态
|
|
|
|
|
|
this.tree.setExpandIds(this.preFilterExpandIds);
|
|
|
|
|
|
this.preFilterExpandIds = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取标题元素的实际标题级别(H1=1, H2=2, 等等)
|
|
|
|
|
|
* @param element li元素
|
|
|
|
|
|
* @returns 标题级别(1-6)
|
|
|
|
|
|
*/
|
|
|
|
|
|
private getHeadingLevel(element: HTMLElement) {
|
|
|
|
|
|
return parseInt(element.getAttribute("data-subtype")?.replace("h", "") || "0");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 展开到指定标题级别
|
|
|
|
|
|
* @param targetLevel 目标标题级别,1-6级(H1-H6),6级表示全部展开
|
|
|
|
|
|
*/
|
|
|
|
|
|
private expandToLevel(targetLevel: number) {
|
|
|
|
|
|
if (targetLevel >= 6) {
|
|
|
|
|
|
// 全部展开
|
|
|
|
|
|
this.tree.expandAll();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 展开到指定标题级别
|
|
|
|
|
|
this.element.querySelectorAll("li.b3-list-item").forEach(item => {
|
|
|
|
|
|
const headingLevel = this.getHeadingLevel(item as HTMLElement);
|
|
|
|
|
|
const arrowElement = item.querySelector(".b3-list-item__arrow");
|
|
|
|
|
|
if (item.nextElementSibling && item.nextElementSibling.tagName === "UL" && arrowElement) {
|
|
|
|
|
|
if (headingLevel > 0 && headingLevel < targetLevel) {
|
|
|
|
|
|
// 当前标题级别小于目标级别,展开
|
|
|
|
|
|
arrowElement.classList.add("b3-list-item__arrow--open");
|
|
|
|
|
|
item.nextElementSibling.classList.remove("fn__none");
|
|
|
|
|
|
} else if (headingLevel >= targetLevel) {
|
|
|
|
|
|
// 当前标题级别大于等于目标级别,折叠
|
|
|
|
|
|
arrowElement.classList.remove("b3-list-item__arrow--open");
|
|
|
|
|
|
item.nextElementSibling.classList.add("fn__none");
|
2025-10-15 10:01:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-10-15 20:32:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-15 10:01:47 +08:00
|
|
|
|
|
2025-10-15 20:32:13 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 显示展开层级菜单
|
|
|
|
|
|
*/
|
|
|
|
|
|
private showExpandLevelMenu(target: HTMLElement) {
|
|
|
|
|
|
window.siyuan.menus.menu.remove();
|
|
|
|
|
|
for (let i = 1; i <= 6; i++) {
|
|
|
|
|
|
window.siyuan.menus.menu.append(new MenuItem({
|
|
|
|
|
|
icon: `iconH${i}`,
|
|
|
|
|
|
label: window.siyuan.languages[`heading${i}`],
|
|
|
|
|
|
click: () => this.expandToLevel(i)
|
|
|
|
|
|
}).element);
|
|
|
|
|
|
}
|
|
|
|
|
|
const rect = target.getBoundingClientRect();
|
|
|
|
|
|
window.siyuan.menus.menu.popup({
|
|
|
|
|
|
x: rect.left,
|
|
|
|
|
|
y: rect.bottom,
|
|
|
|
|
|
h: rect.height
|
|
|
|
|
|
});
|
|
|
|
|
|
return window.siyuan.menus.menu;
|
|
|
|
|
|
}
|
2025-10-15 10:01:47 +08:00
|
|
|
|
|
2025-10-15 20:32:13 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 切换同层级的所有标题的展开/折叠状态(基于标题级别而不是DOM层级)
|
2025-10-15 10:01:47 +08:00
|
|
|
|
*/
|
2025-10-15 20:32:13 +08:00
|
|
|
|
private collapseSameLevel(element: HTMLElement, expand?: boolean) {
|
|
|
|
|
|
// 获取所有相同标题级别的元素
|
|
|
|
|
|
this.element.querySelectorAll(`li.b3-list-item[data-subtype="${element.getAttribute("data-subtype")}"]`).forEach(item => {
|
|
|
|
|
|
const arrowElement = item.querySelector(".b3-list-item__arrow");
|
|
|
|
|
|
if (typeof expand === "undefined") {
|
|
|
|
|
|
expand = !element.querySelector(".b3-list-item__arrow").classList.contains("b3-list-item__arrow--open");
|
|
|
|
|
|
}
|
|
|
|
|
|
if (expand) {
|
|
|
|
|
|
if (item.nextElementSibling && item.nextElementSibling.tagName === "UL") {
|
|
|
|
|
|
item.nextElementSibling.classList.remove("fn__none");
|
|
|
|
|
|
arrowElement.classList.add("b3-list-item__arrow--open");
|
|
|
|
|
|
}
|
|
|
|
|
|
let ulElement = item.parentElement;
|
|
|
|
|
|
while (ulElement && !ulElement.classList.contains("b3-list") && ulElement.tagName === "UL") {
|
|
|
|
|
|
ulElement.classList.remove("fn__none");
|
|
|
|
|
|
ulElement.previousElementSibling.querySelector(".b3-list-item__arrow").classList.add("b3-list-item__arrow--open");
|
|
|
|
|
|
ulElement = ulElement.parentElement;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if (item.nextElementSibling && item.nextElementSibling.tagName === "UL") {
|
|
|
|
|
|
item.nextElementSibling.classList.add("fn__none");
|
|
|
|
|
|
arrowElement.classList.remove("b3-list-item__arrow--open");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-15 10:01:47 +08:00
|
|
|
|
});
|
2025-10-15 20:32:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private collapseChildren(element: HTMLElement, expand?: boolean) {
|
|
|
|
|
|
const nextElement = element.nextElementSibling;
|
|
|
|
|
|
if (!nextElement || nextElement.tagName !== "UL") {
|
|
|
|
|
|
return;
|
2025-10-15 10:01:47 +08:00
|
|
|
|
}
|
2025-10-15 20:32:13 +08:00
|
|
|
|
const arrowElement = element.querySelector(".b3-list-item__arrow");
|
|
|
|
|
|
if (typeof expand === "undefined") {
|
|
|
|
|
|
expand = !arrowElement.classList.contains("b3-list-item__arrow--open");
|
|
|
|
|
|
}
|
|
|
|
|
|
if (expand) {
|
|
|
|
|
|
arrowElement.classList.add("b3-list-item__arrow--open");
|
|
|
|
|
|
nextElement.classList.remove("fn__none");
|
|
|
|
|
|
nextElement.querySelectorAll("ul").forEach(item => {
|
|
|
|
|
|
item.previousElementSibling.querySelector(".b3-list-item__arrow").classList.add("b3-list-item__arrow--open");
|
|
|
|
|
|
item.classList.remove("fn__none");
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
arrowElement.classList.remove("b3-list-item__arrow--open");
|
|
|
|
|
|
nextElement.classList.add("fn__none");
|
2025-10-15 10:01:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 显示右键菜单
|
|
|
|
|
|
*/
|
|
|
|
|
|
private showContextMenu(element: HTMLElement, event: MouseEvent) {
|
|
|
|
|
|
if (this.isPreview) {
|
|
|
|
|
|
return; // 预览模式下不显示右键菜单
|
|
|
|
|
|
}
|
|
|
|
|
|
const currentLevel = this.getHeadingLevel(element);
|
|
|
|
|
|
window.siyuan.menus.menu.remove();
|
|
|
|
|
|
// 升级
|
|
|
|
|
|
if (currentLevel > 1) {
|
|
|
|
|
|
window.siyuan.menus.menu.append(new MenuItem({
|
|
|
|
|
|
icon: "iconUp",
|
2025-10-15 20:32:13 +08:00
|
|
|
|
label: window.siyuan.languages.upgrade,
|
2025-10-16 11:09:45 +08:00
|
|
|
|
click: () => {
|
|
|
|
|
|
const data = this.getProtyleAndBlockElement(element);
|
|
|
|
|
|
if (data) {
|
|
|
|
|
|
turnsIntoTransaction({
|
|
|
|
|
|
protyle: data.protyle,
|
|
|
|
|
|
selectsElement: [data.blockElement],
|
|
|
|
|
|
type: "Blocks2Hs",
|
|
|
|
|
|
level: currentLevel - 1
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-15 10:01:47 +08:00
|
|
|
|
}).element);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 降级
|
|
|
|
|
|
if (currentLevel < 6) {
|
|
|
|
|
|
window.siyuan.menus.menu.append(new MenuItem({
|
2025-10-15 20:32:13 +08:00
|
|
|
|
icon: "iconDown",
|
|
|
|
|
|
label: window.siyuan.languages.downgrade,
|
2025-10-16 11:09:45 +08:00
|
|
|
|
click: () => {
|
|
|
|
|
|
const data = this.getProtyleAndBlockElement(element);
|
|
|
|
|
|
if (data) {
|
|
|
|
|
|
turnsIntoTransaction({
|
|
|
|
|
|
protyle: data.protyle,
|
|
|
|
|
|
selectsElement: [data.blockElement],
|
|
|
|
|
|
type: "Blocks2Hs",
|
|
|
|
|
|
level: currentLevel + 1
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-15 10:01:47 +08:00
|
|
|
|
}).element);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 带子标题转换
|
2025-10-16 11:09:45 +08:00
|
|
|
|
const id = element.getAttribute("data-node-id");
|
|
|
|
|
|
checkFold(id, (zoomIn) => {
|
|
|
|
|
|
openFileById({
|
|
|
|
|
|
app: this.app,
|
|
|
|
|
|
id,
|
|
|
|
|
|
action: zoomIn ? [Constants.CB_GET_FOCUS, Constants.CB_GET_ALL, Constants.CB_GET_HTML, Constants.CB_GET_OUTLINE] : [Constants.CB_GET_FOCUS, Constants.CB_GET_OUTLINE, Constants.CB_GET_SETID, Constants.CB_GET_CONTEXT, Constants.CB_GET_HTML],
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
this.setCurrentById(id);
|
2025-10-15 10:01:47 +08:00
|
|
|
|
const headingSubMenu = [];
|
|
|
|
|
|
if (currentLevel !== 1) {
|
|
|
|
|
|
headingSubMenu.push(this.genHeadingTransform(id, 1));
|
|
|
|
|
|
}
|
|
|
|
|
|
if (currentLevel !== 2) {
|
|
|
|
|
|
headingSubMenu.push(this.genHeadingTransform(id, 2));
|
|
|
|
|
|
}
|
|
|
|
|
|
if (currentLevel !== 3) {
|
|
|
|
|
|
headingSubMenu.push(this.genHeadingTransform(id, 3));
|
|
|
|
|
|
}
|
|
|
|
|
|
if (currentLevel !== 4) {
|
|
|
|
|
|
headingSubMenu.push(this.genHeadingTransform(id, 4));
|
|
|
|
|
|
}
|
|
|
|
|
|
if (currentLevel !== 5) {
|
|
|
|
|
|
headingSubMenu.push(this.genHeadingTransform(id, 5));
|
|
|
|
|
|
}
|
|
|
|
|
|
if (currentLevel !== 6) {
|
|
|
|
|
|
headingSubMenu.push(this.genHeadingTransform(id, 6));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (headingSubMenu.length > 0) {
|
|
|
|
|
|
window.siyuan.menus.menu.append(new MenuItem({
|
|
|
|
|
|
id: "tWithSubtitle",
|
|
|
|
|
|
type: "submenu",
|
|
|
|
|
|
icon: "iconRefresh",
|
|
|
|
|
|
label: window.siyuan.languages.tWithSubtitle,
|
|
|
|
|
|
submenu: headingSubMenu
|
|
|
|
|
|
}).element);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
window.siyuan.menus.menu.append(new MenuItem({type: "separator"}).element);
|
|
|
|
|
|
|
|
|
|
|
|
// 在前面插入同级标题
|
|
|
|
|
|
window.siyuan.menus.menu.append(new MenuItem({
|
|
|
|
|
|
icon: "iconBefore",
|
2025-10-15 20:32:13 +08:00
|
|
|
|
label: window.siyuan.languages.insertSameLevelHeadingBefore,
|
2025-10-16 11:09:45 +08:00
|
|
|
|
click: () => {
|
|
|
|
|
|
fetchPost("/api/block/insertBlock", {
|
|
|
|
|
|
data: "#".repeat(this.getHeadingLevel(element)) + " ",
|
|
|
|
|
|
dataType: "markdown",
|
|
|
|
|
|
nextID: element.getAttribute("data-node-id")
|
|
|
|
|
|
}, (response) => {
|
|
|
|
|
|
openFileById({
|
|
|
|
|
|
app: this.app,
|
|
|
|
|
|
id: response.data[0].doOperations[0].id,
|
|
|
|
|
|
action: [Constants.CB_GET_FOCUS, Constants.CB_GET_OUTLINE, Constants.CB_GET_SETID, Constants.CB_GET_CONTEXT, Constants.CB_GET_HTML]
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-10-15 10:01:47 +08:00
|
|
|
|
}).element);
|
|
|
|
|
|
|
|
|
|
|
|
// 在后面插入同级标题
|
|
|
|
|
|
window.siyuan.menus.menu.append(new MenuItem({
|
|
|
|
|
|
icon: "iconAfter",
|
2025-10-15 20:32:13 +08:00
|
|
|
|
label: window.siyuan.languages.insertSameLevelHeadingAfter,
|
2025-10-16 11:09:45 +08:00
|
|
|
|
click: () => {
|
|
|
|
|
|
fetchPost("/api/block/insertBlock", {
|
|
|
|
|
|
data: "#".repeat(this.getHeadingLevel(element)) + " ",
|
|
|
|
|
|
dataType: "markdown",
|
|
|
|
|
|
previousID: element.getAttribute("data-node-id")
|
|
|
|
|
|
}, (response) => {
|
|
|
|
|
|
openFileById({
|
|
|
|
|
|
app: this.app,
|
|
|
|
|
|
id: response.data[0].doOperations[0].id,
|
|
|
|
|
|
action: [Constants.CB_GET_FOCUS, Constants.CB_GET_OUTLINE, Constants.CB_GET_SETID, Constants.CB_GET_CONTEXT, Constants.CB_GET_HTML]
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-10-15 10:01:47 +08:00
|
|
|
|
}).element);
|
|
|
|
|
|
|
|
|
|
|
|
// 添加子标题
|
|
|
|
|
|
if (currentLevel < 6) { // 只有当前级别小于6时才能添加子标题
|
|
|
|
|
|
window.siyuan.menus.menu.append(new MenuItem({
|
|
|
|
|
|
icon: "iconAdd",
|
2025-10-15 20:32:13 +08:00
|
|
|
|
label: window.siyuan.languages.addChildHeading,
|
2025-10-16 11:09:45 +08:00
|
|
|
|
click: () => {
|
|
|
|
|
|
fetchPost("/api/block/prependBlock", {
|
|
|
|
|
|
data: "#".repeat(Math.min(this.getHeadingLevel(element) + 1, 6)) + " ",
|
|
|
|
|
|
dataType: "markdown",
|
|
|
|
|
|
parentID: element.getAttribute("data-node-id")
|
|
|
|
|
|
}, (response) => {
|
|
|
|
|
|
if (response.code === 0 && response.data && response.data.length > 0) {
|
|
|
|
|
|
openFileById({
|
|
|
|
|
|
app: this.app,
|
|
|
|
|
|
id: response.data[0].doOperations[0].id,
|
|
|
|
|
|
action: [Constants.CB_GET_FOCUS, Constants.CB_GET_OUTLINE]
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-10-15 10:01:47 +08:00
|
|
|
|
}).element);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-15 20:32:13 +08:00
|
|
|
|
window.siyuan.menus.menu.append(new MenuItem({type: "separator"}).element);
|
2025-10-15 10:01:47 +08:00
|
|
|
|
|
|
|
|
|
|
// 复制带子标题
|
|
|
|
|
|
window.siyuan.menus.menu.append(new MenuItem({
|
|
|
|
|
|
icon: "iconCopy",
|
|
|
|
|
|
label: `${window.siyuan.languages.copy} ${window.siyuan.languages.headings1}`,
|
2025-10-16 11:09:45 +08:00
|
|
|
|
click: () => {
|
|
|
|
|
|
const data = this.getProtyleAndBlockElement(element);
|
|
|
|
|
|
fetchPost("/api/block/getHeadingChildrenDOM", {
|
|
|
|
|
|
id,
|
|
|
|
|
|
removeFoldAttr: data.blockElement.getAttribute("fold") !== "1"
|
|
|
|
|
|
}, (response) => {
|
|
|
|
|
|
if (isInAndroid()) {
|
|
|
|
|
|
window.JSAndroid.writeHTMLClipboard(data.protyle.lute.BlockDOM2StdMd(response.data).trimEnd(), response.data + Constants.ZWSP);
|
|
|
|
|
|
} else if (isInHarmony()) {
|
|
|
|
|
|
window.JSHarmony.writeHTMLClipboard(data.protyle.lute.BlockDOM2StdMd(response.data).trimEnd(), response.data + Constants.ZWSP);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
writeText(response.data + Constants.ZWSP);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-10-15 10:01:47 +08:00
|
|
|
|
}).element);
|
|
|
|
|
|
|
|
|
|
|
|
// 剪切带子标题
|
|
|
|
|
|
window.siyuan.menus.menu.append(new MenuItem({
|
|
|
|
|
|
icon: "iconCut",
|
|
|
|
|
|
label: `${window.siyuan.languages.cut} ${window.siyuan.languages.headings1}`,
|
2025-10-16 11:09:45 +08:00
|
|
|
|
click: () => {
|
|
|
|
|
|
const data = this.getProtyleAndBlockElement(element);
|
|
|
|
|
|
fetchPost("/api/block/getHeadingChildrenDOM", {
|
|
|
|
|
|
id,
|
|
|
|
|
|
removeFoldAttr: data.blockElement.getAttribute("fold") !== "1"
|
|
|
|
|
|
}, (response) => {
|
|
|
|
|
|
if (isInAndroid()) {
|
|
|
|
|
|
window.JSAndroid.writeHTMLClipboard(data.protyle.lute.BlockDOM2StdMd(response.data).trimEnd(), response.data + Constants.ZWSP);
|
|
|
|
|
|
} else if (isInHarmony()) {
|
|
|
|
|
|
window.JSHarmony.writeHTMLClipboard(data.protyle.lute.BlockDOM2StdMd(response.data).trimEnd(), response.data + Constants.ZWSP);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
writeText(response.data + Constants.ZWSP);
|
|
|
|
|
|
}
|
|
|
|
|
|
fetchPost("/api/block/getHeadingDeleteTransaction", {
|
|
|
|
|
|
id,
|
|
|
|
|
|
}, (deleteResponse) => {
|
|
|
|
|
|
deleteResponse.data.doOperations.forEach((operation: IOperation) => {
|
|
|
|
|
|
data.protyle.wysiwyg.element.querySelectorAll(`[data-node-id="${operation.id}"]`).forEach((itemElement: HTMLElement) => {
|
|
|
|
|
|
itemElement.remove();
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
if (data.protyle.wysiwyg.element.childElementCount === 0) {
|
|
|
|
|
|
const newID = Lute.NewNodeID();
|
|
|
|
|
|
const emptyElement = genEmptyElement(false, false, newID);
|
|
|
|
|
|
data.protyle.wysiwyg.element.insertAdjacentElement("afterbegin", emptyElement);
|
|
|
|
|
|
deleteResponse.data.doOperations.push({
|
|
|
|
|
|
action: "insert",
|
|
|
|
|
|
data: emptyElement.outerHTML,
|
|
|
|
|
|
id: newID,
|
|
|
|
|
|
parentID: data.protyle.block.parentID
|
|
|
|
|
|
});
|
|
|
|
|
|
deleteResponse.data.undoOperations.push({
|
|
|
|
|
|
action: "delete",
|
|
|
|
|
|
id: newID,
|
|
|
|
|
|
});
|
|
|
|
|
|
focusBlock(emptyElement);
|
|
|
|
|
|
}
|
|
|
|
|
|
transaction(data.protyle, deleteResponse.data.doOperations, deleteResponse.data.undoOperations);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-10-15 10:01:47 +08:00
|
|
|
|
}).element);
|
|
|
|
|
|
|
|
|
|
|
|
// 删除
|
|
|
|
|
|
window.siyuan.menus.menu.append(new MenuItem({
|
|
|
|
|
|
icon: "iconTrashcan",
|
|
|
|
|
|
label: `${window.siyuan.languages.delete} ${window.siyuan.languages.headings1}`,
|
2025-10-16 11:09:45 +08:00
|
|
|
|
click: () => {
|
|
|
|
|
|
const data = this.getProtyleAndBlockElement(element);
|
|
|
|
|
|
fetchPost("/api/block/getHeadingDeleteTransaction", {
|
|
|
|
|
|
id,
|
|
|
|
|
|
}, (response) => {
|
|
|
|
|
|
response.data.doOperations.forEach((operation: IOperation) => {
|
|
|
|
|
|
data.protyle.wysiwyg.element.querySelectorAll(`[data-node-id="${operation.id}"]`).forEach((itemElement: HTMLElement) => {
|
|
|
|
|
|
itemElement.remove();
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
if (data.protyle.wysiwyg.element.childElementCount === 0) {
|
|
|
|
|
|
const newID = Lute.NewNodeID();
|
|
|
|
|
|
const emptyElement = genEmptyElement(false, false, newID);
|
|
|
|
|
|
data.protyle.wysiwyg.element.insertAdjacentElement("afterbegin", emptyElement);
|
|
|
|
|
|
response.data.doOperations.push({
|
|
|
|
|
|
action: "insert",
|
|
|
|
|
|
data: emptyElement.outerHTML,
|
|
|
|
|
|
id: newID,
|
|
|
|
|
|
parentID: data.protyle.block.parentID
|
|
|
|
|
|
});
|
|
|
|
|
|
response.data.undoOperations.push({
|
|
|
|
|
|
action: "delete",
|
|
|
|
|
|
id: newID,
|
|
|
|
|
|
});
|
|
|
|
|
|
focusBlock(emptyElement);
|
|
|
|
|
|
}
|
|
|
|
|
|
transaction(data.protyle, response.data.doOperations, response.data.undoOperations);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-10-15 10:01:47 +08:00
|
|
|
|
}).element);
|
|
|
|
|
|
|
|
|
|
|
|
window.siyuan.menus.menu.append(new MenuItem({type: "separator"}).element);
|
|
|
|
|
|
|
|
|
|
|
|
// 展开子标题
|
|
|
|
|
|
window.siyuan.menus.menu.append(new MenuItem({
|
|
|
|
|
|
icon: "iconExpand",
|
2025-10-15 20:32:13 +08:00
|
|
|
|
label: window.siyuan.languages.expandChildHeading,
|
|
|
|
|
|
accelerator: updateHotkeyTip("⌘") + window.siyuan.languages.click,
|
|
|
|
|
|
click: () => this.collapseChildren(element, true)
|
2025-10-15 10:01:47 +08:00
|
|
|
|
}).element);
|
|
|
|
|
|
|
|
|
|
|
|
// 折叠子标题
|
|
|
|
|
|
window.siyuan.menus.menu.append(new MenuItem({
|
|
|
|
|
|
icon: "iconContract",
|
2025-10-15 20:32:13 +08:00
|
|
|
|
label: window.siyuan.languages.foldChildHeading,
|
|
|
|
|
|
accelerator: updateHotkeyTip("⌘") + window.siyuan.languages.click,
|
|
|
|
|
|
click: () => this.collapseChildren(element, false)
|
2025-10-15 10:01:47 +08:00
|
|
|
|
}).element);
|
|
|
|
|
|
|
|
|
|
|
|
// 展开同级标题
|
|
|
|
|
|
window.siyuan.menus.menu.append(new MenuItem({
|
|
|
|
|
|
icon: "iconExpand",
|
2025-10-15 20:32:13 +08:00
|
|
|
|
label: window.siyuan.languages.expandSameLevelHeading,
|
|
|
|
|
|
accelerator: updateHotkeyTip("⌥") + window.siyuan.languages.click,
|
|
|
|
|
|
click: () => this.collapseSameLevel(element, true)
|
2025-10-15 10:01:47 +08:00
|
|
|
|
}).element);
|
|
|
|
|
|
|
|
|
|
|
|
// 折叠同级标题
|
|
|
|
|
|
window.siyuan.menus.menu.append(new MenuItem({
|
2025-10-15 20:32:13 +08:00
|
|
|
|
icon: "iconContract",
|
|
|
|
|
|
label: window.siyuan.languages.foldSameLevelHeading,
|
|
|
|
|
|
accelerator: updateHotkeyTip("⌥") + window.siyuan.languages.click,
|
|
|
|
|
|
click: () => this.collapseSameLevel(element, false)
|
2025-10-15 10:01:47 +08:00
|
|
|
|
}).element);
|
|
|
|
|
|
|
|
|
|
|
|
// 全部展开
|
|
|
|
|
|
window.siyuan.menus.menu.append(new MenuItem({
|
|
|
|
|
|
icon: "iconExpand",
|
2025-10-15 20:32:13 +08:00
|
|
|
|
label: window.siyuan.languages.expandAll,
|
2025-10-15 10:01:47 +08:00
|
|
|
|
click: () => {
|
|
|
|
|
|
this.tree.expandAll();
|
|
|
|
|
|
}
|
|
|
|
|
|
}).element);
|
|
|
|
|
|
|
|
|
|
|
|
// 全部折叠
|
|
|
|
|
|
window.siyuan.menus.menu.append(new MenuItem({
|
|
|
|
|
|
icon: "iconContract",
|
2025-10-15 20:32:13 +08:00
|
|
|
|
label: window.siyuan.languages.foldAll,
|
2025-10-15 10:01:47 +08:00
|
|
|
|
click: () => this.tree.collapseAll()
|
|
|
|
|
|
}).element);
|
|
|
|
|
|
|
|
|
|
|
|
window.siyuan.menus.menu.popup({
|
|
|
|
|
|
x: event.clientX,
|
|
|
|
|
|
y: event.clientY
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-16 11:09:45 +08:00
|
|
|
|
private getProtyleAndBlockElement(element: HTMLElement) {
|
2025-10-15 10:01:47 +08:00
|
|
|
|
const id = element.getAttribute("data-node-id");
|
2025-10-16 11:09:45 +08:00
|
|
|
|
let protyle: IProtyle;
|
2025-10-15 10:01:47 +08:00
|
|
|
|
let blockElement: HTMLElement;
|
|
|
|
|
|
getAllModels().editor.find(editItem => {
|
|
|
|
|
|
if (editItem.editor.protyle.block.rootID === this.blockId) {
|
2025-10-16 11:09:45 +08:00
|
|
|
|
protyle = editItem.editor.protyle;
|
|
|
|
|
|
blockElement = protyle.wysiwyg.element.querySelector(`[data-node-id="${id}"]`);
|
2025-10-15 10:01:47 +08:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-10-16 11:09:45 +08:00
|
|
|
|
if (!protyle || !blockElement) {
|
2025-10-15 10:01:47 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-10-16 11:09:45 +08:00
|
|
|
|
return {
|
|
|
|
|
|
protyle, blockElement
|
|
|
|
|
|
};
|
2025-10-15 10:01:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 生成标题级别转换菜单项
|
|
|
|
|
|
*/
|
|
|
|
|
|
private genHeadingTransform(id: string, level: number) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: "heading" + level,
|
|
|
|
|
|
iconHTML: "",
|
|
|
|
|
|
icon: "iconHeading" + level,
|
|
|
|
|
|
label: window.siyuan.languages["heading" + level],
|
|
|
|
|
|
click: () => {
|
2025-10-16 11:09:45 +08:00
|
|
|
|
let protyle: IProtyle;
|
2025-10-15 10:01:47 +08:00
|
|
|
|
getAllModels().editor.find(editItem => {
|
|
|
|
|
|
if (editItem.editor.protyle.block.rootID === this.blockId) {
|
2025-10-16 11:09:45 +08:00
|
|
|
|
protyle = editItem.editor.protyle;
|
2025-10-15 10:01:47 +08:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-10-16 11:09:45 +08:00
|
|
|
|
if (!protyle) {
|
2025-10-15 10:01:47 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fetchPost("/api/block/getHeadingLevelTransaction", {
|
|
|
|
|
|
id,
|
|
|
|
|
|
level
|
|
|
|
|
|
}, (response) => {
|
|
|
|
|
|
response.data.doOperations.forEach((operation: any, index: number) => {
|
2025-10-16 11:09:45 +08:00
|
|
|
|
protyle.wysiwyg.element.querySelectorAll(`[data-node-id="${operation.id}"]`).forEach((itemElement: HTMLElement) => {
|
2025-10-15 10:01:47 +08:00
|
|
|
|
itemElement.outerHTML = operation.data;
|
|
|
|
|
|
});
|
|
|
|
|
|
// 使用 outer 后元素需要重新查询
|
2025-10-16 11:09:45 +08:00
|
|
|
|
protyle.wysiwyg.element.querySelectorAll(`[data-node-id="${operation.id}"]`).forEach((itemElement: HTMLElement) => {
|
2025-10-15 10:01:47 +08:00
|
|
|
|
mathRender(itemElement);
|
|
|
|
|
|
});
|
|
|
|
|
|
if (index === 0) {
|
2025-10-16 11:09:45 +08:00
|
|
|
|
const focusElement = protyle.wysiwyg.element.querySelector(`[data-node-id="${operation.id}"]`);
|
2025-10-15 10:01:47 +08:00
|
|
|
|
if (focusElement) {
|
|
|
|
|
|
focusElement.scrollIntoView({behavior: "smooth", block: "center"});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-10-16 11:09:45 +08:00
|
|
|
|
transaction(protyle, response.data.doOperations, response.data.undoOperations);
|
2025-10-15 10:01:47 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|