siyuan/app/src/protyle/toolbar/Font.ts

447 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {setStorageVal, updateHotkeyTip} from "../util/compatibility";
import {ToolbarItem} from "./ToolbarItem";
import {setPosition} from "../../util/setPosition";
import {focusByRange, getSelectionPosition} from "../util/selection";
import {Constants} from "../../constants";
import {hasClosestBlock, hasClosestByAttribute} from "../util/hasClosest";
import {updateBatchTransaction} from "../wysiwyg/transaction";
import {lineNumberRender} from "../render/highlightRender";
export class Font extends ToolbarItem {
public element: HTMLElement;
constructor(protyle: IProtyle, menuItem: IMenuItem) {
super(protyle, menuItem);
this.element.addEventListener("click", () => {
protyle.toolbar.element.classList.add("fn__none");
protyle.toolbar.subElement.innerHTML = "";
protyle.toolbar.subElement.style.width = "";
protyle.toolbar.subElement.style.padding = "";
protyle.toolbar.subElement.append(appearanceMenu(protyle, getFontNodeElements(protyle)));
protyle.toolbar.subElement.style.zIndex = (++window.siyuan.zIndex).toString();
protyle.toolbar.subElement.classList.remove("fn__none");
protyle.toolbar.subElementCloseCB = undefined;
focusByRange(protyle.toolbar.range);
/// #if !MOBILE
const position = getSelectionPosition(protyle.wysiwyg.element, protyle.toolbar.range);
setPosition(protyle.toolbar.subElement, position.left, position.top + 18, 26);
/// #endif
});
}
}
export const appearanceMenu = (protyle: IProtyle, nodeElements?: 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) => {
colorHTML += `<button class="color__square" data-type="color" style="color:${item}">A</button>`;
});
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) => {
bgHTML += `<button class="color__square" data-type="backgroundColor" style="background-color:${item}"></button>`;
});
const element = document.createElement("div");
element.classList.add("protyle-font");
let disableFont = false;
nodeElements?.find((item: HTMLElement) => {
if (item.classList.contains("list") || item.classList.contains("li")) {
disableFont = true;
return true;
}
});
let lastColorHTML = "";
const lastFonts = window.siyuan.storage[Constants.LOCAL_FONTSTYLES];
if (lastFonts.length > 0) {
lastColorHTML = `<div class="fn__flex">
${window.siyuan.languages.lastUsed}
<span class="fn__space"></span>
<kbd class="ft__on-surface fn__flex-center">${updateHotkeyTip(window.siyuan.config.keymap.editor.insert.lastUsed.custom)}</kbd>
</div>
<div class="fn__hr--small"></div>
<div class="fn__flex" style="align-items: center">`;
lastFonts.forEach((item: string) => {
const lastFontStatus = item.split(Constants.ZWSP);
switch (lastFontStatus[0]) {
case "color":
lastColorHTML += `<button class="color__square" data-type="${lastFontStatus[0]}" style="color:${lastFontStatus[1]}">A</button>`;
break;
case "backgroundColor":
lastColorHTML += `<button class="color__square" data-type="${lastFontStatus[0]}" style="background-color:${lastFontStatus[1]}"></button>`;
break;
case "style2":
lastColorHTML += `<button data-type="${lastFontStatus[0]}" class="protyle-font__style" style="-webkit-text-stroke: 0.2px var(--b3-theme-on-background);-webkit-text-fill-color : transparent;">${window.siyuan.languages.hollow}</button>`;
break;
case "style4":
lastColorHTML += `<button data-type="${lastFontStatus[0]}" class="protyle-font__style" style="text-shadow: 1px 1px var(--b3-theme-surface-lighter), 2px 2px var(--b3-theme-surface-lighter), 3px 3px var(--b3-theme-surface-lighter), 4px 4px var(--b3-theme-surface-lighter)">${window.siyuan.languages.shadow}</button>`;
break;
case "fontSize":
if (!disableFont) {
lastColorHTML += `<button data-type="${lastFontStatus[0]}" class="protyle-font__style">${lastFontStatus[1]}</button>`;
}
break;
case "style1":
lastColorHTML += `<button data-type="${lastFontStatus[0]}" style="background-color:${lastFontStatus[1]};color:${lastFontStatus[2]}" class="color__square">A</button>`;
break;
case "clear":
lastColorHTML += `<button data-type="${lastFontStatus[0]}" class="protyle-font__style">${window.siyuan.languages.clearFontStyle}</button>`;
break;
}
});
lastColorHTML += "</div>";
}
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";
}
element.innerHTML = `${lastColorHTML}
<div class="fn__hr"></div>
<div>${window.siyuan.languages.color}</div>
<div class="fn__hr--small"></div>
<div class="fn__flex">
<button class="color__square" data-type="style1" style="color: var(--b3-card-error-color);background-color: var(--b3-card-error-background);">A</button>
<button class="color__square" data-type="style1" style="color: var(--b3-card-warning-color);background-color: var(--b3-card-warning-background);">A</button>
<button class="color__square" data-type="style1" style="color: var(--b3-card-info-color);background-color: var(--b3-card-info-background);">A</button>
<button class="color__square" data-type="style1" style="color: var(--b3-card-success-color);background-color: var(--b3-card-success-background);">A</button>
</div>
<div class="fn__hr"></div>
<div>${window.siyuan.languages.colorFont}</div>
<div class="fn__hr--small"></div>
<div class="fn__flex">
${colorHTML}
</div>
<div class="fn__hr"></div>
<div>${window.siyuan.languages.colorPrimary}</div>
<div class="fn__hr--small"></div>
<div class="fn__flex">
${bgHTML}
</div>
<div class="fn__hr"></div>
<div>${window.siyuan.languages.fontStyle}</div>
<div class="fn__hr--small"></div>
<div class="fn__flex">
<button data-type="style2" class="protyle-font__style" style="-webkit-text-stroke: 0.2px var(--b3-theme-on-background);-webkit-text-fill-color : transparent;">${window.siyuan.languages.hollow}</button>
<button data-type="style4" class="protyle-font__style" style="text-shadow: 1px 1px var(--b3-theme-surface-lighter), 2px 2px var(--b3-theme-surface-lighter), 3px 3px var(--b3-theme-surface-lighter), 4px 4px var(--b3-theme-surface-lighter)">${window.siyuan.languages.shadow}</button>
</div>
<div class="fn__hr${disableFont ? " fn__none" : ""}"></div>
<div class="${disableFont ? " fn__none" : ""}">${window.siyuan.languages.fontSize}</div>
<div class="fn__hr--small${disableFont ? " fn__none" : ""}"></div>
<div class="fn__flex${disableFont ? " fn__none" : ""}">
<select class="b3-select fn__block">
<option ${fontSize === "12px" ? "selected" : ""} value="12px">12px</option>
<option ${fontSize === "13px" ? "selected" : ""} value="13px">13px</option>
<option ${fontSize === "14px" ? "selected" : ""} value="14px">14px</option>
<option ${fontSize === "15px" ? "selected" : ""} value="15px">15px</option>
<option ${fontSize === "16px" ? "selected" : ""} value="16px">16px</option>
<option ${fontSize === "19px" ? "selected" : ""} value="19px">19px</option>
<option ${fontSize === "22px" ? "selected" : ""} value="22px">22px</option>
<option ${fontSize === "24px" ? "selected" : ""} value="24px">24px</option>
<option ${fontSize === "29px" ? "selected" : ""} value="29px">29px</option>
<option ${fontSize === "32px" ? "selected" : ""} value="32px">32px</option>
<option ${fontSize === "40px" ? "selected" : ""} value="40px">40px</option>
<option ${fontSize === "48px" ? "selected" : ""} value="48px">48px</option>
</select>
</div>
<div class="fn__hr"></div>
<button class="b3-button b3-button--cancel" data-type="clear">
<svg><use xlink:href="#iconTrashcan"></use></svg>${window.siyuan.languages.clearFontStyle}
</button>`;
element.addEventListener("click", function (event: Event) {
let target = event.target as HTMLElement;
while (target && !target.isEqualNode(element)) {
const dataType = target.getAttribute("data-type");
if (target.tagName === "BUTTON") {
if (dataType === "style1") {
fontEvent(protyle, nodeElements, dataType, target.style.backgroundColor + Constants.ZWSP + target.style.color);
} else if (dataType === "fontSize") {
fontEvent(protyle, nodeElements, dataType, target.textContent.trim());
} else if (dataType === "backgroundColor") {
fontEvent(protyle, nodeElements, dataType, target.style.backgroundColor);
} else if (dataType === "color") {
fontEvent(protyle, nodeElements, dataType, target.style.color);
} else {
fontEvent(protyle, nodeElements, dataType);
}
break;
}
target = target.parentElement;
}
});
element.querySelector("select").addEventListener("change", function (event: Event) {
fontEvent(protyle, nodeElements, "fontSize", (event.target as HTMLSelectElement).value);
});
return element;
};
export const fontEvent = (protyle: IProtyle, nodeElements: Element[], type?: string, color?: string) => {
let localFontStyles = window.siyuan.storage[Constants.LOCAL_FONTSTYLES];
if (type) {
localFontStyles.splice(0, 0, `${type}${Constants.ZWSP}${color}`);
localFontStyles = [...new Set(localFontStyles)];
if (localFontStyles.length > 8) {
localFontStyles.splice(8, 1);
}
window.siyuan.storage[Constants.LOCAL_FONTSTYLES] = localFontStyles;
setStorageVal(Constants.LOCAL_FONTSTYLES, window.siyuan.storage[Constants.LOCAL_FONTSTYLES]);
} else {
if (localFontStyles.length === 0) {
type = "style1";
color = "var(--b3-card-error-color)" + Constants.ZWSP + "var(--b3-card-error-background)";
} else {
const fontStyles = localFontStyles[0].split(Constants.ZWSP);
type = fontStyles[0];
color = fontStyles[1];
}
}
if (nodeElements && nodeElements.length > 0) {
updateBatchTransaction(nodeElements, protyle, (e: HTMLElement) => {
if (type === "clear") {
e.style.color = "";
e.style.webkitTextFillColor = "";
e.style.webkitTextStroke = "";
e.style.textShadow = "";
e.style.backgroundColor = "";
e.style.fontSize = "";
if (e.classList.contains("av")) {
e.querySelector(".av__container").setAttribute("style", "--av-background:--b3-theme-background");
}
} else if (type === "style1") {
const colorList = color.split(Constants.ZWSP);
e.style.backgroundColor = colorList[0];
e.style.color = colorList[1];
if (e.classList.contains("av")) {
e.querySelector(".av__container").setAttribute("style", `--av-background:${colorList[0]}`);
}
} else if (type === "style2") {
e.style.webkitTextStroke = "0.2px var(--b3-theme-on-background)";
e.style.webkitTextFillColor = "transparent";
} else if (type === "style4") {
e.style.textShadow = "1px 1px var(--b3-theme-surface-lighter), 2px 2px var(--b3-theme-surface-lighter), 3px 3px var(--b3-theme-surface-lighter), 4px 4px var(--b3-theme-surface-lighter)";
} else if (type === "color") {
e.style.color = color;
} else if (type === "backgroundColor") {
e.style.backgroundColor = color;
if (e.classList.contains("av")) {
e.querySelector(".av__container").setAttribute("style", `--av-background:${color}`);
}
} else if (type === "fontSize") {
e.style.fontSize = color;
}
if ((type === "fontSize" || type === "clear") && e.getAttribute("data-type") === "NodeCodeBlock") {
lineNumberRender(e.querySelector(".hljs"));
}
});
focusByRange(protyle.toolbar.range);
} else {
if (type === "clear") {
protyle.toolbar.setInlineMark(protyle, "clear", "range", {type: "text"});
} else {
protyle.toolbar.setInlineMark(protyle, "text", "range", {type, color});
}
}
};
export const setFontStyle = (textElement: HTMLElement, textOption: ITextOption) => {
const setBlockRef = (blockRefOption: string) => {
const blockRefData = blockRefOption.split(Constants.ZWSP);
// 标签等元素中包含 ZWSP需移除后拼接 https://github.com/siyuan-note/siyuan/issues/6466
textElement.setAttribute("data-id", blockRefData.splice(0, 1)[0]);
textElement.setAttribute("data-subtype", blockRefData.splice(0, 1)[0]);
textElement.removeAttribute("data-href");
textElement.innerText = blockRefData.join("");
};
const setLink = (textOption: string) => {
const options = textOption.split(Constants.ZWSP);
textElement.setAttribute("data-href", options[0]);
textElement.removeAttribute("data-subtype");
textElement.removeAttribute("data-id");
if (options[1]) {
textElement.textContent = options[1];
}
};
const setFileAnnotation = (textOption: string) => {
const options = textOption.split(Constants.ZWSP);
textElement.setAttribute("data-id", options[0]);
textElement.removeAttribute("data-href");
textElement.removeAttribute("data-subtype");
if (options[1]) {
textElement.textContent = options[1];
}
};
if (textOption) {
switch (textOption.type) {
case "color":
textElement.style.color = textOption.color;
break;
case "fontSize":
textElement.style.fontSize = textOption.color;
break;
case "backgroundColor":
textElement.style.backgroundColor = textOption.color;
break;
case "style1":
textElement.style.backgroundColor = textOption.color.split(Constants.ZWSP)[0];
textElement.style.color = textOption.color.split(Constants.ZWSP)[1];
break;
case "style2":
textElement.style.webkitTextStroke = "0.2px var(--b3-theme-on-background)";
textElement.style.webkitTextFillColor = "transparent";
break;
case "style4":
textElement.style.textShadow = "1px 1px var(--b3-theme-surface-lighter), 2px 2px var(--b3-theme-surface-lighter), 3px 3px var(--b3-theme-surface-lighter), 4px 4px var(--b3-theme-surface-lighter)";
break;
case "id":
setBlockRef(textOption.color);
break;
case "inline-math":
textElement.className = "render-node";
textElement.setAttribute("contenteditable", "false");
textElement.setAttribute("data-subtype", "math");
textElement.setAttribute("data-content", textElement.textContent.replace(Constants.ZWSP, ""));
textElement.removeAttribute("data-render");
textElement.textContent = "";
break;
case "a":
setLink(textOption.color);
break;
case "file-annotation-ref":
setFileAnnotation(textOption.color);
break;
case "inline-memo":
textElement.removeAttribute("contenteditable");
textElement.removeAttribute("data-subtype");
textElement.removeAttribute("data-content");
break;
}
}
};
export const hasSameTextStyle = (currentElement: HTMLElement, sideElement: HTMLElement, textObj: ITextOption) => {
if (!textObj) {
return true;
}
if (textObj.type === "inline-math" || textObj.type === "inline-memo" || textObj.type === "a") {
return false;
}
if (textObj.type === "id") {
if (currentElement.nodeType !== 3) {
return currentElement.getAttribute("data-id") === sideElement.getAttribute("data-id") &&
currentElement.getAttribute("data-subtype") === sideElement.getAttribute("data-subtype") &&
currentElement.textContent === sideElement.textContent;
}
const blockRefData = textObj.color.split(Constants.ZWSP);
return blockRefData[0] === sideElement.getAttribute("data-id") &&
blockRefData[1] === sideElement.getAttribute("data-subtype") &&
blockRefData[2] === sideElement.textContent;
}
if (textObj.type === "file-annotation-ref") {
if (currentElement.nodeType !== 3) {
return currentElement.getAttribute("data-id") === sideElement.getAttribute("data-id") &&
currentElement.textContent === sideElement.textContent;
}
return textObj.color === sideElement.getAttribute("data-id");
}
let color = "";
let webkitTextFillColor = "";
let webkitTextStroke = "";
let textShadow = "";
let backgroundColor = "";
let fontSize = "";
if (currentElement.nodeType !== 3) {
color = currentElement.style.color;
webkitTextFillColor = currentElement.style.webkitTextFillColor;
webkitTextStroke = currentElement.style.webkitTextStroke;
textShadow = currentElement.style.textShadow;
backgroundColor = currentElement.style.backgroundColor;
fontSize = currentElement.style.fontSize;
}
if (textObj.type === "text") {
// 清除样式
return color === sideElement.style.color &&
webkitTextFillColor === sideElement.style.webkitTextFillColor &&
webkitTextStroke === sideElement.style.webkitTextStroke &&
textShadow === sideElement.style.textShadow &&
fontSize === sideElement.style.fontSize &&
backgroundColor === sideElement.style.backgroundColor;
}
if (textObj.type === "color") {
return textObj.color === sideElement.style.color &&
webkitTextFillColor === sideElement.style.webkitTextFillColor &&
webkitTextStroke === sideElement.style.webkitTextStroke &&
textShadow === sideElement.style.textShadow &&
fontSize === sideElement.style.fontSize &&
backgroundColor === sideElement.style.backgroundColor;
}
if (textObj.type === "backgroundColor") {
return color === sideElement.style.color &&
webkitTextFillColor === sideElement.style.webkitTextFillColor &&
webkitTextStroke === sideElement.style.webkitTextStroke &&
textShadow === sideElement.style.textShadow &&
fontSize === sideElement.style.fontSize &&
textObj.color === sideElement.style.backgroundColor;
}
if (textObj.type === "style1") {
return textObj.color.split(Constants.ZWSP)[0] === sideElement.style.color &&
webkitTextFillColor === sideElement.style.webkitTextFillColor &&
webkitTextStroke === sideElement.style.webkitTextStroke &&
textShadow === sideElement.style.textShadow &&
fontSize === sideElement.style.fontSize &&
textObj.color.split(Constants.ZWSP)[1] === sideElement.style.backgroundColor;
}
if (textObj.type === "style2") {
return color === sideElement.style.color &&
"transparent" === sideElement.style.webkitTextFillColor &&
"0.2px var(--b3-theme-on-background)" === sideElement.style.webkitTextStroke &&
textShadow === sideElement.style.textShadow &&
fontSize === sideElement.style.fontSize &&
backgroundColor === sideElement.style.backgroundColor;
}
if (textObj.type === "style4") {
return color === sideElement.style.color &&
webkitTextFillColor === sideElement.style.webkitTextFillColor &&
webkitTextStroke === sideElement.style.webkitTextStroke &&
fontSize === sideElement.style.fontSize &&
"1px 1px var(--b3-theme-surface-lighter), 2px 2px var(--b3-theme-surface-lighter), 3px 3px var(--b3-theme-surface-lighter), 4px 4px var(--b3-theme-surface-lighter)" === sideElement.style.textShadow &&
backgroundColor === sideElement.style.backgroundColor;
}
if (textObj.type === "fontSize") {
return color === sideElement.style.color &&
webkitTextFillColor === sideElement.style.webkitTextFillColor &&
webkitTextStroke === sideElement.style.webkitTextStroke &&
textShadow === sideElement.style.textShadow &&
textObj.color === sideElement.style.fontSize &&
backgroundColor === sideElement.style.backgroundColor;
}
return true; // 清除字体样式会使用 "text" 作为标识
};
export const getFontNodeElements = (protyle: IProtyle) => {
let nodeElements: Element[];
if (protyle.toolbar.range.toString() === "") {
nodeElements = Array.from(protyle.wysiwyg.element.querySelectorAll(".protyle-wysiwyg--select"));
if (nodeElements.length === 0) {
const nodeElement = hasClosestBlock(protyle.toolbar.range.startContainer);
if (nodeElement) {
nodeElements = [nodeElement];
}
}
}
return nodeElements;
};