import {Divider} from "./Divider"; import {Font} from "./Font"; import {ToolbarItem} from "./ToolbarItem"; import { focusByRange, focusByWbr, focusSideBlock, getEditorRange, getSelectionOffset, getSelectionPosition, setFirstNodeRange, setLastNodeRange } from "../util/selection"; import {hasClosestBlock, hasClosestByAttribute, hasClosestByClassName, hasClosestByMatchTag} from "../util/hasClosest"; import {Link} from "./Link"; import {setPosition} from "../../util/setPosition"; import {updateTransaction} from "../wysiwyg/transaction"; import {Constants} from "../../constants"; import {mathRender} from "../markdown/mathRender"; import {getEventName} from "../util/compatibility"; import {upDownHint} from "../../util/upDownHint"; import {highlightRender} from "../markdown/highlightRender"; import { getContenteditableElement, hasNextSibling, hasPreviousSibling } from "../wysiwyg/getBlock"; import {processRender} from "../util/processCode"; import {BlockRef} from "./BlockRef"; import {hintMoveBlock, hintRef, hintRenderAssets, hintRenderTemplate, hintRenderWidget} from "../hint/extend"; import {blockRender} from "../markdown/blockRender"; /// #if !BROWSER import {clipboard, nativeImage, NativeImage} from "electron"; import {getCurrentWindow} from "@electron/remote"; /// #endif import {fetchPost} from "../../util/fetch"; import {isBrowser, isMobile} from "../../util/functions"; import * as dayjs from "dayjs"; import {insertEmptyBlock} from "../../block/util"; import {matchHotKey} from "../util/hotKey"; import {unicode2Emoji} from "../../emoji"; import {escapeHtml} from "../../util/escape"; import {hideElements} from "../ui/hideElements"; export class Toolbar { public element: HTMLElement; public subElement: HTMLElement; public range: Range; public isNewEmptyInline: boolean; private toolbarHeight: number; constructor(protyle: IProtyle) { const options = protyle.options; const element = document.createElement("div"); element.className = "protyle-toolbar fn__none"; this.element = element; this.subElement = document.createElement("div"); this.subElement.className = "protyle-util fn__none"; this.toolbarHeight = 29; options.toolbar.forEach((menuItem: IMenuItem) => { const itemElement = this.genItem(protyle, menuItem); this.element.appendChild(itemElement); }); this.isNewEmptyInline = false; } public render(protyle: IProtyle, range: Range, event?: KeyboardEvent) { this.range = range; const nodeElement = hasClosestBlock(range.startContainer); if (!nodeElement || protyle.disabled) { this.element.classList.add("fn__none"); return; } // shift+方向键或三击选中,不同的块 https://github.com/siyuan-note/siyuan/issues/3891 const startElement = hasClosestBlock(range.startContainer); const endElement = hasClosestBlock(range.endContainer); if (startElement && endElement && !startElement.isSameNode(endElement)) { if (event) { // 在 keyup 中使用 shift+方向键选中 if (event.key === "ArrowLeft") { this.range = setLastNodeRange(getContenteditableElement(startElement), range, false); } else { this.range = setFirstNodeRange(getContenteditableElement(endElement), range); } } else { this.range = setLastNodeRange(getContenteditableElement(nodeElement), range, false); } focusByRange(this.range); if (this.range.toString() === "") { this.element.classList.add("fn__none"); return; } } // 需放在 range 修改之后,否则 https://github.com/siyuan-note/siyuan/issues/4726 if (nodeElement.getAttribute("data-type") === "NodeCodeBlock") { this.element.classList.add("fn__none"); return; } const rangePosition = getSelectionPosition(nodeElement, range); this.element.classList.remove("fn__none"); setPosition(this.element, rangePosition.left - 52, rangePosition.top - this.toolbarHeight - 4); this.element.querySelectorAll(".protyle-toolbar__item--current").forEach(item => { item.classList.remove("protyle-toolbar__item--current"); }); const types = this.getCurrentType(); types.forEach(item => { if (item === "blockRef") { return; } this.element.querySelector(`[data-type="${item}"]`).classList.add("protyle-toolbar__item--current"); }); } public getCurrentType(range = this.range) { const types: string[] = []; let startElement = range.startContainer as HTMLElement; if (startElement.nodeType === 3) { startElement = startElement.parentElement; if (startElement.getAttribute("data-type") === "virtual-block-ref" && !["DIV", "TD", "TH"].includes(startElement.parentElement.tagName)) { startElement = startElement.parentElement; } } else if (startElement.childElementCount > 0 && startElement.childNodes[range.startOffset]?.nodeType !== 3) { startElement = startElement.childNodes[range.startOffset] as HTMLElement; } if (!startElement || startElement.nodeType === 3) { return []; } let endElement = range.endContainer as HTMLElement; if (endElement.nodeType === 3) { endElement = endElement.parentElement; if (endElement.getAttribute("data-type") === "virtual-block-ref" && !["DIV", "TD", "TH"].includes(endElement.parentElement.tagName)) { endElement = endElement.parentElement; } } else if (endElement.childElementCount > 0 && endElement.childNodes[range.endOffset]?.nodeType !== 3) { endElement = endElement.childNodes[range.endOffset] as HTMLElement; } if (!endElement || endElement.nodeType === 3) { return []; } if (range.startOffset === range.startContainer.textContent.length) { const nextSibling = hasNextSibling(range.startContainer as Element); if (nextSibling && nextSibling.nodeType !== 3 && (nextSibling as Element).getAttribute("data-type") === "inline-math") { types.push("inline-math"); } } else if (range.endOffset === 0) { const previousSibling = hasPreviousSibling(range.startContainer as Element); if (previousSibling && previousSibling.nodeType !== 3 && (previousSibling as Element).getAttribute("data-type") === "inline-math") { types.push("inline-math"); } } if (startElement.tagName === "STRONG" || endElement.tagName === "STRONG") { types.push("bold"); } if (startElement.tagName === "EM" || endElement.tagName === "EM") { types.push("italic"); } if (startElement.tagName === "U" || endElement.tagName === "U") { types.push("underline"); } if (startElement.tagName === "S" || endElement.tagName === "S") { types.push("strike"); } if (startElement.tagName === "MARK" || endElement.tagName === "MARK") { types.push("mark"); } if (startElement.tagName === "SUP" || endElement.tagName === "SUP") { types.push("sup"); } if (startElement.tagName === "SUB" || endElement.tagName === "SUB") { types.push("sub"); } if (startElement.tagName === "KBD" || endElement.tagName === "KBD") { types.push("kbd"); } if (startElement.tagName === "SPAN" || endElement.tagName === "SPAN") { const startType = startElement.getAttribute("data-type"); const endType = endElement.getAttribute("data-type"); if (startType === "tag" || endType === "tag") { types.push("tag"); } else if (startType === "a" || endType === "a") { types.push("link"); } else if (startType === "block-ref" || endType === "block-ref") { types.push("blockRef"); } else if (startType === "file-annotation-ref" || endType === "file-annotation-ref") { types.push("blockRef"); } else if (startType === "inline-math") { types.push("inline-math"); } } if (startElement.tagName === "CODE" || endElement.tagName === "CODE") { types.push("inline-code"); } return types; } private genItem(protyle: IProtyle, menuItem: IMenuItem) { let menuItemObj; switch (menuItem.name) { case "bold": case "italic": case "strike": case "inline-code": case "mark": case "tag": case "underline": case "sup": case "sub": case "kbd": case "inline-math": menuItemObj = new ToolbarItem(protyle, menuItem); break; case "blockRef": menuItemObj = new BlockRef(protyle, menuItem); break; case "|": menuItemObj = new Divider(); break; case "font": menuItemObj = new Font(protyle, menuItem); break; case "link": menuItemObj = new Link(protyle, menuItem); break; } if (!menuItemObj) { return; } return menuItemObj.element; } private pushNode(newNodes: Node[], element: Element | DocumentFragment) { element.childNodes.forEach((item: Element) => { if (item.nodeType !== 3 && ( (item.getAttribute("data-type") === "inline-math" && item.textContent !== "") || item.tagName === "BR" || item.getAttribute("data-type") === "backslash" )) { // 软换行、数学公式、转移符不能消失 newNodes.push(item.cloneNode(true)); } else { if (item.textContent === "") { return; } newNodes.push(document.createTextNode(item.textContent)); } }); } public async setInlineMark(protyle: IProtyle, type: string, action: "remove" | "add" | "range" | "toolbar", focusAdd = false) { const nodeElement = hasClosestBlock(this.range.startContainer); if (!nodeElement) { return; } const types = this.getCurrentType(); if (action === "add" && types.length > 0 && types.includes(type) && !focusAdd) { if (type === "link") { this.showLink(protyle, this.range.startContainer.parentElement); } return; } // 对已有字体样式的文字再次添加字体样式 if (focusAdd && action === "add" && types.includes("bold") && this.range.startContainer.nodeType === 3 && this.range.startContainer.parentNode.isSameNode(this.range.endContainer.parentNode)) { return; } let startElement = this.range.startContainer as Element; if (this.range.startContainer.nodeType === 3) { startElement = this.range.startContainer.parentElement; if (startElement.getAttribute("data-type") === "virtual-block-ref" && !["DIV", "TD", "TH"].includes(startElement.parentElement.tagName)) { startElement = startElement.parentElement; } } // table 选中处理 const tableElement = hasClosestByAttribute(startElement, "data-type", "NodeTable"); if (this.range.toString() !== "" && tableElement && this.range.commonAncestorContainer.nodeType !== 3) { const parentTag = (this.range.commonAncestorContainer as Element).tagName; if (parentTag !== "TH" && parentTag !== "TD") { const startCellElement = hasClosestByMatchTag(startElement, "TD") || hasClosestByMatchTag(startElement, "TH"); const endCellElement = hasClosestByMatchTag(this.range.endContainer, "TD") || hasClosestByMatchTag(this.range.endContainer, "TH"); if (!startCellElement && !endCellElement) { const cellElement = tableElement.querySelector("th") || tableElement.querySelector("td"); this.range.setStartBefore(cellElement.firstChild); this.range.setEndAfter(cellElement.lastChild); startElement = cellElement; } else if (startCellElement && // 不能包含自身元素,否则对 cell 中的部分文字两次高亮后就会选中整个 cell。 https://github.com/siyuan-note/siyuan/issues/3649 第二点 !startCellElement.contains(this.range.endContainer)) { const cloneRange = this.range.cloneRange(); this.range.setEndAfter(startCellElement.lastChild); if (this.range.toString() === "" && endCellElement) { this.range.setEnd(cloneRange.endContainer, cloneRange.endOffset); this.range.setStartBefore(endCellElement.lastChild); } if (this.range.toString() === "") { return; } } } } if (this.range.toString() === "" && action === "range" && getSelectionOffset(startElement, protyle.wysiwyg.element).end === startElement.textContent.length && this.range.startContainer.nodeType === 3 && !this.range.startContainer.parentElement.getAttribute("contenteditable") && types.length > 0) { // 跳出行内元素 const textNode = document.createTextNode(Constants.ZWSP); this.range.startContainer.parentElement.after(textNode); this.range.selectNodeContents(textNode); this.range.collapse(false); if (types.includes(type)) { // 如果不是同一种行内元素,需进行后续的渲染操作 return; } } if (types.length > 0 && types.includes("link") && action === "range") { // 链接快捷键不应取消,应该显示链接信息 this.showLink(protyle, this.range.startContainer.parentElement); return; } const wbrElement = document.createElement("wbr"); this.range.insertNode(wbrElement); this.range.setStartAfter(wbrElement); const html = nodeElement.outerHTML; const actionBtn = action === "toolbar" ? this.element.querySelector(`[data-type="${type}"]`) : undefined; // 光标前标签移除 const newNodes: Node[] = []; let startText = ""; if (!["DIV", "TD", "TH"].includes(startElement.tagName)) { startText = startElement.textContent; this.pushNode(newNodes, startElement); startElement.remove(); } // 光标后标签移除 let endText = ""; let endClone; if (!this.range.startContainer.isSameNode(this.range.endContainer)) { let endElement = this.range.endContainer as HTMLElement; if (this.range.endContainer.nodeType === 3) { endElement = this.range.endContainer.parentElement; } if (endElement.getAttribute("data-type") === "virtual-block-ref" && !["DIV", "TD", "TH"].includes(endElement.parentElement.tagName)) { endElement = endElement.parentElement; } if (!["DIV", "TD", "TH"].includes(endElement.tagName)) { endClone = endElement; } } const selectContents = this.range.extractContents(); this.pushNode(newNodes, selectContents); if (endClone) { endText = endClone.textContent; this.pushNode(newNodes, endClone); endClone.remove(); } if ((action === "toolbar" && actionBtn.classList.contains("protyle-toolbar__item--current")) || action === "remove" || (action === "range" && types.length > 0 && types.includes(type))) { // 移除 if (type === "inline-math" && newNodes.length === 1) { const textNode = document.createTextNode(newNodes[0].textContent); this.range.insertNode(textNode); this.range.selectNodeContents(textNode); } else { newNodes.forEach((item, index) => { this.range.insertNode(item); if (index !== newNodes.length - 1) { this.range.collapse(false); } else { this.range.setEnd(item, item.textContent.length); } }); if (newNodes.length > 0) { this.range.setStart(newNodes[0], 0); } } focusByRange(this.range); this.element.querySelectorAll(".protyle-toolbar__item--current").forEach(item => { item.classList.remove("protyle-toolbar__item--current"); }); } else { if (newNodes.length === 0) { newNodes.push(document.createTextNode(Constants.ZWSP)); } // 添加 let newElement: Element; const refText = startText + selectContents.textContent + endText; const refNode = document.createTextNode(refText); switch (type) { case "bold": newElement = document.createElement("strong"); break; case "underline": newElement = document.createElement("u"); break; case "italic": newElement = document.createElement("em"); break; case "strike": newElement = document.createElement("s"); break; case "inline-code": newElement = document.createElement("code"); break; case "mark": newElement = document.createElement("mark"); break; case "sup": newElement = document.createElement("sup"); break; case "sub": newElement = document.createElement("sub"); break; case "kbd": newElement = document.createElement("kbd"); break; case "tag": newElement = document.createElement("span"); newElement.setAttribute("data-type", "tag"); break; case "link": newElement = document.createElement("span"); newElement.setAttribute("data-type", "a"); break; case "blockRef": if (refText === "") { wbrElement.remove(); return; } this.range.insertNode(refNode); this.range.selectNodeContents(refNode); hintRef(refText, protyle, true); break; case "inline-math": newElement = document.createElement("span"); newElement.className = "render-node"; newElement.setAttribute("contenteditable", "false"); newElement.setAttribute("data-type", "inline-math"); newElement.setAttribute("data-subtype", "math"); newElement.setAttribute("data-content", startText + selectContents.textContent + endText); mathRender(newElement); break; } if (newElement) { this.range.insertNode(newElement); } if (type === "inline-math") { this.range.setStartAfter(newElement); this.range.collapse(true); if (startText + selectContents.textContent + endText === "") { this.showRender(protyle, newElement as HTMLElement); } else { focusByRange(this.range); } this.element.classList.add("fn__none"); } else if (type !== "blockRef") { newNodes.forEach(item => { newElement.append(item); }); if (newElement.textContent === Constants.ZWSP) { this.isNewEmptyInline = true; this.range.setStart(newElement.firstChild, 1); this.range.collapse(true); } else { if (!hasPreviousSibling(newElement)) { // 列表内斜体后的最后一个字符无法选中 https://ld246.com/article/1629787455575 const nextSibling = hasNextSibling(newElement); if (nextSibling && nextSibling.nodeType === 3) { const textContent = nextSibling.textContent; nextSibling.textContent = ""; nextSibling.textContent = textContent; } } this.range.setStart(newElement.firstChild, 0); this.range.setEnd(newElement.lastChild, newElement.lastChild.textContent.length); focusByRange(this.range); } if (type === "link") { let needShowLink = true; let focusText = false; try { const clipText = await navigator.clipboard.readText(); // 选中链接时需忽略剪切板内容 https://ld246.com/article/1643035329737 if (protyle.lute.IsValidLinkDest(this.range.toString().trim())) { (newElement as HTMLElement).setAttribute("data-href", this.range.toString().trim()); needShowLink = false; } else if (protyle.lute.IsValidLinkDest(clipText)) { (newElement as HTMLElement).setAttribute("data-href", clipText); if (newElement.textContent.replace(Constants.ZWSP, "") !== "") { needShowLink = false; } focusText = true; } } catch (e) { console.log(e); } if (needShowLink) { this.showLink(protyle, newElement as HTMLElement, focusText); } } } if (actionBtn) { this.element.querySelectorAll(".protyle-toolbar__item--current").forEach(item => { item.classList.remove("protyle-toolbar__item--current"); }); actionBtn.classList.add("protyle-toolbar__item--current"); } } nodeElement.setAttribute("updated", dayjs().format("YYYYMMDDHHmmss")); updateTransaction(protyle, nodeElement.getAttribute("data-node-id"), nodeElement.outerHTML, html); wbrElement.remove(); } public showLink(protyle: IProtyle, linkElement: HTMLElement, focusText = false) { const nodeElement = hasClosestBlock(linkElement); if (!nodeElement) { return; } const id = nodeElement.getAttribute("data-node-id"); this.subElement.style.width = isMobile() ? "80vw" : Math.min(480, window.innerWidth) + "px"; this.subElement.style.padding = ""; this.subElement.innerHTML = `
`; let preventChange = false; let oldHTML = nodeElement.outerHTML; this.subElement.querySelector(".b3-button--outline").addEventListener(getEventName(), (event) => { preventChange = true; linkElement.setAttribute("data-subtype", "s"); linkElement.setAttribute("data-type", "block-ref"); linkElement.setAttribute("data-id", linkElement.getAttribute("data-href")?.replace("siyuan://blocks/", "")); linkElement.removeAttribute("data-href"); linkElement.removeAttribute("data-title"); nodeElement.setAttribute("updated", dayjs().format("YYYYMMDDHHmmss")); updateTransaction(protyle, id, nodeElement.outerHTML, oldHTML); protyle.toolbar.range.selectNode(linkElement); protyle.toolbar.range.collapse(false); focusByRange(protyle.toolbar.range); this.subElement.classList.add("fn__none"); event.stopPropagation(); }); this.subElement.querySelector(".b3-button--cancel").addEventListener(getEventName(), (event) => { preventChange = true; this.setInlineMark(protyle, "link", "remove"); this.subElement.classList.add("fn__none"); event.stopPropagation(); }); const titleElements = this.subElement.querySelectorAll(".b3-text-field") as NodeListOf; titleElements[0].value = linkElement.getAttribute("data-href") || ""; titleElements[1].value = linkElement.textContent.replace(Constants.ZWSP, ""); titleElements[2].value = Lute.UnEscapeHTMLStr(linkElement.getAttribute("data-title") || ""); const updateChange = (event: KeyboardEvent) => { this.isNewEmptyInline = false; event.stopPropagation(); if (event.isComposing) { return; } window.setTimeout(() => { if (preventChange) { return; } if (titleElements[1].value === "" || titleElements[0].value === "") { if (this.subElement.classList.contains("fn__none")) { this.setInlineMark(protyle, "link", "remove"); } return; } linkElement.innerHTML = Lute.EscapeHTMLStr(titleElements[1].value); if (titleElements[2].value) { linkElement.setAttribute("data-title", Lute.EscapeHTMLStr(titleElements[2].value)); } else { linkElement.removeAttribute("data-title"); } linkElement.setAttribute("data-href", titleElements[0].value); if (nodeElement.outerHTML !== oldHTML) { nodeElement.setAttribute("updated", dayjs().format("YYYYMMDDHHmmss")); updateTransaction(protyle, id, nodeElement.outerHTML, oldHTML); oldHTML = nodeElement.outerHTML; } if (this.subElement.classList.contains("fn__none")) { this.range.setEnd(linkElement.lastChild, linkElement.lastChild.textContent.length); this.range.collapse(false); focusByRange(this.range); } }, Constants.TIMEOUT_SEARCH); }; const hideSubElement = (event: KeyboardEvent) => { if ((event.key === "Enter" || event.key === "Escape") && !event.isComposing) { event.stopPropagation(); event.preventDefault(); this.subElement.classList.add("fn__none"); } }; titleElements[0].addEventListener("keydown", (event: KeyboardEvent) => { hideSubElement(event); }); titleElements[1].addEventListener("keydown", (event: KeyboardEvent) => { hideSubElement(event); }); titleElements[2].addEventListener("keydown", (event: KeyboardEvent) => { hideSubElement(event); }); titleElements[0].addEventListener("blur", (event: KeyboardEvent) => { updateChange(event); }); titleElements[1].addEventListener("blur", (event: KeyboardEvent) => { updateChange(event); }); titleElements[1].addEventListener("compositionend", () => { linkElement.innerHTML = Lute.EscapeHTMLStr(titleElements[1].value) || ""; }); titleElements[1].addEventListener("input", (event: KeyboardEvent) => { if (!event.isComposing) { linkElement.innerHTML = Lute.EscapeHTMLStr(titleElements[1].value) || ""; } }); titleElements[2].addEventListener("blur", (event: KeyboardEvent) => { updateChange(event); }); titleElements[2].addEventListener("compositionend", () => { if (titleElements[2].value) { linkElement.setAttribute("data-title", Lute.EscapeHTMLStr(titleElements[2].value) || ""); } else { linkElement.removeAttribute("data-title"); } }); titleElements[2].addEventListener("input", (event: KeyboardEvent) => { if (!event.isComposing) { if (titleElements[2].value) { linkElement.setAttribute("data-title", Lute.EscapeHTMLStr(titleElements[2].value) || ""); } else { linkElement.removeAttribute("data-title"); } } }); this.subElement.classList.remove("fn__none"); const nodeRect = linkElement.getBoundingClientRect(); setPosition(this.subElement, nodeRect.left, nodeRect.bottom, nodeRect.height + 4); this.element.classList.add("fn__none"); if (focusText || protyle.lute.IsValidLinkDest(titleElements[0].value)) { titleElements[1].select(); } else { titleElements[0].select(); } } public showFileAnnotationRef(protyle: IProtyle, refElement: HTMLElement) { const nodeElement = hasClosestBlock(refElement); if (!nodeElement) { return; } const id = nodeElement.getAttribute("data-node-id"); let html = nodeElement.outerHTML; this.subElement.style.width = isMobile() ? "80vw" : Math.min(480, window.innerWidth) + "px"; this.subElement.style.padding = ""; this.subElement.innerHTML = `
`; this.subElement.querySelector(".b3-button--cancel").addEventListener(getEventName(), () => { refElement.insertAdjacentHTML("afterend", ""); const oldHTML = nodeElement.outerHTML; refElement.outerHTML = refElement.textContent; nodeElement.setAttribute("updated", dayjs().format("YYYYMMDDHHmmss")); updateTransaction(protyle, id, nodeElement.outerHTML, oldHTML); this.subElement.classList.add("fn__none"); focusByWbr(nodeElement, this.range); }); const anchorElement = this.subElement.querySelector('[data-type="anchor"]') as HTMLInputElement; if (refElement.getAttribute("data-subtype") === "s") { anchorElement.value = refElement.textContent; } anchorElement.addEventListener("change", (event) => { refElement.after(document.createElement("wbr")); nodeElement.setAttribute("updated", dayjs().format("YYYYMMDDHHmmss")); updateTransaction(protyle, id, nodeElement.outerHTML, html); html = nodeElement.outerHTML; nodeElement.querySelector("wbr").remove(); event.stopPropagation(); }); anchorElement.addEventListener("input", (event) => { const target = event.target as HTMLInputElement; if (target.value) { refElement.innerHTML = Lute.EscapeHTMLStr(target.value); } else { refElement.innerHTML = "*"; } event.stopPropagation(); }); anchorElement.addEventListener("keydown", (event: KeyboardEvent) => { event.stopPropagation(); if (event.isComposing) { return; } if (event.key === "Enter" || event.key === "Escape") { this.subElement.classList.add("fn__none"); this.range.setStart(refElement.firstChild, 0); this.range.setEnd(refElement.lastChild, refElement.lastChild.textContent.length); focusByRange(this.range); event.preventDefault(); event.stopPropagation(); } }); this.subElement.classList.remove("fn__none"); const nodeRect = refElement.getBoundingClientRect(); setPosition(this.subElement, nodeRect.left, nodeRect.bottom, nodeRect.height + 4); this.element.classList.add("fn__none"); anchorElement.select(); } public showRender(protyle: IProtyle, renderElement: Element) { const nodeElement = hasClosestBlock(renderElement); if (!nodeElement) { return; } const id = nodeElement.getAttribute("data-node-id"); const type = renderElement.getAttribute("data-type"); let html = nodeElement.outerHTML; let title = "HTML"; let placeholder = ""; switch (renderElement.getAttribute("data-subtype")) { case "abc": title = window.siyuan.languages.staff; break; case "echarts": title = window.siyuan.languages.chart; break; case "flowchart": title = "Flow Chart"; break; case "graphviz": title = "Graphviz"; break; case "mermaid": title = "Mermaid"; break; case "mindmap": placeholder = `- foo - bar - baz`; title = window.siyuan.languages.mindmap; break; case "plantuml": title = "UML"; break; case "math": if (type === "NodeMathBlock") { title = window.siyuan.languages.math; } else { title = window.siyuan.languages["inline-math"]; } break; } if (type === "NodeBlockQueryEmbed") { title = window.siyuan.languages.blockEmbed; } const isPin = this.subElement.querySelector('[data-type="pin"]')?.classList.contains("ft__primary"); const pinData: IObject = {}; if (isPin) { const textElement = this.subElement.querySelector(".b3-text-field") as HTMLTextAreaElement; pinData.styleH = textElement.style.height; pinData.styleW = textElement.style.width; } else { this.subElement.style.width = ""; this.subElement.style.padding = "0"; } this.subElement.innerHTML = `
${title}
`; const autoHeight = () => { textElement.style.height = textElement.scrollHeight + "px"; if (this.subElement.firstElementChild.getAttribute("data-drag") === "true") { if (textElement.getBoundingClientRect().bottom > window.innerHeight) { this.subElement.style.top = window.innerHeight - this.subElement.clientHeight + "px"; } return; } if (this.subElement.clientHeight <= window.innerHeight - nodeRect.bottom || this.subElement.clientHeight <= nodeRect.top) { if (type === "inline-math") { setPosition(this.subElement, nodeRect.left, nodeRect.bottom, nodeRect.height); } else { setPosition(this.subElement, nodeRect.left + (nodeRect.width - this.subElement.clientWidth) / 2, nodeRect.bottom, nodeRect.height); } } else { setPosition(this.subElement, nodeRect.right, nodeRect.bottom); } }; this.subElement.querySelector(".block__icons").addEventListener("click", (event: MouseEvent) => { const target = event.target as HTMLElement; const btnElement = hasClosestByClassName(target, "b3-tooltips"); if (!btnElement) { return; } event.stopPropagation(); switch (btnElement.getAttribute("data-type")) { case "close": this.subElement.classList.add("fn__none"); this.subElement.querySelector('[data-type="pin"]').classList.remove("ft__primary"); break; case "pin": btnElement.classList.toggle("ft__primary"); break; case "refresh": btnElement.classList.toggle("ft__primary"); break; case "before": insertEmptyBlock(protyle, "beforebegin", id); hideElements(["util"], protyle); break; case "after": insertEmptyBlock(protyle, "afterend", id); hideElements(["util"], protyle); break; case "copy": /// #if !BROWSER hideElements(["util"], protyle); setTimeout(() => { const rect = renderElement.getBoundingClientRect(); getCurrentWindow().webContents.capturePage({ x: Math.floor(rect.x), y: Math.floor(rect.y) - 4, // 行内数学公式头部截不到 width: Math.floor(rect.width), height: Math.floor(rect.height) + 4 }).then((image: NativeImage) => { clipboard.writeImage(nativeImage.createFromBuffer(image.toPNG())); }); }, 100); /// #endif break; } }); this.subElement.querySelector(".block__icons").addEventListener("mousedown", (event: MouseEvent) => { if (hasClosestByClassName(event.target as HTMLElement, "block__icon")) { return; } const documentSelf = document; this.subElement.style.userSelect = "none"; const dragBgElement = documentSelf.querySelector("#dragBg"); dragBgElement.classList.remove("fn__none"); const x = event.clientX - parseInt(this.subElement.style.left); const y = event.clientY - parseInt(this.subElement.style.top); setTimeout(() => { // windows 需等待 dragBgElement 显示后才可以进行 move https://github.com/siyuan-note/siyuan/issues/2950 documentSelf.onmousemove = (moveEvent: MouseEvent) => { let positionX = moveEvent.clientX - x; let positionY = moveEvent.clientY - y; if (positionX > window.innerWidth - this.subElement.clientWidth) { positionX = window.innerWidth - this.subElement.clientWidth; } if (positionY > window.innerHeight - this.subElement.clientHeight) { positionY = window.innerHeight - this.subElement.clientHeight; } this.subElement.style.left = Math.max(positionX, 0) + "px"; this.subElement.style.top = Math.max(positionY, Constants.SIZE_TOOLBAR_HEIGHT) + "px"; this.subElement.firstElementChild.setAttribute("data-drag", "true"); }; documentSelf.onmouseup = () => { this.subElement.style.userSelect = "auto"; documentSelf.onmousemove = null; documentSelf.onmouseup = null; documentSelf.ondragstart = null; documentSelf.onselectstart = null; documentSelf.onselect = null; dragBgElement.classList.add("fn__none"); }; }); return; }); const textElement = this.subElement.querySelector(".b3-text-field") as HTMLTextAreaElement; if (type === "NodeHTMLBlock") { textElement.value = Lute.UnEscapeHTMLStr(renderElement.querySelector("protyle-html").getAttribute("data-content") || ""); } else { const switchElement = this.subElement.querySelector(".b3-switch") as HTMLInputElement; if (nodeElement.getAttribute("custom-heading-mode") === "1") { switchElement.checked = true; } switchElement.addEventListener("change", () => { hideElements(["util"], protyle); nodeElement.setAttribute("custom-heading-mode", switchElement.checked ? "1" : "0"); fetchPost("/api/attr/setBlockAttrs", { id, attrs: {"custom-heading-mode": switchElement.checked ? "1" : "0"} }); renderElement.removeAttribute("data-render"); blockRender(protyle, renderElement); }); textElement.value = Lute.UnEscapeHTMLStr(renderElement.getAttribute("data-content") || ""); } textElement.addEventListener("input", (event) => { if (!renderElement.parentElement) { return; } if (textElement.clientHeight !== textElement.scrollHeight) { autoHeight(); } if (!this.subElement.querySelector('[data-type="refresh"]').classList.contains("ft__primary")) { return; } const target = event.target as HTMLTextAreaElement; if (type === "NodeHTMLBlock") { renderElement.querySelector("protyle-html").setAttribute("data-content", Lute.EscapeHTMLStr(target.value)); } else { renderElement.setAttribute("data-content", Lute.EscapeHTMLStr(target.value)); renderElement.removeAttribute("data-render"); } if (!["NodeBlockQueryEmbed", "NodeHTMLBlock"].includes(type)) { processRender(renderElement); } event.stopPropagation(); }); textElement.addEventListener("change", (event) => { if (!renderElement.parentElement) { return; } if (!this.subElement.querySelector('[data-type="refresh"]').classList.contains("ft__primary")) { const target = event.target as HTMLTextAreaElement; if (type === "NodeHTMLBlock") { renderElement.querySelector("protyle-html").setAttribute("data-content", Lute.EscapeHTMLStr(target.value)); } else { renderElement.setAttribute("data-content", Lute.EscapeHTMLStr(target.value)); renderElement.removeAttribute("data-render"); } if (!["NodeBlockQueryEmbed", "NodeHTMLBlock"].includes(type)) { processRender(renderElement); } } if (type === "NodeBlockQueryEmbed") { blockRender(protyle, renderElement); } if (this.range) { focusByRange(this.range); } if (type === "inline-math") { // 行内数学公式不允许换行 https://github.com/siyuan-note/siyuan/issues/2187 renderElement.setAttribute("data-content", renderElement.getAttribute("data-content").replace(/\n/g, "")); } nodeElement.setAttribute("updated", dayjs().format("YYYYMMDDHHmmss")); updateTransaction(protyle, id, nodeElement.outerHTML, html); html = nodeElement.outerHTML; event.stopPropagation(); }); textElement.addEventListener("keydown", (event: KeyboardEvent) => { event.stopPropagation(); if (event.isComposing) { return; } if (event.key === "Escape" || matchHotKey("⌘Enter", event)) { this.subElement.classList.add("fn__none"); this.subElement.querySelector('[data-type="pin"]').classList.remove("ft__primary"); if (renderElement.tagName === "SPAN") { const range = getEditorRange(renderElement); range.setStartAfter(renderElement); range.collapse(true); focusByRange(range); } else { focusSideBlock(renderElement); } } else if (event.key === "Tab") { const start = textElement.selectionStart; textElement.value = textElement.value.substring(0, start) + "\t" + textElement.value.substring(textElement.selectionEnd); textElement.selectionStart = textElement.selectionEnd = start + 1; event.preventDefault(); } }); this.subElement.classList.remove("fn__none"); const nodeRect = renderElement.getBoundingClientRect(); this.element.classList.add("fn__none"); if (isPin) { textElement.style.width = pinData.styleW; textElement.style.height = pinData.styleH; } else { autoHeight(); } textElement.select(); } public showCodeLanguage(protyle: IProtyle, languageElement: HTMLElement) { const nodeElement = hasClosestBlock(languageElement); if (!nodeElement) { return; } this.range = getEditorRange(nodeElement); const id = nodeElement.getAttribute("data-node-id"); let oldHtml = nodeElement.outerHTML; let html = ""; Constants.CODE_LANGUAGES.forEach((item, index) => { html += `
${item}
`; }); this.subElement.style.width = ""; this.subElement.style.padding = ""; this.subElement.innerHTML = `
${html}
`; const inputElement = this.subElement.querySelector("input"); inputElement.addEventListener("keydown", (event: KeyboardEvent) => { event.stopPropagation(); if (event.isComposing) { return; } upDownHint(this.subElement.lastElementChild.lastElementChild as HTMLElement, event); if (event.key === "Enter") { languageElement.textContent = this.subElement.querySelector(".b3-list-item--focus").textContent; localStorage.setItem(Constants.LOCAL_CODELANG, languageElement.textContent); const editElement = getContenteditableElement(nodeElement); const lineNumber = nodeElement.getAttribute("linenumber"); if (lineNumber === "true" || (lineNumber !== "false" && window.siyuan.config.editor.codeSyntaxHighlightLineNum)) { editElement.classList.add("protyle-linenumber"); } else { editElement.classList.remove("protyle-linenumber"); } (editElement as HTMLElement).textContent = editElement.textContent; editElement.removeAttribute("data-render"); highlightRender(nodeElement); nodeElement.setAttribute("updated", dayjs().format("YYYYMMDDHHmmss")); updateTransaction(protyle, id, nodeElement.outerHTML, oldHtml); oldHtml = nodeElement.outerHTML; event.preventDefault(); event.stopPropagation(); } if (event.key === "Escape" || event.key === "Enter") { this.subElement.classList.add("fn__none"); focusByRange(this.range); } }); inputElement.addEventListener("input", (event) => { const mathLanguages: string[] = []; Constants.CODE_LANGUAGES.forEach((item) => { if (item.indexOf(inputElement.value.toLowerCase()) > -1) { mathLanguages.push(item); } }); let html = ""; // sort mathLanguages.sort((a, b) => { if (a.startsWith(inputElement.value.toLowerCase()) && b.startsWith(inputElement.value.toLowerCase())) { if (a.length < b.length) { return -1; } else if (a.length === b.length) { return 0; } else { return 1; } } else if (a.startsWith(inputElement.value.toLowerCase())) { return -1; } else if (b.startsWith(inputElement.value.toLowerCase())) { return 1; } else { return 0; } }).forEach((item) => { html += `
${item.replace(inputElement.value.toLowerCase(), "" + inputElement.value.toLowerCase() + "")}
`; }); this.subElement.firstElementChild.lastElementChild.innerHTML = html; if (html) { this.subElement.firstElementChild.lastElementChild.firstElementChild.classList.add("b3-list-item--focus"); } event.stopPropagation(); }); this.subElement.lastElementChild.lastElementChild.addEventListener("click", (event) => { const target = event.target as HTMLElement; const listElement = hasClosestByClassName(target, "b3-list-item"); if (!listElement) { return; } languageElement.textContent = listElement.textContent; localStorage.setItem(Constants.LOCAL_CODELANG, languageElement.textContent); const nodeElement = hasClosestBlock(languageElement); if (nodeElement) { const editElement = getContenteditableElement(nodeElement); const lineNumber = nodeElement.getAttribute("linenumber"); if (lineNumber === "true" || (lineNumber !== "false" && window.siyuan.config.editor.codeSyntaxHighlightLineNum)) { editElement.classList.add("protyle-linenumber"); } else { editElement.classList.remove("protyle-linenumber"); } (editElement as HTMLElement).textContent = editElement.textContent; editElement.removeAttribute("data-render"); highlightRender(nodeElement); nodeElement.setAttribute("updated", dayjs().format("YYYYMMDDHHmmss")); updateTransaction(protyle, id, nodeElement.outerHTML, oldHtml); oldHtml = nodeElement.outerHTML; this.subElement.classList.add("fn__none"); focusByRange(this.range); } }); this.subElement.classList.remove("fn__none"); const nodeRect = languageElement.getBoundingClientRect(); setPosition(this.subElement, nodeRect.left, nodeRect.bottom, nodeRect.height); this.element.classList.add("fn__none"); inputElement.select(); } public showTpl(protyle: IProtyle, nodeElement: HTMLElement, range: Range) { this.range = range; fetchPost("/api/search/searchTemplate", { k: "", }, (response) => { let html = ""; response.data.blocks.forEach((item: { path: string, content: string }, index: number) => { html += `
${item.content}
`; }); this.subElement.style.width = ""; this.subElement.style.padding = ""; this.subElement.innerHTML = `
${html}
`; const inputElement = this.subElement.querySelector("input"); inputElement.addEventListener("keydown", (event: KeyboardEvent) => { event.stopPropagation(); if (event.isComposing) { return; } upDownHint(this.subElement.lastElementChild.lastElementChild as HTMLElement, event); if (event.key === "Enter") { hintRenderTemplate(decodeURIComponent(this.subElement.querySelector(".b3-list-item--focus").getAttribute("data-value")), protyle, nodeElement); this.subElement.classList.add("fn__none"); event.preventDefault(); } else if (event.key === "Escape") { this.subElement.classList.add("fn__none"); focusByRange(this.range); } }); inputElement.addEventListener("input", (event) => { event.stopPropagation(); fetchPost("/api/search/searchTemplate", { k: inputElement.value, }, (response) => { let searchHTML = ""; response.data.blocks.forEach((item: { path: string, content: string }, index: number) => { searchHTML += `
${item.content}
`; }); this.subElement.firstElementChild.lastElementChild.innerHTML = searchHTML; }); }); this.subElement.lastElementChild.addEventListener("click", (event) => { const target = event.target as HTMLElement; const listElement = hasClosestByClassName(target, "b3-list-item"); if (!listElement) { return; } hintRenderTemplate(decodeURIComponent(listElement.getAttribute("data-value")), protyle, nodeElement); }); const rangePosition = getSelectionPosition(nodeElement, range); this.subElement.classList.remove("fn__none"); setPosition(this.subElement, rangePosition.left, rangePosition.top + 18, Constants.SIZE_TOOLBAR_HEIGHT); this.element.classList.add("fn__none"); inputElement.select(); }); } public showWidget(protyle: IProtyle, nodeElement: HTMLElement, range: Range) { this.range = range; fetchPost("/api/search/searchWidget", { k: "", }, (response) => { let html = ""; response.data.blocks.forEach((item: { content: string }, index: number) => { html += `
${item.content}
`; }); this.subElement.style.width = ""; this.subElement.style.padding = ""; this.subElement.innerHTML = `
${html}
`; const inputElement = this.subElement.querySelector("input"); inputElement.addEventListener("keydown", (event: KeyboardEvent) => { event.stopPropagation(); if (event.isComposing) { return; } upDownHint(this.subElement.lastElementChild.lastElementChild as HTMLElement, event); if (event.key === "Enter") { hintRenderWidget(this.subElement.querySelector(".b3-list-item--focus").textContent, protyle); this.subElement.classList.add("fn__none"); event.preventDefault(); } else if (event.key === "Escape") { this.subElement.classList.add("fn__none"); focusByRange(this.range); } }); inputElement.addEventListener("input", (event) => { event.stopPropagation(); fetchPost("/api/search/searchWidget", { k: inputElement.value, }, (response) => { let searchHTML = ""; response.data.blocks.forEach((item: { path: string, content: string }, index: number) => { searchHTML += `
${item.content}
`; }); this.subElement.firstElementChild.lastElementChild.innerHTML = searchHTML; }); }); this.subElement.lastElementChild.addEventListener("click", (event) => { const target = event.target as HTMLElement; const listElement = hasClosestByClassName(target, "b3-list-item"); if (!listElement) { return; } hintRenderWidget(listElement.textContent, protyle); }); const rangePosition = getSelectionPosition(nodeElement, range); this.subElement.classList.remove("fn__none"); setPosition(this.subElement, rangePosition.left, rangePosition.top + 18, Constants.SIZE_TOOLBAR_HEIGHT); this.element.classList.add("fn__none"); inputElement.select(); }); } public showAssets(protyle: IProtyle, nodeElement: HTMLElement, range: Range) { this.range = range; fetchPost("/api/search/searchAsset", { k: "", }, (response) => { let html = ""; response.data.forEach((item: { hName: string, path: string }, index: number) => { html += `${item.hName}`; }); this.subElement.style.width = ""; this.subElement.style.padding = ""; this.subElement.innerHTML = `
${html}
`; const inputElement = this.subElement.querySelector("input"); inputElement.addEventListener("keydown", (event: KeyboardEvent) => { event.stopPropagation(); if (event.isComposing) { return; } upDownHint(this.subElement.lastElementChild.lastElementChild as HTMLElement, event); if (event.key === "Enter") { hintRenderAssets(this.subElement.querySelector(".b3-list-item--focus").getAttribute("data-value"), protyle); } else if (event.key === "Escape") { this.subElement.classList.add("fn__none"); focusByRange(this.range); } }); inputElement.addEventListener("input", (event) => { event.stopPropagation(); fetchPost("/api/search/searchAsset", { k: inputElement.value, }, (response) => { let searchHTML = ""; response.data.forEach((item: { path: string, hName: string }, index: number) => { searchHTML += `
${item.hName}
`; }); this.subElement.firstElementChild.lastElementChild.innerHTML = searchHTML; }); }); this.subElement.lastElementChild.addEventListener("click", (event) => { const target = event.target as HTMLElement; const listElement = hasClosestByClassName(target, "b3-list-item"); if (!listElement) { return; } hintRenderAssets(listElement.getAttribute("data-value"), protyle); }); const rangePosition = getSelectionPosition(nodeElement, range); this.subElement.classList.remove("fn__none"); setPosition(this.subElement, rangePosition.left, rangePosition.top + 18, Constants.SIZE_TOOLBAR_HEIGHT); this.element.classList.add("fn__none"); inputElement.select(); }); } public showFile(protyle: IProtyle, nodeElements: Element[], range: Range) { this.range = range; fetchPost("/api/filetree/searchDocs", { k: "", }, (response) => { let html = ""; response.data.forEach((item: { boxIcon: string, box: string, hPath: string, path: string }) => { if (item.path === "/") { return; } html += `
${item.boxIcon ? ('' + unicode2Emoji(item.boxIcon) + "") : ""} ${escapeHtml(item.hPath)}
`; }); this.subElement.style.width = ""; this.subElement.style.padding = ""; this.subElement.innerHTML = `
${html}
`; const inputElement = this.subElement.querySelector("input"); inputElement.addEventListener("keydown", (event: KeyboardEvent) => { event.stopPropagation(); if (event.isComposing) { return; } upDownHint(this.subElement.lastElementChild.lastElementChild as HTMLElement, event); if (event.key === "Enter") { hintMoveBlock(this.subElement.querySelector(".b3-list-item--focus").getAttribute("data-path"), nodeElements, protyle); event.preventDefault(); } else if (event.key === "Escape") { this.subElement.classList.add("fn__none"); focusByRange(this.range); } }); inputElement.addEventListener("input", (event) => { event.stopPropagation(); fetchPost("/api/filetree/searchDocs", { k: inputElement.value, }, (response) => { let searchHTML = ""; response.data.forEach((item: { boxIcon: string, box: string, hPath: string, path: string }) => { if (item.path === "/") { return; } searchHTML += `
${item.boxIcon ? ('' + unicode2Emoji(item.boxIcon) + "") : ""} ${escapeHtml(item.hPath)}
`; }); this.subElement.firstElementChild.lastElementChild.innerHTML = searchHTML; }); }); this.subElement.lastElementChild.addEventListener("click", (event) => { const target = event.target as HTMLElement; const listElement = hasClosestByClassName(target, "b3-list-item"); if (!listElement) { return; } hintMoveBlock(listElement.getAttribute("data-path"), nodeElements, protyle); }); const rangePosition = getSelectionPosition(nodeElements[0], range); this.subElement.classList.remove("fn__none"); setPosition(this.subElement, rangePosition.left, rangePosition.top + 18, Constants.SIZE_TOOLBAR_HEIGHT); this.element.classList.add("fn__none"); inputElement.select(); }); } }