From 31e37ff3d07461669402a1423ebbc9d0ab4e85ba Mon Sep 17 00:00:00 2001 From: Vanessa Date: Wed, 4 Feb 2026 00:12:03 +0800 Subject: [PATCH] :art: https://github.com/siyuan-note/siyuan/issues/15381 --- app/src/mobile/util/keyboardToolbar.ts | 48 +++++++++++++++------- app/src/protyle/wysiwyg/codeBlock.ts | 56 ++++++++++++++++++++++++++ app/src/protyle/wysiwyg/keydown.ts | 53 ++---------------------- 3 files changed, 93 insertions(+), 64 deletions(-) create mode 100644 app/src/protyle/wysiwyg/codeBlock.ts diff --git a/app/src/mobile/util/keyboardToolbar.ts b/app/src/mobile/util/keyboardToolbar.ts index 215249025..696ade80c 100644 --- a/app/src/mobile/util/keyboardToolbar.ts +++ b/app/src/mobile/util/keyboardToolbar.ts @@ -13,6 +13,7 @@ import {fontEvent, getFontNodeElements} from "../../protyle/toolbar/Font"; import {hideElements} from "../../protyle/ui/hideElements"; import {softEnter} from "../../protyle/wysiwyg/enter"; import {isInAndroid, isInHarmony} from "../../protyle/util/compatibility"; +import {tabCodeBlock} from "../../protyle/wysiwyg/codeBlock"; let renderKeyboardToolbarTimeout: number; let showUtil = false; @@ -346,14 +347,17 @@ const renderKeyboardToolbar = () => { const dynamicElements = document.querySelectorAll("#keyboardToolbar .keyboard__dynamic"); const range = getSelection().getRangeAt(0); const isProtyle = hasClosestByClassName(range.startContainer, "protyle-wysiwyg", true); - if (!isProtyle) { + const nodeElement = hasClosestBlock(range.startContainer); + if (!isProtyle || !nodeElement) { dynamicElements[0].classList.add("fn__none"); dynamicElements[1].classList.add("fn__none"); return; } const selectText = range.toString(); - if (selectText || dynamicElements[0].querySelector('[data-type="goinline"]').classList.contains("protyle-toolbar__item--current")) { + + if (!nodeElement.classList.contains("code-block") && + (selectText || dynamicElements[0].querySelector('[data-type="goinline"]').classList.contains("protyle-toolbar__item--current"))) { dynamicElements[0].classList.add("fn__none"); dynamicElements[1].classList.remove("fn__none"); } else { @@ -374,16 +378,20 @@ const renderKeyboardToolbar = () => { } else { dynamicElements[0].querySelector('[data-type="redo"]').removeAttribute("disabled"); } - const nodeElement = hasClosestBlock(range.startContainer); - if (nodeElement) { - const outdentElement = dynamicElements[0].querySelector('[data-type="outdent"]'); - if (nodeElement.parentElement.classList.contains("li")) { - outdentElement.classList.remove("fn__none"); - outdentElement.nextElementSibling.classList.remove("fn__none"); - } else { - outdentElement.classList.add("fn__none"); - outdentElement.nextElementSibling.classList.add("fn__none"); - } + const outdentElement = dynamicElements[0].querySelector('[data-type="outdent"]'); + const goinlineElement = dynamicElements[0].querySelector('[data-type="goinline"]'); + if (nodeElement.classList.contains("code-block")) { + goinlineElement.classList.add("fn__none"); + } else { + goinlineElement.classList.remove("fn__none"); + } + if (nodeElement.parentElement.classList.contains("li") || + nodeElement.classList.contains("code-block")) { + outdentElement.classList.remove("fn__none"); + outdentElement.nextElementSibling.classList.remove("fn__none"); + } else { + outdentElement.classList.add("fn__none"); + outdentElement.nextElementSibling.classList.add("fn__none"); } } @@ -689,11 +697,23 @@ export const initKeyboardToolbar = () => { activeBlur(); return; } else if (type === "outdent") { - listOutdent(protyle, [nodeElement.parentElement], range); + if (nodeElement.classList.contains("code-block")) { + if (range.toString() !== "") { + tabCodeBlock(protyle, nodeElement, range, true); + } + } else { + listOutdent(protyle, [nodeElement.parentElement], range); + } focusByRange(range); return; } else if (type === "indent") { - listIndent(protyle, [nodeElement.parentElement], range); + if (nodeElement.classList.contains("code-block")) { + if (range.toString() !== "") { + tabCodeBlock(protyle, nodeElement, range); + } + } else { + listIndent(protyle, [nodeElement.parentElement], range); + } focusByRange(range); return; } diff --git a/app/src/protyle/wysiwyg/codeBlock.ts b/app/src/protyle/wysiwyg/codeBlock.ts new file mode 100644 index 000000000..b148c75aa --- /dev/null +++ b/app/src/protyle/wysiwyg/codeBlock.ts @@ -0,0 +1,56 @@ +import {hasNextSibling} from "./getBlock"; +import {setLastNodeRange} from "../util/selection"; +import {updateTransaction} from "./transaction"; + +export const tabCodeBlock = (protyle: IProtyle, nodeElement: HTMLElement, + range: Range, outdent = false) => { + // https://github.com/siyuan-note/siyuan/issues/12650 + if (!hasNextSibling(range.endContainer) && range.endContainer.textContent.endsWith("\n") && range.endOffset > 0) { + range.setEnd(range.endContainer, range.endOffset - 1); + } + const wbrElement = document.createElement("wbr"); + range.insertNode(wbrElement); + range.setStartAfter(wbrElement); + const oldHTML = nodeElement.outerHTML; + let text = ""; + const tabSpace = window.siyuan.config.editor.codeTabSpaces === 0 ? "\t" : "".padStart(window.siyuan.config.editor.codeTabSpaces, " "); + if (!outdent) { + range.extractContents().textContent.split("\n").forEach((item: string) => { + text += tabSpace + item + "\n"; + }); + } else { + range.extractContents().textContent.split("\n").forEach((item: string) => { + if (item.startsWith(tabSpace)) { + text += item.replace(tabSpace, "") + "\n"; + } else { + text += item + "\n"; + } + }); + } + let language = nodeElement.querySelector(".protyle-action__language").textContent; + // 语言优先级处理 https://github.com/siyuan-note/siyuan/issues/14767 + if (range.commonAncestorContainer.nodeType === 1) { + const snippetClassName = (range.commonAncestorContainer as HTMLElement).className; + if (snippetClassName.startsWith("language-")) { + language = snippetClassName.replace("language-", ""); + // https://github.com/siyuan-note/siyuan/issues/14767 + if (wbrElement.parentElement !== range.commonAncestorContainer) { + wbrElement.parentElement.after(wbrElement); + wbrElement.previousElementSibling.remove(); + } + } + } + if (!window.hljs.getLanguage(language)) { + language = "plaintext"; + } + wbrElement.insertAdjacentHTML("afterend", window.hljs.highlight(text.substr(0, text.length - 1), { + language, + ignoreIllegals: true + }).value + "
"); + range.setStart(wbrElement.nextSibling, 0); + const brElement = wbrElement.parentElement.querySelector("br"); + setLastNodeRange(brElement.previousSibling as Element, range, false); + brElement.remove(); + updateTransaction(protyle, nodeElement.getAttribute("data-node-id"), nodeElement.outerHTML, oldHTML); + wbrElement.remove(); +}; diff --git a/app/src/protyle/wysiwyg/keydown.ts b/app/src/protyle/wysiwyg/keydown.ts index 4f52b53d1..3a8504bd1 100644 --- a/app/src/protyle/wysiwyg/keydown.ts +++ b/app/src/protyle/wysiwyg/keydown.ts @@ -75,6 +75,7 @@ import {openLink} from "../../editor/openLink"; import {onlyProtyleCommand} from "../../boot/globalEvent/command/protyle"; import {AIChat} from "../../ai/chat"; import {updateCalloutType} from "./callout"; +import {tabCodeBlock} from "./codeBlock"; export const getContentByInlineHTML = (range: Range, cb: (content: string) => void) => { let html = ""; @@ -1870,56 +1871,8 @@ export const keydown = (protyle: IProtyle, editorElement: HTMLElement) => { // tab 需等待 list 和 table 处理完成 if (event.key === "Tab" && isNotCtrl(event) && !event.altKey) { event.preventDefault(); - const tabSpace = window.siyuan.config.editor.codeTabSpaces === 0 ? "\t" : "".padStart(window.siyuan.config.editor.codeTabSpaces, " "); if (nodeType === "NodeCodeBlock" && selectText !== "") { - // https://github.com/siyuan-note/siyuan/issues/12650 - if (!hasNextSibling(range.endContainer) && range.endContainer.textContent.endsWith("\n") && range.endOffset > 0) { - range.setEnd(range.endContainer, range.endOffset - 1); - } - const wbrElement = document.createElement("wbr"); - range.insertNode(wbrElement); - range.setStartAfter(wbrElement); - const oldHTML = nodeElement.outerHTML; - let text = ""; - if (!event.shiftKey) { - range.extractContents().textContent.split("\n").forEach((item: string) => { - text += tabSpace + item + "\n"; - }); - } else { - range.extractContents().textContent.split("\n").forEach((item: string) => { - if (item.startsWith(tabSpace)) { - text += item.replace(tabSpace, "") + "\n"; - } else { - text += item + "\n"; - } - }); - } - let language = nodeElement.querySelector(".protyle-action__language").textContent; - // 语言优先级处理 https://github.com/siyuan-note/siyuan/issues/14767 - if (range.commonAncestorContainer.nodeType === 1) { - const snippetClassName = (range.commonAncestorContainer as HTMLElement).className; - if (snippetClassName.startsWith("language-")) { - language = snippetClassName.replace("language-", ""); - // https://github.com/siyuan-note/siyuan/issues/14767 - if (wbrElement.parentElement !== range.commonAncestorContainer) { - wbrElement.parentElement.after(wbrElement); - wbrElement.previousElementSibling.remove(); - } - } - } - if (!window.hljs.getLanguage(language)) { - language = "plaintext"; - } - wbrElement.insertAdjacentHTML("afterend", window.hljs.highlight(text.substr(0, text.length - 1), { - language, - ignoreIllegals: true - }).value + "
"); - range.setStart(wbrElement.nextSibling, 0); - const brElement = wbrElement.parentElement.querySelector("br"); - setLastNodeRange(brElement.previousSibling as Element, range, false); - brElement.remove(); - updateTransaction(protyle, nodeElement.getAttribute("data-node-id"), nodeElement.outerHTML, oldHTML); - wbrElement.remove(); + tabCodeBlock(protyle, nodeElement, range, event.shiftKey); return; } if (!event.shiftKey) { @@ -1946,7 +1899,7 @@ export const keydown = (protyle: IProtyle, editorElement: HTMLElement) => { range.insertNode(wbrElement); const oldHTML = nodeElement.outerHTML; range.extractContents(); - range.insertNode(document.createTextNode(tabSpace)); + range.insertNode(document.createTextNode(window.siyuan.config.editor.codeTabSpaces === 0 ? "\t" : "".padStart(window.siyuan.config.editor.codeTabSpaces, " "))); range.collapse(false); focusByRange(range); wbrElement.remove();