A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
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};