A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
at lambda-fork/main 466 lines 20 kB view raw
1import {Dialog} from "../../../dialog"; 2import {App} from "../../../index"; 3import {upDownHint} from "../../../util/upDownHint"; 4import {updateHotkeyTip} from "../../../protyle/util/compatibility"; 5import {isMobile} from "../../../util/functions"; 6import {Constants} from "../../../constants"; 7import {Editor} from "../../../editor"; 8/// #if MOBILE 9import {getCurrentEditor} from "../../../mobile/editor"; 10import {popSearch} from "../../../mobile/menu/search"; 11/// #else 12import {getActiveTab, getDockByType} from "../../../layout/tabUtil"; 13import {Custom} from "../../../layout/dock/Custom"; 14import {getAllModels} from "../../../layout/getAll"; 15import {Files} from "../../../layout/dock/Files"; 16import {Search} from "../../../search"; 17import {openSearch} from "../../../search/spread"; 18/// #endif 19import {addEditorToDatabase, addFilesToDatabase} from "../../../protyle/render/av/addToDatabase"; 20import {hasClosestBlock, hasClosestByClassName, hasTopClosestByTag} from "../../../protyle/util/hasClosest"; 21import {onlyProtyleCommand} from "./protyle"; 22import {globalCommand} from "./global"; 23import {getDisplayName, getNotebookName, getTopPaths, movePathTo, moveToPath, pathPosix} from "../../../util/pathName"; 24import {hintMoveBlock} from "../../../protyle/hint/extend"; 25import {fetchSyncPost} from "../../../util/fetch"; 26import {focusByRange} from "../../../protyle/util/selection"; 27 28export const commandPanel = (app: App) => { 29 const range = getSelection().rangeCount > 0 ? getSelection().getRangeAt(0) : undefined; 30 const dialog = new Dialog({ 31 width: isMobile() ? "92vw" : "80vw", 32 height: isMobile() ? "80vh" : "70vh", 33 title: window.siyuan.languages.commandPanel, 34 content: `<div class="fn__flex-column"> 35 <div class="b3-form__icon search__header" style="border-top: 0;border-bottom: 1px solid var(--b3-theme-surface-lighter);"> 36 <svg class="b3-form__icon-icon"><use xlink:href="#iconSearch"></use></svg> 37 <input class="b3-text-field b3-text-field--text" style="padding-left: 32px !important;"> 38 </div> 39 <ul class="b3-list b3-list--background search__list" id="commands"></ul> 40 <div class="search__tip"> 41 <kbd>↑/↓</kbd> ${window.siyuan.languages.searchTip1} 42 <kbd>${window.siyuan.languages.enterKey}/${window.siyuan.languages.click}</kbd> ${window.siyuan.languages.confirm} 43 <kbd>Esc</kbd> ${window.siyuan.languages.close} 44 </div> 45</div>`, 46 destroyCallback() { 47 if (range) { 48 focusByRange(range); 49 } 50 }, 51 }); 52 dialog.element.setAttribute("data-key", Constants.DIALOG_COMMANDPANEL); 53 const listElement = dialog.element.querySelector("#commands"); 54 let html = ""; 55 Object.keys(window.siyuan.config.keymap.general).forEach((key) => { 56 let keys; 57 /// #if MOBILE 58 keys = ["addToDatabase", "fileTree", "outline", "bookmark", "tag", "dailyNote", "inbox", "backlinks", 59 "dataHistory", "editReadonly", "enter", "enterBack", "globalSearch", "lockScreen", "mainMenu", "move", 60 "newFile", "recentDocs", "replace", "riffCard", "search", "selectOpen1", "syncNow"]; 61 /// #else 62 keys = ["addToDatabase", "fileTree", "outline", "bookmark", "tag", "dailyNote", "inbox", "backlinks", 63 "graphView", "globalGraph", "closeAll", "closeLeft", "closeOthers", "closeRight", "closeTab", 64 "closeUnmodified", "config", "dataHistory", "editReadonly", "enter", "enterBack", "globalSearch", "goBack", 65 "goForward", "goToEditTabNext", "goToEditTabPrev", "goToTab1", "goToTab2", "goToTab3", "goToTab4", 66 "goToTab5", "goToTab6", "goToTab7", "goToTab8", "goToTab9", "goToTabNext", "goToTabPrev", "lockScreen", 67 "mainMenu", "move", "newFile", "recentDocs", "replace", "riffCard", "search", "selectOpen1", "syncNow", 68 "splitLR", "splitMoveB", "splitMoveR", "splitTB", "tabToWindow", "stickSearch", "toggleDock", "unsplitAll", 69 "unsplit", "recentClosed"]; 70 /// #if !BROWSER 71 keys.push("toggleWin"); 72 /// #endif 73 /// #endif 74 if (keys.includes(key)) { 75 html += `<li class="b3-list-item" data-command="${key}"> 76 <span class="b3-list-item__text">${window.siyuan.languages[key]}</span> 77 <span class="b3-list-item__meta${isMobile() ? " fn__none" : ""}">${updateHotkeyTip(window.siyuan.config.keymap.general[key].custom)}</span> 78</li>`; 79 } 80 }); 81 Object.keys(window.siyuan.config.keymap.editor.general).forEach((key) => { 82 if (["switchReadonly", "switchAdjust"].includes(key)) { 83 html += `<li class="b3-list-item" data-command="${key}"> 84 <span class="b3-list-item__text">${window.siyuan.languages[key]}</span> 85 <span class="b3-list-item__meta${isMobile() ? " fn__none" : ""}">${updateHotkeyTip(window.siyuan.config.keymap.editor.general[key].custom)}</span> 86</li>`; 87 } 88 }); 89 listElement.insertAdjacentHTML("beforeend", html); 90 app.plugins.forEach(plugin => { 91 plugin.commands.forEach(command => { 92 const liElement = document.createElement("li"); 93 liElement.classList.add("b3-list-item"); 94 liElement.innerHTML = `<span class="b3-list-item__text">${plugin.displayName}: ${command.langText || plugin.i18n[command.langKey]}</span> 95<span class="b3-list-item__meta${isMobile() ? " fn__none" : ""}">${updateHotkeyTip(command.customHotkey)}</span>`; 96 liElement.addEventListener("click", (event) => { 97 if (command.callback) { 98 command.callback(); 99 } else if (command.globalCallback) { 100 command.globalCallback(); 101 } 102 dialog.destroy(); 103 event.preventDefault(); 104 event.stopPropagation(); 105 }); 106 listElement.insertAdjacentElement("beforeend", liElement); 107 }); 108 }); 109 110 if (listElement.childElementCount === 0) { 111 const liElement = document.createElement("li"); 112 liElement.classList.add("b3-list-item", "b3-list-item--focus"); 113 liElement.innerHTML = `<span class="b3-list-item__text" style="-webkit-line-clamp: inherit;">${window.siyuan.languages._kernel[122]}</span>`; 114 liElement.addEventListener("click", () => { 115 dialog.destroy(); 116 }); 117 listElement.insertAdjacentElement("beforeend", liElement); 118 } else { 119 listElement.firstElementChild.classList.add("b3-list-item--focus"); 120 } 121 122 const inputElement = dialog.element.querySelector(".b3-text-field") as HTMLInputElement; 123 inputElement.focus(); 124 listElement.addEventListener("click", (event: KeyboardEvent) => { 125 const liElement = hasClosestByClassName(event.target as HTMLElement, "b3-list-item"); 126 if (liElement) { 127 const command = liElement.getAttribute("data-command"); 128 if (command) { 129 execByCommand({command, app, previousRange: range}); 130 dialog.destroy(); 131 event.preventDefault(); 132 event.stopPropagation(); 133 } 134 } 135 }); 136 inputElement.addEventListener("keydown", (event: KeyboardEvent) => { 137 event.stopPropagation(); 138 if (event.isComposing) { 139 return; 140 } 141 upDownHint(listElement, event); 142 if (event.key === "Enter") { 143 const currentElement = listElement.querySelector(".b3-list-item--focus"); 144 if (currentElement) { 145 const command = currentElement.getAttribute("data-command"); 146 if (command) { 147 execByCommand({command, app, previousRange: range}); 148 } else { 149 currentElement.dispatchEvent(new CustomEvent("click")); 150 } 151 } 152 dialog.destroy(); 153 } else if (event.key === "Escape") { 154 dialog.destroy(); 155 } 156 }); 157 inputElement.addEventListener("compositionend", () => { 158 filterList(inputElement, listElement); 159 }); 160 inputElement.addEventListener("input", (event: InputEvent) => { 161 if (event.isComposing) { 162 return; 163 } 164 event.stopPropagation(); 165 filterList(inputElement, listElement); 166 }); 167}; 168 169const filterList = (inputElement: HTMLInputElement, listElement: Element) => { 170 const inputValue = inputElement.value.toLowerCase(); 171 listElement.querySelector(".b3-list-item--focus")?.classList.remove("b3-list-item--focus"); 172 let hasFocus = false; 173 Array.from(listElement.children).forEach((element: HTMLElement) => { 174 const elementValue = element.querySelector(".b3-list-item__text").textContent.toLowerCase(); 175 const command = element.dataset.command; 176 if (inputValue.indexOf(elementValue) > -1 || elementValue.indexOf(inputValue) > -1 || 177 inputValue.indexOf(command) > -1 || command?.indexOf(inputValue) > -1) { 178 if (!hasFocus) { 179 element.classList.add("b3-list-item--focus"); 180 } 181 hasFocus = true; 182 element.classList.remove("fn__none"); 183 } else { 184 element.classList.add("fn__none"); 185 } 186 }); 187}; 188 189export const execByCommand = async (options: { 190 command: string, 191 app?: App, 192 previousRange?: Range, 193 protyle?: IProtyle, 194 fileLiElements?: Element[] 195}) => { 196 if (globalCommand(options.command, options.app)) { 197 return; 198 } 199 200 const isFileFocus = document.querySelector(".layout__tab--active")?.classList.contains("sy__file"); 201 202 let protyle = options.protyle; 203 /// #if MOBILE 204 if (!protyle) { 205 protyle = getCurrentEditor().protyle; 206 options.previousRange = protyle.toolbar.range; 207 } 208 /// #endif 209 const range: Range = options.previousRange || (getSelection().rangeCount > 0 ? getSelection().getRangeAt(0) : document.createRange()); 210 let fileLiElements = options.fileLiElements; 211 if (!isFileFocus && !protyle) { 212 if (range) { 213 window.siyuan.dialogs.find(item => { 214 if (item.editors) { 215 Object.keys(item.editors).find(key => { 216 if (item.editors[key].protyle.element.contains(range.startContainer)) { 217 protyle = item.editors[key].protyle; 218 return true; 219 } 220 }); 221 if (protyle) { 222 return true; 223 } 224 } 225 }); 226 } 227 const activeTab = getActiveTab(); 228 if (!protyle && activeTab) { 229 if (activeTab.model instanceof Editor) { 230 protyle = activeTab.model.editor.protyle; 231 } else if (activeTab.model instanceof Search) { 232 if (activeTab.model.element.querySelector("#searchUnRefPanel").classList.contains("fn__none")) { 233 protyle = activeTab.model.editors.edit.protyle; 234 } else { 235 protyle = activeTab.model.editors.unRefEdit.protyle; 236 } 237 } else if (activeTab.model instanceof Custom && activeTab.model.editors?.length > 0) { 238 if (range) { 239 activeTab.model.editors.find(item => { 240 if (item.protyle.element.contains(range.startContainer)) { 241 protyle = item.protyle; 242 return true; 243 } 244 }); 245 } 246 } 247 } else if (!protyle) { 248 if (!protyle && range) { 249 window.siyuan.blockPanels.find(item => { 250 item.editors.find(editorItem => { 251 if (editorItem.protyle.element.contains(range.startContainer)) { 252 protyle = editorItem.protyle; 253 return true; 254 } 255 }); 256 if (protyle) { 257 return true; 258 } 259 }); 260 } 261 const models = getAllModels(); 262 if (!protyle) { 263 models.backlink.find(item => { 264 if (item.element.classList.contains("layout__tab--active")) { 265 if (range) { 266 item.editors.find(editor => { 267 if (editor.protyle.element.contains(range.startContainer)) { 268 protyle = editor.protyle; 269 return true; 270 } 271 }); 272 } 273 if (!protyle && item.editors.length > 0) { 274 protyle = item.editors[0].protyle; 275 } 276 return true; 277 } 278 }); 279 } 280 if (!protyle) { 281 models.editor.find(item => { 282 if (item.parent.headElement.classList.contains("item--focus")) { 283 protyle = item.editor.protyle; 284 return true; 285 } 286 }); 287 } 288 } 289 } 290 291 // only protyle 292 if (!isFileFocus && protyle && onlyProtyleCommand({ 293 command: options.command, 294 previousRange: range, 295 protyle 296 })) { 297 return; 298 } 299 300 if (isFileFocus && !fileLiElements) { 301 const dockFile = getDockByType("file"); 302 if (!dockFile) { 303 return false; 304 } 305 const files = dockFile.data.file as Files; 306 fileLiElements = Array.from(files.element.querySelectorAll(".b3-list-item--focus")); 307 } 308 309 // 全局命令,在没有 protyle 和文件树没聚焦的情况下执行 310 if ((!protyle && !isFileFocus) || 311 (isFileFocus && (!fileLiElements || fileLiElements.length === 0)) || 312 (isMobile() && !document.getElementById("empty").classList.contains("fn__none"))) { 313 if (options.command === "replace") { 314 /// #if MOBILE 315 popSearch(options.app, {hasReplace: true, page: 1}); 316 /// #else 317 openSearch({ 318 app: options.app, 319 hotkey: Constants.DIALOG_REPLACE, 320 key: range.toString() 321 }); 322 /// #endif 323 } else if (options.command === "search") { 324 /// #if MOBILE 325 popSearch(options.app, {hasReplace: false, page: 1}); 326 /// #else 327 openSearch({ 328 app: options.app, 329 hotkey: Constants.DIALOG_SEARCH, 330 key: range.toString() 331 }); 332 /// #endif 333 } 334 return; 335 } 336 337 // protyle and file tree 338 switch (options.command) { 339 case "replace": 340 if (!isFileFocus) { 341 /// #if MOBILE 342 const response = await fetchSyncPost("/api/filetree/getHPathByPath", { 343 notebook: protyle.notebookId, 344 path: protyle.path.endsWith(".sy") ? protyle.path : protyle.path + ".sy" 345 }); 346 popSearch(options.app, { 347 page: 1, 348 hasReplace: true, 349 hPath: pathPosix().join(getNotebookName(protyle.notebookId), response.data), 350 idPath: [pathPosix().join(protyle.notebookId, protyle.path)] 351 }); 352 /// #else 353 openSearch({ 354 app: options.app, 355 hotkey: Constants.DIALOG_REPLACE, 356 key: range.toString(), 357 notebookId: protyle.notebookId, 358 searchPath: protyle.path 359 }); 360 /// #endif 361 } else { 362 /// #if !MOBILE 363 const topULElement = hasTopClosestByTag(fileLiElements[0], "UL"); 364 if (!topULElement) { 365 return false; 366 } 367 const notebookId = topULElement.getAttribute("data-url"); 368 const pathString = fileLiElements[0].getAttribute("data-path"); 369 const isFile = fileLiElements[0].getAttribute("data-type") === "navigation-file"; 370 if (isFile) { 371 openSearch({ 372 app: options.app, 373 hotkey: Constants.DIALOG_REPLACE, 374 notebookId: notebookId, 375 searchPath: getDisplayName(pathString, false, true) 376 }); 377 } else { 378 openSearch({ 379 app: options.app, 380 hotkey: Constants.DIALOG_REPLACE, 381 notebookId: notebookId, 382 }); 383 } 384 /// #endif 385 } 386 break; 387 case "search": 388 if (!isFileFocus) { 389 /// #if MOBILE 390 const response = await fetchSyncPost("/api/filetree/getHPathByPath", { 391 notebook: protyle.notebookId, 392 path: protyle.path.endsWith(".sy") ? protyle.path : protyle.path + ".sy" 393 }); 394 popSearch(options.app, { 395 page: 1, 396 hasReplace: false, 397 hPath: pathPosix().join(getNotebookName(protyle.notebookId), response.data), 398 idPath: [pathPosix().join(protyle.notebookId, protyle.path)] 399 }); 400 /// #else 401 openSearch({ 402 app: options.app, 403 hotkey: Constants.DIALOG_SEARCH, 404 key: range.toString(), 405 notebookId: protyle.notebookId, 406 searchPath: protyle.path 407 }); 408 /// #endif 409 } else { 410 /// #if !MOBILE 411 const topULElement = hasTopClosestByTag(fileLiElements[0], "UL"); 412 if (!topULElement) { 413 return false; 414 } 415 const notebookId = topULElement.getAttribute("data-url"); 416 const pathString = fileLiElements[0].getAttribute("data-path"); 417 const isFile = fileLiElements[0].getAttribute("data-type") === "navigation-file"; 418 if (isFile) { 419 openSearch({ 420 app: options.app, 421 hotkey: Constants.DIALOG_SEARCH, 422 notebookId: notebookId, 423 searchPath: getDisplayName(pathString, false, true) 424 }); 425 } else { 426 openSearch({ 427 app: options.app, 428 hotkey: Constants.DIALOG_SEARCH, 429 notebookId: notebookId, 430 }); 431 } 432 /// #endif 433 } 434 break; 435 case "addToDatabase": 436 if (!isFileFocus) { 437 addEditorToDatabase(protyle, range); 438 } else { 439 addFilesToDatabase(fileLiElements); 440 } 441 break; 442 case "move": 443 if (!isFileFocus) { 444 const nodeElement = hasClosestBlock(range.startContainer); 445 if (protyle.title?.editElement.contains(range.startContainer) || !nodeElement || window.siyuan.menus.menu.element.getAttribute("data-name") === Constants.MENU_TITLE) { 446 movePathTo((toPath, toNotebook) => { 447 moveToPath([protyle.path], toNotebook[0], toPath[0]); 448 }, [protyle.path], range); 449 } else if (nodeElement && range && protyle.element.contains(range.startContainer)) { 450 let selectElements = Array.from(protyle.wysiwyg.element.querySelectorAll(".protyle-wysiwyg--select")); 451 if (selectElements.length === 0) { 452 selectElements = [nodeElement]; 453 } 454 movePathTo((toPath) => { 455 hintMoveBlock(toPath[0], selectElements, protyle); 456 }); 457 } 458 } else { 459 const paths = getTopPaths(fileLiElements); 460 movePathTo((toPath, toNotebook) => { 461 moveToPath(paths, toNotebook[0], toPath[0]); 462 }, paths); 463 } 464 break; 465 } 466};