A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
1import {Menu} from "../../../plugin/Menu";
2import {unicode2Emoji} from "../../../emoji";
3import {transaction} from "../../wysiwyg/transaction";
4import {openMenuPanel} from "./openMenuPanel";
5import {focusBlock} from "../../util/selection";
6import {upDownHint} from "../../../util/upDownHint";
7import {escapeAriaLabel, escapeAttr, escapeHtml} from "../../../util/escape";
8import {hasClosestByClassName} from "../../util/hasClosest";
9import {Constants} from "../../../constants";
10
11export const openViewMenu = (options: { protyle: IProtyle, blockElement: HTMLElement, element: HTMLElement }) => {
12 if (options.protyle.disabled) {
13 return;
14 }
15 const menu = new Menu(Constants.MENU_AV_VIEW);
16 if (menu.isOpen) {
17 return;
18 }
19 menu.addItem({
20 id: "rename",
21 icon: "iconEdit",
22 label: window.siyuan.languages.rename,
23 click() {
24 document.querySelector(".av__panel")?.remove();
25 openMenuPanel({
26 protyle: options.protyle,
27 blockElement: options.blockElement,
28 type: "config",
29 cb: (avPanelElement) => {
30 (avPanelElement.querySelector('.b3-text-field[data-type="name"]') as HTMLInputElement).focus();
31 }
32 });
33 }
34 });
35 menu.addItem({
36 id: "config",
37 icon: "iconSettings",
38 label: window.siyuan.languages.config,
39 click() {
40 document.querySelector(".av__panel")?.remove();
41 openMenuPanel({
42 protyle: options.protyle,
43 blockElement: options.blockElement,
44 type: "config"
45 });
46 }
47 });
48 menu.addSeparator();
49 menu.addItem({
50 id: "duplicate",
51 icon: "iconCopy",
52 label: window.siyuan.languages.duplicate,
53 click() {
54 document.querySelector(".av__panel")?.remove();
55 const id = Lute.NewNodeID();
56 transaction(options.protyle, [{
57 action: "duplicateAttrViewView",
58 avID: options.blockElement.dataset.avId,
59 previousID: options.element.dataset.id,
60 id,
61 blockID: options.blockElement.dataset.nodeId
62 }], [{
63 action: "removeAttrViewView",
64 avID: options.blockElement.dataset.avId,
65 id,
66 blockID: options.blockElement.dataset.nodeId
67 }]);
68 }
69 });
70 if (options.blockElement.querySelectorAll(".layout-tab-bar .item").length > 1) {
71 menu.addItem({
72 id: "delete",
73 icon: "iconTrashcan",
74 label: window.siyuan.languages.delete,
75 click() {
76 document.querySelector(".av__panel")?.remove();
77 transaction(options.protyle, [{
78 action: "removeAttrViewView",
79 avID: options.blockElement.dataset.avId,
80 id: options.element.dataset.id,
81 blockID: options.blockElement.dataset.nodeId
82 }]);
83 }
84 });
85 }
86 const rect = options.element.getBoundingClientRect();
87 menu.open({
88 x: rect.left,
89 y: rect.bottom
90 });
91};
92
93export const bindViewEvent = (options: {
94 protyle: IProtyle,
95 data: IAV,
96 menuElement: HTMLElement
97 blockElement: Element
98}) => {
99 const inputElement = options.menuElement.querySelector('.b3-text-field[data-type="name"]') as HTMLInputElement;
100 inputElement.addEventListener("blur", () => {
101 if (inputElement.value !== inputElement.dataset.value) {
102 transaction(options.protyle, [{
103 action: "setAttrViewViewName",
104 avID: options.data.id,
105 id: options.data.viewID,
106 data: inputElement.value
107 }], [{
108 action: "setAttrViewViewName",
109 avID: options.data.id,
110 id: options.data.viewID,
111 data: inputElement.dataset.value
112 }]);
113 inputElement.dataset.value = inputElement.value;
114 }
115 });
116 inputElement.addEventListener("keydown", (event) => {
117 if (event.isComposing) {
118 return;
119 }
120 if (event.key === "Enter") {
121 event.preventDefault();
122 inputElement.blur();
123 options.menuElement.parentElement.remove();
124 }
125 });
126 inputElement.select();
127 inputElement.value = inputElement.dataset.value;
128 const descElement = options.menuElement.querySelector('.b3-text-field[data-type="desc"]') as HTMLTextAreaElement;
129 inputElement.nextElementSibling.addEventListener("click", () => {
130 const descPanelElement = descElement.parentElement;
131 descPanelElement.classList.toggle("fn__none");
132 if (!descPanelElement.classList.contains("fn__none")) {
133 descElement.focus();
134 }
135 });
136 descElement.addEventListener("blur", () => {
137 if (descElement.value !== descElement.dataset.value) {
138 transaction(options.protyle, [{
139 action: "setAttrViewViewDesc",
140 avID: options.data.id,
141 id: options.data.viewID,
142 data: descElement.value
143 }], [{
144 action: "setAttrViewViewDesc",
145 avID: options.data.id,
146 id: options.data.viewID,
147 data: descElement.dataset.value
148 }]);
149 descElement.dataset.value = descElement.value;
150 }
151 });
152 descElement.addEventListener("keydown", (event) => {
153 if (event.isComposing) {
154 return;
155 }
156 if (event.key === "Enter") {
157 event.preventDefault();
158 descElement.blur();
159 options.menuElement.parentElement.remove();
160 }
161 });
162 descElement.addEventListener("input", () => {
163 inputElement.nextElementSibling.setAttribute("aria-label", descElement.value ? escapeHtml(descElement.value) : window.siyuan.languages.addDesc);
164 });
165};
166
167export const getViewHTML = (data: IAV) => {
168 const view = data.view;
169 const fields = getFieldsByData(data);
170 return `<div class="b3-menu__items">
171<button class="b3-menu__item" data-type="nobg">
172 <span class="b3-menu__label ft__center">${window.siyuan.languages.config}</span>
173</button>
174<button class="b3-menu__separator"></button>
175<button class="b3-menu__item" data-type="nobg">
176 <div class="fn__block">
177 <div class="fn__flex">
178 <span class="b3-menu__avemoji" data-type="update-view-icon">${view.icon ? unicode2Emoji(view.icon) : `<svg style="height: 14px;width: 14px"><use xlink:href="#${getViewIcon(data.viewType)}"></use></svg>`}</span>
179 <div class="b3-form__icona fn__block">
180 <input data-type="name" class="b3-text-field b3-form__icona-input" type="text" data-value="${escapeAttr(view.name)}">
181 <svg data-position="north" class="b3-form__icona-icon ariaLabel" aria-label="${view.desc ? escapeAriaLabel(view.desc) : window.siyuan.languages.addDesc}"><use xlink:href="#iconInfo"></use></svg>
182 </div>
183 </div>
184 <div class="fn__none">
185 <div class="fn__hr"></div>
186 <textarea placeholder="${window.siyuan.languages.addDesc}" rows="1" data-type="desc" class="b3-text-field fn__block" type="text" data-value="${escapeAttr(view.desc)}">${view.desc}</textarea>
187 </div>
188 <div class="fn__hr"></div>
189 </div>
190</button>
191<button class="b3-menu__item" data-type="go-layout">
192 <svg class="b3-menu__icon"><use xlink:href="#${getViewIcon(data.viewType)}"></use></svg>
193 <span class="b3-menu__label">${window.siyuan.languages.layout}</span>
194 <span class="b3-menu__accelerator">${getViewName(data.viewType)}</span>
195 <svg class="b3-menu__icon b3-menu__icon--small"><use xlink:href="#iconRight"></use></svg>
196</button>
197<button class="b3-menu__separator"></button>
198<button class="b3-menu__item" data-type="go-properties">
199 <svg class="b3-menu__icon"><use xlink:href="#iconList"></use></svg>
200 <span class="b3-menu__label">${window.siyuan.languages.fields}</span>
201 <span class="b3-menu__accelerator">${fields.filter((item: IAVColumn) => !item.hidden).length}/${fields.length}</span>
202 <svg class="b3-menu__icon b3-menu__icon--small"><use xlink:href="#iconRight"></use></svg>
203</button>
204<button class="b3-menu__item" data-type="goFilters">
205 <svg class="b3-menu__icon"><use xlink:href="#iconFilter"></use></svg>
206 <span class="b3-menu__label">${window.siyuan.languages.filter}</span>
207 <span class="b3-menu__accelerator">${view.filters.length}</span>
208 <svg class="b3-menu__icon b3-menu__icon--small"><use xlink:href="#iconRight"></use></svg>
209</button>
210<button class="b3-menu__item" data-type="goSorts">
211 <svg class="b3-menu__icon"><use xlink:href="#iconSort"></use></svg>
212 <span class="b3-menu__label">${window.siyuan.languages.sort}</span>
213 <span class="b3-menu__accelerator">${view.sorts.length}</span>
214 <svg class="b3-menu__icon b3-menu__icon--small"><use xlink:href="#iconRight"></use></svg>
215</button>
216<button class="b3-menu__item" data-type="goGroups">
217 <svg class="b3-menu__icon"><use xlink:href="#iconGroups"></use></svg>
218 <span class="b3-menu__label">${window.siyuan.languages.group}</span>
219 <span class="b3-menu__accelerator">${(data.view.group && data.view.group.field) ? fields.filter((item: IAVColumn) => item.id === data.view.group.field)[0].name : ""}</span>
220 <svg class="b3-menu__icon b3-menu__icon--small"><use xlink:href="#iconRight"></use></svg>
221</button>
222<button class="b3-menu__separator"></button>
223<button class="b3-menu__item" data-type="duplicate-view">
224 <svg class="b3-menu__icon">
225 <use xlink:href="#iconCopy"></use>
226 </svg>
227 <span class="b3-menu__label">${window.siyuan.languages.duplicate}</span>
228</button>
229<button class="b3-menu__item${data.views.length > 1 ? "" : " fn__none"}" data-type="delete-view">
230 <svg class="b3-menu__icon"><use xlink:href="#iconTrashcan"></use></svg>
231 <span class="b3-menu__label">${window.siyuan.languages.delete}</span>
232</button>
233</div>`;
234};
235
236export const bindSwitcherEvent = (options: { protyle: IProtyle, menuElement: Element, blockElement: Element }) => {
237 const inputElement = options.menuElement.querySelector(".b3-text-field") as HTMLInputElement;
238 inputElement.focus();
239 inputElement.addEventListener("keydown", (event) => {
240 event.stopPropagation();
241 if (event.isComposing) {
242 return;
243 }
244 upDownHint(options.menuElement.querySelector(".fn__flex-1"), event, "b3-menu__item--current");
245 if (event.key === "Enter") {
246 const currentElement = options.menuElement.querySelector(".b3-menu__item--current") as HTMLElement;
247 if (currentElement) {
248 transaction(options.protyle, [{
249 action: "setAttrViewBlockView",
250 blockID: options.blockElement.getAttribute("data-node-id"),
251 id: currentElement.dataset.id,
252 avID: options.blockElement.getAttribute("data-av-id"),
253 }], [{
254 action: "setAttrViewBlockView",
255 blockID: options.blockElement.getAttribute("data-node-id"),
256 id: options.blockElement.querySelector(".av__views .item--focus").getAttribute("data-id"),
257 avID: options.blockElement.getAttribute("data-av-id"),
258 }]);
259 options.menuElement.remove();
260 focusBlock(options.blockElement);
261 }
262 } else if (event.key === "Escape") {
263 options.menuElement.remove();
264 focusBlock(options.blockElement);
265 }
266 });
267 inputElement.addEventListener("input", (event: InputEvent) => {
268 if (event.isComposing) {
269 return;
270 }
271 filterSwitcher(options.menuElement);
272 });
273 inputElement.addEventListener("compositionend", () => {
274 filterSwitcher(options.menuElement);
275 });
276};
277
278const filterSwitcher = (menuElement: Element) => {
279 const inputElement = menuElement.querySelector(".b3-text-field") as HTMLInputElement;
280 const key = inputElement.value;
281 menuElement.querySelectorAll('.b3-menu__item[draggable="true"]').forEach(item => {
282 if (!key ||
283 (key.toLowerCase().indexOf(item.textContent.trim().toLowerCase()) > -1 ||
284 item.textContent.trim().toLowerCase().indexOf(key.toLowerCase()) > -1)) {
285 item.classList.remove("fn__none");
286 } else {
287 item.classList.add("fn__none");
288 item.classList.remove("b3-menu__item--current");
289 }
290 });
291 if (!menuElement.querySelector(".b3-menu__item--current")) {
292 menuElement.querySelector(".fn__flex-1 .b3-menu__item:not(.fn__none)")?.classList.add("b3-menu__item--current");
293 }
294};
295
296export const getSwitcherHTML = (views: IAVView[], viewId: string) => {
297 let html = "";
298 views.forEach((item) => {
299 html += `<button draggable="true" class="b3-menu__item${item.id === viewId ? " b3-menu__item--current" : ""}" data-id="${item.id}">
300 <svg class="b3-menu__icon fn__grab"><use xlink:href="#iconDrag"></use></svg>
301 <div class="b3-menu__label fn__flex" data-type="av-view-switch" data-av-type="${item.type}">
302 ${item.icon ? unicode2Emoji(item.icon, "b3-menu__icon", true) : `<svg class="b3-menu__icon"><use xlink:href="#${getViewIcon(item.type)}"></use></svg>`}
303 <span class="fn__ellipsis">${item.name}</span>
304 </div>
305 <svg class="b3-menu__action" data-type="av-view-edit"><use xlink:href="#iconEdit"></use></svg>
306</button>`;
307 });
308 return `<div class="b3-menu__items fn__flex-column">
309<button class="b3-menu__item" data-type="av-add">
310 <svg class="b3-menu__icon"><use xlink:href="#iconAdd"></use></svg>
311 <span class="b3-menu__label">${window.siyuan.languages.newView}</span>
312</button>
313<button class="b3-menu__separator"></button>
314<div class="b3-menu__item fn__flex-shrink" data-type="nobg">
315 <input class="b3-text-field fn__block" type="text" style="margin: 4px 0" placeholder="${window.siyuan.languages.search}">
316</div>
317<div class="fn__flex-1" style="overflow: auto">
318 ${html}
319</div>
320</div>`;
321};
322
323export const addView = (protyle: IProtyle, blockElement: Element) => {
324 const id = Lute.NewNodeID();
325 const avID = blockElement.getAttribute("data-av-id");
326 const viewElement = blockElement.querySelector(".av__views");
327 const addMenu = new Menu(undefined, () => {
328 viewElement.classList.remove("av__views--show");
329 });
330 addMenu.addItem({
331 icon: "iconTable",
332 label: window.siyuan.languages.table,
333 click() {
334 transaction(protyle, [{
335 action: "addAttrViewView",
336 avID,
337 id,
338 blockID: blockElement.getAttribute("data-node-id")
339 }], [{
340 action: "removeAttrViewView",
341 avID,
342 id,
343 blockID: blockElement.getAttribute("data-node-id")
344 }]);
345 }
346 });
347 addMenu.addItem({
348 icon: "iconGallery",
349 label: window.siyuan.languages.gallery,
350 click() {
351 transaction(protyle, [{
352 action: "addAttrViewView",
353 avID,
354 layout: "gallery",
355 id,
356 blockID: blockElement.getAttribute("data-node-id")
357 }], [{
358 action: "removeAttrViewView",
359 layout: "gallery",
360 avID,
361 id,
362 blockID: blockElement.getAttribute("data-node-id")
363 }]);
364 }
365 });
366 viewElement.classList.add("av__views--show");
367 const addRect = viewElement.querySelector('.block__icon[data-type="av-add"]')?.getBoundingClientRect();
368 addMenu.open({
369 x: addRect.left,
370 y: addRect.bottom + 8
371 });
372};
373
374export const getViewIcon = (type: string) => {
375 switch (type) {
376 case "table":
377 return "iconTable";
378 case "gallery":
379 return "iconGallery";
380 }
381};
382
383export const getViewName = (type: string) => {
384 switch (type) {
385 case "table":
386 return window.siyuan.languages.table;
387 case "gallery":
388 return window.siyuan.languages.gallery;
389 }
390};
391
392export const getFieldsByData = (data: IAV) => {
393 return data.viewType === "table" ? (data.view as IAVTable).columns : (data.view as IAVGallery).fields;
394};
395
396export const dragoverTab = (event: DragEvent) => {
397 const viewTabElement = window.siyuan.dragElement.parentElement;
398 if (viewTabElement.scrollWidth > viewTabElement.clientWidth) {
399 const viewTabRect = viewTabElement.getBoundingClientRect();
400 if (event.clientX < viewTabRect.left) {
401 viewTabElement.scroll({
402 left: viewTabElement.scrollLeft - Constants.SIZE_SCROLL_STEP,
403 behavior: "smooth"
404 });
405 } else if (event.clientX > viewTabRect.right) {
406 viewTabElement.scroll({
407 left: viewTabElement.scrollLeft + Constants.SIZE_SCROLL_STEP,
408 behavior: "smooth"
409 });
410 }
411 }
412 const target = hasClosestByClassName(document.elementFromPoint(event.clientX, window.siyuan.dragElement.getBoundingClientRect().top + 10), "item");
413 if (!target) {
414 return;
415 }
416 if (viewTabElement !== window.siyuan.dragElement.parentElement || (target === window.siyuan.dragElement)) {
417 return;
418 }
419 const targetRect = target.getBoundingClientRect();
420 if (targetRect.left + targetRect.width / 2 < event.clientX) {
421 if (target.nextElementSibling && target.nextElementSibling === window.siyuan.dragElement) {
422 return;
423 }
424 target.after(window.siyuan.dragElement);
425 } else {
426 if (target.previousElementSibling && target.previousElementSibling === window.siyuan.dragElement) {
427 return;
428 }
429 target.before(window.siyuan.dragElement);
430 }
431};