diff --git a/app/src/mobile/dock/MobileOutline.ts b/app/src/mobile/dock/MobileOutline.ts index b2a2666f1..8cc5d5711 100644 --- a/app/src/mobile/dock/MobileOutline.ts +++ b/app/src/mobile/dock/MobileOutline.ts @@ -1,133 +1,207 @@ import {Tree} from "../../util/Tree"; import {fetchPost} from "../../util/fetch"; -import {openMobileFileById} from "../editor"; +import {hasClosestBlock, hasClosestByClassName} from "../../protyle/util/hasClosest"; +import { + isInAndroid, + isInHarmony, + setStorageVal, + writeText +} from "../../protyle/util/compatibility"; import {Constants} from "../../constants"; -import {getEventName} from "../../protyle/util/compatibility"; -import {App} from "../../index"; -import {closePanel} from "../util/closePanel"; -import {checkFold} from "../../util/noRelyPCFunction"; -import {hasClosestBlock} from "../../protyle/util/hasClosest"; +import {MenuItem} from "../../menus/Menu"; import {getPreviousBlock} from "../../protyle/wysiwyg/getBlock"; +import {App} from "../../index"; +import {checkFold} from "../../util/noRelyPCFunction"; +import {transaction, turnsIntoTransaction} from "../../protyle/wysiwyg/transaction"; +import {mathRender} from "../../protyle/render/mathRender"; +import {genEmptyElement} from "../../block/util"; +import {focusBlock, focusByWbr} from "../../protyle/util/selection"; +import {openMobileFileById} from "../editor"; +import {Model} from "../../layout/Model"; +import {genUUID} from "../../util/genID"; -export class MobileOutline { - private tree: Tree; - private openNodes: { [key: string]: string[] } = {}; - private element: Element; +export class MobileOutline extends Model { + public tree: Tree; + public element: HTMLElement; + public blockId: string; + public isPreview: boolean; + private preFilterExpandIds: string[] | null = null; - constructor(app: App) { + constructor(options: { + app: App, + blockId: string, + isPreview: boolean + }) { + super({ + app: options.app, + id: genUUID(), + msgCallback(data) { + if (data) { + switch (data.cmd) { + case "savedoc": + this.onTransaction(data); + break; + } + } + } + }); + + this.isPreview = options.isPreview; + this.blockId = options.blockId; this.element = document.querySelector('#sidebar [data-type="sidebar-outline"]'); this.element.innerHTML = `
-`; +`; + const inputElement = this.element.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("toolbar__icon--active"); + } else { + filterIconElement.classList.remove("toolbar__icon--active"); + } + 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(); + } + }); this.tree = new Tree({ element: this.element.lastElementChild as HTMLElement, data: null, - click: (element: HTMLElement) => { + click: (element: HTMLElement, event) => { + if (event) { + const actionElement = hasClosestByClassName(event.target as HTMLElement, "b3-list-item__action"); + if (actionElement) { + this.showContextMenu(element); + return; + } + } const id = element.getAttribute("data-node-id"); - if (!window.siyuan.mobile.editor.protyle.preview.element.classList.contains("fn__none")) { - closePanel(); - document.getElementById(id)?.scrollIntoView(); + if (this.isPreview) { + const headElement = document.getElementById(id); + if (headElement) { + headElement.scrollIntoView(); + } else { + openMobileFileById(options.app, this.blockId); + } } else { checkFold(id, (zoomIn) => { - openMobileFileById(app, id, zoomIn ? [Constants.CB_GET_ALL, Constants.CB_GET_HTML, Constants.CB_GET_OUTLINE] : [Constants.CB_GET_HL, Constants.CB_GET_OUTLINE, Constants.CB_GET_SETID, Constants.CB_GET_CONTEXT, Constants.CB_GET_HTML]); + openMobileFileById(options.app, id, zoomIn ? [Constants.CB_GET_HL, Constants.CB_GET_ALL, Constants.CB_GET_HTML, Constants.CB_GET_OUTLINE] : [Constants.CB_GET_HL, Constants.CB_GET_OUTLINE, Constants.CB_GET_SETID, Constants.CB_GET_CONTEXT, Constants.CB_GET_HTML]); }); } - } - }); - this.element.firstElementChild.querySelector('[data-type="collapse"]').addEventListener(getEventName(), () => { - this.tree.collapseAll(); - }); - const expandElement = this.element.firstElementChild.querySelector('[data-type="expand"]'); - expandElement.addEventListener(getEventName(), () => { - if (expandElement.classList.contains("toolbar__icon--active")) { - expandElement.classList.remove("toolbar__icon--active"); - } else { - expandElement.classList.add("toolbar__icon--active"); - } - this.tree.expandAll(); - }); - this.update(); - } - - public update() { - fetchPost("/api/outline/getDocOutline", { - id: window.siyuan.mobile.editor.protyle.block.rootID, - preview: !window.siyuan.mobile.editor.protyle.preview.element.classList.contains("fn__none") - }, response => { - let currentId; - let currentElement = this.element.querySelector(".b3-list-item--focus"); - if (currentElement) { - currentId = currentElement.getAttribute("data-node-id"); - } - - const blockId = window.siyuan.mobile.editor.protyle.block.rootID; - if (this.openNodes[blockId]) { - this.openNodes[blockId] = this.tree.getExpandIds(); - } - this.tree.updateData(response.data); - if (this.openNodes[blockId] && !this.element.firstElementChild.querySelector('[data-type="expand"]').classList.contains("toolbar__icon--active")) { - this.tree.setExpandIds(this.openNodes[blockId]); - } else { - this.tree.expandAll(); - this.openNodes[blockId] = this.tree.getExpandIds(); - } - - if (window.siyuan.mobile.editor?.protyle?.toolbar.range) { - const blockElement = hasClosestBlock(window.siyuan.mobile.editor.protyle.toolbar.range.startContainer); - if (blockElement) { - this.setCurrent(blockElement); + }, + toggleClick: (liElement) => { + if (!liElement.nextElementSibling) { return; } + const svgElement = liElement.firstElementChild.firstElementChild; + if (svgElement.classList.contains("b3-list-item__arrow--open")) { + svgElement.classList.remove("b3-list-item__arrow--open"); + liElement.nextElementSibling.classList.add("fn__none"); + if (liElement.nextElementSibling.nextElementSibling && liElement.nextElementSibling.nextElementSibling.tagName === "UL") { + liElement.nextElementSibling.nextElementSibling.classList.add("fn__none"); + } + } else { + svgElement.classList.add("b3-list-item__arrow--open"); + liElement.nextElementSibling.classList.remove("fn__none"); + if (liElement.nextElementSibling.nextElementSibling && liElement.nextElementSibling.nextElementSibling.tagName === "UL") { + liElement.nextElementSibling.nextElementSibling.classList.remove("fn__none"); + } + } + this.saveExpendIds(); + }, + blockExtHTML: window.siyuan.config.readonly ? undefined : '', + }); + // 为了快捷键的 dispatch + this.element.querySelector('[data-type="collapse"]').addEventListener("click", () => { + this.tree.collapseAll(); + this.saveExpendIds(); + }); + + // 普通的全部展开按钮 + this.element.querySelector('[data-type="expand"]').addEventListener("click", () => { + this.tree.expandAll(); + this.saveExpendIds(); + }); + + // 保持当前标题展开功能 + this.element.querySelector('[data-type="keepCurrentExpand"]').addEventListener("click", (event: MouseEvent & { + target: Element + }) => { + const iconElement = hasClosestByClassName(event.target, "toolbar__icon"); + if (!iconElement) { + return; } - if (currentId) { - currentElement = this.element.querySelector(`[data-node-id="${currentId}"]`); - if (currentElement) { - currentElement.classList.add("b3-list-item--focus"); + if (iconElement.classList.contains("toolbar__icon--active")) { + iconElement.classList.remove("toolbar__icon--active"); + window.siyuan.storage[Constants.LOCAL_OUTLINE].keepCurrentExpand = false; + } else { + iconElement.classList.add("toolbar__icon--active"); + window.siyuan.storage[Constants.LOCAL_OUTLINE].keepCurrentExpand = true; + let focusElement; + const blockElement = hasClosestBlock(window.siyuan.mobile.editor.protyle.toolbar.range?.startContainer); + if (blockElement) { + focusElement = blockElement; + } + if (focusElement) { + this.setCurrent(focusElement); } } + // 保存keepCurrentExpand状态到localStorage + setStorageVal(Constants.LOCAL_OUTLINE, window.siyuan.storage[Constants.LOCAL_OUTLINE]); }); - } - - private 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; + this.element.addEventListener("click", (event: MouseEvent & { target: HTMLElement }) => { + let target = event.target as HTMLElement; + if (target.tagName === "INPUT") { + return; + } + while (target && !target.isEqualNode(this.element)) { + if (target.classList.contains("toolbar__icon")) { + const type = target.getAttribute("data-type"); + switch (type) { + case "search": + inputElement.classList.remove("fn__none"); + inputElement.select(); + break; + case "expandLevel": + this.showExpandLevelMenu(); + event.preventDefault(); + event.stopPropagation(); + break; + } + break; + } + target = target.parentElement; } - } - if (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.tree.element.scrollTop = currentElement.offsetTop - this.element.clientHeight / 2 - 30; - } + + fetchPost("/api/outline/getDocOutline", { + id: this.blockId, + preview: this.isPreview + }, response => { + this.update(response); + }); } - private setCurrent(nodeElement: HTMLElement) { + public setCurrent(nodeElement: HTMLElement) { if (!nodeElement) { return; } @@ -159,4 +233,739 @@ export class MobileOutline { } } } + + 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; + 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; + } + this.saveExpendIds(); + } else { + while (currentElement && currentElement.clientHeight === 0) { + currentElement = currentElement.parentElement.previousElementSibling as HTMLElement; + } + } + 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)); + } + } + + 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"); + } + const scrollTop = this.element.scrollTop; + if (typeof callbackId !== "undefined") { + this.blockId = callbackId; + } + this.tree.updateData(data.data); + + if (this.isPreview) { + this.tree.element.querySelectorAll(".popover__block").forEach(item => { + item.classList.remove("popover__block"); + }); + this.element.scrollTop = scrollTop; + } else if (this.blockId) { + if ((this.element.querySelector("input.b3-text-field.search__label") as HTMLInputElement).value) { + this.setFilter(); + } + this.element.scrollTop = scrollTop; + } + if (currentId) { + currentElement = this.element.querySelector(`[data-node-id="${currentId}"]`); + if (currentElement) { + currentElement.classList.add("b3-list-item--focus"); + } + } + this.element.removeAttribute("data-loading"); + } + + public saveExpendIds() { + if (window.siyuan.config.readonly || window.siyuan.isPublish) { + return; + } + + if (!this.isPreview) { + fetchPost("/api/storage/setOutlineStorage", { + docID: this.blockId, + val: { + expandIds: this.tree.getExpandIds() + } + }); + } + } + + /** + * 应用大纲筛选 + */ + private setFilter() { + // 还原 display + this.element.querySelectorAll('li.b3-list-item[style$="display: none;"]').forEach((item: HTMLElement) => { + item.style.display = ""; + }); + this.element.querySelectorAll("ul.fn__none").forEach((item) => { + item.previousElementSibling.querySelector(".b3-list-item__toggle").classList.remove("fn__hidden"); + }); + const keyword = (this.element.querySelector("input.b3-text-field.search__label") as HTMLInputElement).value.toLowerCase(); + if (keyword) { + // 首次筛选时记录折叠状态 + if (!this.preFilterExpandIds) { + this.preFilterExpandIds = this.tree.getExpandIds(); + } + const processUL = (ul: Element) => { + let hasMatch = false; + let hasChildMatch = false; + const children = ul.querySelectorAll(":scope > li.b3-list-item"); + + children.forEach((liItem: HTMLElement) => { + const nextUlElement = (liItem.nextElementSibling && liItem.nextElementSibling.tagName === "UL") ? liItem.nextElementSibling as HTMLElement : undefined; + + let childResult = {hasMatch: false, hasChildMatch: false}; + if (nextUlElement) { + childResult = processUL(nextUlElement); + } + + 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"); + arrowElement.parentElement.classList.add("fn__hidden"); + nextUlElement.classList.add("fn__none"); + } + } + } 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"); + } + } + }); + return {hasMatch, hasChildMatch}; + }; + + processUL(this.element.lastElementChild.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"); + } + } + }); + } + this.saveExpendIds(); + } + + /** + * 显示展开层级菜单 + */ + private showExpandLevelMenu() { + window.siyuan.menus.menu.remove(); + window.siyuan.menus.menu.element.setAttribute("data-name", Constants.MENU_OUTLINE_EXPAND_LEVEL); + for (let i = 1; i <= 6; i++) { + window.siyuan.menus.menu.append(new MenuItem({ + id: `heading${i}`, + icon: `iconH${i}`, + label: window.siyuan.languages[`heading${i}`], + click: () => this.expandToLevel(i) + }).element); + } + window.siyuan.menus.menu.fullscreen("bottom"); + return window.siyuan.menus.menu; + } + + /** + * 切换同层级的所有标题的展开/折叠状态(基于标题级别而不是DOM层级) + */ + 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"); + } + } + }); + this.saveExpendIds(); + } + + private collapseChildren(element: HTMLElement, expand?: boolean) { + const nextElement = element.nextElementSibling; + if (!nextElement || nextElement.tagName !== "UL") { + return; + } + 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"); + } + this.saveExpendIds(); + } + + 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); + } + } + }); + } + } + + /** + * 显示右键菜单 + */ + private showContextMenu(element: HTMLElement) { + if (this.isPreview) { + return; // 预览模式下不显示右键菜单 + } + const currentLevel = this.getHeadingLevel(element); + window.siyuan.menus.menu.remove(); + window.siyuan.menus.menu.element.setAttribute("data-name", Constants.MENU_OUTLINE_CONTEXT); + const id = element.getAttribute("data-node-id"); + if (!window.siyuan.config.readonly) { + // 升级 + if (currentLevel > 1) { + window.siyuan.menus.menu.append(new MenuItem({ + id: "upgrade", + icon: "iconUp", + label: window.siyuan.languages.upgrade, + click: () => { + const data = this.getProtyleAndBlockElement(element); + if (data) { + turnsIntoTransaction({ + protyle: data.protyle, + selectsElement: [data.blockElement], + type: "Blocks2Hs", + level: currentLevel - 1 + }); + } + } + }).element); + } + + // 降级 + if (currentLevel < 6) { + window.siyuan.menus.menu.append(new MenuItem({ + id: "downgrade", + icon: "iconDown", + label: window.siyuan.languages.downgrade, + click: () => { + const data = this.getProtyleAndBlockElement(element); + if (data) { + turnsIntoTransaction({ + protyle: data.protyle, + selectsElement: [data.blockElement], + type: "Blocks2Hs", + level: currentLevel + 1 + }); + } + } + }).element); + } + this.setCurrentById(id); + 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({id: "separator_1", type: "separator"}).element); + + // 在前面插入同级标题 + window.siyuan.menus.menu.append(new MenuItem({ + id: "insertSameLevelHeadingBefore", + icon: "iconBefore", + label: window.siyuan.languages.insertSameLevelHeadingBefore, + click: () => { + const data = this.getProtyleAndBlockElement(element); + const newId = Lute.NewNodeID(); + const html = `