diff --git a/app/src/protyle/util/hasClosest.ts b/app/src/protyle/util/hasClosest.ts
index 2d61ae7bb86..71078464e58 100644
--- a/app/src/protyle/util/hasClosest.ts
+++ b/app/src/protyle/util/hasClosest.ts
@@ -104,12 +104,19 @@ export const hasClosestByClassName = (element: Node, className: string, top = fa
export const hasClosestBlock = (element: Node) => {
const nodeElement = hasClosestByAttribute(element, "data-node-id", null);
- if (nodeElement && nodeElement.tagName !== "BUTTON" && nodeElement.getAttribute("data-type")?.startsWith("Node")) {
+ if (isBlockElement(nodeElement)) {
return nodeElement;
}
return false;
};
+export const isBlockElement = (element: Element | false | undefined) => {
+ if (!element) {
+ return false;
+ }
+ return element.hasAttribute("data-node-id") && element.tagName !== "BUTTON" && (element.getAttribute("data-type")?.startsWith("Node") ?? false);
+};
+
export const isInEmbedBlock = (element: Element) => {
const embedElement = hasTopClosestByAttribute(element, "data-type", "NodeBlockQueryEmbed");
if (embedElement) {
diff --git a/app/src/protyle/wysiwyg/keydown.ts b/app/src/protyle/wysiwyg/keydown.ts
index 06ab687bed6..52d85d2c2de 100644
--- a/app/src/protyle/wysiwyg/keydown.ts
+++ b/app/src/protyle/wysiwyg/keydown.ts
@@ -1057,31 +1057,40 @@ export const keydown = (protyle: IProtyle, editorElement: HTMLElement) => {
}
// 软换行
- if (matchHotKey("⇧↩", event) && selectText === "" && softEnter(range, nodeElement, protyle)) {
+ if (selectText === "" && matchHotKey("⇧↩", event) && softEnter(range, nodeElement, protyle)) {
event.stopPropagation();
event.preventDefault();
return;
}
// 代码块语言选择 https://github.com/siyuan-note/siyuan/issues/14126
- if (matchHotKey("⌥↩", event) && selectText === "") {
+ // 列表插入末尾子项 https://github.com/siyuan-note/siyuan/issues/11164
+ if (selectText === "" && matchHotKey("⌥↩", event) && !isIncludesHotKey("⌥↩")) {
const selectElements = Array.from(protyle.wysiwyg.element.querySelectorAll(".protyle-wysiwyg--select"));
if (selectElements.length === 0) {
selectElements.push(nodeElement);
}
- if (selectElements.length > 0 && !isIncludesHotKey("⌥↩")) {
- const otherElement = selectElements.find(item => {
- return !item.classList.contains("code-block");
+
+ const codeBlockElements = selectElements.filter(item => {
+ return item.classList.contains("code-block");
+ });
+ if (codeBlockElements.length > 0) {
+ const languageElements: HTMLElement[] = [];
+ codeBlockElements.forEach(item => {
+ languageElements.push(item.querySelector(".protyle-action__language"));
});
- if (!otherElement) {
- const languageElements: HTMLElement[] = [];
- selectElements.forEach(item => {
- languageElements.push(item.querySelector(".protyle-action__language"));
- });
- protyle.toolbar.showCodeLanguage(protyle, languageElements);
- } else {
- addSubList(protyle, nodeElement, range);
- }
+ protyle.toolbar.showCodeLanguage(protyle, languageElements);
+ event.stopPropagation();
+ event.preventDefault();
+ return;
+ }
+
+ const liBlockElement = hasClosestByClassName(nodeElement, "li");
+ if (liBlockElement) {
+ selectElements.forEach(item => {
+ item.classList.remove("protyle-wysiwyg--select");
+ });
+ addSubList(protyle, nodeElement, range);
event.stopPropagation();
event.preventDefault();
return;
diff --git a/app/src/protyle/wysiwyg/list.ts b/app/src/protyle/wysiwyg/list.ts
index 3ff5beb8e41..420b0aec1ef 100644
--- a/app/src/protyle/wysiwyg/list.ts
+++ b/app/src/protyle/wysiwyg/list.ts
@@ -4,9 +4,23 @@ import {genEmptyBlock} from "../../block/util";
import * as dayjs from "dayjs";
import {Constants} from "../../constants";
import {moveToPrevious, removeBlock} from "./remove";
-import {hasClosestByClassName} from "../util/hasClosest";
+import {hasClosestByClassName, isBlockElement} from "../util/hasClosest";
import {setFold} from "../../menus/protyle";
+const getLastChildBlock = (element: Element): Element | null => {
+ if (!element || !element.lastElementChild) {
+ return null;
+ }
+ let current = element.lastElementChild.previousElementSibling;
+ while (current) {
+ if (isBlockElement(current)) {
+ return current;
+ }
+ current = current.previousElementSibling;
+ }
+ return null;
+};
+
export const updateListOrder = (listElement: Element, sIndex?: number) => {
if (listElement.getAttribute("data-subtype") !== "o") {
return;
@@ -30,49 +44,99 @@ export const updateListOrder = (listElement: Element, sIndex?: number) => {
});
};
-export const genListItemElement = (listItemElement: Element, offset = 0, wbr = false) => {
+export const genListItemElement = (listItemElement: Element, offset = 0, wbr = false, startIndex?: number) => {
const element = document.createElement("template");
- const type = listItemElement.getAttribute("data-subtype");
+ const type = listItemElement.getAttribute("data-subtype") || "u";
if (type === "o") {
- const index = parseInt(listItemElement.getAttribute("data-marker")) + offset;
- element.innerHTML = `
${index + 1}.
${genEmptyBlock(false, wbr)}
`;
+ const index = startIndex !== undefined ? startIndex : parseInt(listItemElement.getAttribute("data-marker")) + offset + 1;
+ element.innerHTML = `${index}.
${genEmptyBlock(false, wbr)}
`;
} else if (type === "t") {
- element.innerHTML = `${genEmptyBlock(false, wbr)}
`;
+ element.innerHTML = `${genEmptyBlock(false, wbr)}
`;
} else {
- element.innerHTML = `${genEmptyBlock(false, wbr)}
`;
+ element.innerHTML = `${genEmptyBlock(false, wbr)}
`;
}
return element.content.firstElementChild as HTMLElement;
};
export const addSubList = (protyle: IProtyle, nodeElement: Element, range: Range) => {
- const parentItemElement = hasClosestByClassName(nodeElement, "li");
- if (!parentItemElement) {
+ const liElement = hasClosestByClassName(nodeElement, "li");
+ if (!liElement) {
+ // 上层必须有列表项块才插入子列表
return;
}
- const lastSubItem = parentItemElement.querySelector(".list")?.lastElementChild.previousElementSibling;
- if (!lastSubItem) {
+ if (nodeElement.classList.contains("list") || nodeElement.classList.contains("li")) {
+ // 不存在 nodeElement 为列表块或列表项块的情况,如果以后需要的话再实现
return;
}
- const newListElement = genListItemElement(lastSubItem, 0, true);
- const id = newListElement.getAttribute("data-node-id");
- lastSubItem.after(newListElement);
- if (lastSubItem.parentElement.getAttribute("fold") === "1") {
- setFold(protyle, lastSubItem.parentElement, true);
+ let listElement: Element | null | false = null;
+ // 向上遍历到列表项块,得到列表项块的直接子块
+ let blockElement = nodeElement;
+ while (blockElement.parentElement !== liElement) {
+ blockElement = blockElement.parentElement;
}
- if (parentItemElement.getAttribute("fold") === "1") {
- setFold(protyle, parentItemElement, true);
+ // 考虑到列表项块内可能存在多个字列表块,在 nodeElement 的后面查找最近的同级列表块,如果不存在则在列表项块的最后一个子块后面插入新的列表块
+ let nextSibling = blockElement?.nextElementSibling;
+ while (nextSibling) {
+ if (nextSibling.classList.contains("list")) {
+ listElement = nextSibling;
+ break;
+ }
+ nextSibling = nextSibling.nextElementSibling;
+ }
+
+ // 无列表块:在列表项块的最后一个子块后面插入新的列表块
+ if (!listElement) {
+ const lastChildBlock = getLastChildBlock(liElement);
+ if (!lastChildBlock) {
+ return;
+ }
+ const subType = liElement.getAttribute("data-subtype") || "u";
+ const id = Lute.NewNodeID();
+ const newListItemElement = genListItemElement(liElement, 0, true, 1);
+ const newListHTML = `${newListItemElement.outerHTML}
${Constants.ZWSP}
`;
+ lastChildBlock.insertAdjacentHTML("afterend", newListHTML);
+ transaction(protyle, [{
+ action: "insert",
+ id,
+ data: newListHTML,
+ previousID: lastChildBlock.getAttribute("data-node-id"),
+ }], [{
+ action: "delete",
+ id,
+ }]);
+ focusByWbr(lastChildBlock.nextElementSibling, range);
+ return;
+ }
+
+ // 有列表块:在列表块的最后一个列表项块后插入新的列表项块
+ const lastSubItem = getLastChildBlock(listElement);
+ if (lastSubItem) {
+ const newListElement = genListItemElement(lastSubItem, 0, true);
+ const id = newListElement.getAttribute("data-node-id");
+ lastSubItem.after(newListElement);
+ if (lastSubItem.parentElement.getAttribute("fold") === "1") {
+ setFold(protyle, lastSubItem.parentElement, true);
+ }
+ if (liElement.getAttribute("fold") === "1") {
+ setFold(protyle, liElement, true);
+ }
+ const parentListElement = hasClosestByClassName(liElement, "list");
+ if (parentListElement && parentListElement.getAttribute("fold") === "1") {
+ setFold(protyle, parentListElement, true);
+ }
+ transaction(protyle, [{
+ action: "insert",
+ id,
+ data: newListElement.outerHTML,
+ previousID: lastSubItem.getAttribute("data-node-id"),
+ }], [{
+ action: "delete",
+ id,
+ }]);
+ focusByWbr(newListElement, range);
+ return;
}
- transaction(protyle, [{
- action: "insert",
- id,
- data: newListElement.outerHTML,
- previousID: lastSubItem.getAttribute("data-node-id"),
- }], [{
- action: "delete",
- id,
- }]);
- focusByWbr(newListElement, range);
};
export const listIndent = (protyle: IProtyle, liItemElements: Element[], range: Range) => {
@@ -94,15 +158,17 @@ export const listIndent = (protyle: IProtyle, liItemElements: Element[], range:
range.collapse(false);
range.insertNode(document.createElement("wbr"));
const html = previousElement.parentElement.outerHTML;
- if (previousElement.lastElementChild.previousElementSibling.getAttribute("data-type") === "NodeList") {
+ const previousLastBlock = getLastChildBlock(previousElement);
+ if (previousLastBlock && previousLastBlock.getAttribute("data-type") === "NodeList") {
// 上一个列表的最后一项为子列表
- const previousLastListHTML = previousElement.lastElementChild.previousElementSibling.outerHTML;
+ const previousLastListHTML = previousLastBlock.outerHTML;
const doOperations: IOperation[] = [];
const undoOperations: IOperation[] = [];
- const subtype = previousElement.lastElementChild.previousElementSibling.getAttribute("data-subtype");
- let previousID = previousElement.lastElementChild.previousElementSibling.lastElementChild.previousElementSibling.getAttribute("data-node-id");
+ const subtype = previousLastBlock.getAttribute("data-subtype");
+ const previousLastListLastBlock = getLastChildBlock(previousLastBlock);
+ let previousID = previousLastListLastBlock ? previousLastListLastBlock.getAttribute("data-node-id") : undefined;
liItemElements.forEach((item, index) => {
doOperations.push({
action: "move",
@@ -120,23 +186,23 @@ export const listIndent = (protyle: IProtyle, liItemElements: Element[], range:
if (subtype === "o") {
actionElement.classList.add("protyle-action--order");
actionElement.classList.remove("protyle-action--task");
- previousElement.lastElementChild.previousElementSibling.lastElementChild.before(item);
+ previousLastBlock.lastElementChild.before(item);
} else if (subtype === "t") {
item.setAttribute("data-marker", "*");
actionElement.innerHTML = ``;
actionElement.classList.remove("protyle-action--order");
actionElement.classList.add("protyle-action--task");
- previousElement.lastElementChild.previousElementSibling.lastElementChild.before(item);
+ previousLastBlock.lastElementChild.before(item);
} else {
item.setAttribute("data-marker", "*");
actionElement.innerHTML = '';
actionElement.classList.remove("protyle-action--order", "protyle-action--task");
- previousElement.lastElementChild.previousElementSibling.lastElementChild.before(item);
+ previousLastBlock.lastElementChild.before(item);
}
});
if (subtype === "o") {
- updateListOrder(previousElement.lastElementChild.previousElementSibling);
+ updateListOrder(previousLastBlock);
updateListOrder(previousElement.parentElement);
} else if (previousElement.getAttribute("data-subtype") === "o") {
updateListOrder(previousElement.parentElement);
@@ -145,13 +211,13 @@ export const listIndent = (protyle: IProtyle, liItemElements: Element[], range:
if (previousElement.parentElement.classList.contains("protyle-wysiwyg")) {
doOperations.push({
action: "update",
- data: previousElement.lastElementChild.previousElementSibling.outerHTML,
- id: previousElement.lastElementChild.previousElementSibling.getAttribute("data-node-id")
+ data: previousLastBlock.outerHTML,
+ id: previousLastBlock.getAttribute("data-node-id")
});
undoOperations.push({
action: "update",
data: previousLastListHTML,
- id: previousElement.lastElementChild.previousElementSibling.getAttribute("data-node-id")
+ id: previousLastBlock.getAttribute("data-node-id")
});
transaction(protyle, doOperations, undoOperations);
}
@@ -165,11 +231,12 @@ export const listIndent = (protyle: IProtyle, liItemElements: Element[], range:
newListElement.setAttribute("class", "list");
newListElement.setAttribute("data-subtype", subType);
newListElement.innerHTML = '';
+ const previousLastBlockForNewList = getLastChildBlock(previousElement);
const doOperations: IOperation[] = [{
action: "insert",
data: newListElement.outerHTML,
id: newListId,
- previousID: previousElement.lastElementChild.previousElementSibling.getAttribute("data-node-id")
+ previousID: previousLastBlockForNewList ? previousLastBlockForNewList.getAttribute("data-node-id") : undefined
}];
previousElement.lastElementChild.before(newListElement);
const undoOperations: IOperation[] = [];
@@ -367,7 +434,7 @@ export const listOutdent = (protyle: IProtyle, liItemElements: Element[], range:
let topPreviousID = liId;
let previousElement: Element = liElement;
let nextElement = liItemElements[liItemElements.length - 1].nextElementSibling;
- let lastBlockElement = liItemElements[liItemElements.length - 1].lastElementChild.previousElementSibling;
+ let lastBlockElement = getLastChildBlock(liItemElements[liItemElements.length - 1]);
liItemElements.forEach(item => {
Array.from(item.children).forEach((blockElement, index) => {
const id = blockElement.getAttribute("data-node-id");
@@ -395,7 +462,7 @@ export const listOutdent = (protyle: IProtyle, liItemElements: Element[], range:
if (!window.siyuan.config.editor.listLogicalOutdent && !nextElement.classList.contains("protyle-attr")) {
// 传统缩进
let newId;
- if (lastBlockElement.getAttribute("data-subtype") !== nextElement.getAttribute("data-subtype")) {
+ if (!lastBlockElement || lastBlockElement.getAttribute("data-subtype") !== nextElement.getAttribute("data-subtype")) {
newId = Lute.NewNodeID();
lastBlockElement = document.createElement("div");
lastBlockElement.classList.add("list");
@@ -414,10 +481,11 @@ export const listOutdent = (protyle: IProtyle, liItemElements: Element[], range:
}
let topOldPreviousID;
while (nextElement && !nextElement.classList.contains("protyle-attr")) {
+ const lastBlockLastBlock = lastBlockElement ? getLastChildBlock(lastBlockElement) : null;
topDoOperations.push({
action: "move",
id: nextElement.getAttribute("data-node-id"),
- previousID: topOldPreviousID || lastBlockElement.lastElementChild.previousElementSibling?.getAttribute("data-node-id"),
+ previousID: topOldPreviousID || (lastBlockLastBlock ? lastBlockLastBlock.getAttribute("data-node-id") : undefined),
parentID: lastBlockElement.getAttribute("data-node-id")
});
topUndoOperations.push({
@@ -539,7 +607,7 @@ export const listOutdent = (protyle: IProtyle, liItemElements: Element[], range:
}
const html = parentLiItemElement.parentElement.outerHTML;
let nextElement = liItemElements[liItemElements.length - 1].nextElementSibling;
- let lastBlockElement = liItemElements[liItemElements.length - 1].lastElementChild.previousElementSibling;
+ let lastBlockElement = getLastChildBlock(liItemElements[liItemElements.length - 1]);
liItemElements.reverse().forEach(item => {
const itemId = item.getAttribute("data-node-id");
doOperations.push({
@@ -605,7 +673,7 @@ export const listOutdent = (protyle: IProtyle, liItemElements: Element[], range:
if (!window.siyuan.config.editor.listLogicalOutdent && !nextElement.classList.contains("protyle-attr")) {
// 传统缩进
let newId;
- if (!lastBlockElement.classList.contains("list")) {
+ if (!lastBlockElement || !lastBlockElement.classList.contains("list")) {
newId = Lute.NewNodeID();
lastBlockElement = document.createElement("div");
lastBlockElement.classList.add("list");
@@ -614,11 +682,12 @@ export const listOutdent = (protyle: IProtyle, liItemElements: Element[], range:
lastBlockElement.setAttribute("data-type", "NodeList");
lastBlockElement.setAttribute("updated", dayjs().format("YYYYMMDDHHmmss"));
lastBlockElement.innerHTML = `${Constants.ZWSP}
`;
+ const firstItemLastBlock = getLastChildBlock(liItemElements[0]);
doOperations.push({
action: "insert",
id: newId,
data: lastBlockElement.outerHTML,
- previousID: liItemElements[0].lastElementChild.previousElementSibling.getAttribute("data-node-id"),
+ previousID: firstItemLastBlock ? firstItemLastBlock.getAttribute("data-node-id") : undefined,
});
liItemElements[0].lastElementChild.before(lastBlockElement);
}
@@ -640,10 +709,11 @@ export const listOutdent = (protyle: IProtyle, liItemElements: Element[], range:
data: nextElement.outerHTML
});
}
+ const lastBlockLastBlock = getLastChildBlock(lastBlockElement);
doOperations.push({
action: "move",
id: nextId,
- previousID: subPreviousID || lastBlockElement.lastElementChild.previousElementSibling?.getAttribute("data-node-id"),
+ previousID: subPreviousID || (lastBlockLastBlock ? lastBlockLastBlock.getAttribute("data-node-id") : undefined),
parentID: lastBlockElement.getAttribute("data-node-id")
});
undoOperations.push({