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