A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
at lambda-fork/main 361 lines 17 kB view raw
1import {setEditMode} from "../util/setEditMode"; 2import {scrollEvent} from "../scroll/event"; 3import {isMobile} from "../../util/functions"; 4import {Constants} from "../../constants"; 5import {isMac} from "../util/compatibility"; 6import {setInlineStyle} from "../../util/assets"; 7import {fetchPost} from "../../util/fetch"; 8import {lineNumberRender} from "../render/highlightRender"; 9import {hideMessage, showMessage} from "../../dialog/message"; 10import {genUUID} from "../../util/genID"; 11import {getContenteditableElement, getLastBlock} from "../wysiwyg/getBlock"; 12import {genEmptyElement, genHeadingElement} from "../../block/util"; 13import {transaction} from "../wysiwyg/transaction"; 14import {focusByRange} from "../util/selection"; 15/// #if !MOBILE 16import {moveResize} from "../../dialog/moveResize"; 17/// #endif 18import { 19 hasClosestBlock, 20 hasClosestByAttribute, 21 hasClosestByClassName, 22 hasClosestByTag, 23 isInEmbedBlock 24} from "../util/hasClosest"; 25 26export const initUI = (protyle: IProtyle) => { 27 protyle.contentElement = document.createElement("div"); 28 protyle.contentElement.className = "protyle-content"; 29 30 if (protyle.options.render.background || protyle.options.render.title) { 31 protyle.contentElement.innerHTML = '<div class="protyle-top"></div>'; 32 if (protyle.options.render.background) { 33 protyle.contentElement.firstElementChild.appendChild(protyle.background.element); 34 } 35 if (protyle.options.render.title) { 36 protyle.contentElement.firstElementChild.appendChild(protyle.title.element); 37 } 38 } 39 40 protyle.contentElement.appendChild(protyle.wysiwyg.element); 41 if (!protyle.options.action.includes(Constants.CB_GET_HISTORY)) { 42 scrollEvent(protyle, protyle.contentElement); 43 } 44 protyle.element.append(protyle.contentElement); 45 protyle.element.appendChild(protyle.preview.element); 46 if (protyle.upload) { 47 protyle.element.appendChild(protyle.upload.element); 48 } 49 if (protyle.options.render.scroll) { 50 protyle.element.appendChild(protyle.scroll.element.parentElement); 51 } 52 if (protyle.gutter) { 53 protyle.element.appendChild(protyle.gutter.element); 54 } 55 56 protyle.element.appendChild(protyle.hint.element); 57 58 protyle.selectElement = document.createElement("div"); 59 protyle.selectElement.className = "protyle-select fn__none"; 60 protyle.element.appendChild(protyle.selectElement); 61 62 protyle.element.appendChild(protyle.toolbar.element); 63 protyle.element.appendChild(protyle.toolbar.subElement); 64 /// #if !MOBILE 65 moveResize(protyle.toolbar.subElement, () => { 66 const pinElement = protyle.toolbar.subElement.querySelector('.block__icons [data-type="pin"]'); 67 if (pinElement) { 68 pinElement.querySelector("svg use").setAttribute("xlink:href", "#iconUnpin"); 69 pinElement.setAttribute("aria-label", window.siyuan.languages.unpin); 70 protyle.toolbar.subElement.firstElementChild.setAttribute("data-drag", "true"); 71 } 72 }); 73 /// #endif 74 75 protyle.element.append(protyle.highlight.styleElement); 76 77 addLoading(protyle); 78 79 setEditMode(protyle, protyle.options.mode); 80 document.execCommand("DefaultParagraphSeparator", false, "p"); 81 82 let wheelTimeout: number; 83 const wheelId = genUUID(); 84 const isMacOS = isMac(); 85 protyle.contentElement.addEventListener("mousewheel", (event: WheelEvent) => { 86 if (!window.siyuan.config.editor.fontSizeScrollZoom || (isMacOS && !event.metaKey) || (!isMacOS && !event.ctrlKey) || event.deltaX !== 0) { 87 return; 88 } 89 event.stopPropagation(); 90 if (event.deltaY < 0) { 91 if (window.siyuan.config.editor.fontSize < 72) { 92 window.siyuan.config.editor.fontSize++; 93 } else { 94 return; 95 } 96 } else if (event.deltaY > 0) { 97 if (window.siyuan.config.editor.fontSize > 9) { 98 window.siyuan.config.editor.fontSize--; 99 } else { 100 return; 101 } 102 } 103 setInlineStyle(); 104 clearTimeout(wheelTimeout); 105 showMessage(`${window.siyuan.languages.fontSize} ${window.siyuan.config.editor.fontSize}px<span class="fn__space"></span> 106<button class="b3-button b3-button--white">${window.siyuan.languages.reset} 16px</button>`, undefined, undefined, wheelId); 107 wheelTimeout = window.setTimeout(() => { 108 fetchPost("/api/setting/setEditor", window.siyuan.config.editor); 109 protyle.wysiwyg.element.querySelectorAll(".code-block .protyle-linenumber__rows").forEach((block: HTMLElement) => { 110 lineNumberRender(block.parentElement); 111 }); 112 document.querySelector(`#message [data-id="${wheelId}"] button`)?.addEventListener("click", () => { 113 window.siyuan.config.editor.fontSize = 16; 114 setInlineStyle(); 115 fetchPost("/api/setting/setEditor", window.siyuan.config.editor); 116 hideMessage(wheelId); 117 protyle.wysiwyg.element.querySelectorAll(".code-block .protyle-linenumber__rows").forEach((block: HTMLElement) => { 118 lineNumberRender(block.parentElement); 119 }); 120 }); 121 }, Constants.TIMEOUT_LOAD); 122 }, {passive: true}); 123 protyle.contentElement.addEventListener("click", (event: MouseEvent & { target: HTMLElement }) => { 124 // wysiwyg 元素下方点击无效果 https://github.com/siyuan-note/siyuan/issues/12009 125 if (protyle.disabled || 126 // 选中块时,禁止添加空块 https://github.com/siyuan-note/siyuan/issues/13905 127 protyle.contentElement.querySelector(".protyle-wysiwyg--select") || 128 (!event.target.classList.contains("protyle-content") && !event.target.classList.contains("protyle-wysiwyg"))) { 129 return; 130 } 131 // 选中最后一个块末尾点击底部时,range 会有值,需等待 132 setTimeout(() => { 133 // 选中文本禁止添加空块 https://github.com/siyuan-note/siyuan/issues/13905 134 if (window.getSelection().rangeCount > 0) { 135 const currentRange = window.getSelection().getRangeAt(0); 136 if (currentRange.toString() !== "" && protyle.wysiwyg.element.contains(currentRange.startContainer)) { 137 return; 138 } 139 } 140 const lastElement = protyle.wysiwyg.element.lastElementChild; 141 const lastRect = lastElement.getBoundingClientRect(); 142 const range = document.createRange(); 143 if (event.y > lastRect.bottom) { 144 const lastEditElement = getContenteditableElement(getLastBlock(lastElement)); 145 if (!protyle.options.click.preventInsetEmptyBlock && ( 146 !lastEditElement || 147 (lastElement.getAttribute("data-type") !== "NodeParagraph" && protyle.wysiwyg.element.getAttribute("data-doc-type") !== "NodeListItem") || 148 (lastElement.getAttribute("data-type") === "NodeParagraph" && getContenteditableElement(lastEditElement).innerHTML !== "")) 149 ) { 150 let emptyElement: Element; 151 if (lastElement.getAttribute("data-type") === "NodeHeading" && lastElement.getAttribute("fold") === "1") { 152 emptyElement = genHeadingElement(lastElement) as Element; 153 } else { 154 emptyElement = genEmptyElement(false, false); 155 } 156 protyle.wysiwyg.element.insertAdjacentElement("beforeend", emptyElement); 157 transaction(protyle, [{ 158 action: "insert", 159 data: emptyElement.outerHTML, 160 id: emptyElement.getAttribute("data-node-id"), 161 previousID: emptyElement.previousElementSibling.getAttribute("data-node-id"), 162 parentID: protyle.block.parentID 163 }], [{ 164 action: "delete", 165 id: emptyElement.getAttribute("data-node-id") 166 }]); 167 const emptyEditElement = getContenteditableElement(emptyElement) as HTMLInputElement; 168 range.selectNodeContents(emptyEditElement); 169 range.collapse(true); 170 focusByRange(range); 171 // 需等待 range 更新再次进行渲染 172 if (protyle.options.render.breadcrumb) { 173 setTimeout(() => { 174 protyle.breadcrumb.render(protyle); 175 }, Constants.TIMEOUT_TRANSITION); 176 } 177 } else if (lastEditElement) { 178 range.selectNodeContents(lastEditElement); 179 range.collapse(false); 180 focusByRange(range); 181 } 182 protyle.toolbar.range = range; 183 } 184 }); 185 }); 186 let overAttr = false; 187 protyle.element.addEventListener("mouseover", (event: KeyboardEvent & { target: HTMLElement }) => { 188 // attr 189 const attrElement = hasClosestByClassName(event.target, "protyle-attr"); 190 if (attrElement && !attrElement.parentElement.classList.contains("protyle-title")) { 191 const hlElement = protyle.wysiwyg.element.querySelector(".protyle-wysiwyg--hl"); 192 if (hlElement) { 193 hlElement.classList.remove("protyle-wysiwyg--hl"); 194 } 195 overAttr = true; 196 attrElement.parentElement.classList.add("protyle-wysiwyg--hl"); 197 return; 198 } else if (overAttr) { 199 const hlElement = protyle.wysiwyg.element.querySelector(".protyle-wysiwyg--hl"); 200 if (hlElement) { 201 hlElement.classList.remove("protyle-wysiwyg--hl"); 202 } 203 overAttr = false; 204 } 205 206 const nodeElement = hasClosestBlock(event.target); 207 if (protyle.options.render.gutter && nodeElement) { 208 if (nodeElement && (nodeElement.classList.contains("list") || nodeElement.classList.contains("li"))) { 209 // 光标在列表下部应显示右侧的元素,而不是列表本身。放在 windowEvent 中的 mousemove 下处理 210 return; 211 } 212 const embedElement = isInEmbedBlock(nodeElement); 213 if (embedElement) { 214 protyle.gutter.render(protyle, embedElement, protyle.wysiwyg.element); 215 return; 216 } 217 protyle.gutter.render(protyle, nodeElement, protyle.wysiwyg.element, event.target); 218 return; 219 } 220 221 // gutter 222 const buttonElement = hasClosestByTag(event.target, "BUTTON"); 223 if (buttonElement && buttonElement.parentElement.classList.contains("protyle-gutters")) { 224 const type = buttonElement.getAttribute("data-type"); 225 if (type === "fold" || type === "NodeAttributeViewRow") { 226 Array.from(protyle.wysiwyg.element.querySelectorAll(".protyle-wysiwyg--hl, .av__row--hl")).forEach(item => { 227 item.classList.remove("protyle-wysiwyg--hl", "av__row--hl"); 228 }); 229 return; 230 } 231 Array.from(protyle.wysiwyg.element.querySelectorAll(`[data-node-id="${buttonElement.getAttribute("data-node-id")}"]`)).find(item => { 232 if (!isInEmbedBlock(item) && protyle.gutter.isMatchNode(item)) { 233 const bodyQueryClass = (buttonElement.dataset.groupId && buttonElement.dataset.groupId !== "undefined") ? `.av__body[data-group-id="${buttonElement.dataset.groupId}"] ` : ""; 234 const rowItem = item.querySelector(bodyQueryClass + `.av__row[data-id="${buttonElement.dataset.rowId}"]`); 235 Array.from(protyle.wysiwyg.element.querySelectorAll(".protyle-wysiwyg--hl, .av__row--hl")).forEach(hlItem => { 236 if (item !== hlItem) { 237 hlItem.classList.remove("protyle-wysiwyg--hl"); 238 } 239 if (rowItem && rowItem !== hlItem) { 240 rowItem.classList.remove("av__row--hl"); 241 } 242 }); 243 if (type === "NodeAttributeViewRowMenu") { 244 rowItem.classList.add("av__row--hl"); 245 } else { 246 item.classList.add("protyle-wysiwyg--hl"); 247 } 248 return true; 249 } 250 }); 251 event.preventDefault(); 252 return; 253 } 254 255 // 面包屑 256 /// #if !MOBILE 257 if (protyle.selectElement.classList.contains("fn__none")) { 258 const svgElement = hasClosestByAttribute(event.target, "data-node-id", null); 259 if (svgElement && svgElement.parentElement.classList.contains("protyle-breadcrumb__bar")) { 260 protyle.wysiwyg.element.querySelectorAll(".protyle-wysiwyg--hl").forEach(item => { 261 item.classList.remove("protyle-wysiwyg--hl"); 262 }); 263 const nodeElement = protyle.wysiwyg.element.querySelector(`[data-node-id="${svgElement.getAttribute("data-node-id")}"]`); 264 if (nodeElement) { 265 nodeElement.classList.add("protyle-wysiwyg--hl"); 266 } 267 } 268 } 269 /// #endif 270 }); 271}; 272 273export const addLoading = (protyle: IProtyle, msg?: string) => { 274 protyle.element.removeAttribute("data-loading"); 275 setTimeout(() => { 276 if (protyle.element.getAttribute("data-loading") !== "finished") { 277 protyle.element.insertAdjacentHTML("beforeend", `<div style="background-color: var(--b3-theme-background);flex-direction: column;" class="fn__loading wysiwygLoading"> 278 <img width="48px" src="/stage/loading-pure.svg"> 279 <div style="color: var(--b3-theme-on-surface);margin-top: 8px;">${msg || ""}</div> 280</div>`); 281 } 282 }, Constants.TIMEOUT_LOAD); 283}; 284 285export const removeLoading = (protyle: IProtyle) => { 286 protyle.element.setAttribute("data-loading", "finished"); 287 protyle.element.querySelectorAll(".wysiwygLoading").forEach(item => { 288 item.remove(); 289 }); 290}; 291 292export const setPadding = (protyle: IProtyle) => { 293 if (protyle.options.action.includes(Constants.CB_GET_HISTORY)) { 294 return { 295 width: 0, 296 padding: 0 297 }; 298 } 299 const padding = getPadding(protyle); 300 const paddingLeft = padding.left; 301 const paddingRight = padding.right; 302 303 if (protyle.options.backlinkData) { 304 protyle.wysiwyg.element.style.padding = `4px ${paddingRight}px 4px ${paddingLeft}px`; 305 } else { 306 protyle.wysiwyg.element.style.padding = `${padding.top}px ${paddingRight}px ${padding.bottom}px ${paddingLeft}px`; 307 } 308 if (protyle.options.render.background) { 309 protyle.background.element.querySelector(".protyle-background__ia").setAttribute("style", `margin-left:${paddingLeft}px;margin-right:${paddingRight}px`); 310 } 311 if (protyle.options.render.title) { 312 // pc 端 文档名 attr 过长和添加标签等按钮重合 313 protyle.title.element.style.margin = `16px ${paddingRight}px 0 ${paddingLeft}px`; 314 } 315 316 // https://github.com/siyuan-note/siyuan/issues/15021 317 protyle.element.style.setProperty("--b3-width-protyle", protyle.element.clientWidth + "px"); 318 protyle.element.style.setProperty("--b3-width-protyle-content", protyle.contentElement.clientWidth + "px"); 319 const realWidth = protyle.wysiwyg.element.getAttribute("data-realwidth"); 320 const newWidth = protyle.wysiwyg.element.clientWidth - paddingLeft - paddingRight; 321 protyle.wysiwyg.element.setAttribute("data-realwidth", newWidth.toString()); 322 protyle.element.style.setProperty("--b3-width-protyle-wysiwyg", newWidth.toString() + "px"); 323 return { 324 width: realWidth ? Math.abs(parseFloat(realWidth) - newWidth) : 0, 325 }; 326}; 327 328export const getPadding = (protyle: IProtyle) => { 329 let right = 16; 330 let left = 24; 331 let bottom = 16; 332 if (protyle.options.typewriterMode) { 333 if (isMobile()) { 334 bottom = window.innerHeight / 5; 335 } else { 336 bottom = protyle.element.clientHeight / 2; 337 } 338 } 339 if (!isMobile()) { 340 let isFullWidth = protyle.wysiwyg.element.getAttribute(Constants.CUSTOM_SY_FULLWIDTH); 341 if (!isFullWidth) { 342 isFullWidth = window.siyuan.config.editor.fullWidth ? "true" : "false"; 343 } 344 let padding = (protyle.element.clientWidth - Constants.SIZE_EDITOR_WIDTH) / 2; 345 if (isFullWidth === "false" && padding > 96) { 346 if (padding > Constants.SIZE_EDITOR_WIDTH) { 347 // 超宽屏调整 https://ld246.com/article/1668266637363 348 padding = protyle.element.clientWidth * .382 / 1.382; 349 } 350 padding = Math.ceil(padding); 351 left = padding; 352 right = padding; 353 } else if (protyle.element.clientWidth > Constants.SIZE_EDITOR_WIDTH) { 354 left = 96; 355 right = 96; 356 } 357 } 358 return { 359 left, right, bottom, top: 16 360 }; 361};