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({