diff --git a/app/src/assets/scss/util/_function.scss b/app/src/assets/scss/util/_function.scss index 93ef7ec9c..2e98067e9 100644 --- a/app/src/assets/scss/util/_function.scss +++ b/app/src/assets/scss/util/_function.scss @@ -1,8 +1,12 @@ @use "mixin"; .fn { - &__hidescrollbar::-webkit-scrollbar { - display: none; + &__hidescrollbar { + &::-webkit-scrollbar { + display: none; + } + + overflow: auto; } &__ellipsis { diff --git a/app/src/layout/dock/Backlink.ts b/app/src/layout/dock/Backlink.ts index fb3c44d26..e1da971bf 100644 --- a/app/src/layout/dock/Backlink.ts +++ b/app/src/layout/dock/Backlink.ts @@ -150,14 +150,6 @@ export class Backlink extends Model { this.searchBacklinks(); } }); - item.addEventListener("input", (event: KeyboardEvent) => { - const inputElement = event.target as HTMLInputElement; - if (inputElement.value === "") { - inputElement.classList.remove("search__input--block"); - } else { - inputElement.classList.add("search__input--block"); - } - }); }); this.tree = new Tree({ element: this.element.querySelector(".backlinkList") as HTMLElement, diff --git a/app/src/layout/dock/Graph.ts b/app/src/layout/dock/Graph.ts index 4c6603fa1..c60c2a72e 100644 --- a/app/src/layout/dock/Graph.ts +++ b/app/src/layout/dock/Graph.ts @@ -341,7 +341,6 @@ export class Graph extends Model { }); this.inputElement.addEventListener("compositionend", () => { this.searchGraph(false); - this.inputElement.classList.add("search__input--block"); }); this.inputElement.addEventListener("blur", (event: InputEvent) => { const inputElement = event.target as HTMLInputElement; @@ -351,11 +350,6 @@ export class Graph extends Model { if (event.isComposing) { return; } - if (this.inputElement.value === "") { - this.inputElement.classList.remove("search__input--block"); - } else { - this.inputElement.classList.add("search__input--block"); - } this.searchGraph(false); }); this.element.querySelectorAll(".b3-slider").forEach((item: HTMLInputElement) => { diff --git a/app/src/layout/dock/Outline.ts b/app/src/layout/dock/Outline.ts index 7a269dd9e..4eca40dfd 100644 --- a/app/src/layout/dock/Outline.ts +++ b/app/src/layout/dock/Outline.ts @@ -1,25 +1,25 @@ -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 } from "../../protyle/util/compatibility"; -import { openFileById } from "../../editor/util"; -import { Constants } from "../../constants"; -import { MenuItem } from "../../menus/Menu"; -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, 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"; +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"; export class Outline extends Model { public tree: Tree; @@ -28,9 +28,6 @@ export class Outline extends Model { public type: "pin" | "local"; public blockId: string; public isPreview: boolean; - // 筛选相关 - private searchInput: HTMLInputElement; - private searchKeyword = ""; private preFilterExpandIds: string[] | null = null; constructor(options: { @@ -45,7 +42,7 @@ export class Outline extends Model { id: options.tab.id, callback() { if (this.type === "local") { - fetchPost("/api/block/checkBlockExist", { id: this.blockId }, existResponse => { + fetchPost("/api/block/checkBlockExist", {id: this.blockId}, existResponse => { if (!existResponse.data) { this.parent.parent.removeTab(this.parent.id); } @@ -70,7 +67,7 @@ export class Outline extends Model { break; case "unmount": if (this.type === "local") { - fetchPost("/api/block/checkBlockExist", { id: this.blockId }, existResponse => { + fetchPost("/api/block/checkBlockExist", {id: this.blockId}, existResponse => { if (!existResponse.data) { this.parent.parent.removeTab(this.parent.id); } @@ -90,77 +87,64 @@ export class Outline extends Model { this.blockId = options.blockId; this.type = options.type; options.tab.panelElement.classList.add("fn__flex-column", "file-tree", "sy__outline"); - options.tab.panelElement.innerHTML = `
+ options.tab.panelElement.innerHTML = `
- - - - + + - + - + - + + + + + - + + +
`; - this.element = options.tab.panelElement.children[2] as HTMLElement; // 更新为第三个子元素(大纲内容) + this.element = options.tab.panelElement.lastElementChild as HTMLElement; this.headerElement = options.tab.panelElement.firstElementChild as HTMLElement; - // 绑定筛选输入框交互,参考 Backlink.ts - this.searchInput = this.headerElement.querySelector("input.b3-text-field.search__label") as HTMLInputElement; - if (this.searchInput) { - this.searchInput.addEventListener("blur", (event: KeyboardEvent) => { - const inputElement = event.target as HTMLInputElement; - inputElement.classList.add("fn__none"); - const filterIconElement = inputElement.nextElementSibling as HTMLElement; // search 图标 - const val = inputElement.value.trim(); - if (val) { - filterIconElement.classList.add("block__icon--active"); - filterIconElement.setAttribute("aria-label", window.siyuan.languages.filter + " " + val); - } else { - filterIconElement.classList.remove("block__icon--active"); - filterIconElement.setAttribute("aria-label", window.siyuan.languages.filter); - // 若之前有筛选,且清空,则恢复 - if (this.searchKeyword) { - this.clearFilter(); - } - } - }); - this.searchInput.addEventListener("keydown", (event: KeyboardEvent) => { - if (!event.isComposing && event.key === "Enter") { - const kw = this.searchInput.value.trim(); - if (kw) { - this.applyFilter(kw); - } else { - this.clearFilter(); - } - } - }); - this.searchInput.addEventListener("input", (event: KeyboardEvent) => { - const inputElement = event.target as HTMLInputElement; - if (inputElement.value === "") { - inputElement.classList.remove("search__input--block"); - } else { - inputElement.classList.add("search__input--block"); - } - }); - } + 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(); + } + }); this.tree = new Tree({ - element: options.tab.panelElement.children[2] as HTMLElement, // 使用第三个子元素作为树容器 + element: this.element, data: null, click: (element: HTMLElement) => { const id = element.getAttribute("data-node-id"); @@ -190,7 +174,12 @@ export class Outline extends Model { }); } }, - ctrlClick(element: HTMLElement) { + 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; + } const id = element.getAttribute("data-node-id"); openFileById({ app: options.app, @@ -199,33 +188,15 @@ export class Outline extends Model { zoomIn: true, }); }, - altClick: (element: HTMLElement, event?: MouseEvent) => { - // 检查是否点击的是标题层级图标 - if (event && event.target) { - const target = event.target as HTMLElement; - const graphicElement = target.closest(".b3-list-item__graphic.popover__block"); - if (graphicElement) { - this.collapseSameLevel(element); - } + altClick: (element: HTMLElement, event: MouseEvent) => { + // alt 点击箭头,切换同层级的所有标题的展开/折叠状态 + const arrowElement = hasClosestByClassName(event.target as HTMLElement, "b3-list-item__toggle"); + if (arrowElement) { + this.collapseSameLevel(element); } }, rightClick: (element: HTMLElement, event: MouseEvent) => { - // 右键菜单 - event.preventDefault(); - event.stopPropagation(); this.showContextMenu(element, event); - }, - onToggleChange: () => { - // 实时保存折叠状态变化 - if (!this.isPreview) { - const expandIds = this.tree.getExpandIds(); - fetchPost("/api/storage/setOutlineStorage", { - docID: this.blockId, - val: { - expandIds: expandIds - } - }); - } } }); // 为了快捷键的 dispatch @@ -236,15 +207,6 @@ export class Outline extends Model { // 普通的全部展开按钮 options.tab.panelElement.querySelector('[data-type="expand"]').addEventListener("click", () => { this.tree.expandAll(); - // 保存展开状态 - if (!this.isPreview) { - fetchPost("/api/storage/setOutlineStorage", { - docID: this.blockId, - val: { - expandIds: this.tree.getExpandIds() - } - }); - } }); // 保持当前标题展开功能 @@ -255,27 +217,37 @@ export class Outline extends Model { if (!iconElement) { return; } - - // 确保存储对象存在 - if (!window.siyuan.storage[Constants.LOCAL_OUTLINE]) { - window.siyuan.storage[Constants.LOCAL_OUTLINE] = {}; - } - if (iconElement.classList.contains("block__icon--active")) { iconElement.classList.remove("block__icon--active"); window.siyuan.storage[Constants.LOCAL_OUTLINE].keepCurrentExpand = false; } else { iconElement.classList.add("block__icon--active"); window.siyuan.storage[Constants.LOCAL_OUTLINE].keepCurrentExpand = true; - // 立即展开到真正的当前标题 - this.expandToCurrentHeading(); + 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); + } } - // 保存keepCurrentExpand状态到localStorage 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; + if (target.tagName === "INPUT") { + return; + } let isFocus = true; while (target && !target.isEqualNode(options.tab.panelElement)) { if (target.classList.contains("block__icon")) { @@ -285,14 +257,11 @@ export class Outline extends Model { getDockByType("outline").toggleModel("outline", false, true); break; case "search": - // 显示输入框并选中 - if (this.searchInput) { - this.searchInput.classList.remove("fn__none"); - this.searchInput.select(); - } + inputElement.classList.remove("fn__none"); + inputElement.select(); break; case "expandLevel": - this.showExpandLevelMenu(event); + this.showExpandLevelMenu(target); event.preventDefault(); event.stopPropagation(); break; @@ -341,423 +310,11 @@ export class Outline extends Model { if (storageData && storageData.expandIds) { this.tree.setExpandIds(storageData.expandIds); } - // 若存在筛选关键词,初始化后应用一次筛选 - if (this.searchKeyword) { - this.applyFilter(this.searchKeyword); - } }); } }); } - /** - * 切换同层级的所有标题的展开/折叠状态(基于标题级别而不是DOM层级) - * @param element 当前点击的元素 - */ - private collapseSameLevel(element: HTMLElement) { - if (!element) { - return; - } - - // 获取当前元素的标题级别 - const currentHeadingLevel = this.getHeadingLevel(element); - if (currentHeadingLevel === 0) { - return; // 如果不是有效的标题,直接返回 - } - - // 获取所有相同标题级别的元素 - const allListItems = this.element.querySelectorAll("li.b3-list-item"); - const sameLevelElements: HTMLElement[] = []; - - allListItems.forEach(item => { - const headingLevel = this.getHeadingLevel(item as HTMLElement); - if (headingLevel === currentHeadingLevel) { - sameLevelElements.push(item as HTMLElement); - } - }); - - // 过滤出有子元素的项 - const elementsWithChildren = sameLevelElements.filter(item => - item.nextElementSibling && item.nextElementSibling.tagName === "UL" - ); - - if (elementsWithChildren.length === 0) { - return; - } - - // 检查当前状态:如果大部分元素是展开的,则执行折叠;否则执行展开 - let expandedCount = 0; - elementsWithChildren.forEach(item => { - const arrowElement = item.querySelector(".b3-list-item__arrow"); - if (arrowElement && arrowElement.classList.contains("b3-list-item__arrow--open")) { - expandedCount++; - } - }); - - // 如果超过一半的元素是展开的,则折叠所有;否则展开所有 - const shouldCollapse = expandedCount > elementsWithChildren.length / 2; - - elementsWithChildren.forEach(item => { - const arrowElement = item.querySelector(".b3-list-item__arrow"); - - if (shouldCollapse) { - // 折叠 - if (arrowElement && arrowElement.classList.contains("b3-list-item__arrow--open")) { - arrowElement.classList.remove("b3-list-item__arrow--open"); - item.nextElementSibling.classList.add("fn__none"); - } - } else { - // 展开 - if (arrowElement && !arrowElement.classList.contains("b3-list-item__arrow--open")) { - arrowElement.classList.add("b3-list-item__arrow--open"); - item.nextElementSibling.classList.remove("fn__none"); - } - } - }); - - // 触发折叠状态变化事件,保存状态 - if (this.tree.onToggleChange) { - this.tree.onToggleChange(); - } - } - - /** - * 获取元素在大纲中的层级深度 - * @param element li元素 - * @returns 层级深度(从0开始) - */ - private getElementLevel(element: HTMLElement): number { - let level = 0; - let parent = element.parentElement; - - while (parent && !parent.classList.contains("fn__flex-1")) { - if (parent.tagName === "UL" && !parent.classList.contains("b3-list")) { - level++; - } - parent = parent.parentElement; - } - - return level; - } - - /** - * 获取所有同层级的元素 - * @param level 目标层级 - * @returns 同层级的li元素数组 - */ - private getSameLevelElements(level: number): HTMLElement[] { - const allListItems = this.element.querySelectorAll("li.b3-list-item"); - const sameLevelElements: HTMLElement[] = []; - - allListItems.forEach(item => { - if (this.getElementLevel(item as HTMLElement) === level) { - sameLevelElements.push(item as HTMLElement); - } - }); - - return sameLevelElements; - } - - /** - * 展开到当前标题路径 - */ - private expandToCurrentHeading() { - // 获取当前真正的标题ID - this.getCurrentHeadingId((currentHeadingId) => { - if (currentHeadingId) { - this.expandToHeadingByIdSmart(currentHeadingId); - } - }); - } - - /** - * 智能展开到指定标题ID,保持兄弟分支的原有状态 - */ - private expandToHeadingByIdSmart(headingId: string) { - // 确保目标标题在大纲中可见 - this.ensureHeadingVisibleSmart(headingId); - - // 设置为当前焦点(这会触发自动展开) - this.setCurrentById(headingId); - } - - /** - * 智能确保指定标题在大纲中可见(展开其所有父级路径,但保持兄弟分支状态) - */ - private ensureHeadingVisibleSmart(headingId: string) { - const targetElement = this.element.querySelector(`.b3-list-item[data-node-id="${headingId}"]`) as HTMLElement; - if (targetElement) { - this.expandPathToElement(targetElement); - - // 额外检查:确保目标元素真的可见 - setTimeout(() => { - const checkElement = this.element.querySelector(`.b3-list-item[data-node-id="${headingId}"]`) as HTMLElement; - if (checkElement && checkElement.offsetParent === null) { - // 如果元素仍然不可见,再次尝试展开路径 - this.expandPathToElement(checkElement); - } - }, 50); - } - } - - /** - * 获取当前真正的标题ID - */ - private getCurrentHeadingId(callback: (id: string) => void) { - // 首先尝试从编辑器获取当前光标位置的块 - let currentBlockId: string = null; - - 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) { - currentBlockId = blockElement.getAttribute("data-node-id"); - return true; - } - } - } - }); - - if (currentBlockId) { - // 如果当前块就是标题,直接使用 - const currentBlockElement = document.querySelector(`[data-node-id="${currentBlockId}"]`); - if (currentBlockElement && currentBlockElement.getAttribute("data-type") === "NodeHeading") { - callback(currentBlockId); - return; - } - - // 如果当前块不是标题,查找前面最近的标题 - let previousElement = getPreviousBlock(currentBlockElement as HTMLElement); - while (previousElement) { - if (previousElement.getAttribute("data-type") === "NodeHeading") { - callback(previousElement.getAttribute("data-node-id")); - return; - } - previousElement = getPreviousBlock(previousElement); - } - - // 如果没有找到前面的标题,通过API获取面包屑 - fetchPost("/api/block/getBlockBreadcrumb", { - id: currentBlockId, - excludeTypes: [] - }, (response) => { - const headingItem = response.data.reverse().find((item: IBreadcrumb) => { - return item.type === "NodeHeading"; - }); - if (headingItem) { - callback(headingItem.id); - } - }); - } else { - // 如果无法获取当前块,使用现有的focus元素 - const currentElement = this.element.querySelector(".b3-list-item--focus"); - if (currentElement) { - callback(currentElement.getAttribute("data-node-id")); - } - } - } - - /** - * 展开到指定标题ID - */ - private expandToHeadingById(headingId: string) { - // 确保目标标题在大纲中可见 - this.ensureHeadingVisible(headingId); - - // 设置为当前焦点(这会触发自动展开) - this.setCurrentById(headingId); - } - - /** - * 确保指定标题在大纲中可见(展开其所有父级路径) - */ - private ensureHeadingVisible(headingId: string) { - const targetElement = this.element.querySelector(`.b3-list-item[data-node-id="${headingId}"]`) as HTMLElement; - if (targetElement) { - this.expandPathToElement(targetElement); - } - } - - /** - * 展开到指定元素的路径,智能处理兄弟分支的折叠状态 - */ - private expandPathToElement(element: HTMLElement) { - if (!element) { - return; - } - - // 收集所有需要展开的ul元素路径,以及它们的折叠状态 - const pathToExpand: Array<{ ul: HTMLElement, wasCollapsed: boolean, parentLi: HTMLElement }> = []; - let current = element.parentElement; // 从父级ul开始 - - while (current && !current.classList.contains("fn__flex-1")) { - if (current.tagName === "UL" && !current.classList.contains("b3-list")) { - // 这是一个可折叠的ul元素 - const parentLi = current.previousElementSibling as HTMLElement; - const wasCollapsed = current.classList.contains("fn__none"); - - pathToExpand.push({ - ul: current, - wasCollapsed: wasCollapsed, - parentLi: parentLi - }); - } - current = current.parentElement; - } - - // 从最外层开始展开,确保每一层都能正确展开 - pathToExpand.reverse().forEach((pathItem, index) => { - const { ul, wasCollapsed, parentLi } = pathItem; - - if (ul.classList.contains("fn__none")) { - ul.classList.remove("fn__none"); - - // 设置箭头状态 - if (parentLi && parentLi.classList.contains("b3-list-item")) { - const arrowElement = parentLi.querySelector(".b3-list-item__arrow"); - if (arrowElement && !arrowElement.classList.contains("b3-list-item__arrow--open")) { - arrowElement.classList.add("b3-list-item__arrow--open"); - } - } - } - - // 如果这个节点原本是折叠的,需要折叠其他分支 - if (wasCollapsed && parentLi) { - this.collapseSiblingsExceptPath(parentLi, pathToExpand.slice(index + 1)); - } - }); - - // 保存展开状态 - if (!this.isPreview) { - fetchPost("/api/storage/setOutlineStorage", { - docID: this.blockId, - val: { - expandIds: this.tree.getExpandIds() - } - }); - } - } - - /** - * 折叠指定li下的所有兄弟分支,除了通往目标的路径 - * @param parentLi 父级li元素 - * @param targetPath 目标路径上的ul元素列表 - */ - private collapseSiblingsExceptPath(parentLi: HTMLElement, targetPath: Array<{ ul: HTMLElement, wasCollapsed: boolean, parentLi: HTMLElement }>) { - // 获取父li下的直接ul子元素 - const directChildUl = parentLi.nextElementSibling; - if (!directChildUl || directChildUl.tagName !== "UL") { - return; - } - - // 获取目标路径上的下一个ul(如果存在) - const nextTargetUl = targetPath.length > 0 ? targetPath[0].ul : null; - - // 遍历所有直接子li元素 - const childLiElements = directChildUl.children; - for (let i = 0; i < childLiElements.length; i++) { - const childLi = childLiElements[i] as HTMLElement; - if (!childLi.classList.contains("b3-list-item")) { - continue; - } - - // 获取这个li的子ul - const childUl = childLi.nextElementSibling; - if (childUl && childUl.tagName === "UL") { - // 如果这个ul不是目标路径上的ul,则折叠它 - if (childUl !== nextTargetUl) { - if (!childUl.classList.contains("fn__none")) { - childUl.classList.add("fn__none"); - - // 更新箭头状态 - const arrowElement = childLi.querySelector(".b3-list-item__arrow"); - if (arrowElement && arrowElement.classList.contains("b3-list-item__arrow--open")) { - arrowElement.classList.remove("b3-list-item__arrow--open"); - } - } - } - } - } - } - - /** - * 获取标题元素的实际标题级别(H1=1, H2=2, 等等) - * @param element li元素 - * @returns 标题级别(1-6) - */ - private getHeadingLevel(element: HTMLElement): number { - const subtype = element.getAttribute("data-subtype"); - if (!subtype) { - return 0; - } - - // 从data-subtype属性中提取标题级别(h1=1, h2=2, h3=3, 等等) - const match = subtype.match(/^h(\d+)$/); - if (match) { - return parseInt(match[1], 10); - } - - return 0; - } - - /** - * 展开到指定标题级别 - * @param targetLevel 目标标题级别,1-6级(H1-H6),6级表示全部展开 - */ - private expandToLevel(targetLevel: number) { - if (targetLevel >= 6) { - // 全部展开 - this.tree.expandAll(); - } else { - // 展开到指定标题级别 - const allListItems = this.element.querySelectorAll("li.b3-list-item"); - - allListItems.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"); - } - } - }); - } - - // 保存状态 - if (this.tree.onToggleChange) { - this.tree.onToggleChange(); - } - } - - /** - * 显示展开层级菜单 - */ - private showExpandLevelMenu(event: MouseEvent) { - 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); - } - window.siyuan.menus.menu.popup({ - x: event.clientX - 11, - y: event.clientY + 11, - w: 12 - }); - return window.siyuan.menus.menu; - } - private bindSort() { this.element.addEventListener("mousedown", (event: MouseEvent) => { const item = hasClosestByClassName(event.target as HTMLElement, "b3-list-item"); @@ -876,9 +433,6 @@ export class Outline extends Model { if (hasChange) { this.element.setAttribute("data-loading", "true"); - // 保存拖拽前的折叠状态 - const expandIdsBeforeDrag = this.tree.getExpandIds(); - transaction(editor, [{ action: "moveOutlineHeading", id: item.dataset.nodeId, @@ -891,16 +445,6 @@ export class Outline extends Model { parentID: undoParentID, }]); - // 拖拽操作完成后恢复折叠状态 - setTimeout(() => { - fetchPost("/api/storage/setOutlineStorage", { - docID: this.blockId, - val: { - expandIds: expandIdsBeforeDrag - } - }); - }, 300); - // 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"); @@ -1037,46 +581,24 @@ export class Outline extends Model { this.element.querySelectorAll(".b3-list-item.b3-list-item--focus").forEach(item => { item.classList.remove("b3-list-item--focus"); }); - - // 如果启用了保持当前标题展开功能,先确保目标标题可见 - if (window.siyuan.storage[Constants.LOCAL_OUTLINE]?.keepCurrentExpand) { - this.ensureHeadingVisibleSmart(id); - } - let currentElement = this.element.querySelector(`.b3-list-item[data-node-id="${id}"]`) as HTMLElement; - - // 如果元素仍然不可见,尝试多次查找和展开 - let retryCount = 0; - const maxRetries = 3; - - const trySetCurrent = () => { - currentElement = this.element.querySelector(`.b3-list-item[data-node-id="${id}"]`) as HTMLElement; - - while (currentElement && currentElement.clientHeight === 0 && retryCount < maxRetries) { - // 如果启用了保持当前标题展开功能,再次尝试展开路径 - if (window.siyuan.storage[Constants.LOCAL_OUTLINE]?.keepCurrentExpand) { - this.expandPathToElement(currentElement); - } - currentElement = currentElement.parentElement?.previousElementSibling as HTMLElement; - retryCount++; + 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; } - - 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)); - } else if (retryCount < maxRetries && window.siyuan.storage[Constants.LOCAL_OUTLINE]?.keepCurrentExpand) { - // 如果还没找到元素且启用了展开功能,延迟重试 - setTimeout(() => { - retryCount++; - this.ensureHeadingVisibleSmart(id); - setTimeout(trySetCurrent, 50); - }, 50); + } else { + while (currentElement && currentElement.clientHeight === 0) { + currentElement = currentElement.parentElement.previousElementSibling as HTMLElement; } - }; - - trySetCurrent(); + } + 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) { @@ -1112,17 +634,9 @@ export class Outline extends Model { this.tree.setExpandIds(storageData.expandIds); } else { this.tree.expandAll(); - // 保存展开全部的状态到新的存储 - fetchPost("/api/storage/setOutlineStorage", { - docID: this.blockId, - val: { - expandIds: this.tree.getExpandIds() - } - }); } - // 若当前存在筛选词,更新后重新应用筛选 - if (this.searchKeyword) { - this.applyFilter(this.searchKeyword); + if ((this.headerElement.querySelector("input.b3-text-field.search__label") as HTMLInputElement).value) { + this.setFilter(); } }); } @@ -1142,143 +656,202 @@ export class Outline extends Model { this.element.removeAttribute("data-loading"); } + public saveExpendIds() { + if (!this.isPreview) { + fetchPost("/api/storage/setOutlineStorage", { + docID: this.blockId, + val: { + expandIds: this.tree.getExpandIds() + } + }); + } + } + /** * 应用大纲筛选 */ - private applyFilter(keyword: string) { - const kw = keyword.trim(); - if (!kw) { - this.clearFilter(); + 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) { + 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"); + // 折叠但不完全隐藏,保持子项可访问性 + 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.firstElementChild); return; } - this.searchKeyword = kw; - // 首次筛选时记录折叠状态 - if (!this.preFilterExpandIds) { - try { - this.preFilterExpandIds = this.tree.getExpandIds(); - } catch (e) { - this.preFilterExpandIds = []; - } - } - - // 递归过滤 DOM - const rootUL = this.element.querySelector("ul.b3-list"); - if (!rootUL) return; - - const kwLower = kw.toLowerCase(); - const matchedItems = new Set(); // 记录所有命中的标题 - - // 第一遍:收集所有命中的项目 - const collectMatches = (ul: Element) => { - ul.querySelectorAll("li.b3-list-item").forEach(li => { - const textEl = (li as HTMLElement).querySelector(".b3-list-item__text") as HTMLElement; - const textContent = (textEl?.textContent || "").trim().toLowerCase(); - if (textContent.includes(kwLower)) { - matchedItems.add(li as HTMLElement); - } - }); - }; - collectMatches(rootUL); - - // 展开所有命中项的父级路径 - matchedItems.forEach(matchedLi => { - this.expandPathToElement(matchedLi); - }); - - const processUL = (ul: Element): { hasMatch: boolean, hasChildMatch: boolean } => { - let hasMatch = false; - let hasChildMatch = false; - const children = ul.querySelectorAll(":scope > li.b3-list-item"); - - children.forEach((li) => { - const textEl = (li as HTMLElement).querySelector(".b3-list-item__text") as HTMLElement; - const textContent = (textEl?.textContent || "").trim().toLowerCase(); - const selfMatch = textContent.includes(kwLower); - const next = (li as HTMLElement).nextElementSibling; - - let childResult = { hasMatch: false, hasChildMatch: false }; - if (next && next.tagName === "UL") { - childResult = processUL(next); - } - - if (selfMatch) { - // 当前标题命中 - (li as HTMLElement).style.display = ""; - hasMatch = true; - - if (next && next.tagName === "UL") { - (next as HTMLElement).style.display = ""; - - if (childResult.hasMatch || childResult.hasChildMatch) { - // 子项也有命中,保持展开状态,但隐藏未命中的子项由子级处理 - const arrow = li.querySelector(".b3-list-item__arrow"); - if (arrow) { - arrow.classList.add("b3-list-item__arrow--open"); - } - } else { - // 子项无命中,折叠所有子项但保持可展开 - const arrow = li.querySelector(".b3-list-item__arrow"); - if (arrow) { - arrow.classList.remove("b3-list-item__arrow--open"); - } - // 折叠但不完全隐藏,保持子项可访问性 - next.classList.add("fn__none"); - // 确保所有子项内容保持可见状态,用户可以手动展开查看 - next.querySelectorAll("li.b3-list-item").forEach(childLi => { - (childLi as HTMLElement).style.display = ""; - }); - next.querySelectorAll("ul").forEach(childUl => { - (childUl as HTMLElement).style.display = ""; - // 移除子ul的fn__none,保证嵌套结构的可访问性 - childUl.classList.remove("fn__none"); - }); - } - } - } else if (childResult.hasMatch || childResult.hasChildMatch) { - // 当前标题未命中,但子级有命中 - (li as HTMLElement).style.display = ""; - hasChildMatch = true; - - if (next && next.tagName === "UL") { - (next as HTMLElement).style.display = ""; - // 展开以显示命中的子项 - const arrow = li.querySelector(".b3-list-item__arrow"); - if (arrow) { - arrow.classList.add("b3-list-item__arrow--open"); - } - } - } else { - // 当前标题和子级都未命中,隐藏 - (li as HTMLElement).style.display = "none"; - if (next && next.tagName === "UL") { - (next as HTMLElement).style.display = "none"; - } - } - }); - - return { hasMatch, hasChildMatch }; - }; - - processUL(rootUL); - } /** - * 清除大纲筛选并恢复展开状态 - */ - private clearFilter() { - this.searchKeyword = ""; - // 还原 display - this.element.querySelectorAll("li.b3-list-item, .fn__flex-1 ul").forEach((el) => { - (el as HTMLElement).style.display = ""; - }); // 恢复折叠状态 - if (this.preFilterExpandIds) { - this.tree.setExpandIds(this.preFilterExpandIds); - } + this.tree.setExpandIds(this.preFilterExpandIds); this.preFilterExpandIds = null; - // 复位图标状态 - const filterIconElement = this.headerElement.querySelector('[data-type="search"]') as HTMLElement; - if (filterIconElement) { - filterIconElement.classList.remove("block__icon--active"); - filterIconElement.setAttribute("aria-label", window.siyuan.languages.filter); + } + + /** + * 获取标题元素的实际标题级别(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"); + } + } + }); + } + } + + /** + * 显示展开层级菜单 + */ + 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; + } + + /** + * 切换同层级的所有标题的展开/折叠状态(基于标题级别而不是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"); + } + } + }); + } + + 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"); } } @@ -1297,14 +870,14 @@ export class Outline extends Model { } const currentLevel = this.getHeadingLevel(element); - + window.siyuan.menus.menu.remove(); // 升级 if (currentLevel > 1) { window.siyuan.menus.menu.append(new MenuItem({ icon: "iconUp", - label: "升级", + label: window.siyuan.languages.upgrade, click: () => this.upgradeHeading(element) }).element); } @@ -1312,8 +885,8 @@ export class Outline extends Model { // 降级 if (currentLevel < 6) { window.siyuan.menus.menu.append(new MenuItem({ - icon: "iconDown", - label: "降级", + icon: "iconDown", + label: window.siyuan.languages.downgrade, click: () => this.downgradeHeading(element) }).element); } @@ -1354,14 +927,14 @@ export class Outline extends Model { // 在前面插入同级标题 window.siyuan.menus.menu.append(new MenuItem({ icon: "iconBefore", - label: "在前面插入同级标题", + label: window.siyuan.languages.insertSameLevelHeadingBefore, click: () => this.insertHeadingBefore(element) }).element); // 在后面插入同级标题 window.siyuan.menus.menu.append(new MenuItem({ icon: "iconAfter", - label: "在后面插入同级标题", + label: window.siyuan.languages.insertSameLevelHeadingAfter, click: () => this.insertHeadingAfter(element) }).element); @@ -1369,13 +942,12 @@ export class Outline extends Model { if (currentLevel < 6) { // 只有当前级别小于6时才能添加子标题 window.siyuan.menus.menu.append(new MenuItem({ icon: "iconAdd", - label: "添加子标题", + label: window.siyuan.languages.addChildHeading, click: () => this.addChildHeading(element) }).element); } - window.siyuan.menus.menu.append(new MenuItem({ type: "separator" }).element); - + window.siyuan.menus.menu.append(new MenuItem({type: "separator"}).element); // 复制带子标题 window.siyuan.menus.menu.append(new MenuItem({ @@ -1404,52 +976,48 @@ export class Outline extends Model { // 展开子标题 window.siyuan.menus.menu.append(new MenuItem({ icon: "iconExpand", - label: "展开子标题", - click: () => this.expandChildren(element) + label: window.siyuan.languages.expandChildHeading, + accelerator: updateHotkeyTip("⌘") + window.siyuan.languages.click, + click: () => this.collapseChildren(element, true) }).element); // 折叠子标题 window.siyuan.menus.menu.append(new MenuItem({ icon: "iconContract", - label: "折叠子标题", - click: () => this.collapseChildren(element) + label: window.siyuan.languages.foldChildHeading, + accelerator: updateHotkeyTip("⌘") + window.siyuan.languages.click, + click: () => this.collapseChildren(element, false) }).element); // 展开同级标题 window.siyuan.menus.menu.append(new MenuItem({ icon: "iconExpand", - label: "展开同级标题", - click: () => this.expandSameLevel(element) + label: window.siyuan.languages.expandSameLevelHeading, + accelerator: updateHotkeyTip("⌥") + window.siyuan.languages.click, + click: () => this.collapseSameLevel(element, true) }).element); // 折叠同级标题 window.siyuan.menus.menu.append(new MenuItem({ - icon: "iconContract", - label: "折叠同级标题", - click: () => this.collapseSameLevel(element) + icon: "iconContract", + label: window.siyuan.languages.foldSameLevelHeading, + accelerator: updateHotkeyTip("⌥") + window.siyuan.languages.click, + click: () => this.collapseSameLevel(element, false) }).element); // 全部展开 window.siyuan.menus.menu.append(new MenuItem({ icon: "iconExpand", - label: "全部展开", + label: window.siyuan.languages.expandAll, click: () => { this.tree.expandAll(); - if (!this.isPreview) { - fetchPost("/api/storage/setOutlineStorage", { - docID: this.blockId, - val: { - expandIds: this.tree.getExpandIds() - } - }); - } } }).element); // 全部折叠 window.siyuan.menus.menu.append(new MenuItem({ icon: "iconContract", - label: "全部折叠", + label: window.siyuan.languages.foldAll, click: () => this.tree.collapseAll() }).element); @@ -1465,7 +1033,7 @@ export class Outline extends Model { private upgradeHeading(element: HTMLElement) { const id = element.getAttribute("data-node-id"); const currentLevel = this.getHeadingLevel(element); - + if (currentLevel <= 1) { return; } @@ -1500,7 +1068,7 @@ export class Outline extends Model { private downgradeHeading(element: HTMLElement) { const id = element.getAttribute("data-node-id"); const currentLevel = this.getHeadingLevel(element); - + if (currentLevel >= 6) { return; } @@ -1564,7 +1132,7 @@ export class Outline extends Model { // 获取父节点ID,如果当前标题是顶级标题,使用文档根ID const parentElement = element.parentElement; let parentID = this.blockId; // 默认为文档根ID - + if (parentElement && parentElement.tagName === "UL") { const parentLi = parentElement.previousElementSibling; if (parentLi && parentLi.classList.contains("b3-list-item")) { @@ -1574,7 +1142,7 @@ export class Outline extends Model { fetchPost("/api/block/appendBlock", { data: headingPrefix, - dataType: "markdown", + dataType: "markdown", parentID: parentID }, (response) => { if (response.code === 0) { @@ -1594,7 +1162,7 @@ export class Outline extends Model { */ private deleteHeading(element: HTMLElement) { const id = element.getAttribute("data-node-id"); - + // 找到编辑器实例 let editor: any; getAllModels().editor.find(editItem => { @@ -1620,124 +1188,6 @@ export class Outline extends Model { }); } - /** - * 展开子标题 - */ - private expandChildren(element: HTMLElement) { - const nextElement = element.nextElementSibling; - if (nextElement && nextElement.tagName === "UL") { - const arrowElement = element.querySelector(".b3-list-item__arrow"); - if (arrowElement) { - arrowElement.classList.add("b3-list-item__arrow--open"); - nextElement.classList.remove("fn__none"); - - // 递归展开所有子元素 - const expandAllChildren = (ul: Element) => { - ul.querySelectorAll(":scope > li.b3-list-item").forEach(li => { - const childUl = li.nextElementSibling; - if (childUl && childUl.tagName === "UL") { - const childArrow = li.querySelector(".b3-list-item__arrow"); - if (childArrow) { - childArrow.classList.add("b3-list-item__arrow--open"); - childUl.classList.remove("fn__none"); - expandAllChildren(childUl); - } - } - }); - }; - expandAllChildren(nextElement); - - // 保存展开状态 - if (this.tree.onToggleChange) { - this.tree.onToggleChange(); - } - } - } - } - - /** - * 折叠子标题 - */ - private collapseChildren(element: HTMLElement) { - const nextElement = element.nextElementSibling; - if (nextElement && nextElement.tagName === "UL") { - const arrowElement = element.querySelector(".b3-list-item__arrow"); - if (arrowElement) { - arrowElement.classList.remove("b3-list-item__arrow--open"); - nextElement.classList.add("fn__none"); - - // 保存折叠状态 - if (this.tree.onToggleChange) { - this.tree.onToggleChange(); - } - } - } - } - - /** - * 展开同级标题 - 基于标题级别而不是DOM层级 - */ - private expandSameLevel(element: HTMLElement) { - const currentHeadingLevel = this.getHeadingLevel(element); - if (currentHeadingLevel === 0) { - return; // 如果不是有效的标题,直接返回 - } - - // 获取所有相同标题级别的元素 - const allListItems = this.element.querySelectorAll("li.b3-list-item"); - const sameLevelElements: HTMLElement[] = []; - - allListItems.forEach(item => { - const headingLevel = this.getHeadingLevel(item as HTMLElement); - if (headingLevel === currentHeadingLevel) { - sameLevelElements.push(item as HTMLElement); - } - }); - - // 检查当前状态:如果大部分同级标题是展开的,则折叠;否则展开 - let expandedCount = 0; - const elementsWithChildren = sameLevelElements.filter(item => { - const nextElement = item.nextElementSibling; - return nextElement && nextElement.tagName === "UL"; - }); - - elementsWithChildren.forEach(item => { - const arrowElement = item.querySelector(".b3-list-item__arrow"); - if (arrowElement && arrowElement.classList.contains("b3-list-item__arrow--open")) { - expandedCount++; - } - }); - - // 如果超过一半的元素是展开的,则折叠所有;否则展开所有 - const shouldExpand = expandedCount <= elementsWithChildren.length / 2; - - elementsWithChildren.forEach(item => { - const nextElement = item.nextElementSibling; - const arrowElement = item.querySelector(".b3-list-item__arrow"); - - if (arrowElement && nextElement && nextElement.tagName === "UL") { - if (shouldExpand) { - // 展开 - if (!arrowElement.classList.contains("b3-list-item__arrow--open")) { - arrowElement.classList.add("b3-list-item__arrow--open"); - nextElement.classList.remove("fn__none"); - } - } else { - // 折叠 - if (arrowElement.classList.contains("b3-list-item__arrow--open")) { - arrowElement.classList.remove("b3-list-item__arrow--open"); - nextElement.classList.add("fn__none"); - } - } - } - }); - - // 保存展开状态 - if (this.tree.onToggleChange) { - this.tree.onToggleChange(); - } - } - /** * 复制标题带子标题 */ @@ -1761,7 +1211,7 @@ export class Outline extends Model { } fetchPost("/api/block/getHeadingChildrenDOM", { - id: id, + id: id, removeFoldAttr: false }, (response) => { if (isInAndroid()) { @@ -1797,7 +1247,7 @@ export class Outline extends Model { } fetchPost("/api/block/getHeadingChildrenDOM", { - id: id, + id: id, removeFoldAttr: false }, (response) => { if (isInAndroid()) { @@ -1807,7 +1257,7 @@ export class Outline extends Model { } else { writeText(response.data + Constants.ZWSP); } - + // 复制完成后删除标题及其子标题 fetchPost("/api/block/getHeadingDeleteTransaction", { id: id, @@ -1895,7 +1345,7 @@ export class Outline extends Model { if (!currentExpandIds.includes(id)) { currentExpandIds.push(id); this.tree.setExpandIds(currentExpandIds); - + // 保存展开状态到持久化存储 if (!this.isPreview) { fetchPost("/api/storage/setOutlineStorage", { @@ -1906,7 +1356,7 @@ export class Outline extends Model { }); } } - + // 插入成功后,聚焦到新插入的标题 const newId = response.data[0].doOperations[0].id; openFileById({ diff --git a/app/src/protyle/util/compatibility.ts b/app/src/protyle/util/compatibility.ts index 9e227c5cc..f418110cf 100644 --- a/app/src/protyle/util/compatibility.ts +++ b/app/src/protyle/util/compatibility.ts @@ -388,7 +388,7 @@ export const getLocalStorage = (cb: () => void) => { defaultStorage[Constants.LOCAL_AI] = []; // {name: "", memo: ""} defaultStorage[Constants.LOCAL_PLUGIN_DOCKS] = {}; // { pluginName: {dockId: IPluginDockTab}} defaultStorage[Constants.LOCAL_PLUGINTOPUNPIN] = []; - defaultStorage[Constants.LOCAL_OUTLINE] = {keepExpand: true, expand: {}}; + defaultStorage[Constants.LOCAL_OUTLINE] = {keepCurrentExpand: false}; defaultStorage[Constants.LOCAL_FILEPOSITION] = {}; // {id: IScrollAttr} defaultStorage[Constants.LOCAL_DIALOGPOSITION] = {}; // {id: IPosition} defaultStorage[Constants.LOCAL_HISTORY] = { diff --git a/app/src/util/Tree.ts b/app/src/util/Tree.ts index 07bea0cb4..0e1927edf 100644 --- a/app/src/util/Tree.ts +++ b/app/src/util/Tree.ts @@ -13,12 +13,11 @@ export class Tree { private topExtHTML: string; public click: (element: Element, event?: MouseEvent) => void; - private ctrlClick: (element: HTMLElement) => void; + private ctrlClick: (element: HTMLElement, event: MouseEvent) => void; private toggleClick: (element: Element) => void; private shiftClick: (element: HTMLElement) => void; - private altClick: (element: HTMLElement, event?: MouseEvent) => void; + private altClick: (element: HTMLElement, event: MouseEvent) => void; private rightClick: (element: HTMLElement, event: MouseEvent) => void; - public onToggleChange: () => void; constructor(options: { element: HTMLElement, @@ -26,12 +25,11 @@ export class Tree { blockExtHTML?: string, topExtHTML?: string, click?(element: HTMLElement, event: MouseEvent): void - ctrlClick?(element: HTMLElement): void - altClick?(element: HTMLElement, event?: MouseEvent): void + ctrlClick?(element: HTMLElement, event: MouseEvent): void + altClick?(element: HTMLElement, event: MouseEvent): void shiftClick?(element: HTMLElement): void toggleClick?(element: HTMLElement): void rightClick?(element: HTMLElement, event: MouseEvent): void - onToggleChange?: () => void }) { this.click = options.click; this.ctrlClick = options.ctrlClick; @@ -39,7 +37,6 @@ export class Tree { this.shiftClick = options.shiftClick; this.rightClick = options.rightClick; this.toggleClick = options.toggleClick; - this.onToggleChange = options.onToggleChange; this.element = options.element; this.blockExtHTML = options.blockExtHTML; this.topExtHTML = options.topExtHTML; @@ -207,18 +204,7 @@ data-def-path="${item.defPath}"> this.element.addEventListener("contextmenu", (event) => { let target = event.target as HTMLElement; while (target && !target.isEqualNode(this.element)) { - if (target.classList.contains("b3-list-item__toggle") && !target.classList.contains("fn__hidden")) { - // 右键点击toggle时,展开所有子标题 - this.expandAllChildren(target.parentElement); - this.setCurrent(target.parentElement); - // 触发折叠状态变化事件 - if (this.onToggleChange) { - this.onToggleChange(); - } - event.preventDefault(); - event.stopPropagation(); - break; - } else if (target.tagName === "LI" && this.rightClick) { + if (target.tagName === "LI" && this.rightClick) { this.rightClick(target, event); event.preventDefault(); event.stopPropagation(); @@ -230,13 +216,10 @@ data-def-path="${item.defPath}"> this.element.addEventListener("click", (event: MouseEvent & { target: HTMLElement }) => { let target = event.target as HTMLElement; while (target && !target.isEqualNode(this.element)) { - if (target.classList.contains("b3-list-item__toggle") && !target.classList.contains("fn__hidden")) { + if (target.classList.contains("b3-list-item__toggle") && + !target.classList.contains("fn__hidden") && !window.siyuan.ctrlIsPressed && !window.siyuan.altIsPressed) { this.toggleBlocks(target.parentElement); this.setCurrent(target.parentElement); - // 触发折叠状态变化事件 - if (this.onToggleChange) { - this.onToggleChange(); - } event.preventDefault(); break; } @@ -253,7 +236,7 @@ data-def-path="${item.defPath}"> this.setCurrent(target); if (target.getAttribute("data-node-id") || target.getAttribute("data-treetype") === "tag") { if (this.ctrlClick && window.siyuan.ctrlIsPressed) { - this.ctrlClick(target); + this.ctrlClick(target, event); } else if (this.altClick && window.siyuan.altIsPressed) { this.altClick(target, event); } else if (this.shiftClick && window.siyuan.shiftIsPressed) { @@ -289,86 +272,6 @@ data-def-path="${item.defPath}"> }); } - public expandAllChildren(liElement: Element) { - if (!liElement || !liElement.nextElementSibling) { - return; - } - - // 获取当前项的子列表 - const nextElement = liElement.nextElementSibling; - if (!nextElement || nextElement.tagName !== "UL") { - return; - } - - // 检查子元素的展开状态,如果所有子元素都已展开,则折叠;否则展开所有 - const areAllChildrenExpanded = this.areAllChildrenExpanded(nextElement); - - // 确保当前元素保持展开状态 - const svgElement = liElement.firstElementChild.firstElementChild; - if (!svgElement.classList.contains("b3-list-item__arrow--open")) { - svgElement.classList.add("b3-list-item__arrow--open"); - nextElement.classList.remove("fn__none"); - if (nextElement.nextElementSibling && nextElement.nextElementSibling.tagName === "UL") { - nextElement.nextElementSibling.classList.remove("fn__none"); - } - } - - if (areAllChildrenExpanded) { - // 折叠所有子元素,但保持当前元素展开 - this.collapseAllChildren(nextElement); - } else { - // 展开所有子元素 - this.expandAllChildrenRecursive(nextElement); - } - } - - private areAllChildrenExpanded(ulElement: Element): boolean { - const childItems = ulElement.querySelectorAll(":scope > li"); - for (const childLi of childItems) { - const arrow = childLi.querySelector(".b3-list-item__arrow"); - if (arrow && !arrow.classList.contains("b3-list-item__arrow--open")) { - return false; - } - const childUl = childLi.nextElementSibling; - if (childUl && childUl.tagName === "UL") { - if (!this.areAllChildrenExpanded(childUl)) { - return false; - } - } - } - return true; - } - - private expandAllChildrenRecursive(ulElement: Element) { - ulElement.classList.remove("fn__none"); - const childItems = ulElement.querySelectorAll(":scope > li"); - childItems.forEach(childLi => { - const arrow = childLi.querySelector(".b3-list-item__arrow"); - if (arrow) { - arrow.classList.add("b3-list-item__arrow--open"); - } - const childUl = childLi.nextElementSibling; - if (childUl && childUl.tagName === "UL") { - this.expandAllChildrenRecursive(childUl); - } - }); - } - - private collapseAllChildren(ulElement: Element) { - const childItems = ulElement.querySelectorAll(":scope > li"); - childItems.forEach(childLi => { - const arrow = childLi.querySelector(".b3-list-item__arrow"); - if (arrow) { - arrow.classList.remove("b3-list-item__arrow--open"); - } - const childUl = childLi.nextElementSibling; - if (childUl && childUl.tagName === "UL") { - childUl.classList.add("fn__none"); - this.collapseAllChildren(childUl); - } - }); - } - public expandAll() { this.element.querySelectorAll("ul").forEach(item => { if (!item.classList.contains("b3-list")) { @@ -400,6 +303,9 @@ data-def-path="${item.defPath}"> } public setExpandIds(ids: string[]) { + if (!ids || ids.length === 0) { + return; + } this.element.querySelectorAll(".b3-list-item__arrow").forEach(item => { if (ids.includes(item.getAttribute("data-id"))) { item.classList.add("b3-list-item__arrow--open"); diff --git a/app/src/util/pathName.ts b/app/src/util/pathName.ts index 22be5434f..3b3d527b4 100644 --- a/app/src/util/pathName.ts +++ b/app/src/util/pathName.ts @@ -162,7 +162,7 @@ export const movePathTo = (cb: (toPath: string[], toNotebook: string[]) => void, const dialog = new Dialog({ title: `
${title || window.siyuan.languages.move} -
+
`, content: `