mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-08-10 10:22:29 +08: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/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,36 +1,44 @@
|
|||||||
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(),
|
||||||
{ title: t("link_context_menu.open_note_in_new_tab"), command: "openNoteInNewTab", uiIcon: "bx bx-link-external" },
|
selectMenuItemHandler: ({ command }) => handleLinkContextMenuItem(command, notePath, viewScope, hoistedNoteId)
|
||||||
{ 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 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user