A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
at lambda-fork/main 431 lines 18 kB view raw
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};