mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 18:36:30 +01:00 
			
		
		
		
	Merge pull request #1017 from TriliumNext/feature/map_note_type
Map note type
This commit is contained in:
		| @@ -100,7 +100,8 @@ const copy = async () => { | |||||||
|         "node_modules/codemirror/keymap/", |         "node_modules/codemirror/keymap/", | ||||||
|         "node_modules/mind-elixir/dist/", |         "node_modules/mind-elixir/dist/", | ||||||
|         "node_modules/@highlightjs/cdn-assets/languages", |         "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) { |     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", |         "@mermaid-js/layout-elk": "0.1.7", | ||||||
|         "@mind-elixir/node-menu": "1.0.3", |         "@mind-elixir/node-menu": "1.0.3", | ||||||
|         "@triliumnext/express-partial-content": "1.0.1", |         "@triliumnext/express-partial-content": "1.0.1", | ||||||
|  |         "@types/leaflet": "1.9.16", | ||||||
|         "@types/react-dom": "18.3.5", |         "@types/react-dom": "18.3.5", | ||||||
|         "archiver": "7.0.1", |         "archiver": "7.0.1", | ||||||
|         "async-mutex": "0.5.0", |         "async-mutex": "0.5.0", | ||||||
| @@ -67,6 +68,7 @@ | |||||||
|         "jsplumb": "2.15.6", |         "jsplumb": "2.15.6", | ||||||
|         "katex": "0.16.21", |         "katex": "0.16.21", | ||||||
|         "knockout": "3.5.1", |         "knockout": "3.5.1", | ||||||
|  |         "leaflet": "1.9.4", | ||||||
|         "mark.js": "8.11.1", |         "mark.js": "8.11.1", | ||||||
|         "marked": "15.0.6", |         "marked": "15.0.6", | ||||||
|         "mermaid": "11.4.1", |         "mermaid": "11.4.1", | ||||||
| @@ -3847,6 +3849,15 @@ | |||||||
|         "@types/node": "*" |         "@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": { |     "node_modules/@types/linkify-it": { | ||||||
|       "version": "5.0.0", |       "version": "5.0.0", | ||||||
|       "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", |       "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", | ||||||
| @@ -11437,6 +11448,12 @@ | |||||||
|         "safe-buffer": "~5.1.0" |         "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": { |     "node_modules/limiter": { | ||||||
|       "version": "1.1.5", |       "version": "1.1.5", | ||||||
|       "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", |       "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", | ||||||
|   | |||||||
| @@ -61,6 +61,7 @@ | |||||||
|     "@mermaid-js/layout-elk": "0.1.7", |     "@mermaid-js/layout-elk": "0.1.7", | ||||||
|     "@mind-elixir/node-menu": "1.0.3", |     "@mind-elixir/node-menu": "1.0.3", | ||||||
|     "@triliumnext/express-partial-content": "1.0.1", |     "@triliumnext/express-partial-content": "1.0.1", | ||||||
|  |     "@types/leaflet": "1.9.16", | ||||||
|     "@types/react-dom": "18.3.5", |     "@types/react-dom": "18.3.5", | ||||||
|     "archiver": "7.0.1", |     "archiver": "7.0.1", | ||||||
|     "async-mutex": "0.5.0", |     "async-mutex": "0.5.0", | ||||||
| @@ -112,6 +113,7 @@ | |||||||
|     "jsplumb": "2.15.6", |     "jsplumb": "2.15.6", | ||||||
|     "katex": "0.16.21", |     "katex": "0.16.21", | ||||||
|     "knockout": "3.5.1", |     "knockout": "3.5.1", | ||||||
|  |     "leaflet": "1.9.4", | ||||||
|     "mark.js": "8.11.1", |     "mark.js": "8.11.1", | ||||||
|     "marked": "15.0.6", |     "marked": "15.0.6", | ||||||
|     "mermaid": "11.4.1", |     "mermaid": "11.4.1", | ||||||
|   | |||||||
| @@ -116,7 +116,8 @@ export const ALLOWED_NOTE_TYPES = [ | |||||||
|     "book", |     "book", | ||||||
|     "webView", |     "webView", | ||||||
|     "code", |     "code", | ||||||
|     "mindMap" |     "mindMap", | ||||||
|  |     "geoMap" | ||||||
| ] as const; | ] as const; | ||||||
| export type NoteType = (typeof ALLOWED_NOTE_TYPES)[number]; | 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 { Attribute } from "../services/attribute_parser.js"; | ||||||
| import type NoteTreeWidget from "../widgets/note_tree.js"; | import type NoteTreeWidget from "../widgets/note_tree.js"; | ||||||
| import type { default as NoteContext, GetTextEditorCallback } from "./note_context.js"; | import type { default as NoteContext, GetTextEditorCallback } from "./note_context.js"; | ||||||
|  | import type { ContextMenuEvent } from "../menus/context_menu.js"; | ||||||
|  |  | ||||||
| interface Layout { | interface Layout { | ||||||
|     getRootWidget: (appContext: AppContext) => RootWidget; |     getRootWidget: (appContext: AppContext) => RootWidget; | ||||||
| @@ -69,6 +70,7 @@ export interface ExecuteCommandData extends CommandData { | |||||||
|  */ |  */ | ||||||
| export type CommandMappings = { | export type CommandMappings = { | ||||||
|     "api-log-messages": CommandData; |     "api-log-messages": CommandData; | ||||||
|  |     focusTree: CommandData, | ||||||
|     focusOnDetail: Required<CommandData>; |     focusOnDetail: Required<CommandData>; | ||||||
|     focusOnSearchDefinition: Required<CommandData>; |     focusOnSearchDefinition: Required<CommandData>; | ||||||
|     searchNotes: CommandData & { |     searchNotes: CommandData & { | ||||||
| @@ -193,6 +195,10 @@ export type CommandMappings = { | |||||||
|     setZoomFactorAndSave: { |     setZoomFactorAndSave: { | ||||||
|         zoomFactor: string; |         zoomFactor: string; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     // Geomap | ||||||
|  |     deleteFromMap: { noteId: string }, | ||||||
|  |     openGeoLocation: { noteId: string, event: JQuery.MouseDownEvent } | ||||||
| }; | }; | ||||||
|  |  | ||||||
| type EventMappings = { | type EventMappings = { | ||||||
| @@ -227,9 +233,12 @@ type EventMappings = { | |||||||
|     activeContextChanged: { |     activeContextChanged: { | ||||||
|         noteContext: NoteContext; |         noteContext: NoteContext; | ||||||
|     }; |     }; | ||||||
|  |     beforeNoteSwitch: { | ||||||
|  |         noteContext: NoteContext; | ||||||
|  |     }; | ||||||
|     noteSwitched: { |     noteSwitched: { | ||||||
|         noteContext: NoteContext; |         noteContext: NoteContext; | ||||||
|         notePath: string; |         notePath: string | null; | ||||||
|     }; |     }; | ||||||
|     noteSwitchedAndActivatedEvent: { |     noteSwitchedAndActivatedEvent: { | ||||||
|         noteContext: NoteContext; |         noteContext: NoteContext; | ||||||
| @@ -248,12 +257,16 @@ type EventMappings = { | |||||||
|         noteId: string; |         noteId: string; | ||||||
|     }; |     }; | ||||||
|     hoistedNoteChanged: { |     hoistedNoteChanged: { | ||||||
|         ntxId: string; |         noteId: string; | ||||||
|  |         ntxId: string | null; | ||||||
|     }; |     }; | ||||||
|     contextsReopenedEvent: { |     contextsReopenedEvent: { | ||||||
|         mainNtxId: string; |         mainNtxId: string; | ||||||
|         tabPosition: number; |         tabPosition: number; | ||||||
|     }; |     }; | ||||||
|  |     noteDetailRefreshed: { | ||||||
|  |         ntxId?: string | null; | ||||||
|  |     }; | ||||||
|     noteContextReorderEvent: { |     noteContextReorderEvent: { | ||||||
|         oldMainNtxId: string; |         oldMainNtxId: string; | ||||||
|         newMainNtxId: string; |         newMainNtxId: string; | ||||||
| @@ -266,7 +279,13 @@ type EventMappings = { | |||||||
|     }; |     }; | ||||||
|     exportSvg: { |     exportSvg: { | ||||||
|         ntxId: string; |         ntxId: string; | ||||||
|     } |     }; | ||||||
|  |     geoMapCreateChildNote: { | ||||||
|  |         ntxId: string | null | undefined; // TODO: deduplicate ntxId | ||||||
|  |     }; | ||||||
|  |     tabReorder: { | ||||||
|  |         ntxIdsInOrder: string[] | ||||||
|  |     }; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export type EventListener<T extends EventNames> = { | 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); |         return this.parent?.triggerEvent(name, data); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -27,7 +27,8 @@ const NOTE_TYPE_ICONS = { | |||||||
|     launcher: "bx bx-link", |     launcher: "bx bx-link", | ||||||
|     doc: "bx bxs-file-doc", |     doc: "bx bxs-file-doc", | ||||||
|     contentWidget: "bx bxs-widget", |     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 |  * end user. Those types should be used only for checking against, they are | ||||||
|  * not for direct use. |  * 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 { | interface NotePathRecord { | ||||||
|     isArchived: boolean; |     isArchived: boolean; | ||||||
|   | |||||||
| @@ -85,6 +85,7 @@ import ScrollPaddingWidget from "../widgets/scroll_padding.js"; | |||||||
| import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js"; | import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js"; | ||||||
| import options from "../services/options.js"; | import options from "../services/options.js"; | ||||||
| import utils from "../services/utils.js"; | import utils from "../services/utils.js"; | ||||||
|  | import GeoMapButtons from "../widgets/floating_buttons/geo_map_button.js"; | ||||||
|  |  | ||||||
| export default class DesktopLayout { | export default class DesktopLayout { | ||||||
|     constructor(customWidgets) { |     constructor(customWidgets) { | ||||||
| @@ -200,6 +201,7 @@ export default class DesktopLayout { | |||||||
|                                                                 .child(new ShowHighlightsListWidgetButton()) |                                                                 .child(new ShowHighlightsListWidgetButton()) | ||||||
|                                                                 .child(new CodeButtonsWidget()) |                                                                 .child(new CodeButtonsWidget()) | ||||||
|                                                                 .child(new RelationMapButtons()) |                                                                 .child(new RelationMapButtons()) | ||||||
|  |                                                                 .child(new GeoMapButtons()) | ||||||
|                                                                 .child(new CopyImageReferenceButton()) |                                                                 .child(new CopyImageReferenceButton()) | ||||||
|                                                                 .child(new SvgExportButton()) |                                                                 .child(new SvgExportButton()) | ||||||
|                                                                 .child(new BacklinksWidget()) |                                                                 .child(new BacklinksWidget()) | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| import type { CommandNames } from "../components/app_context.js"; | import type { CommandNames } from "../components/app_context.js"; | ||||||
| import keyboardActionService from "../services/keyboard_actions.js"; | import keyboardActionService from "../services/keyboard_actions.js"; | ||||||
|  | import note_tooltip from "../services/note_tooltip.js"; | ||||||
| import utils from "../services/utils.js"; | import utils from "../services/utils.js"; | ||||||
|  |  | ||||||
| interface ContextMenuOptions<T extends CommandNames> { | 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 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 MenuHandler<T extends CommandNames> = (item: MenuCommandItem<T>, e: JQuery.MouseDownEvent<HTMLElement, undefined, HTMLElement, HTMLElement>) => void; | ||||||
|  | export type ContextMenuEvent = PointerEvent | MouseEvent | JQuery.ContextMenuEvent; | ||||||
|  |  | ||||||
| class ContextMenu { | class ContextMenu { | ||||||
|     private $widget: JQuery<HTMLElement>; |     private $widget: JQuery<HTMLElement>; | ||||||
| @@ -56,6 +58,8 @@ class ContextMenu { | |||||||
|     async show<T extends CommandNames>(options: ContextMenuOptions<T>) { |     async show<T extends CommandNames>(options: ContextMenuOptions<T>) { | ||||||
|         this.options = options; |         this.options = options; | ||||||
|  |  | ||||||
|  |         note_tooltip.dismissAllTooltips(); | ||||||
|  |  | ||||||
|         if (this.$widget.hasClass("show")) { |         if (this.$widget.hasClass("show")) { | ||||||
|             // The menu is already visible. Hide the menu then open it again |             // The menu is already visible. Hide the menu then open it again | ||||||
|             // at the new location to re-trigger the opening animation. |             // at the new location to re-trigger the opening animation. | ||||||
|   | |||||||
| @@ -1,18 +1,26 @@ | |||||||
| import { t } from "../services/i18n.js"; | import { t } from "../services/i18n.js"; | ||||||
| import contextMenu from "./context_menu.js"; | import contextMenu, { type ContextMenuEvent, type MenuItem } from "./context_menu.js"; | ||||||
| import appContext from "../components/app_context.js"; | import appContext, { type CommandNames } from "../components/app_context.js"; | ||||||
| import type { ViewScope } from "../services/link.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({ |     contextMenu.show({ | ||||||
|         x: e.pageX, |         x: e.pageX, | ||||||
|         y: e.pageY, |         y: e.pageY, | ||||||
|         items: [ |         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_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_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" } |         { title: t("link_context_menu.open_note_in_new_window"), command: "openNoteInNewWindow", uiIcon: "bx bx-window-open" } | ||||||
|         ], |     ]; | ||||||
|         selectMenuItemHandler: ({ command }) => { | } | ||||||
|  |  | ||||||
|  | function handleLinkContextMenuItem(command: string | undefined, notePath: string, viewScope = {}, hoistedNoteId: string | null = null) { | ||||||
|     if (!hoistedNoteId) { |     if (!hoistedNoteId) { | ||||||
|         hoistedNoteId = appContext.tabManager.getActiveContext().hoistedNoteId; |         hoistedNoteId = appContext.tabManager.getActiveContext().hoistedNoteId; | ||||||
|     } |     } | ||||||
| @@ -27,10 +35,10 @@ function openContextMenu(notePath: string, e: PointerEvent | MouseEvent | JQuery | |||||||
|     } else if (command === "openNoteInNewWindow") { |     } else if (command === "openNoteInNewWindow") { | ||||||
|         appContext.triggerCommand("openInWindow", { notePath, hoistedNoteId, viewScope }); |         appContext.triggerCommand("openInWindow", { notePath, hoistedNoteId, viewScope }); | ||||||
|     } |     } | ||||||
|         } |  | ||||||
|     }); |  | ||||||
| } | } | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|  |     getItems, | ||||||
|  |     handleLinkContextMenuItem, | ||||||
|     openContextMenu |     openContextMenu | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -106,6 +106,10 @@ const HIGHLIGHT_JS: Library = { | |||||||
|     } |     } | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | const LEAFLET: Library = { | ||||||
|  |     css: [ "node_modules/leaflet/dist/leaflet.css" ], | ||||||
|  | } | ||||||
|  |  | ||||||
| async function requireLibrary(library: Library) { | async function requireLibrary(library: Library) { | ||||||
|     if (library.css) { |     if (library.css) { | ||||||
|         library.css.map((cssUrl) => requireCss(cssUrl)); |         library.css.map((cssUrl) => requireCss(cssUrl)); | ||||||
| @@ -196,5 +200,6 @@ export default { | |||||||
|     MERMAID, |     MERMAID, | ||||||
|     MARKJS, |     MARKJS, | ||||||
|     I18NEXT, |     I18NEXT, | ||||||
|     HIGHLIGHT_JS |     HIGHLIGHT_JS, | ||||||
|  |     LEAFLET | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -234,7 +234,7 @@ function goToLink(evt: MouseEvent | JQuery.ClickEvent) { | |||||||
|     return goToLinkExt(evt, hrefLink, $link); |     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:")) { |     if (hrefLink?.startsWith("data:")) { | ||||||
|         return true; |         return true; | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -18,11 +18,11 @@ function setupGlobalTooltip() { | |||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         cleanUpTooltips(); |         dismissAllTooltips(); | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
|  |  | ||||||
| function cleanUpTooltips() { | function dismissAllTooltips() { | ||||||
|     $(".note-tooltip").remove(); |     $(".note-tooltip").remove(); | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -102,12 +102,12 @@ async function mouseEnterHandler(this: HTMLElement) { | |||||||
|             customClass: linkId |             customClass: linkId | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         cleanUpTooltips(); |         dismissAllTooltips(); | ||||||
|         $(this).tooltip("show"); |         $(this).tooltip("show"); | ||||||
|  |  | ||||||
|         // Dismiss the tooltip immediately if a link was clicked inside the tooltip. |         // Dismiss the tooltip immediately if a link was clicked inside the tooltip. | ||||||
|         $(`.${tooltipClass} a`).on("click", (e) => { |         $(`.${tooltipClass} a`).on("click", (e) => { | ||||||
|             cleanUpTooltips(); |             dismissAllTooltips(); | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         // the purpose of the code below is to: |         // the purpose of the code below is to: | ||||||
| @@ -117,7 +117,7 @@ async function mouseEnterHandler(this: HTMLElement) { | |||||||
|         const checkTooltip = () => { |         const checkTooltip = () => { | ||||||
|             if (!$(this).filter(":hover").length && !$(`.${linkId}:hover`).length) { |             if (!$(this).filter(":hover").length && !$(`.${linkId}:hover`).length) { | ||||||
|                 // cursor is neither over the link nor over the tooltip, user likely is not interested |                 // cursor is neither over the link nor over the tooltip, user likely is not interested | ||||||
|                 cleanUpTooltips(); |                 dismissAllTooltips(); | ||||||
|             } else { |             } else { | ||||||
|                 setTimeout(checkTooltip, 1000); |                 setTimeout(checkTooltip, 1000); | ||||||
|             } |             } | ||||||
| @@ -172,5 +172,6 @@ function renderFootnote($link: JQuery<HTMLElement>, url: string) { | |||||||
|  |  | ||||||
| export default { | export default { | ||||||
|     setupGlobalTooltip, |     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.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.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.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"); |     const templateNoteIds = await server.get<string[]>("search-templates"); | ||||||
|   | |||||||
| @@ -154,7 +154,7 @@ export default class NoteActionsWidget extends NoteContextAwareWidget { | |||||||
|         this.toggleDisabled(this.$findInTextButton, ["text", "code", "book"].includes(note.type)); |         this.toggleDisabled(this.$findInTextButton, ["text", "code", "book"].includes(note.type)); | ||||||
|  |  | ||||||
|         this.toggleDisabled(this.$showAttachmentsButton, !isInOptions); |         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)); |         this.toggleDisabled(this.$printActiveNoteButton, ["text", "code"].includes(note.type)); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -22,7 +22,7 @@ export default class LeftPaneContainer extends FlexContainer<Component> { | |||||||
|             this.toggleInt(visible); |             this.toggleInt(visible); | ||||||
|  |  | ||||||
|             if (visible) { |             if (visible) { | ||||||
|                 this.triggerEvent("focusTree"); |                 this.triggerEvent("focusTree", {}); | ||||||
|             } else { |             } else { | ||||||
|                 const activeNoteContext = appContext.tabManager.getActiveContext(); |                 const activeNoteContext = appContext.tabManager.getActiveContext(); | ||||||
|                 this.triggerEvent("focusOnDetail", { ntxId: activeNoteContext.ntxId }); |                 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 })); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -43,6 +43,7 @@ export default class RelationMapButtons extends NoteContextAwareWidget { | |||||||
|         this.$zoomOutButton = this.$widget.find(".relation-map-zoom-out"); |         this.$zoomOutButton = this.$widget.find(".relation-map-zoom-out"); | ||||||
|         this.$resetPanZoomButton = this.$widget.find(".relation-map-reset-pan-zoom"); |         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.$createChildNote.on("click", () => this.triggerEvent("relationMapCreateChildNote", { ntxId: this.ntxId })); | ||||||
|         this.$resetPanZoomButton.on("click", () => this.triggerEvent("relationMapResetPanZoom", { 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 AttachmentDetailTypeWidget from "./type_widgets/attachment_detail.js"; | ||||||
| import MindMapWidget from "./type_widgets/mind_map.js"; | import MindMapWidget from "./type_widgets/mind_map.js"; | ||||||
| import { getStylesheetUrl, isSyntaxHighlightEnabled } from "../services/syntax_highlight.js"; | import { getStylesheetUrl, isSyntaxHighlightEnabled } from "../services/syntax_highlight.js"; | ||||||
|  | import GeoMapTypeWidget from "./type_widgets/geo_map.js"; | ||||||
|  |  | ||||||
| const TPL = ` | const TPL = ` | ||||||
| <div class="note-detail"> | <div class="note-detail"> | ||||||
| @@ -67,7 +68,8 @@ const typeWidgetClasses = { | |||||||
|     contentWidget: ContentWidgetTypeWidget, |     contentWidget: ContentWidgetTypeWidget, | ||||||
|     attachmentDetail: AttachmentDetailTypeWidget, |     attachmentDetail: AttachmentDetailTypeWidget, | ||||||
|     attachmentList: AttachmentListTypeWidget, |     attachmentList: AttachmentListTypeWidget, | ||||||
|     mindMap: MindMapWidget |     mindMap: MindMapWidget, | ||||||
|  |     geoMap: GeoMapTypeWidget | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export default class NoteDetailWidget extends NoteContextAwareWidget { | export default class NoteDetailWidget extends NoteContextAwareWidget { | ||||||
| @@ -147,7 +149,7 @@ export default class NoteDetailWidget extends NoteContextAwareWidget { | |||||||
|         // https://github.com/zadam/trilium/issues/2522 |         // https://github.com/zadam/trilium/issues/2522 | ||||||
|         this.$widget.toggleClass( |         this.$widget.toggleClass( | ||||||
|             "full-height", |             "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" |                 this.noteContext.viewScope.viewMode === "attachments" | ||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -41,7 +41,7 @@ export default class NoteWrapperWidget extends FlexContainer { | |||||||
|             return; |             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()); |         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", |     "launcher": "Launcher", | ||||||
|     "doc": "Doc", |     "doc": "Doc", | ||||||
|     "widget": "Widget", |     "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": { |   "protect_note": { | ||||||
|     "toggle-on": "Protect the note", |     "toggle-on": "Protect the note", | ||||||
| @@ -1629,5 +1630,14 @@ | |||||||
|   }, |   }, | ||||||
|   "note_tooltip": { |   "note_tooltip": { | ||||||
|     "note-has-been-deleted": "Note has been deleted." |     "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", |     "image": "Imagine", | ||||||
|     "launcher": "Scurtătură", |     "launcher": "Scurtătură", | ||||||
|     "widget": "Widget", |     "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": { |   "protect_note": { | ||||||
|     "toggle-off": "Deprotejează notița", |     "toggle-off": "Deprotejează notița", | ||||||
| @@ -1633,5 +1634,13 @@ | |||||||
|   "notes": { |   "notes": { | ||||||
|     "duplicate-note-suffix": "(dupl.)", |     "duplicate-note-suffix": "(dupl.)", | ||||||
|     "duplicate-note-title": "{{ noteTitle }} {{ duplicateNoteSuffix }}" |     "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/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/@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/@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 { | export default { | ||||||
|   | |||||||
| @@ -14,7 +14,8 @@ const noteTypes = [ | |||||||
|     { type: "launcher", defaultMime: "" }, |     { type: "launcher", defaultMime: "" }, | ||||||
|     { type: "doc", defaultMime: "" }, |     { type: "doc", defaultMime: "" }, | ||||||
|     { type: "contentWidget", defaultMime: "" }, |     { type: "contentWidget", defaultMime: "" }, | ||||||
|     { type: "mindMap", defaultMime: "application/json" } |     { type: "mindMap", defaultMime: "application/json" }, | ||||||
|  |     { type: "geoMap", defaultMime: "application/json" } | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
| function getDefaultMimeForNoteType(typeName: string) { | function getDefaultMimeForNoteType(typeName: string) { | ||||||
|   | |||||||
| @@ -248,5 +248,8 @@ | |||||||
|   "backend_log": { |   "backend_log": { | ||||||
|     "log-does-not-exist": "Fișierul de loguri de backend „{{ fileName }}” nu există (încă).", |     "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}}”." |     "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." | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user