import {listIndent, listOutdent} from "../../protyle/wysiwyg/list";
import {
hasClosestBlock,
hasClosestByAttribute,
hasClosestByClassName, hasClosestByTag,
} from "../../protyle/util/hasClosest";
import {moveToDown, moveToUp} from "../../protyle/wysiwyg/move";
import {Constants} from "../../constants";
import {focusByRange, getSelectionPosition} from "../../protyle/util/selection";
import {getCurrentEditor} from "../editor";
import {fontEvent, getFontNodeElements} from "../../protyle/toolbar/Font";
import {hideElements} from "../../protyle/ui/hideElements";
import {softEnter} from "../../protyle/wysiwyg/enter";
import {isInAndroid} from "../../protyle/util/compatibility";
let renderKeyboardToolbarTimeout: number;
let showUtil = false;
const getSlashItem = (value: string, icon: string, text: string, focus = "false") => {
let iconHTML;
if (icon && icon.startsWith("icon")) {
iconHTML = ``;
} else {
iconHTML = icon;
}
return ``;
};
export const renderTextMenu = (protyle: IProtyle, toolbarElement: Element) => {
let colorHTML = "";
["", "var(--b3-font-color1)", "var(--b3-font-color2)", "var(--b3-font-color3)", "var(--b3-font-color4)",
"var(--b3-font-color5)", "var(--b3-font-color6)", "var(--b3-font-color7)", "var(--b3-font-color8)",
"var(--b3-font-color9)", "var(--b3-font-color10)", "var(--b3-font-color11)", "var(--b3-font-color12)",
"var(--b3-font-color13)"].forEach((item, index) => {
colorHTML += ``;
});
let bgHTML = "";
["", "var(--b3-font-background1)", "var(--b3-font-background2)", "var(--b3-font-background3)", "var(--b3-font-background4)",
"var(--b3-font-background5)", "var(--b3-font-background6)", "var(--b3-font-background7)", "var(--b3-font-background8)",
"var(--b3-font-background9)", "var(--b3-font-background10)", "var(--b3-font-background11)", "var(--b3-font-background12)",
"var(--b3-font-background13)"].forEach((item, index) => {
bgHTML += ``;
});
const nodeElements = getFontNodeElements(protyle);
let disableFont = false;
nodeElements?.find((item: HTMLElement) => {
if (item.classList.contains("li")) {
disableFont = true;
return true;
}
});
let lastColorHTML = "";
const lastFonts = window.siyuan.storage[Constants.LOCAL_FONTSTYLES];
if (lastFonts.length > 0) {
lastColorHTML = `
${window.siyuan.languages.lastUsed}
`;
lastFonts.forEach((item: string) => {
const lastFontStatus = item.split(Constants.ZWSP);
switch (lastFontStatus[0]) {
case "color":
lastColorHTML += ``;
break;
case "backgroundColor":
lastColorHTML += ``;
break;
case "style2":
lastColorHTML += ``;
break;
case "style4":
lastColorHTML += ``;
break;
case "fontSize":
if (!disableFont) {
lastColorHTML += ``;
}
break;
case "style1":
if (lastFontStatus[1]) {
lastColorHTML += ``;
} else {
lastColorHTML += ``;
}
break;
case "clear":
lastColorHTML += ``;
break;
}
});
lastColorHTML += "
";
}
let textElement: HTMLElement;
let fontSize = "16px";
if (nodeElements && nodeElements.length > 0) {
textElement = nodeElements[0] as HTMLElement;
} else {
textElement = protyle.toolbar.range.cloneContents().querySelector('[data-type~="text"]') as HTMLElement;
if (!textElement) {
textElement = hasClosestByAttribute(protyle.toolbar.range.startContainer, "data-type", "text") as HTMLElement;
}
}
if (textElement) {
fontSize = textElement.style.fontSize || "16px";
}
const utilElement = toolbarElement.querySelector(".keyboard__util") as HTMLElement;
utilElement.innerHTML = `${lastColorHTML}
${window.siyuan.languages.color}
${window.siyuan.languages.colorFont}
${colorHTML}
${window.siyuan.languages.colorPrimary}
${bgHTML}
${window.siyuan.languages.fontStyle}
${window.siyuan.languages.fontSize}
`;
utilElement.querySelector("select").addEventListener("change", function (event: Event) {
fontEvent(protyle, nodeElements, "fontSize", (event.target as HTMLSelectElement).value);
});
};
const renderSlashMenu = (protyle: IProtyle, toolbarElement: Element) => {
protyle.hint.splitChar = "/";
protyle.hint.lastIndex = -1;
let pluginHTML = "";
protyle.app.plugins.forEach((plugin) => {
plugin.protyleSlash.forEach(slash => {
pluginHTML += getSlashItem(`plugin${Constants.ZWSP}${plugin.name}${Constants.ZWSP}${slash.id}`,
"", slash.html, "true");
});
});
if (pluginHTML) {
pluginHTML = `${pluginHTML}
`;
}
const utilElement = toolbarElement.querySelector(".keyboard__util") as HTMLElement;
utilElement.innerHTML = `
${getSlashItem(Constants.ZWSP, "iconMarkdown", window.siyuan.languages.template)}
${getSlashItem(Constants.ZWSP + 1, "iconBoth", window.siyuan.languages.widget)}
${getSlashItem(Constants.ZWSP + 2, "iconImage", window.siyuan.languages.assets)}
${getSlashItem("((", "iconRef", window.siyuan.languages.ref, "true")}
${getSlashItem("{{", "iconSQL", window.siyuan.languages.blockEmbed, "true")}
${getSlashItem(Constants.ZWSP + 5, "iconSparkles", window.siyuan.languages.aiWriting)}
${getSlashItem('
', "iconDatabase", window.siyuan.languages.database, "true")}
${getSlashItem(Constants.ZWSP + 4, "iconFile", window.siyuan.languages.newSubDocRef)}
${getSlashItem(Constants.ZWSP + 3, "iconDownload", window.siyuan.languages.insertAsset + '
", "true")}
${isInAndroid() ? getSlashItem(Constants.ZWSP + 3, "iconCamera", window.siyuan.languages.insertPhoto + '
", "true") : ""}
${getSlashItem('
', "iconLanguage", window.siyuan.languages.insertIframeURL, "true")}
${getSlashItem("![]()", "iconImage", window.siyuan.languages.insertImgURL, "true")}
${getSlashItem('
', "iconVideo", window.siyuan.languages.insertVideoURL, "true")}
${getSlashItem('
', "iconRecord", window.siyuan.languages.insertAudioURL, "true")}
${getSlashItem("emoji", "iconEmoji", window.siyuan.languages.emoji, "true")}
${getSlashItem("# " + Lute.Caret, "iconH1", window.siyuan.languages.heading1, "true")}
${getSlashItem("## " + Lute.Caret, "iconH2", window.siyuan.languages.heading2, "true")}
${getSlashItem("### " + Lute.Caret, "iconH3", window.siyuan.languages.heading3, "true")}
${getSlashItem("#### " + Lute.Caret, "iconH4", window.siyuan.languages.heading4, "true")}
${getSlashItem("##### " + Lute.Caret, "iconH5", window.siyuan.languages.heading5, "true")}
${getSlashItem("###### " + Lute.Caret, "iconH6", window.siyuan.languages.heading6, "true")}
${getSlashItem("* " + Lute.Caret, "iconList", window.siyuan.languages.list, "true")}
${getSlashItem("1. " + Lute.Caret, "iconOrderedList", window.siyuan.languages["ordered-list"], "true")}
${getSlashItem("* [ ] " + Lute.Caret, "iconCheck", window.siyuan.languages.check, "true")}
${getSlashItem("> " + Lute.Caret, "iconQuote", window.siyuan.languages.quote, "true")}
${getSlashItem("```", "iconCode", window.siyuan.languages.code, "true")}
${getSlashItem(`| ${Lute.Caret} | | |\n| --- | --- | --- |\n| | | |\n| | | |`, "iconTable", window.siyuan.languages.table, "true")}
${getSlashItem("---", "iconLine", window.siyuan.languages.line, "true")}
${getSlashItem("$$", "iconMath", window.siyuan.languages.math)}
${getSlashItem("
", "iconHTML5", "HTML")}
${getSlashItem("```abc\n```", "", window.siyuan.languages.staff, "true")}
${getSlashItem("```echarts\n```", "", window.siyuan.languages.chart, "true")}
${getSlashItem("```flowchart\n```", "", "Flow Chart", "true")}
${getSlashItem("```graphviz\n```", "", "Graph", "true")}
${getSlashItem("```mermaid\n```", "", "Mermaid", "true")}
${getSlashItem("```mindmap\n```", "", window.siyuan.languages.mindmap, "true")}
${getSlashItem("```plantuml\n```", "", "UML", "true")}
${getSlashItem(`style${Constants.ZWSP}color: var(--b3-card-info-color);background-color: var(--b3-card-info-background);`, '
A
', window.siyuan.languages.infoStyle, "true")}
${getSlashItem(`style${Constants.ZWSP}color: var(--b3-card-success-color);background-color: var(--b3-card-success-background);`, '
A
', window.siyuan.languages.successStyle, "true")}
${getSlashItem(`style${Constants.ZWSP}color: var(--b3-card-warning-color);background-color: var(--b3-card-warning-background);`, '
A
', window.siyuan.languages.warningStyle, "true")}
${getSlashItem(`style${Constants.ZWSP}color: var(--b3-card-error-color);background-color: var(--b3-card-error-background);`, '
A
', window.siyuan.languages.errorStyle, "true")}
${getSlashItem(`style${Constants.ZWSP}`, '
A
', window.siyuan.languages.clearFontStyle, "true")}
${pluginHTML}`;
protyle.hint.bindUploadEvent(protyle, utilElement);
};
export const showKeyboardToolbarUtil = (oldScrollTop: number) => {
window.siyuan.menus.menu.remove();
showUtil = true;
const toolbarElement = document.getElementById("keyboardToolbar");
let keyboardHeight = toolbarElement.getAttribute("data-keyboardheight");
keyboardHeight = (keyboardHeight ? (parseInt(keyboardHeight) + 42) : window.outerHeight / 2) + "px";
const editor = getCurrentEditor();
if (editor) {
editor.protyle.element.parentElement.style.paddingBottom = keyboardHeight;
editor.protyle.contentElement.scrollTop = oldScrollTop;
}
setTimeout(() => {
toolbarElement.style.height = keyboardHeight;
}, Constants.TIMEOUT_TRANSITION); // 防止抖动
setTimeout(() => {
showUtil = false;
}, 1000); // 防止光标改变后斜杆菜单消失
};
const hideKeyboardToolbarUtil = () => {
const toolbarElement = document.getElementById("keyboardToolbar");
toolbarElement.style.height = "";
const editor = getCurrentEditor();
if (editor) {
editor.protyle.element.parentElement.style.paddingBottom = "42px";
}
toolbarElement.querySelector('.keyboard__action[data-type="add"]').classList.remove("protyle-toolbar__item--current");
toolbarElement.querySelector('.keyboard__action[data-type="text"]').classList.remove("protyle-toolbar__item--current");
toolbarElement.querySelector('.keyboard__action[data-type="done"] use').setAttribute("xlink:href", "#iconKeyboardHide");
};
const renderKeyboardToolbar = () => {
clearTimeout(renderKeyboardToolbarTimeout);
renderKeyboardToolbarTimeout = window.setTimeout(() => {
if (getSelection().rangeCount === 0 ||
window.siyuan.config.readonly ||
document.getElementById("toolbarName").getAttribute("readonly") === "readonly" ||
window.screen.height - window.innerHeight < 160 || // reloadSync 会导致 selectionchange,从而导致键盘没有弹起的情况下出现工具栏
!document.activeElement || (
document.activeElement &&
!["INPUT", "TEXTAREA"].includes(document.activeElement.tagName) &&
!document.activeElement.classList.contains("protyle-wysiwyg") &&
document.activeElement.getAttribute("contenteditable") !== "true"
)) {
hideKeyboardToolbar();
return;
}
// 编辑器设置界面点击空白或关闭,焦点不知何故会飘移到编辑器上
if (document.activeElement &&
!["INPUT", "TEXTAREA"].includes(document.activeElement.tagName) && (
document.getElementById("menu").style.transform === "translateX(0px)" ||
document.getElementById("model").style.transform === "translateY(0px)"
)) {
hideKeyboardToolbar();
return;
}
if (!showUtil) {
hideKeyboardToolbarUtil();
}
showKeyboardToolbar();
const dynamicElements = document.querySelectorAll("#keyboardToolbar .keyboard__dynamic");
const range = getSelection().getRangeAt(0);
const isProtyle = hasClosestByClassName(range.startContainer, "protyle-wysiwyg", true);
if (!isProtyle) {
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")) {
dynamicElements[0].classList.add("fn__none");
dynamicElements[1].classList.remove("fn__none");
} else {
dynamicElements[0].classList.remove("fn__none");
dynamicElements[1].classList.add("fn__none");
}
const protyle = getCurrentEditor().protyle;
if (!dynamicElements[0].classList.contains("fn__none")) {
if (protyle.undo.undoStack.length === 0) {
dynamicElements[0].querySelector('[data-type="undo"]').setAttribute("disabled", "disabled");
} else {
dynamicElements[0].querySelector('[data-type="undo"]').removeAttribute("disabled");
}
if (protyle.undo.redoStack.length === 0) {
dynamicElements[0].querySelector('[data-type="redo"]').setAttribute("disabled", "disabled");
} 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");
}
}
}
if (!dynamicElements[1].classList.contains("fn__none")) {
dynamicElements[1].querySelectorAll(".protyle-toolbar__item--current").forEach(item => {
item.classList.remove("protyle-toolbar__item--current");
});
const types = protyle.toolbar.getCurrentType(range);
types.forEach(item => {
if (["search-mark", "a", "block-ref", "virtual-block-ref", "text", "file-annotation-ref", "inline-math",
"inline-memo", "", "backslash"].includes(item)) {
return;
}
const itemElement = dynamicElements[1].querySelector(`[data-type="${item}"]`);
if (itemElement) {
itemElement.classList.add("protyle-toolbar__item--current");
}
});
}
}, 620); // 需等待 range 更新
};
export const showKeyboardToolbar = () => {
if (!showUtil) {
hideKeyboardToolbarUtil();
}
const toolbarElement = document.getElementById("keyboardToolbar");
if (!toolbarElement.classList.contains("fn__none")) {
return;
}
toolbarElement.classList.remove("fn__none");
toolbarElement.style.zIndex = (++window.siyuan.zIndex).toString();
const modelElement = document.getElementById("model");
if (modelElement.style.transform === "translateY(0px)") {
modelElement.style.paddingBottom = "42px";
}
const range = getSelection().getRangeAt(0);
const editor = getCurrentEditor();
if (editor && editor.protyle.wysiwyg.element.contains(range.startContainer)) {
editor.protyle.element.parentElement.style.paddingBottom = "42px";
}
getCurrentEditor().protyle.app.plugins.forEach(item => {
item.eventBus.emit("mobile-keyboard-show");
});
setTimeout(() => {
const contentElement = hasClosestByClassName(range.startContainer, "protyle-content", true);
if (contentElement) {
const contentTop = contentElement.getBoundingClientRect().top;
const cursorTop = getSelectionPosition(contentElement).top;
if (cursorTop < window.innerHeight - 42 && cursorTop > contentTop) {
return;
}
contentElement.scroll({
top: contentElement.scrollTop + cursorTop - window.innerHeight + 42 + 26,
left: contentElement.scrollLeft,
behavior: "smooth"
});
}
}, Constants.TIMEOUT_TRANSITION);
};
export const hideKeyboardToolbar = () => {
if (showUtil) {
return;
}
const toolbarElement = document.getElementById("keyboardToolbar");
if (toolbarElement.classList.contains("fn__none")) {
return;
}
toolbarElement.classList.add("fn__none");
toolbarElement.style.height = "";
const editor = getCurrentEditor();
if (editor) {
editor.protyle.element.parentElement.style.paddingBottom = "";
}
const modelElement = document.getElementById("model");
if (modelElement.style.transform === "translateY(0px)") {
modelElement.style.paddingBottom = "";
}
getCurrentEditor().protyle.app.plugins.forEach(item => {
item.eventBus.emit("mobile-keyboard-hide");
});
};
export const activeBlur = () => {
window.JSAndroid?.hideKeyboard();
hideKeyboardToolbar();
(document.activeElement as HTMLElement).blur();
};
export const initKeyboardToolbar = () => {
let preventRender = false;
document.addEventListener("selectionchange", () => {
if (!preventRender) {
renderKeyboardToolbar();
}
}, false);
const toolbarElement = document.getElementById("keyboardToolbar");
toolbarElement.innerHTML = `
`;
toolbarElement.addEventListener("click", (event) => {
const protyle = getCurrentEditor()?.protyle;
const target = event.target as HTMLElement;
const slashBtnElement = hasClosestByClassName(event.target as HTMLElement, "keyboard__slash-item");
if (slashBtnElement && !slashBtnElement.getAttribute("data-type")) {
const dataValue = decodeURIComponent(slashBtnElement.getAttribute("data-value"));
protyle.hint.fill(dataValue, protyle, false); // 点击后 range 会改变
if (dataValue !== Constants.ZWSP + 3) {
event.preventDefault();
event.stopPropagation();
}
if (slashBtnElement.getAttribute("data-focus") === "true") {
focusByRange(protyle.toolbar.range);
}
return;
}
const buttonElement = hasClosestByTag(target, "BUTTON");
if (!buttonElement || buttonElement.getAttribute("disabled")) {
return;
}
const type = buttonElement.getAttribute("data-type");
// appearance
if (["clear", "style2", "style4", "color", "backgroundColor", "fontSize", "style1"].includes(type)) {
const nodeElements = getFontNodeElements(protyle);
const itemElement = buttonElement.firstElementChild as HTMLElement;
if (type === "style1") {
fontEvent(protyle, nodeElements, type, itemElement.style.backgroundColor + Constants.ZWSP + itemElement.style.color);
} else if (type === "fontSize") {
fontEvent(protyle, nodeElements, type, itemElement.textContent.trim());
} else if (type === "backgroundColor") {
fontEvent(protyle, nodeElements, type, itemElement.style.backgroundColor);
} else if (type === "color") {
fontEvent(protyle, nodeElements, type, itemElement.style.color);
} else {
fontEvent(protyle, nodeElements, type);
}
}
event.preventDefault();
event.stopPropagation();
if (getSelection().rangeCount === 0) {
return;
}
const range = getSelection().getRangeAt(0);
if (type === "done") {
if (toolbarElement.clientHeight > 100) {
hideKeyboardToolbarUtil();
focusByRange(range);
} else {
activeBlur();
}
return;
}
if (window.siyuan.config.readonly || !protyle || protyle.disabled) {
return;
}
if (type === "undo") {
protyle.undo.undo(protyle);
return;
} else if (type === "redo") {
protyle.undo.redo(protyle);
return;
}
if (getSelection().rangeCount === 0) {
return;
}
const nodeElement = hasClosestBlock(range.startContainer);
if (!nodeElement) {
return;
}
// inline element
if (type === "goback") {
toolbarElement.querySelector('.keyboard__action[data-type="goinline"]').classList.remove("protyle-toolbar__item--current");
const dynamicElements = document.querySelectorAll("#keyboardToolbar .keyboard__dynamic");
dynamicElements[0].classList.remove("fn__none");
dynamicElements[1].classList.add("fn__none");
focusByRange(range);
preventRender = true;
setTimeout(() => {
preventRender = false;
}, 1000);
return;
} else if (type === "goinline") {
buttonElement.classList.add("protyle-toolbar__item--current");
const dynamicElements = document.querySelectorAll("#keyboardToolbar .keyboard__dynamic");
dynamicElements[1].classList.remove("fn__none");
dynamicElements[0].classList.add("fn__none");
focusByRange(range);
return;
} else if (["a", "block-ref", "inline-math", "inline-memo"].includes(type)) {
if (!hasClosestByAttribute(range.startContainer, "data-type", "NodeCodeBlock")) {
hideElements(["util"], protyle);
protyle.toolbar.element.querySelector(`[data-type="${type}"]`).dispatchEvent(new CustomEvent("click"));
}
return;
} else if (buttonElement.classList.contains("keyboard__action") && ["strong", "em", "s", "code", "mark", "tag", "u", "sup", "clear", "sub", "kbd"].includes(type)) {
if (!hasClosestByAttribute(range.startContainer, "data-type", "NodeCodeBlock")) {
protyle.toolbar.setInlineMark(protyle, type, "toolbar");
}
return;
} else if (type === "text") {
if (buttonElement.classList.contains("protyle-toolbar__item--current")) {
hideKeyboardToolbarUtil();
focusByRange(range);
} else {
buttonElement.classList.add("protyle-toolbar__item--current");
toolbarElement.querySelector('.keyboard__action[data-type="done"] use').setAttribute("xlink:href", "#iconCloseRound");
const oldScrollTop = protyle.contentElement.scrollTop;
renderTextMenu(protyle, toolbarElement);
showKeyboardToolbarUtil(oldScrollTop);
}
return;
} else if (type === "moveup") {
moveToUp(protyle, nodeElement, range);
focusByRange(range);
return;
} else if (type === "movedown") {
moveToDown(protyle, nodeElement, range);
focusByRange(range);
return;
} else if (type === "softLine") {
range.extractContents();
softEnter(range, nodeElement, protyle);
focusByRange(range);
return;
} else if (type === "add") {
if (buttonElement.classList.contains("protyle-toolbar__item--current")) {
hideKeyboardToolbarUtil();
focusByRange(range);
} else {
buttonElement.classList.add("protyle-toolbar__item--current");
toolbarElement.querySelector('.keyboard__action[data-type="done"] use').setAttribute("xlink:href", "#iconCloseRound");
const oldScrollTop = protyle.contentElement.scrollTop;
renderSlashMenu(protyle, toolbarElement);
showKeyboardToolbarUtil(oldScrollTop);
}
return;
} else if (type === "block") {
protyle.gutter.renderMenu(protyle, nodeElement);
window.siyuan.menus.menu.fullscreen();
activeBlur();
return;
} else if (type === "outdent") {
listOutdent(protyle, [nodeElement.parentElement], range);
focusByRange(range);
return;
} else if (type === "indent") {
listIndent(protyle, [nodeElement.parentElement], range);
focusByRange(range);
return;
}
});
};