mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 19:49:01 +01:00 
			
		
		
		
	Merge pull request #1017 from TriliumNext/feature/map_note_type
Map note type
This commit is contained in:
		
						commit
						16b5eef650
					
				| @ -100,7 +100,8 @@ const copy = async () => { | ||||
|         "node_modules/codemirror/keymap/", | ||||
|         "node_modules/mind-elixir/dist/", | ||||
|         "node_modules/@highlightjs/cdn-assets/languages", | ||||
|         "node_modules/@highlightjs/cdn-assets/styles" | ||||
|         "node_modules/@highlightjs/cdn-assets/styles", | ||||
|         "node_modules/leaflet/dist" | ||||
|     ]; | ||||
| 
 | ||||
|     for (const folder of nodeModulesFolder) { | ||||
|  | ||||
							
								
								
									
										17
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										17
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -16,6 +16,7 @@ | ||||
|         "@mermaid-js/layout-elk": "0.1.7", | ||||
|         "@mind-elixir/node-menu": "1.0.3", | ||||
|         "@triliumnext/express-partial-content": "1.0.1", | ||||
|         "@types/leaflet": "1.9.16", | ||||
|         "@types/react-dom": "18.3.5", | ||||
|         "archiver": "7.0.1", | ||||
|         "async-mutex": "0.5.0", | ||||
| @ -67,6 +68,7 @@ | ||||
|         "jsplumb": "2.15.6", | ||||
|         "katex": "0.16.21", | ||||
|         "knockout": "3.5.1", | ||||
|         "leaflet": "1.9.4", | ||||
|         "mark.js": "8.11.1", | ||||
|         "marked": "15.0.6", | ||||
|         "mermaid": "11.4.1", | ||||
| @ -3847,6 +3849,15 @@ | ||||
|         "@types/node": "*" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@types/leaflet": { | ||||
|       "version": "1.9.16", | ||||
|       "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.16.tgz", | ||||
|       "integrity": "sha512-wzZoyySUxkgMZ0ihJ7IaUIblG8Rdc8AbbZKLneyn+QjYsj5q1QU7TEKYqwTr10BGSzY5LI7tJk9Ifo+mEjdFRw==", | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
|         "@types/geojson": "*" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@types/linkify-it": { | ||||
|       "version": "5.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", | ||||
| @ -11437,6 +11448,12 @@ | ||||
|         "safe-buffer": "~5.1.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/leaflet": { | ||||
|       "version": "1.9.4", | ||||
|       "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", | ||||
|       "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", | ||||
|       "license": "BSD-2-Clause" | ||||
|     }, | ||||
|     "node_modules/limiter": { | ||||
|       "version": "1.1.5", | ||||
|       "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", | ||||
|  | ||||
| @ -61,6 +61,7 @@ | ||||
|     "@mermaid-js/layout-elk": "0.1.7", | ||||
|     "@mind-elixir/node-menu": "1.0.3", | ||||
|     "@triliumnext/express-partial-content": "1.0.1", | ||||
|     "@types/leaflet": "1.9.16", | ||||
|     "@types/react-dom": "18.3.5", | ||||
|     "archiver": "7.0.1", | ||||
|     "async-mutex": "0.5.0", | ||||
| @ -112,6 +113,7 @@ | ||||
|     "jsplumb": "2.15.6", | ||||
|     "katex": "0.16.21", | ||||
|     "knockout": "3.5.1", | ||||
|     "leaflet": "1.9.4", | ||||
|     "mark.js": "8.11.1", | ||||
|     "marked": "15.0.6", | ||||
|     "mermaid": "11.4.1", | ||||
|  | ||||
| @ -116,7 +116,8 @@ export const ALLOWED_NOTE_TYPES = [ | ||||
|     "book", | ||||
|     "webView", | ||||
|     "code", | ||||
|     "mindMap" | ||||
|     "mindMap", | ||||
|     "geoMap" | ||||
| ] as const; | ||||
| export type NoteType = (typeof ALLOWED_NOTE_TYPES)[number]; | ||||
| 
 | ||||
|  | ||||
| @ -23,6 +23,7 @@ import type LoadResults from "../services/load_results.js"; | ||||
| import type { Attribute } from "../services/attribute_parser.js"; | ||||
| import type NoteTreeWidget from "../widgets/note_tree.js"; | ||||
| import type { default as NoteContext, GetTextEditorCallback } from "./note_context.js"; | ||||
| import type { ContextMenuEvent } from "../menus/context_menu.js"; | ||||
| 
 | ||||
| interface Layout { | ||||
|     getRootWidget: (appContext: AppContext) => RootWidget; | ||||
| @ -69,6 +70,7 @@ export interface ExecuteCommandData extends CommandData { | ||||
|  */ | ||||
| export type CommandMappings = { | ||||
|     "api-log-messages": CommandData; | ||||
|     focusTree: CommandData, | ||||
|     focusOnDetail: Required<CommandData>; | ||||
|     focusOnSearchDefinition: Required<CommandData>; | ||||
|     searchNotes: CommandData & { | ||||
| @ -193,6 +195,10 @@ export type CommandMappings = { | ||||
|     setZoomFactorAndSave: { | ||||
|         zoomFactor: string; | ||||
|     } | ||||
| 
 | ||||
|     // Geomap
 | ||||
|     deleteFromMap: { noteId: string }, | ||||
|     openGeoLocation: { noteId: string, event: JQuery.MouseDownEvent } | ||||
| }; | ||||
| 
 | ||||
| type EventMappings = { | ||||
| @ -227,9 +233,12 @@ type EventMappings = { | ||||
|     activeContextChanged: { | ||||
|         noteContext: NoteContext; | ||||
|     }; | ||||
|     beforeNoteSwitch: { | ||||
|         noteContext: NoteContext; | ||||
|     }; | ||||
|     noteSwitched: { | ||||
|         noteContext: NoteContext; | ||||
|         notePath: string; | ||||
|         notePath: string | null; | ||||
|     }; | ||||
|     noteSwitchedAndActivatedEvent: { | ||||
|         noteContext: NoteContext; | ||||
| @ -248,12 +257,16 @@ type EventMappings = { | ||||
|         noteId: string; | ||||
|     }; | ||||
|     hoistedNoteChanged: { | ||||
|         ntxId: string; | ||||
|         noteId: string; | ||||
|         ntxId: string | null; | ||||
|     }; | ||||
|     contextsReopenedEvent: { | ||||
|         mainNtxId: string; | ||||
|         tabPosition: number; | ||||
|     }; | ||||
|     noteDetailRefreshed: { | ||||
|         ntxId?: string | null; | ||||
|     }; | ||||
|     noteContextReorderEvent: { | ||||
|         oldMainNtxId: string; | ||||
|         newMainNtxId: string; | ||||
| @ -266,7 +279,13 @@ type EventMappings = { | ||||
|     }; | ||||
|     exportSvg: { | ||||
|         ntxId: string; | ||||
|     } | ||||
|     }; | ||||
|     geoMapCreateChildNote: { | ||||
|         ntxId: string | null | undefined; // TODO: deduplicate ntxId
 | ||||
|     }; | ||||
|     tabReorder: { | ||||
|         ntxIdsInOrder: string[] | ||||
|     }; | ||||
| }; | ||||
| 
 | ||||
| export type EventListener<T extends EventNames> = { | ||||
|  | ||||
| @ -61,7 +61,7 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     triggerEvent(name: string, data = {}): Promise<unknown> | undefined | null { | ||||
|     triggerEvent<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown> | undefined | null { | ||||
|         return this.parent?.triggerEvent(name, data); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -27,7 +27,8 @@ const NOTE_TYPE_ICONS = { | ||||
|     launcher: "bx bx-link", | ||||
|     doc: "bx bxs-file-doc", | ||||
|     contentWidget: "bx bxs-widget", | ||||
|     mindMap: "bx bx-sitemap" | ||||
|     mindMap: "bx bx-sitemap", | ||||
|     geoMap: "bx bx-map-alt" | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
| @ -35,7 +36,7 @@ const NOTE_TYPE_ICONS = { | ||||
|  * end user. Those types should be used only for checking against, they are | ||||
|  * not for direct use. | ||||
|  */ | ||||
| type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code" | "mindMap"; | ||||
| type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code" | "mindMap" | "geoMap"; | ||||
| 
 | ||||
| interface NotePathRecord { | ||||
|     isArchived: boolean; | ||||
|  | ||||
| @ -85,6 +85,7 @@ import ScrollPaddingWidget from "../widgets/scroll_padding.js"; | ||||
| import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js"; | ||||
| import options from "../services/options.js"; | ||||
| import utils from "../services/utils.js"; | ||||
| import GeoMapButtons from "../widgets/floating_buttons/geo_map_button.js"; | ||||
| 
 | ||||
| export default class DesktopLayout { | ||||
|     constructor(customWidgets) { | ||||
| @ -200,6 +201,7 @@ export default class DesktopLayout { | ||||
|                                                                 .child(new ShowHighlightsListWidgetButton()) | ||||
|                                                                 .child(new CodeButtonsWidget()) | ||||
|                                                                 .child(new RelationMapButtons()) | ||||
|                                                                 .child(new GeoMapButtons()) | ||||
|                                                                 .child(new CopyImageReferenceButton()) | ||||
|                                                                 .child(new SvgExportButton()) | ||||
|                                                                 .child(new BacklinksWidget()) | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| import type { CommandNames } from "../components/app_context.js"; | ||||
| import keyboardActionService from "../services/keyboard_actions.js"; | ||||
| import note_tooltip from "../services/note_tooltip.js"; | ||||
| import utils from "../services/utils.js"; | ||||
| 
 | ||||
| interface ContextMenuOptions<T extends CommandNames> { | ||||
| @ -31,6 +32,7 @@ export interface MenuCommandItem<T extends CommandNames> { | ||||
| 
 | ||||
| export type MenuItem<T extends CommandNames> = MenuCommandItem<T> | MenuSeparatorItem; | ||||
| export type MenuHandler<T extends CommandNames> = (item: MenuCommandItem<T>, e: JQuery.MouseDownEvent<HTMLElement, undefined, HTMLElement, HTMLElement>) => void; | ||||
| export type ContextMenuEvent = PointerEvent | MouseEvent | JQuery.ContextMenuEvent; | ||||
| 
 | ||||
| class ContextMenu { | ||||
|     private $widget: JQuery<HTMLElement>; | ||||
| @ -56,6 +58,8 @@ class ContextMenu { | ||||
|     async show<T extends CommandNames>(options: ContextMenuOptions<T>) { | ||||
|         this.options = options; | ||||
| 
 | ||||
|         note_tooltip.dismissAllTooltips(); | ||||
| 
 | ||||
|         if (this.$widget.hasClass("show")) { | ||||
|             // The menu is already visible. Hide the menu then open it again
 | ||||
|             // at the new location to re-trigger the opening animation.
 | ||||
|  | ||||
| @ -1,36 +1,44 @@ | ||||
| import { t } from "../services/i18n.js"; | ||||
| import contextMenu from "./context_menu.js"; | ||||
| import appContext from "../components/app_context.js"; | ||||
| import contextMenu, { type ContextMenuEvent, type MenuItem } from "./context_menu.js"; | ||||
| import appContext, { type CommandNames } from "../components/app_context.js"; | ||||
| import type { ViewScope } from "../services/link.js"; | ||||
| 
 | ||||
| function openContextMenu(notePath: string, e: PointerEvent | MouseEvent | JQuery.ContextMenuEvent, viewScope: ViewScope = {}, hoistedNoteId: string | null = null) { | ||||
| function openContextMenu(notePath: string, e: ContextMenuEvent, viewScope: ViewScope = {}, hoistedNoteId: string | null = null) { | ||||
|     contextMenu.show({ | ||||
|         x: e.pageX, | ||||
|         y: e.pageY, | ||||
|         items: [ | ||||
|             { title: t("link_context_menu.open_note_in_new_tab"), command: "openNoteInNewTab", uiIcon: "bx bx-link-external" }, | ||||
|             { title: t("link_context_menu.open_note_in_new_split"), command: "openNoteInNewSplit", uiIcon: "bx bx-dock-right" }, | ||||
|             { title: t("link_context_menu.open_note_in_new_window"), command: "openNoteInNewWindow", uiIcon: "bx bx-window-open" } | ||||
|         ], | ||||
|         selectMenuItemHandler: ({ command }) => { | ||||
|             if (!hoistedNoteId) { | ||||
|                 hoistedNoteId = appContext.tabManager.getActiveContext().hoistedNoteId; | ||||
|             } | ||||
| 
 | ||||
|             if (command === "openNoteInNewTab") { | ||||
|                 appContext.tabManager.openContextWithNote(notePath, { hoistedNoteId, viewScope }); | ||||
|             } else if (command === "openNoteInNewSplit") { | ||||
|                 const subContexts = appContext.tabManager.getActiveContext().getSubContexts(); | ||||
|                 const { ntxId } = subContexts[subContexts.length - 1]; | ||||
| 
 | ||||
|                 appContext.triggerCommand("openNewNoteSplit", { ntxId, notePath, hoistedNoteId, viewScope }); | ||||
|             } else if (command === "openNoteInNewWindow") { | ||||
|                 appContext.triggerCommand("openInWindow", { notePath, hoistedNoteId, viewScope }); | ||||
|             } | ||||
|         } | ||||
|         items: getItems(), | ||||
|         selectMenuItemHandler: ({ command }) => handleLinkContextMenuItem(command, notePath, viewScope, hoistedNoteId) | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| function getItems(): MenuItem<CommandNames>[] { | ||||
|     return [ | ||||
|         { title: t("link_context_menu.open_note_in_new_tab"), command: "openNoteInNewTab", uiIcon: "bx bx-link-external" }, | ||||
|         { title: t("link_context_menu.open_note_in_new_split"), command: "openNoteInNewSplit", uiIcon: "bx bx-dock-right" }, | ||||
|         { title: t("link_context_menu.open_note_in_new_window"), command: "openNoteInNewWindow", uiIcon: "bx bx-window-open" } | ||||
|     ]; | ||||
| } | ||||
| 
 | ||||
| function handleLinkContextMenuItem(command: string | undefined, notePath: string, viewScope = {}, hoistedNoteId: string | null = null) { | ||||
|     if (!hoistedNoteId) { | ||||
|         hoistedNoteId = appContext.tabManager.getActiveContext().hoistedNoteId; | ||||
|     } | ||||
| 
 | ||||
|     if (command === "openNoteInNewTab") { | ||||
|         appContext.tabManager.openContextWithNote(notePath, { hoistedNoteId, viewScope }); | ||||
|     } else if (command === "openNoteInNewSplit") { | ||||
|         const subContexts = appContext.tabManager.getActiveContext().getSubContexts(); | ||||
|         const { ntxId } = subContexts[subContexts.length - 1]; | ||||
| 
 | ||||
|         appContext.triggerCommand("openNewNoteSplit", { ntxId, notePath, hoistedNoteId, viewScope }); | ||||
|     } else if (command === "openNoteInNewWindow") { | ||||
|         appContext.triggerCommand("openInWindow", { notePath, hoistedNoteId, viewScope }); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export default { | ||||
|     getItems, | ||||
|     handleLinkContextMenuItem, | ||||
|     openContextMenu | ||||
| }; | ||||
|  | ||||
| @ -106,6 +106,10 @@ const HIGHLIGHT_JS: Library = { | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| const LEAFLET: Library = { | ||||
|     css: [ "node_modules/leaflet/dist/leaflet.css" ], | ||||
| } | ||||
| 
 | ||||
| async function requireLibrary(library: Library) { | ||||
|     if (library.css) { | ||||
|         library.css.map((cssUrl) => requireCss(cssUrl)); | ||||
| @ -196,5 +200,6 @@ export default { | ||||
|     MERMAID, | ||||
|     MARKJS, | ||||
|     I18NEXT, | ||||
|     HIGHLIGHT_JS | ||||
|     HIGHLIGHT_JS, | ||||
|     LEAFLET | ||||
| }; | ||||
|  | ||||
| @ -234,7 +234,7 @@ function goToLink(evt: MouseEvent | JQuery.ClickEvent) { | ||||
|     return goToLinkExt(evt, hrefLink, $link); | ||||
| } | ||||
| 
 | ||||
| function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | React.PointerEvent<HTMLCanvasElement>, hrefLink: string | undefined, $link: JQuery<HTMLElement> | null) { | ||||
| function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent | React.PointerEvent<HTMLCanvasElement>, hrefLink: string | undefined, $link?: JQuery<HTMLElement> | null) { | ||||
|     if (hrefLink?.startsWith("data:")) { | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
| @ -18,11 +18,11 @@ function setupGlobalTooltip() { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         cleanUpTooltips(); | ||||
|         dismissAllTooltips(); | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| function cleanUpTooltips() { | ||||
| function dismissAllTooltips() { | ||||
|     $(".note-tooltip").remove(); | ||||
| } | ||||
| 
 | ||||
| @ -102,12 +102,12 @@ async function mouseEnterHandler(this: HTMLElement) { | ||||
|             customClass: linkId | ||||
|         }); | ||||
| 
 | ||||
|         cleanUpTooltips(); | ||||
|         dismissAllTooltips(); | ||||
|         $(this).tooltip("show"); | ||||
| 
 | ||||
|         // Dismiss the tooltip immediately if a link was clicked inside the tooltip.
 | ||||
|         $(`.${tooltipClass} a`).on("click", (e) => { | ||||
|             cleanUpTooltips(); | ||||
|             dismissAllTooltips(); | ||||
|         }); | ||||
| 
 | ||||
|         // the purpose of the code below is to:
 | ||||
| @ -117,7 +117,7 @@ async function mouseEnterHandler(this: HTMLElement) { | ||||
|         const checkTooltip = () => { | ||||
|             if (!$(this).filter(":hover").length && !$(`.${linkId}:hover`).length) { | ||||
|                 // cursor is neither over the link nor over the tooltip, user likely is not interested
 | ||||
|                 cleanUpTooltips(); | ||||
|                 dismissAllTooltips(); | ||||
|             } else { | ||||
|                 setTimeout(checkTooltip, 1000); | ||||
|             } | ||||
| @ -172,5 +172,6 @@ function renderFootnote($link: JQuery<HTMLElement>, url: string) { | ||||
| 
 | ||||
| export default { | ||||
|     setupGlobalTooltip, | ||||
|     setupElementTooltip | ||||
|     setupElementTooltip, | ||||
|     dismissAllTooltips | ||||
| }; | ||||
|  | ||||
| @ -18,7 +18,8 @@ async function getNoteTypeItems(command?: NoteTypeCommandNames) { | ||||
|         { title: t("note_types.mermaid-diagram"), command, type: "mermaid", uiIcon: "bx bx-selection" }, | ||||
|         { title: t("note_types.canvas"), command, type: "canvas", uiIcon: "bx bx-pen" }, | ||||
|         { title: t("note_types.web-view"), command, type: "webView", uiIcon: "bx bx-globe-alt" }, | ||||
|         { title: t("note_types.mind-map"), command, type: "mindMap", uiIcon: "bx bx-sitemap" } | ||||
|         { title: t("note_types.mind-map"), command, type: "mindMap", uiIcon: "bx bx-sitemap" }, | ||||
|         { title: t("note_types.geo-map"), command, type: "geoMap", uiIcon: "bx bx-map-alt" }, | ||||
|     ]; | ||||
| 
 | ||||
|     const templateNoteIds = await server.get<string[]>("search-templates"); | ||||
|  | ||||
| @ -42,7 +42,7 @@ const TPL = ` | ||||
|         <li data-trigger-command="convertNoteIntoAttachment" class="dropdown-item"> | ||||
|             <span class="bx bx-paperclip"></span> ${t("note_actions.convert_into_attachment")} | ||||
|         </li> | ||||
|          | ||||
| 
 | ||||
|         <li data-trigger-command="renderActiveNote" class="dropdown-item render-note-button"> | ||||
|             <span class="bx bx-extension"></span> ${t("note_actions.re_render_note")}<kbd data-command="renderActiveNote"></kbd> | ||||
|         </li> | ||||
| @ -54,15 +54,15 @@ const TPL = ` | ||||
|         <li data-trigger-command="printActiveNote" class="dropdown-item print-active-note-button"> | ||||
|             <span class="bx bx-printer"></span> ${t("note_actions.print_note")}<kbd data-command="printActiveNote"></kbd></li> | ||||
| 
 | ||||
|          | ||||
| 
 | ||||
|         <div class="dropdown-divider"></div> | ||||
| 
 | ||||
|          | ||||
| 
 | ||||
|         <li class="dropdown-item import-files-button"><span class="bx bx-import"></span> ${t("note_actions.import_files")}</li> | ||||
| 
 | ||||
|         <li class="dropdown-item export-note-button"><span class="bx bx-export"></span> ${t("note_actions.export_note")}</li> | ||||
| 
 | ||||
|          | ||||
| 
 | ||||
|         <div class="dropdown-divider"></div> | ||||
| 
 | ||||
| 
 | ||||
| @ -79,7 +79,7 @@ const TPL = ` | ||||
|             <span class="bx bx-code"></span> ${t("note_actions.note_source")}<kbd data-command="showNoteSource"></kbd> | ||||
|         </li> | ||||
| 
 | ||||
|          | ||||
| 
 | ||||
|         <div class="dropdown-divider"></div> | ||||
| 
 | ||||
| 
 | ||||
| @ -89,10 +89,10 @@ const TPL = ` | ||||
| 
 | ||||
|         <li class="dropdown-item delete-note-button"><span class="bx bx-trash destructive-action-icon"></span> ${t("note_actions.delete_note")}</li> | ||||
| 
 | ||||
|          | ||||
| 
 | ||||
|         <div class="dropdown-divider"></div> | ||||
| 
 | ||||
|          | ||||
| 
 | ||||
|         <li data-trigger-command="showAttachments" class="dropdown-item show-attachments-button"> | ||||
|             <span class="bx bx-paperclip"></span> ${t("note_actions.note_attachments")}<kbd data-command="showAttachments"></kbd> | ||||
|         </li> | ||||
| @ -154,7 +154,7 @@ export default class NoteActionsWidget extends NoteContextAwareWidget { | ||||
|         this.toggleDisabled(this.$findInTextButton, ["text", "code", "book"].includes(note.type)); | ||||
| 
 | ||||
|         this.toggleDisabled(this.$showAttachmentsButton, !isInOptions); | ||||
|         this.toggleDisabled(this.$showSourceButton, ["text", "code", "relationMap", "mermaid", "canvas", "mindMap"].includes(note.type)); | ||||
|         this.toggleDisabled(this.$showSourceButton, ["text", "code", "relationMap", "mermaid", "canvas", "mindMap", "geoMap"].includes(note.type)); | ||||
| 
 | ||||
|         this.toggleDisabled(this.$printActiveNoteButton, ["text", "code"].includes(note.type)); | ||||
| 
 | ||||
|  | ||||
| @ -22,7 +22,7 @@ export default class LeftPaneContainer extends FlexContainer<Component> { | ||||
|             this.toggleInt(visible); | ||||
| 
 | ||||
|             if (visible) { | ||||
|                 this.triggerEvent("focusTree"); | ||||
|                 this.triggerEvent("focusTree", {}); | ||||
|             } else { | ||||
|                 const activeNoteContext = appContext.tabManager.getActiveContext(); | ||||
|                 this.triggerEvent("focusOnDetail", { ntxId: activeNoteContext.ntxId }); | ||||
|  | ||||
							
								
								
									
										42
									
								
								src/public/app/widgets/floating_buttons/geo_map_button.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/public/app/widgets/floating_buttons/geo_map_button.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,42 @@ | ||||
| import { t } from "../../services/i18n.js"; | ||||
| import NoteContextAwareWidget from "../note_context_aware_widget.js" | ||||
| 
 | ||||
| const TPL = `\ | ||||
| <div class="geo-map-buttons"> | ||||
|     <style> | ||||
|         .geo-map-buttons { | ||||
|             display: flex; | ||||
|             gap: 10px; | ||||
|         } | ||||
| 
 | ||||
|         .leaflet-pane { | ||||
|             z-index: 50; | ||||
|         } | ||||
| 
 | ||||
|         .geo-map-buttons { | ||||
|             contain: none; | ||||
|             background: var(--main-background-color); | ||||
|             box-shadow: 0px 10px 20px rgba(0, 0, 0, var(--dropdown-shadow-opacity)); | ||||
|             border-radius: 4px; | ||||
|         } | ||||
|     </style> | ||||
| 
 | ||||
|     <button type="button" | ||||
|         class="geo-map-create-child-note floating-button btn bx bx-folder-plus" | ||||
|         title="${t("geo-map.create-child-note-title")}" /> | ||||
| </div>`;
 | ||||
| 
 | ||||
| export default class GeoMapButtons extends NoteContextAwareWidget { | ||||
| 
 | ||||
|     isEnabled() { | ||||
|         return super.isEnabled() && this.note?.type === "geoMap"; | ||||
|     } | ||||
| 
 | ||||
|     doRender() { | ||||
|         super.doRender(); | ||||
| 
 | ||||
|         this.$widget = $(TPL); | ||||
|         this.$widget.find(".geo-map-create-child-note").on("click", () => this.triggerEvent("geoMapCreateChildNote", { ntxId: this.ntxId })); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -13,16 +13,16 @@ const TPL = ` | ||||
|     <button type="button" | ||||
|             class="relation-map-create-child-note floating-button btn bx bx-folder-plus" | ||||
|             title="${t("relation_map_buttons.create_child_note_title")}"></button> | ||||
|      | ||||
| 
 | ||||
|     <button type="button" | ||||
|             class="relation-map-reset-pan-zoom floating-button btn bx bx-crop" | ||||
|             title="${t("relation_map_buttons.reset_pan_zoom_title")}"></button> | ||||
|      | ||||
| 
 | ||||
|     <div class="btn-group"> | ||||
|         <button type="button" | ||||
|                 class="relation-map-zoom-in floating-button btn bx bx-zoom-in" | ||||
|                 title="${t("relation_map_buttons.zoom_in_title")}"></button> | ||||
|      | ||||
| 
 | ||||
|         <button type="button" | ||||
|                 class="relation-map-zoom-out floating-button btn bx bx-zoom-out" | ||||
|                 title="${t("relation_map_buttons.zoom_out_title")}"></button> | ||||
| @ -43,6 +43,7 @@ export default class RelationMapButtons extends NoteContextAwareWidget { | ||||
|         this.$zoomOutButton = this.$widget.find(".relation-map-zoom-out"); | ||||
|         this.$resetPanZoomButton = this.$widget.find(".relation-map-reset-pan-zoom"); | ||||
| 
 | ||||
|         // TODO: Deduplicate object creation here.
 | ||||
|         this.$createChildNote.on("click", () => this.triggerEvent("relationMapCreateChildNote", { ntxId: this.ntxId })); | ||||
|         this.$resetPanZoomButton.on("click", () => this.triggerEvent("relationMapResetPanZoom", { ntxId: this.ntxId })); | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										57
									
								
								src/public/app/widgets/geo_map.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								src/public/app/widgets/geo_map.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,57 @@ | ||||
| import type { Map } from "leaflet"; | ||||
| import library_loader from "../services/library_loader.js"; | ||||
| import NoteContextAwareWidget from "./note_context_aware_widget.js"; | ||||
| 
 | ||||
| const TPL = `\ | ||||
| <div class="geo-map-widget"> | ||||
|     <style> | ||||
|         .note-detail-geo-map, | ||||
|         .geo-map-widget, | ||||
|         .geo-map-container { | ||||
|             height: 100%; | ||||
|             overflow: hidden; | ||||
|         } | ||||
|     </style> | ||||
| 
 | ||||
|     <div class="geo-map-container"></div> | ||||
| </div>` | ||||
| 
 | ||||
| export type Leaflet = typeof import("leaflet"); | ||||
| export type InitCallback = ((L: Leaflet) => void); | ||||
| 
 | ||||
| export default class GeoMapWidget extends NoteContextAwareWidget { | ||||
| 
 | ||||
|     map?: Map; | ||||
|     $container!: JQuery<HTMLElement>; | ||||
|     private initCallback?: InitCallback; | ||||
| 
 | ||||
|     constructor(widgetMode: "type", initCallback?: InitCallback) { | ||||
|         super(); | ||||
|         this.initCallback = initCallback; | ||||
|     } | ||||
| 
 | ||||
|     doRender() { | ||||
|         this.$widget = $(TPL); | ||||
| 
 | ||||
|         this.$container = this.$widget.find(".geo-map-container"); | ||||
| 
 | ||||
|         library_loader.requireLibrary(library_loader.LEAFLET) | ||||
|             .then(async () => { | ||||
|                 const L = (await import("leaflet")).default; | ||||
| 
 | ||||
|                 const map = L.map(this.$container[0], { | ||||
| 
 | ||||
|                 }); | ||||
| 
 | ||||
|                 this.map = map; | ||||
|                 if (this.initCallback) { | ||||
|                     this.initCallback(L); | ||||
|                 } | ||||
| 
 | ||||
|                 L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { | ||||
|                     attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' | ||||
|                 }).addTo(map); | ||||
|             }); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -31,6 +31,7 @@ import AttachmentListTypeWidget from "./type_widgets/attachment_list.js"; | ||||
| import AttachmentDetailTypeWidget from "./type_widgets/attachment_detail.js"; | ||||
| import MindMapWidget from "./type_widgets/mind_map.js"; | ||||
| import { getStylesheetUrl, isSyntaxHighlightEnabled } from "../services/syntax_highlight.js"; | ||||
| import GeoMapTypeWidget from "./type_widgets/geo_map.js"; | ||||
| 
 | ||||
| const TPL = ` | ||||
| <div class="note-detail"> | ||||
| @ -39,7 +40,7 @@ const TPL = ` | ||||
|         font-family: var(--detail-font-family); | ||||
|         font-size: var(--detail-font-size); | ||||
|     } | ||||
|      | ||||
| 
 | ||||
|     .note-detail.full-height { | ||||
|         height: 100%; | ||||
|     } | ||||
| @ -67,7 +68,8 @@ const typeWidgetClasses = { | ||||
|     contentWidget: ContentWidgetTypeWidget, | ||||
|     attachmentDetail: AttachmentDetailTypeWidget, | ||||
|     attachmentList: AttachmentListTypeWidget, | ||||
|     mindMap: MindMapWidget | ||||
|     mindMap: MindMapWidget, | ||||
|     geoMap: GeoMapTypeWidget | ||||
| }; | ||||
| 
 | ||||
| export default class NoteDetailWidget extends NoteContextAwareWidget { | ||||
| @ -147,7 +149,7 @@ export default class NoteDetailWidget extends NoteContextAwareWidget { | ||||
|         // https://github.com/zadam/trilium/issues/2522
 | ||||
|         this.$widget.toggleClass( | ||||
|             "full-height", | ||||
|             (!this.noteContext.hasNoteList() && ["canvas", "webView", "noteMap", "mindMap"].includes(this.type) && this.mime !== "text/x-sqlite;schema=trilium") || | ||||
|             (!this.noteContext.hasNoteList() && ["canvas", "webView", "noteMap", "mindMap", "geoMap"].includes(this.type) && this.mime !== "text/x-sqlite;schema=trilium") || | ||||
|                 this.noteContext.viewScope.viewMode === "attachments" | ||||
|         ); | ||||
|     } | ||||
| @ -276,7 +278,7 @@ export default class NoteDetailWidget extends NoteContextAwareWidget { | ||||
| <script src="${assetPath}/node_modules/katex/dist/contrib/auto-render.min.js"></script> | ||||
| <script> | ||||
|     document.body.className += ' ck-content printed-content'; | ||||
|      | ||||
| 
 | ||||
|     renderMathInElement(document.body, {trust: true}); | ||||
| </script> | ||||
| `,
 | ||||
|  | ||||
| @ -41,7 +41,7 @@ export default class NoteWrapperWidget extends FlexContainer { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.$widget.toggleClass("full-content-width", ["image", "mermaid", "book", "render", "canvas", "webView", "mindMap"].includes(note.type) || !!note?.isLabelTruthy("fullContentWidth")); | ||||
|         this.$widget.toggleClass("full-content-width", ["image", "mermaid", "book", "render", "canvas", "webView", "mindMap", "geoMap"].includes(note.type) || !!note?.isLabelTruthy("fullContentWidth")); | ||||
| 
 | ||||
|         this.$widget.addClass(note.getCssClass()); | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										323
									
								
								src/public/app/widgets/type_widgets/geo_map.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										323
									
								
								src/public/app/widgets/type_widgets/geo_map.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,323 @@ | ||||
| import { Marker, type LatLng, type LeafletMouseEvent } from "leaflet"; | ||||
| import type FNote from "../../entities/fnote.js"; | ||||
| import GeoMapWidget, { type InitCallback, type Leaflet } from "../geo_map.js"; | ||||
| import TypeWidget from "./type_widget.js" | ||||
| import server from "../../services/server.js"; | ||||
| import toastService from "../../services/toast.js"; | ||||
| import dialogService from "../../services/dialog.js"; | ||||
| import type { EventData } from "../../components/app_context.js"; | ||||
| import { t } from "../../services/i18n.js"; | ||||
| import attributes from "../../services/attributes.js"; | ||||
| import asset_path from "../../../../services/asset_path.js"; | ||||
| import openContextMenu from "./geo_map_context_menu.js"; | ||||
| import link from "../../services/link.js"; | ||||
| import note_tooltip from "../../services/note_tooltip.js"; | ||||
| 
 | ||||
| const TPL = `\ | ||||
| <div class="note-detail-geo-map note-detail-printable"> | ||||
|     <style> | ||||
|         .leaflet-pane { | ||||
|             z-index: 1; | ||||
|         } | ||||
| 
 | ||||
|         .geo-map-container.placing-note { | ||||
|             cursor: crosshair; | ||||
|         } | ||||
| 
 | ||||
|         .geo-map-container .marker-pin { | ||||
|             position: relative; | ||||
|         } | ||||
| 
 | ||||
|         .geo-map-container .leaflet-div-icon { | ||||
|             position: relative; | ||||
|             background: transparent; | ||||
|             border: 0; | ||||
|             overflow: visible; | ||||
|         } | ||||
| 
 | ||||
|         .geo-map-container .leaflet-div-icon .icon-shadow { | ||||
|             position: absolute; | ||||
|             top: 0; | ||||
|             left: 0; | ||||
|             z-index: -1; | ||||
|         } | ||||
| 
 | ||||
|         .geo-map-container .leaflet-div-icon .bx { | ||||
|             position: absolute; | ||||
|             top: 3px; | ||||
|             left: 2px; | ||||
|             background-color: white; | ||||
|             color: black; | ||||
|             padding: 2px; | ||||
|             border-radius: 50%; | ||||
|             font-size: 17px; | ||||
|         } | ||||
| 
 | ||||
|         .geo-map-container .leaflet-div-icon .title-label { | ||||
|             display: block; | ||||
|             position: absolute; | ||||
|             top: 100%; | ||||
|             left: 50%; | ||||
|             transform: translateX(-50%); | ||||
|             font-size: 0.75rem; | ||||
|             height: 1rem; | ||||
|             color: black; | ||||
|             width: 100px; | ||||
|             text-align: center; | ||||
|             text-overflow: ellipsis; | ||||
|             text-shadow: -1px -1px 0 white, 1px -1px 0 white, -1px 1px 0 white, 1px 1px 0 white; | ||||
|             white-space: no-wrap; | ||||
|             overflow: hidden; | ||||
|         } | ||||
|     </style> | ||||
| </div>`;
 | ||||
| 
 | ||||
| const LOCATION_ATTRIBUTE = "geolocation"; | ||||
| const CHILD_NOTE_ICON = "bx bx-pin"; | ||||
| const DEFAULT_COORDINATES: [ number, number ] = [ 3.878638227135724, 446.6630455551659 ]; | ||||
| const DEFAULT_ZOOM = 2; | ||||
| 
 | ||||
| interface MapData { | ||||
|     view?: { | ||||
|         center?: LatLng | [ number, number ]; | ||||
|         zoom?: number; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // TODO: Deduplicate
 | ||||
| interface CreateChildResponse { | ||||
|     note: { | ||||
|         noteId: string; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| type MarkerData = Record<string, Marker>; | ||||
| 
 | ||||
| enum State { | ||||
|     Normal, | ||||
|     NewNote | ||||
| } | ||||
| 
 | ||||
| export default class GeoMapTypeWidget extends TypeWidget { | ||||
| 
 | ||||
|     private geoMapWidget: GeoMapWidget; | ||||
|     private _state: State; | ||||
|     private L!: Leaflet; | ||||
|     private currentMarkerData: MarkerData; | ||||
| 
 | ||||
|     static getType() { | ||||
|         return "geoMap"; | ||||
|     } | ||||
| 
 | ||||
|     constructor() { | ||||
|         super(); | ||||
| 
 | ||||
|         this.geoMapWidget = new GeoMapWidget("type", (L: Leaflet) => this.#onMapInitialized(L)); | ||||
|         this.currentMarkerData = {}; | ||||
|         this._state = State.Normal; | ||||
| 
 | ||||
|         this.child(this.geoMapWidget); | ||||
|     } | ||||
| 
 | ||||
|     doRender() { | ||||
|         this.$widget = $(TPL); | ||||
|         this.$widget.append(this.geoMapWidget.render()); | ||||
| 
 | ||||
|         super.doRender(); | ||||
|     } | ||||
| 
 | ||||
|     async #onMapInitialized(L: Leaflet) { | ||||
|         this.L = L; | ||||
|         const map = this.geoMapWidget.map; | ||||
|         if (!map) { | ||||
|             throw new Error(t("geo-map.unable-to-load-map")); | ||||
|         } | ||||
| 
 | ||||
|         if (!this.note) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const blob = await this.note.getBlob(); | ||||
| 
 | ||||
|         let parsedContent: MapData = {}; | ||||
|         if (blob && blob.content) { | ||||
|             parsedContent = JSON.parse(blob.content); | ||||
|         } | ||||
| 
 | ||||
|         // Restore viewport position & zoom
 | ||||
|         const center = parsedContent.view?.center ?? DEFAULT_COORDINATES; | ||||
|         const zoom = parsedContent.view?.zoom ?? DEFAULT_ZOOM; | ||||
|         map.setView(center, zoom); | ||||
| 
 | ||||
|         // Restore markers.
 | ||||
|         await this.#reloadMarkers(); | ||||
| 
 | ||||
|         const updateFn = () => this.spacedUpdate.scheduleUpdate(); | ||||
|         map.on("moveend", updateFn); | ||||
|         map.on("zoomend", updateFn); | ||||
|         map.on("click", (e) => this.#onMapClicked(e)); | ||||
|     } | ||||
| 
 | ||||
|     async #reloadMarkers() { | ||||
|         const map = this.geoMapWidget.map; | ||||
| 
 | ||||
|         if (!this.note || !map) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         // Delete all existing markers
 | ||||
|         for (const marker of Object.values(this.currentMarkerData)) { | ||||
|             marker.remove(); | ||||
|         } | ||||
| 
 | ||||
|         // Add the new markers.
 | ||||
|         this.currentMarkerData = {}; | ||||
|         const childNotes = await this.note.getChildNotes(); | ||||
|         const L = this.L; | ||||
|         for (const childNote of childNotes) { | ||||
|             const latLng = childNote.getAttributeValue("label", LOCATION_ATTRIBUTE); | ||||
|             if (!latLng) { | ||||
|                 continue; | ||||
|             } | ||||
| 
 | ||||
|             const [ lat, lng ] = latLng.split(",", 2).map((el) => parseFloat(el)); | ||||
|             const icon = L.divIcon({ | ||||
|                 html: `\ | ||||
|                     <img class="icon" src="${asset_path}/node_modules/leaflet/dist/images/marker-icon.png" /> | ||||
|                     <img class="icon-shadow" src="${asset_path}/node_modules/leaflet/dist/images/marker-shadow.png" /> | ||||
|                     <span class="bx ${childNote.getIcon()}"></span> | ||||
|                     <span class="title-label">${childNote.title}</span>`,
 | ||||
|                 iconSize: [ 25, 41 ], | ||||
|                 iconAnchor: [ 12, 41 ] | ||||
|             }) | ||||
| 
 | ||||
|             const marker = L.marker(L.latLng(lat, lng), { | ||||
|                 icon, | ||||
|                 draggable: true, | ||||
|                 autoPan: true, | ||||
|                 autoPanSpeed: 5, | ||||
|             }) | ||||
|                 .addTo(map) | ||||
|                 .on("moveend", e => { | ||||
|                     this.moveMarker(childNote.noteId, (e.target as Marker).getLatLng()); | ||||
|                 }); | ||||
| 
 | ||||
|             marker.on("contextmenu", (e) => { | ||||
|                 openContextMenu(childNote.noteId, e.originalEvent); | ||||
|             }); | ||||
| 
 | ||||
|             const el = marker.getElement(); | ||||
|             if (el) { | ||||
|                 const $el = $(el); | ||||
|                 $el.attr("data-href", `#${childNote.noteId}`); | ||||
|                 note_tooltip.setupElementTooltip($($el)); | ||||
|             } | ||||
| 
 | ||||
|             this.currentMarkerData[childNote.noteId] = marker; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     #changeState(newState: State) { | ||||
|         this._state = newState; | ||||
|         this.geoMapWidget.$container.toggleClass("placing-note", newState === State.NewNote); | ||||
|     } | ||||
| 
 | ||||
|     async #onMapClicked(e: LeafletMouseEvent) { | ||||
|         if (this._state !== State.NewNote) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         toastService.closePersistent("geo-new-note"); | ||||
|         const title = await dialogService.prompt({ message: t("relation_map.enter_title_of_new_note"), defaultValue: t("relation_map.default_new_note_title") }); | ||||
| 
 | ||||
|         if (title?.trim()) { | ||||
|             const { note } = await server.post<CreateChildResponse>(`notes/${this.noteId}/children?target=into`, { | ||||
|                 title, | ||||
|                 content: "", | ||||
|                 type: "text" | ||||
|             }); | ||||
|             attributes.setLabel(note.noteId, "iconClass", CHILD_NOTE_ICON); | ||||
|             this.moveMarker(note.noteId, e.latlng); | ||||
|         } | ||||
| 
 | ||||
|         this.#changeState(State.Normal); | ||||
|     } | ||||
| 
 | ||||
|     async moveMarker(noteId: string, latLng: LatLng | null) { | ||||
|         const value = (latLng ? [latLng.lat, latLng.lng].join(",") : ""); | ||||
|         await attributes.setLabel(noteId, LOCATION_ATTRIBUTE, value); | ||||
|     } | ||||
| 
 | ||||
|     getData(): any { | ||||
|         const map = this.geoMapWidget.map; | ||||
|         if (!map) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const data: MapData = { | ||||
|             view: { | ||||
|                 center: map.getBounds().getCenter(), | ||||
|                 zoom: map.getZoom() | ||||
|             } | ||||
|         }; | ||||
| 
 | ||||
|         return { | ||||
|             content: JSON.stringify(data) | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     async geoMapCreateChildNoteEvent({ ntxId }: EventData<"geoMapCreateChildNote">) { | ||||
|         if (!this.isNoteContext(ntxId)) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         toastService.showPersistent({ | ||||
|             icon: "plus", | ||||
|             id: "geo-new-note", | ||||
|             title: "New note", | ||||
|             message: t("geo-map.create-child-note-instruction") | ||||
|         }); | ||||
| 
 | ||||
|         this.#changeState(State.NewNote); | ||||
| 
 | ||||
|         const globalKeyListener: (this: Window, ev: KeyboardEvent) => any = (e) => { | ||||
|             if (e.key !== "Escape") { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             this.#changeState(State.Normal); | ||||
| 
 | ||||
|             window.removeEventListener("keydown", globalKeyListener); | ||||
|             toastService.closePersistent("geo-new-note"); | ||||
|         }; | ||||
|         window.addEventListener("keydown", globalKeyListener); | ||||
|     } | ||||
| 
 | ||||
|     async doRefresh(note: FNote) { | ||||
|         await this.geoMapWidget.refresh(); | ||||
|         await this.#reloadMarkers(); | ||||
|     } | ||||
| 
 | ||||
|     entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { | ||||
|         const attributeRows = loadResults.getAttributeRows(); | ||||
|         if (attributeRows.find((at) => at.name === LOCATION_ATTRIBUTE)) { | ||||
|             this.#reloadMarkers(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     openGeoLocationEvent({ noteId, event }: EventData<"openGeoLocation">) { | ||||
|         const marker = this.currentMarkerData[noteId]; | ||||
|         if (!marker) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const latLng = this.currentMarkerData[noteId].getLatLng(); | ||||
|         const url = `geo:${latLng.lat},${latLng.lng}`; | ||||
|         link.goToLinkExt(event, url); | ||||
|     } | ||||
| 
 | ||||
|     deleteFromMapEvent({ noteId }: EventData<"deleteFromMap">) { | ||||
|         this.moveMarker(noteId, null); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										32
									
								
								src/public/app/widgets/type_widgets/geo_map_context_menu.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/public/app/widgets/type_widgets/geo_map_context_menu.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,32 @@ | ||||
| import appContext from "../../components/app_context.js"; | ||||
| import type { ContextMenuEvent } from "../../menus/context_menu.js"; | ||||
| import contextMenu from "../../menus/context_menu.js"; | ||||
| import linkContextMenu from "../../menus/link_context_menu.js"; | ||||
| import { t } from "../../services/i18n.js"; | ||||
| 
 | ||||
| export default function openContextMenu(noteId: string, e: ContextMenuEvent) { | ||||
|     contextMenu.show({ | ||||
|         x: e.pageX, | ||||
|         y: e.pageY, | ||||
|         items: [ | ||||
|             ...linkContextMenu.getItems(), | ||||
|             { title: t("geo-map-context.open-location"), command: "openGeoLocation", uiIcon: "bx bx-map-alt" }, | ||||
|             { title: "----" }, | ||||
|             { title: t("geo-map-context.remove-from-map"), command: "deleteFromMap", uiIcon: "bx bx-trash" } | ||||
|         ], | ||||
|         selectMenuItemHandler: ({ command }, e) => { | ||||
|             if (command === "deleteFromMap") { | ||||
|                 appContext.triggerCommand(command, { noteId }); | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             if (command === "openGeoLocation") { | ||||
|                 appContext.triggerCommand(command, { noteId, event: e }); | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             // Pass the events to the link context menu
 | ||||
|             linkContextMenu.handleLinkContextMenuItem(command, noteId); | ||||
|         } | ||||
|     }); | ||||
| } | ||||
| @ -1409,7 +1409,8 @@ | ||||
|     "launcher": "Launcher", | ||||
|     "doc": "Doc", | ||||
|     "widget": "Widget", | ||||
|     "confirm-change": "It is not recommended to change note type when note content is not empty. Do you want to continue anyway?" | ||||
|     "confirm-change": "It is not recommended to change note type when note content is not empty. Do you want to continue anyway?", | ||||
|     "geo-map": "Geo Map (beta)" | ||||
|   }, | ||||
|   "protect_note": { | ||||
|     "toggle-on": "Protect the note", | ||||
| @ -1629,5 +1630,14 @@ | ||||
|   }, | ||||
|   "note_tooltip": { | ||||
|     "note-has-been-deleted": "Note has been deleted." | ||||
|   }, | ||||
|   "geo-map": { | ||||
|     "create-child-note-title": "Create a new child note and add it to the map", | ||||
|     "create-child-note-instruction": "Click on the map to create a new note at that location or press Escape to dismiss.", | ||||
|     "unable-to-load-map": "Unable to load map." | ||||
|   }, | ||||
|   "geo-map-context": { | ||||
|     "open-location": "Open location", | ||||
|     "remove-from-map": "Remove from map" | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -1379,7 +1379,8 @@ | ||||
|     "image": "Imagine", | ||||
|     "launcher": "Scurtătură", | ||||
|     "widget": "Widget", | ||||
|     "confirm-change": "Nu se recomandă schimbarea tipului notiței atunci când ea are un conținut. Procedați oricum?" | ||||
|     "confirm-change": "Nu se recomandă schimbarea tipului notiței atunci când ea are un conținut. Procedați oricum?", | ||||
|     "geo-map": "Hartă geografică (beta)" | ||||
|   }, | ||||
|   "protect_note": { | ||||
|     "toggle-off": "Deprotejează notița", | ||||
| @ -1633,5 +1634,13 @@ | ||||
|   "notes": { | ||||
|     "duplicate-note-suffix": "(dupl.)", | ||||
|     "duplicate-note-title": "{{ noteTitle }} {{ duplicateNoteSuffix }}" | ||||
|   }, | ||||
|   "geo-map-context": { | ||||
|     "open-location": "Deschide locația", | ||||
|     "remove-from-map": "Înlătură de pe hartă" | ||||
|   }, | ||||
|   "geo-map": { | ||||
|     "create-child-note-title": "Crează o notiță nouă și adaug-o pe hartă", | ||||
|     "unable-to-load-map": "Nu s-a putut încărca harta." | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -105,6 +105,8 @@ async function register(app: express.Application) { | ||||
|     app.use(`/${assetPath}/node_modules/mind-elixir/dist/`, persistentCacheStatic(path.join(srcRoot, "..", "node_modules/mind-elixir/dist/"))); | ||||
|     app.use(`/${assetPath}/node_modules/@mind-elixir/node-menu/dist/`, persistentCacheStatic(path.join(srcRoot, "..", "node_modules/@mind-elixir/node-menu/dist/"))); | ||||
|     app.use(`/${assetPath}/node_modules/@highlightjs/cdn-assets/`, persistentCacheStatic(path.join(srcRoot, "..", "node_modules/@highlightjs/cdn-assets/"))); | ||||
| 
 | ||||
|     app.use(`/${assetPath}/node_modules/leaflet/dist/`, persistentCacheStatic(path.join(srcRoot, "..", "node_modules/leaflet/dist/"))); | ||||
| } | ||||
| 
 | ||||
| export default { | ||||
|  | ||||
| @ -14,7 +14,8 @@ const noteTypes = [ | ||||
|     { type: "launcher", defaultMime: "" }, | ||||
|     { type: "doc", defaultMime: "" }, | ||||
|     { type: "contentWidget", defaultMime: "" }, | ||||
|     { type: "mindMap", defaultMime: "application/json" } | ||||
|     { type: "mindMap", defaultMime: "application/json" }, | ||||
|     { type: "geoMap", defaultMime: "application/json" } | ||||
| ]; | ||||
| 
 | ||||
| function getDefaultMimeForNoteType(typeName: string) { | ||||
|  | ||||
| @ -248,5 +248,8 @@ | ||||
|   "backend_log": { | ||||
|     "log-does-not-exist": "Fișierul de loguri de backend „{{ fileName }}” nu există (încă).", | ||||
|     "reading-log-failed": "Nu s-a putut citi fișierul de loguri de backend „{{fileName}}”." | ||||
|   }, | ||||
|   "geo-map": { | ||||
|     "create-child-note-instruction": "Clic pe hartă pentru a crea o nouă notiță la acea poziție sau apăsați Escape pentru a renunța." | ||||
|   } | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Elian Doran
						Elian Doran