Vanessa 2025-10-16 11:09:45 +08:00
parent 14effb402f
commit 219ec0f292
2 changed files with 179 additions and 312 deletions

View file

@ -20,6 +20,8 @@ import {goHome} from "../../protyle/wysiwyg/commonHotkey";
import {Editor} from "../../editor"; import {Editor} from "../../editor";
import {writeText, isInAndroid, isInHarmony} from "../../protyle/util/compatibility"; import {writeText, isInAndroid, isInHarmony} from "../../protyle/util/compatibility";
import {mathRender} from "../../protyle/render/mathRender"; import {mathRender} from "../../protyle/render/mathRender";
import {genEmptyElement} from "../../block/util";
import {focusBlock} from "../../protyle/util/selection";
export class Outline extends Model { export class Outline extends Model {
public tree: Tree; public tree: Tree;
@ -862,23 +864,24 @@ export class Outline extends Model {
if (this.isPreview) { if (this.isPreview) {
return; // 预览模式下不显示右键菜单 return; // 预览模式下不显示右键菜单
} }
const id = element.getAttribute("data-node-id");
const subtype = element.getAttribute("data-subtype");
if (!id || !subtype) {
return;
}
const currentLevel = this.getHeadingLevel(element); const currentLevel = this.getHeadingLevel(element);
window.siyuan.menus.menu.remove(); window.siyuan.menus.menu.remove();
// 升级 // 升级
if (currentLevel > 1) { if (currentLevel > 1) {
window.siyuan.menus.menu.append(new MenuItem({ window.siyuan.menus.menu.append(new MenuItem({
icon: "iconUp", icon: "iconUp",
label: window.siyuan.languages.upgrade, label: window.siyuan.languages.upgrade,
click: () => this.upgradeHeading(element) click: () => {
const data = this.getProtyleAndBlockElement(element);
if (data) {
turnsIntoTransaction({
protyle: data.protyle,
selectsElement: [data.blockElement],
type: "Blocks2Hs",
level: currentLevel - 1
});
}
}
}).element); }).element);
} }
@ -887,11 +890,30 @@ export class Outline extends Model {
window.siyuan.menus.menu.append(new MenuItem({ window.siyuan.menus.menu.append(new MenuItem({
icon: "iconDown", icon: "iconDown",
label: window.siyuan.languages.downgrade, label: window.siyuan.languages.downgrade,
click: () => this.downgradeHeading(element) click: () => {
const data = this.getProtyleAndBlockElement(element);
if (data) {
turnsIntoTransaction({
protyle: data.protyle,
selectsElement: [data.blockElement],
type: "Blocks2Hs",
level: currentLevel + 1
});
}
}
}).element); }).element);
} }
// 带子标题转换 // 带子标题转换
const id = element.getAttribute("data-node-id");
checkFold(id, (zoomIn) => {
openFileById({
app: this.app,
id,
action: zoomIn ? [Constants.CB_GET_FOCUS, Constants.CB_GET_ALL, Constants.CB_GET_HTML, Constants.CB_GET_OUTLINE] : [Constants.CB_GET_FOCUS, Constants.CB_GET_OUTLINE, Constants.CB_GET_SETID, Constants.CB_GET_CONTEXT, Constants.CB_GET_HTML],
});
});
this.setCurrentById(id);
const headingSubMenu = []; const headingSubMenu = [];
if (currentLevel !== 1) { if (currentLevel !== 1) {
headingSubMenu.push(this.genHeadingTransform(id, 1)); headingSubMenu.push(this.genHeadingTransform(id, 1));
@ -928,14 +950,38 @@ export class Outline extends Model {
window.siyuan.menus.menu.append(new MenuItem({ window.siyuan.menus.menu.append(new MenuItem({
icon: "iconBefore", icon: "iconBefore",
label: window.siyuan.languages.insertSameLevelHeadingBefore, label: window.siyuan.languages.insertSameLevelHeadingBefore,
click: () => this.insertHeadingBefore(element) click: () => {
fetchPost("/api/block/insertBlock", {
data: "#".repeat(this.getHeadingLevel(element)) + " ",
dataType: "markdown",
nextID: element.getAttribute("data-node-id")
}, (response) => {
openFileById({
app: this.app,
id: response.data[0].doOperations[0].id,
action: [Constants.CB_GET_FOCUS, Constants.CB_GET_OUTLINE, Constants.CB_GET_SETID, Constants.CB_GET_CONTEXT, Constants.CB_GET_HTML]
});
});
}
}).element); }).element);
// 在后面插入同级标题 // 在后面插入同级标题
window.siyuan.menus.menu.append(new MenuItem({ window.siyuan.menus.menu.append(new MenuItem({
icon: "iconAfter", icon: "iconAfter",
label: window.siyuan.languages.insertSameLevelHeadingAfter, label: window.siyuan.languages.insertSameLevelHeadingAfter,
click: () => this.insertHeadingAfter(element) click: () => {
fetchPost("/api/block/insertBlock", {
data: "#".repeat(this.getHeadingLevel(element)) + " ",
dataType: "markdown",
previousID: element.getAttribute("data-node-id")
}, (response) => {
openFileById({
app: this.app,
id: response.data[0].doOperations[0].id,
action: [Constants.CB_GET_FOCUS, Constants.CB_GET_OUTLINE, Constants.CB_GET_SETID, Constants.CB_GET_CONTEXT, Constants.CB_GET_HTML]
});
});
}
}).element); }).element);
// 添加子标题 // 添加子标题
@ -943,7 +989,21 @@ export class Outline extends Model {
window.siyuan.menus.menu.append(new MenuItem({ window.siyuan.menus.menu.append(new MenuItem({
icon: "iconAdd", icon: "iconAdd",
label: window.siyuan.languages.addChildHeading, label: window.siyuan.languages.addChildHeading,
click: () => this.addChildHeading(element) click: () => {
fetchPost("/api/block/prependBlock", {
data: "#".repeat(Math.min(this.getHeadingLevel(element) + 1, 6)) + " ",
dataType: "markdown",
parentID: element.getAttribute("data-node-id")
}, (response) => {
if (response.code === 0 && response.data && response.data.length > 0) {
openFileById({
app: this.app,
id: response.data[0].doOperations[0].id,
action: [Constants.CB_GET_FOCUS, Constants.CB_GET_OUTLINE]
});
}
});
}
}).element); }).element);
} }
@ -953,22 +1013,103 @@ export class Outline extends Model {
window.siyuan.menus.menu.append(new MenuItem({ window.siyuan.menus.menu.append(new MenuItem({
icon: "iconCopy", icon: "iconCopy",
label: `${window.siyuan.languages.copy} ${window.siyuan.languages.headings1}`, label: `${window.siyuan.languages.copy} ${window.siyuan.languages.headings1}`,
click: () => this.copyHeadingWithChildren(element) click: () => {
const data = this.getProtyleAndBlockElement(element);
fetchPost("/api/block/getHeadingChildrenDOM", {
id,
removeFoldAttr: data.blockElement.getAttribute("fold") !== "1"
}, (response) => {
if (isInAndroid()) {
window.JSAndroid.writeHTMLClipboard(data.protyle.lute.BlockDOM2StdMd(response.data).trimEnd(), response.data + Constants.ZWSP);
} else if (isInHarmony()) {
window.JSHarmony.writeHTMLClipboard(data.protyle.lute.BlockDOM2StdMd(response.data).trimEnd(), response.data + Constants.ZWSP);
} else {
writeText(response.data + Constants.ZWSP);
}
});
}
}).element); }).element);
// 剪切带子标题 // 剪切带子标题
window.siyuan.menus.menu.append(new MenuItem({ window.siyuan.menus.menu.append(new MenuItem({
icon: "iconCut", icon: "iconCut",
label: `${window.siyuan.languages.cut} ${window.siyuan.languages.headings1}`, label: `${window.siyuan.languages.cut} ${window.siyuan.languages.headings1}`,
click: () => this.cutHeadingWithChildren(element) click: () => {
const data = this.getProtyleAndBlockElement(element);
fetchPost("/api/block/getHeadingChildrenDOM", {
id,
removeFoldAttr: data.blockElement.getAttribute("fold") !== "1"
}, (response) => {
if (isInAndroid()) {
window.JSAndroid.writeHTMLClipboard(data.protyle.lute.BlockDOM2StdMd(response.data).trimEnd(), response.data + Constants.ZWSP);
} else if (isInHarmony()) {
window.JSHarmony.writeHTMLClipboard(data.protyle.lute.BlockDOM2StdMd(response.data).trimEnd(), response.data + Constants.ZWSP);
} else {
writeText(response.data + Constants.ZWSP);
}
fetchPost("/api/block/getHeadingDeleteTransaction", {
id,
}, (deleteResponse) => {
deleteResponse.data.doOperations.forEach((operation: IOperation) => {
data.protyle.wysiwyg.element.querySelectorAll(`[data-node-id="${operation.id}"]`).forEach((itemElement: HTMLElement) => {
itemElement.remove();
});
});
if (data.protyle.wysiwyg.element.childElementCount === 0) {
const newID = Lute.NewNodeID();
const emptyElement = genEmptyElement(false, false, newID);
data.protyle.wysiwyg.element.insertAdjacentElement("afterbegin", emptyElement);
deleteResponse.data.doOperations.push({
action: "insert",
data: emptyElement.outerHTML,
id: newID,
parentID: data.protyle.block.parentID
});
deleteResponse.data.undoOperations.push({
action: "delete",
id: newID,
});
focusBlock(emptyElement);
}
transaction(data.protyle, deleteResponse.data.doOperations, deleteResponse.data.undoOperations);
});
});
}
}).element); }).element);
// 删除 // 删除
window.siyuan.menus.menu.append(new MenuItem({ window.siyuan.menus.menu.append(new MenuItem({
icon: "iconTrashcan", icon: "iconTrashcan",
label: `${window.siyuan.languages.delete} ${window.siyuan.languages.headings1}`, label: `${window.siyuan.languages.delete} ${window.siyuan.languages.headings1}`,
click: () => this.deleteHeading(element) click: () => {
const data = this.getProtyleAndBlockElement(element);
fetchPost("/api/block/getHeadingDeleteTransaction", {
id,
}, (response) => {
response.data.doOperations.forEach((operation: IOperation) => {
data.protyle.wysiwyg.element.querySelectorAll(`[data-node-id="${operation.id}"]`).forEach((itemElement: HTMLElement) => {
itemElement.remove();
});
});
if (data.protyle.wysiwyg.element.childElementCount === 0) {
const newID = Lute.NewNodeID();
const emptyElement = genEmptyElement(false, false, newID);
data.protyle.wysiwyg.element.insertAdjacentElement("afterbegin", emptyElement);
response.data.doOperations.push({
action: "insert",
data: emptyElement.outerHTML,
id: newID,
parentID: data.protyle.block.parentID
});
response.data.undoOperations.push({
action: "delete",
id: newID,
});
focusBlock(emptyElement);
}
transaction(data.protyle, response.data.doOperations, response.data.undoOperations);
});
}
}).element); }).element);
window.siyuan.menus.menu.append(new MenuItem({type: "separator"}).element); window.siyuan.menus.menu.append(new MenuItem({type: "separator"}).element);
@ -1027,249 +1168,24 @@ export class Outline extends Model {
}); });
} }
/** private getProtyleAndBlockElement(element: HTMLElement) {
*
*/
private upgradeHeading(element: HTMLElement) {
const id = element.getAttribute("data-node-id"); const id = element.getAttribute("data-node-id");
const currentLevel = this.getHeadingLevel(element); let protyle: IProtyle;
if (currentLevel <= 1) {
return;
}
// 找到编辑器实例和文档中的标题元素
let editor: any;
let blockElement: HTMLElement; let blockElement: HTMLElement;
getAllModels().editor.find(editItem => { getAllModels().editor.find(editItem => {
if (editItem.editor.protyle.block.rootID === this.blockId) { if (editItem.editor.protyle.block.rootID === this.blockId) {
editor = editItem.editor.protyle; protyle = editItem.editor.protyle;
blockElement = editor.wysiwyg.element.querySelector(`[data-node-id="${id}"]`); blockElement = protyle.wysiwyg.element.querySelector(`[data-node-id="${id}"]`);
return true; return true;
} }
}); });
if (!editor || !blockElement) { if (!protyle || !blockElement) {
return; return;
} }
return {
// 使用turnsIntoTransaction来变更标题级别 protyle, blockElement
turnsIntoTransaction({ };
protyle: editor,
selectsElement: [blockElement],
type: "Blocks2Hs",
level: currentLevel - 1
});
}
/**
*
*/
private downgradeHeading(element: HTMLElement) {
const id = element.getAttribute("data-node-id");
const currentLevel = this.getHeadingLevel(element);
if (currentLevel >= 6) {
return;
}
// 找到编辑器实例和文档中的标题元素
let editor: any;
let blockElement: HTMLElement;
getAllModels().editor.find(editItem => {
if (editItem.editor.protyle.block.rootID === this.blockId) {
editor = editItem.editor.protyle;
blockElement = editor.wysiwyg.element.querySelector(`[data-node-id="${id}"]`);
return true;
}
});
if (!editor || !blockElement) {
return;
}
// 使用turnsIntoTransaction来变更标题级别
turnsIntoTransaction({
protyle: editor,
selectsElement: [blockElement],
type: "Blocks2Hs",
level: currentLevel + 1
});
}
/**
*
*/
private insertHeadingBefore(element: HTMLElement) {
const id = element.getAttribute("data-node-id");
const currentLevel = this.getHeadingLevel(element);
const headingPrefix = "#".repeat(currentLevel) + " ";
fetchPost("/api/block/insertBlock", {
data: headingPrefix,
dataType: "markdown",
nextID: id
}, (response) => {
if (response.code === 0) {
// 插入成功后,可以选择聚焦到新插入的标题
const newId = response.data[0].doOperations[0].id;
openFileById({
app: this.app,
id: newId,
action: [Constants.CB_GET_FOCUS, Constants.CB_GET_OUTLINE]
});
}
});
}
/**
*
*/
private insertHeadingAfter(element: HTMLElement) {
const currentLevel = this.getHeadingLevel(element);
const headingPrefix = "#".repeat(currentLevel) + " ";
// 获取父节点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")) {
parentID = parentLi.getAttribute("data-node-id");
}
}
fetchPost("/api/block/appendBlock", {
data: headingPrefix,
dataType: "markdown",
parentID: parentID
}, (response) => {
if (response.code === 0) {
// 插入成功后,可以选择聚焦到新插入的标题
const newId = response.data[0].doOperations[0].id;
openFileById({
app: this.app,
id: newId,
action: [Constants.CB_GET_FOCUS, Constants.CB_GET_OUTLINE]
});
}
});
}
/**
*
*/
private deleteHeading(element: HTMLElement) {
const id = element.getAttribute("data-node-id");
// 找到编辑器实例
let editor: any;
getAllModels().editor.find(editItem => {
if (editItem.editor.protyle.block.rootID === this.blockId) {
editor = editItem.editor.protyle;
return true;
}
});
if (!editor) {
return;
}
fetchPost("/api/block/getHeadingDeleteTransaction", {
id: id,
}, (response) => {
response.data.doOperations.forEach((operation: any) => {
editor.wysiwyg.element.querySelectorAll(`[data-node-id="${operation.id}"]`).forEach((itemElement: HTMLElement) => {
itemElement.remove();
});
});
transaction(editor, response.data.doOperations, response.data.undoOperations);
});
}
/**
*
*/
private copyHeadingWithChildren(element: HTMLElement) {
const id = element.getAttribute("data-node-id");
if (!id) {
return;
}
// 找到编辑器实例
let editor: any;
getAllModels().editor.find(editItem => {
if (editItem.editor.protyle.block.rootID === this.blockId) {
editor = editItem.editor.protyle;
return true;
}
});
if (!editor) {
return;
}
fetchPost("/api/block/getHeadingChildrenDOM", {
id: id,
removeFoldAttr: false
}, (response) => {
if (isInAndroid()) {
window.JSAndroid.writeHTMLClipboard(editor.lute.BlockDOM2StdMd(response.data).trimEnd(), response.data + Constants.ZWSP);
} else if (isInHarmony()) {
window.JSHarmony.writeHTMLClipboard(editor.lute.BlockDOM2StdMd(response.data).trimEnd(), response.data + Constants.ZWSP);
} else {
writeText(response.data + Constants.ZWSP);
}
});
}
/**
*
*/
private cutHeadingWithChildren(element: HTMLElement) {
const id = element.getAttribute("data-node-id");
if (!id) {
return;
}
// 找到编辑器实例
let editor: any;
getAllModels().editor.find(editItem => {
if (editItem.editor.protyle.block.rootID === this.blockId) {
editor = editItem.editor.protyle;
return true;
}
});
if (!editor) {
return;
}
fetchPost("/api/block/getHeadingChildrenDOM", {
id: id,
removeFoldAttr: false
}, (response) => {
if (isInAndroid()) {
window.JSAndroid.writeHTMLClipboard(editor.lute.BlockDOM2StdMd(response.data).trimEnd(), response.data + Constants.ZWSP);
} else if (isInHarmony()) {
window.JSHarmony.writeHTMLClipboard(editor.lute.BlockDOM2StdMd(response.data).trimEnd(), response.data + Constants.ZWSP);
} else {
writeText(response.data + Constants.ZWSP);
}
// 复制完成后删除标题及其子标题
fetchPost("/api/block/getHeadingDeleteTransaction", {
id: id,
}, (response) => {
response.data.doOperations.forEach((operation: any) => {
editor.wysiwyg.element.querySelectorAll(`[data-node-id="${operation.id}"]`).forEach((itemElement: HTMLElement) => {
itemElement.remove();
});
});
transaction(editor, response.data.doOperations, response.data.undoOperations);
});
});
} }
/** /**
@ -1282,16 +1198,15 @@ export class Outline extends Model {
icon: "iconHeading" + level, icon: "iconHeading" + level,
label: window.siyuan.languages["heading" + level], label: window.siyuan.languages["heading" + level],
click: () => { click: () => {
// 找到编辑器实例 let protyle: IProtyle;
let editor: any;
getAllModels().editor.find(editItem => { getAllModels().editor.find(editItem => {
if (editItem.editor.protyle.block.rootID === this.blockId) { if (editItem.editor.protyle.block.rootID === this.blockId) {
editor = editItem.editor.protyle; protyle = editItem.editor.protyle;
return true; return true;
} }
}); });
if (!editor) { if (!protyle) {
return; return;
} }
@ -1300,71 +1215,23 @@ export class Outline extends Model {
level level
}, (response) => { }, (response) => {
response.data.doOperations.forEach((operation: any, index: number) => { response.data.doOperations.forEach((operation: any, index: number) => {
editor.wysiwyg.element.querySelectorAll(`[data-node-id="${operation.id}"]`).forEach((itemElement: HTMLElement) => { protyle.wysiwyg.element.querySelectorAll(`[data-node-id="${operation.id}"]`).forEach((itemElement: HTMLElement) => {
itemElement.outerHTML = operation.data; itemElement.outerHTML = operation.data;
}); });
// 使用 outer 后元素需要重新查询 // 使用 outer 后元素需要重新查询
editor.wysiwyg.element.querySelectorAll(`[data-node-id="${operation.id}"]`).forEach((itemElement: HTMLElement) => { protyle.wysiwyg.element.querySelectorAll(`[data-node-id="${operation.id}"]`).forEach((itemElement: HTMLElement) => {
mathRender(itemElement); mathRender(itemElement);
}); });
if (index === 0) { if (index === 0) {
const focusElement = editor.wysiwyg.element.querySelector(`[data-node-id="${operation.id}"]`); const focusElement = protyle.wysiwyg.element.querySelector(`[data-node-id="${operation.id}"]`);
if (focusElement) { if (focusElement) {
focusElement.scrollIntoView({behavior: "smooth", block: "center"}); focusElement.scrollIntoView({behavior: "smooth", block: "center"});
} }
} }
}); });
transaction(editor, response.data.doOperations, response.data.undoOperations); transaction(protyle, response.data.doOperations, response.data.undoOperations);
}); });
} }
}; };
} }
/**
*
*/
private addChildHeading(element: HTMLElement) {
const id = element.getAttribute("data-node-id");
if (!id) {
return;
}
const currentLevel = this.getHeadingLevel(element);
const childLevel = Math.min(currentLevel + 1, 6); // 子标题级别比当前标题高一级最大到H6
const headingPrefix = "#".repeat(childLevel) + " ";
// 使用当前标题作为父标题,在其内部添加子标题
fetchPost("/api/block/appendBlock", {
data: headingPrefix,
dataType: "markdown",
parentID: id
}, (response) => {
if (response.code === 0 && response.data && response.data.length > 0) {
// 确保父标题保持展开状态 - 使用expandIds方式
const currentExpandIds = this.tree.getExpandIds();
if (!currentExpandIds.includes(id)) {
currentExpandIds.push(id);
this.tree.setExpandIds(currentExpandIds);
// 保存展开状态到持久化存储
if (!this.isPreview) {
fetchPost("/api/storage/setOutlineStorage", {
docID: this.blockId,
val: {
expandIds: currentExpandIds
}
});
}
}
// 插入成功后,聚焦到新插入的标题
const newId = response.data[0].doOperations[0].id;
openFileById({
app: this.app,
id: newId,
action: [Constants.CB_GET_FOCUS, Constants.CB_GET_OUTLINE]
});
}
});
}
} }

View file

@ -1769,8 +1769,8 @@ export class Gutter {
} }
fetchPost("/api/block/getHeadingDeleteTransaction", { fetchPost("/api/block/getHeadingDeleteTransaction", {
id, id,
}, (response) => { }, (deleteResponse) => {
response.data.doOperations.forEach((operation: IOperation) => { deleteResponse.data.doOperations.forEach((operation: IOperation) => {
protyle.wysiwyg.element.querySelectorAll(`[data-node-id="${operation.id}"]`).forEach((itemElement: HTMLElement) => { protyle.wysiwyg.element.querySelectorAll(`[data-node-id="${operation.id}"]`).forEach((itemElement: HTMLElement) => {
itemElement.remove(); itemElement.remove();
}); });
@ -1779,19 +1779,19 @@ export class Gutter {
const newID = Lute.NewNodeID(); const newID = Lute.NewNodeID();
const emptyElement = genEmptyElement(false, false, newID); const emptyElement = genEmptyElement(false, false, newID);
protyle.wysiwyg.element.insertAdjacentElement("afterbegin", emptyElement); protyle.wysiwyg.element.insertAdjacentElement("afterbegin", emptyElement);
response.data.doOperations.push({ deleteResponse.data.doOperations.push({
action: "insert", action: "insert",
data: emptyElement.outerHTML, data: emptyElement.outerHTML,
id: newID, id: newID,
parentID: protyle.block.parentID parentID: protyle.block.parentID
}); });
response.data.undoOperations.push({ deleteResponse.data.undoOperations.push({
action: "delete", action: "delete",
id: newID, id: newID,
}); });
focusBlock(emptyElement); focusBlock(emptyElement);
} }
transaction(protyle, response.data.doOperations, response.data.undoOperations); transaction(protyle, deleteResponse.data.doOperations, deleteResponse.data.undoOperations);
}); });
}); });
} }