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: `