A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
at lambda-fork/main 451 lines 24 kB view raw
1import {setStorageVal, updateHotkeyTip} from "../util/compatibility"; 2import {ToolbarItem} from "./ToolbarItem"; 3import {setPosition} from "../../util/setPosition"; 4import {focusByRange, getSelectionPosition} from "../util/selection"; 5import {Constants} from "../../constants"; 6import {hasClosestBlock, hasClosestByAttribute} from "../util/hasClosest"; 7import {updateBatchTransaction} from "../wysiwyg/transaction"; 8import {lineNumberRender} from "../render/highlightRender"; 9 10export class Font extends ToolbarItem { 11 public element: HTMLElement; 12 13 constructor(protyle: IProtyle, menuItem: IMenuItem) { 14 super(protyle, menuItem); 15 this.element.addEventListener("click", () => { 16 protyle.toolbar.element.classList.add("fn__none"); 17 protyle.toolbar.subElement.innerHTML = ""; 18 protyle.toolbar.subElement.style.width = ""; 19 protyle.toolbar.subElement.style.padding = ""; 20 protyle.toolbar.subElement.append(appearanceMenu(protyle, getFontNodeElements(protyle))); 21 protyle.toolbar.subElement.style.zIndex = (++window.siyuan.zIndex).toString(); 22 protyle.toolbar.subElement.classList.remove("fn__none"); 23 protyle.toolbar.subElementCloseCB = undefined; 24 focusByRange(protyle.toolbar.range); 25 /// #if !MOBILE 26 const position = getSelectionPosition(protyle.wysiwyg.element, protyle.toolbar.range); 27 setPosition(protyle.toolbar.subElement, position.left, position.top + 18, 26); 28 /// #endif 29 }); 30 } 31} 32 33export const appearanceMenu = (protyle: IProtyle, nodeElements?: Element[]) => { 34 let colorHTML = ""; 35 ["", "var(--b3-font-color1)", "var(--b3-font-color2)", "var(--b3-font-color3)", "var(--b3-font-color4)", 36 "var(--b3-font-color5)", "var(--b3-font-color6)", "var(--b3-font-color7)", "var(--b3-font-color8)", 37 "var(--b3-font-color9)", "var(--b3-font-color10)", "var(--b3-font-color11)", "var(--b3-font-color12)", 38 "var(--b3-font-color13)"].forEach((item) => { 39 colorHTML += `<button ${item ? `class="color__square" style="color:${item}"` : `class="color__square ariaLabel" data-position="3south" aria-label="${window.siyuan.languages.default}"`} data-type="color">A</button>`; 40 }); 41 let bgHTML = ""; 42 ["", "var(--b3-font-background1)", "var(--b3-font-background2)", "var(--b3-font-background3)", "var(--b3-font-background4)", 43 "var(--b3-font-background5)", "var(--b3-font-background6)", "var(--b3-font-background7)", "var(--b3-font-background8)", 44 "var(--b3-font-background9)", "var(--b3-font-background10)", "var(--b3-font-background11)", "var(--b3-font-background12)", 45 "var(--b3-font-background13)"].forEach((item) => { 46 bgHTML += `<button ${item ? `class="color__square" style="background-color:${item}"` : `class="color__square ariaLabel" data-position="3south" aria-label="${window.siyuan.languages.default}"`} data-type="backgroundColor"></button>`; 47 }); 48 49 const element = document.createElement("div"); 50 element.classList.add("protyle-font"); 51 let disableFont = false; 52 nodeElements?.find((item: HTMLElement) => { 53 if (item.classList.contains("li")) { 54 disableFont = true; 55 return true; 56 } 57 }); 58 let lastColorHTML = ""; 59 const lastFonts = window.siyuan.storage[Constants.LOCAL_FONTSTYLES]; 60 if (lastFonts.length > 0) { 61 lastColorHTML = `<div data-id="lastUsed" class="fn__flex"> 62 ${window.siyuan.languages.lastUsed} 63 <span class="fn__space"></span> 64 <kbd class="fn__kbd fn__flex-center${window.siyuan.config.keymap.editor.insert.lastUsed.custom ? "" : " fn__none"}">${updateHotkeyTip(window.siyuan.config.keymap.editor.insert.lastUsed.custom)}</kbd> 65</div> 66<div class="fn__hr--small"></div> 67<div data-id="lastUsedWrap" class="fn__flex fn__flex-wrap" style="align-items: center">`; 68 lastFonts.forEach((item: string) => { 69 const lastFontStatus = item.split(Constants.ZWSP); 70 switch (lastFontStatus[0]) { 71 case "color": 72 lastColorHTML += `<button class="color__square ariaLabel" data-position="3south" aria-label="${window.siyuan.languages.colorFont}${lastFontStatus[1] ? "" : " " + window.siyuan.languages.default}" ${lastFontStatus[1] ? `style="color:${lastFontStatus[1]}"` : ""} data-type="${lastFontStatus[0]}">A</button>`; 73 break; 74 case "backgroundColor": 75 lastColorHTML += `<button class="color__square ariaLabel" data-position="3south" aria-label="${window.siyuan.languages.colorPrimary}${lastFontStatus[1] ? "" : " " + window.siyuan.languages.default}" ${lastFontStatus[1] ? `style="background-color:${lastFontStatus[1]}"` : ""} data-type="${lastFontStatus[0]}"></button>`; 76 break; 77 case "style2": 78 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>`; 79 break; 80 case "style4": 81 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>`; 82 break; 83 case "fontSize": 84 if (!disableFont) { 85 lastColorHTML += `<button data-type="${lastFontStatus[0]}" class="protyle-font__style">${lastFontStatus[1]}</button>`; 86 } 87 break; 88 case "style1": 89 lastColorHTML += `<button class="color__square ariaLabel" data-position="3south" aria-label="${window.siyuan.languages.color}${lastFontStatus[1] ? "" : " " + window.siyuan.languages.default}" ${lastFontStatus[1] ? `style="background-color:${lastFontStatus[1]};color:${lastFontStatus[2]}"` : ""} data-type="${lastFontStatus[0]}">A</button>`; 90 break; 91 case "clear": 92 lastColorHTML += `<button style="height: 26px;display: flex;align-items: center;padding: 0 5px;" data-type="${lastFontStatus[0]}" class="protyle-font__style ariaLabel" aria-label="${window.siyuan.languages.clearFontStyle}"><svg class="svg--mid"><use xlink:href="#iconTrashcan"></use></svg></button>`; 93 break; 94 } 95 }); 96 lastColorHTML += "</div>"; 97 } 98 let textElement: HTMLElement; 99 let fontSize = window.siyuan.config.editor.fontSize + "px"; 100 if (nodeElements && nodeElements.length > 0) { 101 textElement = nodeElements[0] as HTMLElement; 102 } else { 103 textElement = protyle.toolbar.range.cloneContents().querySelector('[data-type~="text"]') as HTMLElement; 104 if (!textElement) { 105 textElement = hasClosestByAttribute(protyle.toolbar.range.startContainer, "data-type", "text") as HTMLElement; 106 } 107 } 108 if (textElement) { 109 fontSize = textElement.style.fontSize || window.siyuan.config.editor.fontSize + "px"; 110 } 111 element.innerHTML = `${lastColorHTML} 112<div class="fn__hr"></div> 113<div data-id="color">${window.siyuan.languages.color}</div> 114<div class="fn__hr--small"></div> 115<div data-id="colorWrap" class="fn__flex fn__flex-wrap"> 116 <button class="color__square ariaLabel" data-position="3south" data-type="style1" aria-label="${window.siyuan.languages.default}">A</button> 117 <button class="color__square" data-type="style1" style="color: var(--b3-card-error-color);background-color: var(--b3-card-error-background);">A</button> 118 <button class="color__square" data-type="style1" style="color: var(--b3-card-warning-color);background-color: var(--b3-card-warning-background);">A</button> 119 <button class="color__square" data-type="style1" style="color: var(--b3-card-info-color);background-color: var(--b3-card-info-background);">A</button> 120 <button class="color__square" data-type="style1" style="color: var(--b3-card-success-color);background-color: var(--b3-card-success-background);">A</button> 121</div> 122<div class="fn__hr"></div> 123<div data-id="colorFont">${window.siyuan.languages.colorFont}</div> 124<div class="fn__hr--small"></div> 125<div data-id="colorFontWrap" class="fn__flex fn__flex-wrap"> 126 ${colorHTML} 127</div> 128<div class="fn__hr"></div> 129<div data-id="colorPrimary">${window.siyuan.languages.colorPrimary}</div> 130<div class="fn__hr--small"></div> 131<div data-id="colorPrimaryWrap" class="fn__flex fn__flex-wrap"> 132 ${bgHTML} 133</div> 134<div class="fn__hr"></div> 135<div data-id="fontStyle">${window.siyuan.languages.fontStyle}</div> 136<div class="fn__hr--small"></div> 137<div data-id="fontStyleWrap" class="fn__flex"> 138 <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> 139 <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> 140</div> 141<div class="fn__hr${disableFont ? " fn__none" : ""}"></div> 142<div data-id="fontSize" class="fn__flex${disableFont ? " fn__none" : ""}"> 143 ${window.siyuan.languages.fontSize} 144 <span class="fn__flex-1"></span> 145 <label class="fn__flex"> 146 ${window.siyuan.languages.relativeFontSize} 147 <span class="fn__space"></span> 148 <input class="b3-switch fn__flex-center" ${fontSize.endsWith("em") ? "checked" : ""} type="checkbox"> 149 <span class="fn__space--small"></span> 150 </label> 151</div> 152<div data-id="fontSizeWrap" class="${disableFont ? " fn__none" : ""}"> 153 <div class="fn__hr"></div> 154 <div class="b3-tooltips b3-tooltips__n fn__flex${fontSize.endsWith("em") ? " fn__none" : ""}" aria-label="${fontSize}"> 155 <input class="b3-slider fn__block" id="fontSizePX" max="72" min="9" step="1" type="range" value="${parseInt(fontSize)}"> 156 </div> 157 <div class="b3-tooltips b3-tooltips__n fn__flex${fontSize.endsWith("em") ? "" : " fn__none"}" aria-label="${parseFloat(fontSize) * 100}%"> 158 <input class="b3-slider fn__block" id="fontSizeEM" max="4.5" min="0.56" step="0.01" type="range" value="${parseFloat(fontSize)}"> 159 </div> 160</div> 161<div class="fn__hr--b"></div> 162<div data-id="clearFontStyle" class="fn__flex"> 163 <div class="fn__space--small"></div> 164 <button class="b3-button b3-button--remove fn__block" data-type="clear"> 165 <svg><use xlink:href="#iconTrashcan"></use></svg>${window.siyuan.languages.clearFontStyle} 166 </button> 167 <div class="fn__space--small"></div> 168</div>`; 169 element.addEventListener("click", function (event: Event) { 170 let target = event.target as HTMLElement; 171 while (target && !target.isEqualNode(element)) { 172 const dataType = target.getAttribute("data-type"); 173 if (target.tagName === "BUTTON") { 174 if (dataType === "style1") { 175 fontEvent(protyle, nodeElements, dataType, target.style.backgroundColor + Constants.ZWSP + target.style.color); 176 } else if (dataType === "fontSize") { 177 fontEvent(protyle, nodeElements, dataType, target.textContent.trim()); 178 } else if (dataType === "backgroundColor") { 179 fontEvent(protyle, nodeElements, dataType, target.style.backgroundColor); 180 } else if (dataType === "color") { 181 fontEvent(protyle, nodeElements, dataType, target.style.color); 182 } else { 183 fontEvent(protyle, nodeElements, dataType); 184 } 185 break; 186 } 187 target = target.parentElement; 188 } 189 }); 190 const switchElement = element.querySelector(".b3-switch") as HTMLInputElement; 191 const fontSizePXElement = element.querySelector("#fontSizePX") as HTMLInputElement; 192 const fontSizeEMElement = element.querySelector("#fontSizeEM") as HTMLInputElement; 193 switchElement.addEventListener("change", function () { 194 if (switchElement.checked) { 195 // px -> em 196 const em = parseFloat((parseInt(fontSizePXElement.value) / 16).toFixed(2)); 197 fontSizeEMElement.parentElement.setAttribute("aria-label", (em * 100).toString() + "%"); 198 fontSizeEMElement.value = em.toString(); 199 200 fontSizePXElement.parentElement.classList.add("fn__none"); 201 fontSizeEMElement.parentElement.classList.remove("fn__none"); 202 fontEvent(protyle, nodeElements, "fontSize", fontSizeEMElement.value + "em"); 203 } else { 204 const px = Math.round(parseFloat(fontSizeEMElement.value) * 16); 205 fontSizePXElement.parentElement.setAttribute("aria-label", px + "px"); 206 fontSizePXElement.value = px.toString(); 207 208 fontSizePXElement.parentElement.classList.remove("fn__none"); 209 fontSizeEMElement.parentElement.classList.add("fn__none"); 210 fontEvent(protyle, nodeElements, "fontSize", fontSizePXElement.value + "px"); 211 } 212 }); 213 fontSizePXElement.addEventListener("change", function () { 214 fontEvent(protyle, nodeElements, "fontSize", fontSizePXElement.value + "px"); 215 }); 216 fontSizeEMElement.addEventListener("change", function () { 217 fontEvent(protyle, nodeElements, "fontSize", fontSizeEMElement.value + "em"); 218 }); 219 fontSizePXElement.addEventListener("input", function () { 220 fontSizePXElement.parentElement.setAttribute("aria-label", fontSizePXElement.value + "px"); 221 }); 222 fontSizeEMElement.addEventListener("input", function () { 223 fontSizeEMElement.parentElement.setAttribute("aria-label", (parseFloat(fontSizeEMElement.value) * 100).toFixed(0) + "%"); 224 }); 225 return element; 226}; 227 228export const fontEvent = (protyle: IProtyle, nodeElements: Element[], type?: string, color?: string) => { 229 let localFontStyles = window.siyuan.storage[Constants.LOCAL_FONTSTYLES]; 230 if (type) { 231 localFontStyles.splice(0, 0, `${type}${Constants.ZWSP}${color}`); 232 localFontStyles = [...new Set(localFontStyles)]; 233 if (localFontStyles.length > 8) { 234 localFontStyles.splice(8, 1); 235 } 236 window.siyuan.storage[Constants.LOCAL_FONTSTYLES] = localFontStyles; 237 setStorageVal(Constants.LOCAL_FONTSTYLES, window.siyuan.storage[Constants.LOCAL_FONTSTYLES]); 238 } else { 239 if (localFontStyles.length === 0) { 240 type = "style1"; 241 color = "var(--b3-card-error-color)" + Constants.ZWSP + "var(--b3-card-error-background)"; 242 } else { 243 const fontStyles = localFontStyles[0].split(Constants.ZWSP); 244 type = fontStyles.splice(0, 1)[0]; 245 color = fontStyles.join(Constants.ZWSP); 246 } 247 } 248 if (nodeElements && nodeElements.length > 0) { 249 updateBatchTransaction(nodeElements, protyle, (e: HTMLElement) => { 250 if (type === "clear") { 251 e.style.color = ""; 252 e.style.webkitTextFillColor = ""; 253 e.style.webkitTextStroke = ""; 254 e.style.textShadow = ""; 255 e.style.backgroundColor = ""; 256 e.style.fontSize = ""; 257 e.style.removeProperty("--b3-parent-background"); 258 } else if (type === "style1") { 259 const colorList = color.split(Constants.ZWSP); 260 e.style.backgroundColor = colorList[0]; 261 e.style.color = colorList[1]; 262 e.style.setProperty("--b3-parent-background", colorList[0]); 263 } else if (type === "style2") { 264 e.style.webkitTextStroke = "0.2px var(--b3-theme-on-background)"; 265 e.style.webkitTextFillColor = "transparent"; 266 } else if (type === "style4") { 267 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)"; 268 } else if (type === "color") { 269 e.style.color = color; 270 } else if (type === "backgroundColor") { 271 e.style.backgroundColor = color; 272 e.style.setProperty("--b3-parent-background", color); 273 } else if (type === "fontSize") { 274 e.style.fontSize = color; 275 } 276 if ((type === "fontSize" || type === "clear") && e.getAttribute("data-type") === "NodeCodeBlock") { 277 lineNumberRender(e.querySelector(".hljs")); 278 } 279 }); 280 focusByRange(protyle.toolbar.range); 281 } else { 282 if (type === "clear") { 283 protyle.toolbar.setInlineMark(protyle, "clear", "range", {type: "text"}); 284 } else { 285 protyle.toolbar.setInlineMark(protyle, "text", "range", {type, color}); 286 } 287 } 288}; 289 290export const setFontStyle = (textElement: HTMLElement, textOption: ITextOption) => { 291 const setBlockRef = (blockRefOption: string) => { 292 const blockRefData = blockRefOption.split(Constants.ZWSP); 293 // 标签等元素中包含 ZWSP,需移除后拼接 https://github.com/siyuan-note/siyuan/issues/6466 294 const id = blockRefData.splice(0, 1)[0]; 295 textElement.setAttribute("data-id", id); 296 textElement.setAttribute("data-subtype", blockRefData.splice(0, 1)[0]); 297 textElement.removeAttribute("data-href"); 298 let text = blockRefData.join(""); 299 if (text.replace(/\s/g, "") === "") { 300 text = id; 301 } 302 textElement.innerText = text; 303 }; 304 const setLink = (textOption: string) => { 305 const options = textOption.split(Constants.ZWSP); 306 textElement.setAttribute("data-href", options[0]); 307 textElement.removeAttribute("data-subtype"); 308 textElement.removeAttribute("data-id"); 309 if (options[1]) { 310 textElement.textContent = options[1]; 311 } 312 }; 313 const setFileAnnotation = (textOption: string) => { 314 const options = textOption.split(Constants.ZWSP); 315 textElement.setAttribute("data-id", options[0]); 316 textElement.removeAttribute("data-href"); 317 textElement.removeAttribute("data-subtype"); 318 if (options[1]) { 319 textElement.textContent = options[1]; 320 } 321 }; 322 323 if (textOption) { 324 switch (textOption.type) { 325 case "color": 326 textElement.style.color = textOption.color; 327 break; 328 case "fontSize": 329 textElement.style.fontSize = textOption.color; 330 break; 331 case "backgroundColor": 332 textElement.style.backgroundColor = textOption.color; 333 break; 334 case "style1": 335 textElement.style.backgroundColor = textOption.color.split(Constants.ZWSP)[0]; 336 textElement.style.color = textOption.color.split(Constants.ZWSP)[1]; 337 break; 338 case "style2": 339 textElement.style.webkitTextStroke = "0.2px var(--b3-theme-on-background)"; 340 textElement.style.webkitTextFillColor = "transparent"; 341 break; 342 case "style4": 343 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)"; 344 break; 345 case "id": 346 setBlockRef(textOption.color); 347 break; 348 case "inline-math": 349 textElement.className = "render-node"; 350 textElement.setAttribute("contenteditable", "false"); 351 textElement.setAttribute("data-subtype", "math"); 352 textElement.setAttribute("data-content", textElement.textContent.replace(Constants.ZWSP, "")); 353 textElement.removeAttribute("data-render"); 354 textElement.textContent = ""; 355 break; 356 case "a": 357 setLink(textOption.color); 358 break; 359 case "file-annotation-ref": 360 setFileAnnotation(textOption.color); 361 break; 362 case "inline-memo": 363 textElement.removeAttribute("contenteditable"); 364 textElement.removeAttribute("data-content"); 365 break; 366 } 367 368 if (!textElement.getAttribute("style")) { 369 textElement.removeAttribute("style"); 370 } 371 } 372}; 373 374export const hasSameTextStyle = (currentElement: HTMLElement, sideElement: HTMLElement, textObj?: ITextOption) => { 375 if (!textObj && currentElement) { 376 const types = sideElement.getAttribute("data-type").split(" "); 377 if (types.includes("inline-math") || types.includes("inline-memo") || 378 types.includes("a")) { 379 return false; 380 } 381 if (types.includes("block-ref")) { 382 if (currentElement.getAttribute("data-id") !== sideElement.getAttribute("data-id") || 383 currentElement.getAttribute("data-subtype") !== sideElement.getAttribute("data-subtype") || 384 currentElement.textContent !== sideElement.textContent) { 385 return false; 386 } 387 } 388 if (types.includes("file-annotation-ref")) { 389 if (currentElement.getAttribute("data-id") !== sideElement.getAttribute("data-id") || 390 currentElement.textContent !== sideElement.textContent) { 391 return false; 392 } 393 } 394 if (sideElement.style.color === currentElement.style.color && 395 sideElement.style.webkitTextFillColor === currentElement.style.webkitTextFillColor && 396 sideElement.style.webkitTextStroke === currentElement.style.webkitTextStroke && 397 sideElement.style.textShadow === currentElement.style.textShadow && 398 sideElement.style.backgroundColor === currentElement.style.backgroundColor && 399 sideElement.style.fontSize === currentElement.style.fontSize) { 400 return true; 401 } 402 return false; 403 } 404 405 if (textObj) { 406 if (textObj.type === "text") { 407 // 清除样式 408 return !sideElement.style.color && 409 !sideElement.style.webkitTextFillColor && 410 !sideElement.style.webkitTextStroke && 411 !sideElement.style.textShadow && 412 !sideElement.style.fontSize && 413 !sideElement.style.backgroundColor; 414 } 415 if (textObj.type === "color") { 416 return textObj.color === sideElement.style.color; 417 } 418 if (textObj.type === "backgroundColor") { 419 return textObj.color === sideElement.style.backgroundColor; 420 } 421 if (textObj.type === "style1") { 422 return textObj.color.split(Constants.ZWSP)[0] === sideElement.style.color && 423 textObj.color.split(Constants.ZWSP)[1] === sideElement.style.backgroundColor; 424 } 425 if (textObj.type === "style2") { 426 return "transparent" === sideElement.style.webkitTextFillColor && 427 "0.2px var(--b3-theme-on-background)" === sideElement.style.webkitTextStroke; 428 } 429 if (textObj.type === "style4") { 430 return "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; 431 } 432 if (textObj.type === "fontSize") { 433 return textObj.color === sideElement.style.fontSize; 434 } 435 } 436 return false; 437}; 438 439export const getFontNodeElements = (protyle: IProtyle) => { 440 let nodeElements: Element[]; 441 if (protyle.toolbar.range.toString() === "") { 442 nodeElements = Array.from(protyle.wysiwyg.element.querySelectorAll(".protyle-wysiwyg--select")); 443 if (nodeElements.length === 0) { 444 const nodeElement = hasClosestBlock(protyle.toolbar.range.startContainer); 445 if (nodeElement) { 446 nodeElements = [nodeElement]; 447 } 448 } 449 } 450 return nodeElements; 451};