Merge remote-tracking branch 'origin/main' into renovate/mind-elixir-5.x

This commit is contained in:
Elian Doran 2025-07-07 20:50:23 +03:00
commit f88f14c983
No known key found for this signature in database
107 changed files with 1563 additions and 1062 deletions

View File

@ -261,7 +261,6 @@ export type CommandMappings = {
// Geomap
deleteFromMap: { noteId: string };
openGeoLocation: { noteId: string; event: JQuery.MouseDownEvent };
toggleZenMode: CommandData;

View File

@ -326,7 +326,7 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
}
// Some book types must always display a note list, even if no children.
if (["calendar", "table"].includes(note.getLabelValue("viewType") ?? "")) {
if (["calendar", "table", "geoMap"].includes(note.getLabelValue("viewType") ?? "")) {
return true;
}

View File

@ -27,7 +27,6 @@ const NOTE_TYPE_ICONS = {
doc: "bx bxs-file-doc",
contentWidget: "bx bxs-widget",
mindMap: "bx bx-sitemap",
geoMap: "bx bx-map-alt",
aiChat: "bx bx-bot"
};
@ -36,7 +35,7 @@ const NOTE_TYPE_ICONS = {
* end user. Those types should be used only for checking against, they are
* not for direct use.
*/
export type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code" | "mindMap" | "geoMap" | "aiChat";
export type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code" | "mindMap" | "aiChat";
export interface NotePathRecord {
isArchived: boolean;

View File

@ -17,11 +17,17 @@ interface MenuSeparatorItem {
title: "----";
}
export interface MenuItemBadge {
title: string;
className?: string;
}
export interface MenuCommandItem<T> {
title: string;
command?: T;
type?: string;
uiIcon?: string;
badges?: MenuItemBadge[];
templateNoteId?: string;
enabled?: boolean;
handler?: MenuHandler<T>;
@ -161,6 +167,18 @@ class ContextMenu {
.append(" &nbsp; ") // some space between icon and text
.append(item.title);
if ("badges" in item && item.badges) {
for (let badge of item.badges) {
const badgeElement = $(`<span class="badge">`).text(badge.title);
if (badge.className) {
badgeElement.addClass(badge.className);
}
$link.append(badgeElement);
}
}
if ("shortcut" in item && item.shortcut) {
$link.append($("<kbd>").text(item.shortcut));
}

View File

@ -277,13 +277,21 @@ function goToLink(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent) {
return goToLinkExt(evt, hrefLink, $link);
}
function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent | React.PointerEvent<HTMLCanvasElement>, hrefLink: string | undefined, $link?: JQuery<HTMLElement> | null) {
/**
* Handles navigation to a link, which can be an internal note path (e.g., `#root/1234`) or an external URL (e.g., `https://example.com`).
*
* @param evt the event that triggered the link navigation, or `null` if the link was clicked programmatically. Used to determine if the link should be opened in a new tab/window, based on the button presses.
* @param hrefLink the link to navigate to, which can be a note path (e.g., `#root/1234`) or an external URL with any supported protocol (e.g., `https://example.com`).
* @param $link the jQuery element of the link that was clicked, used to determine if the link is an anchor link (e.g., `#fn1` or `#fnref1`) and to handle it accordingly.
* @returns `true` if the link was handled (i.e., the element was found and scrolled to), or a falsy value otherwise.
*/
function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent | React.PointerEvent<HTMLCanvasElement> | null, hrefLink: string | undefined, $link?: JQuery<HTMLElement> | null) {
if (hrefLink?.startsWith("data:")) {
return true;
}
evt.preventDefault();
evt.stopPropagation();
evt?.preventDefault();
evt?.stopPropagation();
if (hrefLink && hrefLink.startsWith("#") && !hrefLink.startsWith("#root/") && $link) {
if (handleAnchor(hrefLink, $link)) {
@ -293,14 +301,14 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent
const { notePath, viewScope } = parseNavigationStateFromUrl(hrefLink);
const ctrlKey = utils.isCtrlKey(evt);
const shiftKey = evt.shiftKey;
const isLeftClick = "which" in evt && evt.which === 1;
const isMiddleClick = "which" in evt && evt.which === 2;
const ctrlKey = evt && utils.isCtrlKey(evt);
const shiftKey = evt?.shiftKey;
const isLeftClick = !evt || ("which" in evt && evt.which === 1);
const isMiddleClick = evt && "which" in evt && evt.which === 2;
const targetIsBlank = ($link?.attr("target") === "_blank");
const openInNewTab = (isLeftClick && ctrlKey) || isMiddleClick || targetIsBlank;
const activate = (isLeftClick && ctrlKey && shiftKey) || (isMiddleClick && shiftKey);
const openInNewWindow = isLeftClick && evt.shiftKey && !ctrlKey;
const openInNewWindow = isLeftClick && evt?.shiftKey && !ctrlKey;
if (notePath) {
if (openInNewWindow) {
@ -311,7 +319,7 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent
viewScope
});
} else if (isLeftClick) {
const ntxId = $(evt.target as any)
const ntxId = $(evt?.target as any)
.closest("[data-ntx-id]")
.attr("data-ntx-id");

View File

@ -1,11 +1,12 @@
import type FNote from "../entities/fnote.js";
import CalendarView from "../widgets/view_widgets/calendar_view.js";
import GeoView from "../widgets/view_widgets/geo_view/index.js";
import ListOrGridView from "../widgets/view_widgets/list_or_grid_view.js";
import TableView from "../widgets/view_widgets/table_view/index.js";
import type { ViewModeArgs } from "../widgets/view_widgets/view_mode.js";
import type ViewMode from "../widgets/view_widgets/view_mode.js";
export type ViewTypeOptions = "list" | "grid" | "calendar" | "table";
export type ViewTypeOptions = "list" | "grid" | "calendar" | "table" | "geoMap";
export default class NoteListRenderer {
@ -26,6 +27,9 @@ export default class NoteListRenderer {
case "table":
this.viewMode = new TableView(args);
break;
case "geoMap":
this.viewMode = new GeoView(args);
break;
default:
this.viewMode = null;
}
@ -34,7 +38,7 @@ export default class NoteListRenderer {
#getViewType(parentNote: FNote): ViewTypeOptions {
const viewType = parentNote.getLabelValue("viewType");
if (!["list", "grid", "calendar", "table"].includes(viewType || "")) {
if (!["list", "grid", "calendar", "table", "geoMap"].includes(viewType || "")) {
// when not explicitly set, decide based on the note type
return parentNote.type === "search" ? "list" : "grid";
} else {

View File

@ -1,42 +1,86 @@
import server from "./server.js";
import froca from "./froca.js";
import { t } from "./i18n.js";
import type { MenuItem } from "../menus/context_menu.js";
import froca from "./froca.js";
import server from "./server.js";
import type { MenuCommandItem, MenuItem, MenuItemBadge } from "../menus/context_menu.js";
import type { NoteType } from "../entities/fnote.js";
import type { TreeCommandNames } from "../menus/tree_context_menu.js";
export interface NoteTypeMapping {
type: NoteType;
mime?: string;
title: string;
icon?: string;
/** Indicates whether this type should be marked as a newly introduced feature. */
isNew?: boolean;
/** Indicates that this note type is part of a beta feature. */
isBeta?: boolean;
/** Indicates that this note type cannot be created by the user. */
reserved?: boolean;
/** Indicates that once a note of this type is created, its type can no longer be changed. */
static?: boolean;
}
export const NOTE_TYPES: NoteTypeMapping[] = [
// The suggested note type ordering method: insert the item into the corresponding group,
// then ensure the items within the group are ordered alphabetically.
// The default note type (always the first item)
{ type: "text", mime: "text/html", title: t("note_types.text"), icon: "bx-note" },
// Text notes group
{ type: "book", mime: "", title: t("note_types.book"), icon: "bx-book" },
// Graphic notes
{ type: "canvas", mime: "application/json", title: t("note_types.canvas"), icon: "bx-pen" },
{ type: "mermaid", mime: "text/mermaid", title: t("note_types.mermaid-diagram"), icon: "bx-selection" },
// Map notes
{ type: "mindMap", mime: "application/json", title: t("note_types.mind-map"), icon: "bx-sitemap" },
{ type: "noteMap", mime: "", title: t("note_types.note-map"), icon: "bxs-network-chart", static: true },
{ type: "relationMap", mime: "application/json", title: t("note_types.relation-map"), icon: "bxs-network-chart" },
// Misc note types
{ type: "render", mime: "", title: t("note_types.render-note"), icon: "bx-extension" },
{ type: "search", title: t("note_types.saved-search"), icon: "bx-file-find", static: true },
{ type: "webView", mime: "", title: t("note_types.web-view"), icon: "bx-globe-alt" },
// Code notes
{ type: "code", mime: "text/plain", title: t("note_types.code"), icon: "bx-code" },
// Reserved types (cannot be created by the user)
{ type: "contentWidget", mime: "", title: t("note_types.widget"), reserved: true },
{ type: "doc", mime: "", title: t("note_types.doc"), reserved: true },
{ type: "file", title: t("note_types.file"), reserved: true },
{ type: "image", title: t("note_types.image"), reserved: true },
{ type: "launcher", mime: "", title: t("note_types.launcher"), reserved: true },
{ type: "aiChat", mime: "application/json", title: t("note_types.ai-chat"), reserved: true }
];
/** The maximum age in days for a template to be marked with the "New" badge */
const NEW_TEMPLATE_MAX_AGE = 3;
/** The length of a day in milliseconds. */
const DAY_LENGTH = 1000 * 60 * 60 * 24;
/** The menu item badge used to mark new note types and templates */
const NEW_BADGE: MenuItemBadge = {
title: t("note_types.new-feature"),
className: "new-note-type-badge"
};
/** The menu item badge used to mark note types that are part of a beta feature */
const BETA_BADGE = {
title: t("note_types.beta-feature")
};
const SEPARATOR = { title: "----" };
const creationDateCache = new Map<string, Date>();
let rootCreationDate: Date | undefined;
async function getNoteTypeItems(command?: TreeCommandNames) {
const items: MenuItem<TreeCommandNames>[] = [
// The suggested note type ordering method: insert the item into the corresponding group,
// then ensure the items within the group are ordered alphabetically.
// Please keep the order synced with the listing found also in aps/client/src/widgets/note_types.ts.
// The default note type (always the first item)
{ title: t("note_types.text"), command, type: "text", uiIcon: "bx bx-note" },
// Text notes group
{ title: t("note_types.book"), command, type: "book", uiIcon: "bx bx-book" },
// Graphic notes
{ title: t("note_types.canvas"), command, type: "canvas", uiIcon: "bx bx-pen" },
{ title: t("note_types.mermaid-diagram"), command, type: "mermaid", uiIcon: "bx bx-selection" },
// Map notes
{ title: t("note_types.geo-map"), command, type: "geoMap", uiIcon: "bx bx-map-alt" },
{ title: t("note_types.mind-map"), command, type: "mindMap", uiIcon: "bx bx-sitemap" },
{ title: t("note_types.note-map"), command, type: "noteMap", uiIcon: "bx bxs-network-chart" },
{ title: t("note_types.relation-map"), command, type: "relationMap", uiIcon: "bx bxs-network-chart" },
// Misc note types
{ title: t("note_types.render-note"), command, type: "render", uiIcon: "bx bx-extension" },
{ title: t("note_types.saved-search"), command, type: "search", uiIcon: "bx bx-file-find" },
{ title: t("note_types.web-view"), command, type: "webView", uiIcon: "bx bx-globe-alt" },
// Code notes
{ title: t("note_types.code"), command, type: "code", uiIcon: "bx bx-code" },
// Templates
...getBlankNoteTypes(command),
...await getBuiltInTemplates(command),
...await getUserTemplates(command)
];
@ -44,6 +88,28 @@ async function getNoteTypeItems(command?: TreeCommandNames) {
return items;
}
function getBlankNoteTypes(command): MenuItem<TreeCommandNames>[] {
return NOTE_TYPES.filter((nt) => !nt.reserved).map((nt) => {
const menuItem: MenuCommandItem<TreeCommandNames> = {
title: nt.title,
command,
type: nt.type,
uiIcon: "bx " + nt.icon,
badges: []
}
if (nt.isNew) {
menuItem.badges?.push(NEW_BADGE);
}
if (nt.isBeta) {
menuItem.badges?.push(BETA_BADGE);
}
return menuItem;
});
}
async function getUserTemplates(command?: TreeCommandNames) {
const templateNoteIds = await server.get<string[]>("search-templates");
const templateNotes = await froca.getNotes(templateNoteIds);
@ -54,14 +120,21 @@ async function getUserTemplates(command?: TreeCommandNames) {
const items: MenuItem<TreeCommandNames>[] = [
SEPARATOR
];
for (const templateNote of templateNotes) {
items.push({
const item: MenuItem<TreeCommandNames> = {
title: templateNote.title,
uiIcon: templateNote.getIcon(),
command: command,
type: templateNote.type,
templateNoteId: templateNote.noteId
});
};
if (await isNewTemplate(templateNote.noteId)) {
item.badges = [NEW_BADGE];
}
items.push(item);
}
return items;
}
@ -81,18 +154,71 @@ async function getBuiltInTemplates(command?: TreeCommandNames) {
const items: MenuItem<TreeCommandNames>[] = [
SEPARATOR
];
for (const templateNote of childNotes) {
items.push({
const item: MenuItem<TreeCommandNames> = {
title: templateNote.title,
uiIcon: templateNote.getIcon(),
command: command,
type: templateNote.type,
templateNoteId: templateNote.noteId
});
};
if (await isNewTemplate(templateNote.noteId)) {
item.badges = [NEW_BADGE];
}
items.push(item);
}
return items;
}
async function isNewTemplate(templateNoteId) {
if (rootCreationDate === undefined) {
// Retrieve the root note creation date
try {
let rootNoteInfo: any = await server.get("notes/root");
if ("dateCreated" in rootNoteInfo) {
rootCreationDate = new Date(rootNoteInfo.dateCreated);
}
} catch (ex) {
console.error(ex);
}
}
// Try to retrieve the template's creation date from the cache
let creationDate: Date | undefined = creationDateCache.get(templateNoteId);
if (creationDate === undefined) {
// The creation date isn't available in the cache, try to retrieve it from the server
try {
const noteInfo: any = await server.get("notes/" + templateNoteId);
if ("dateCreated" in noteInfo) {
creationDate = new Date(noteInfo.dateCreated);
creationDateCache.set(templateNoteId, creationDate);
}
} catch (ex) {
console.error(ex);
}
}
if (creationDate) {
if (rootCreationDate && creationDate.getTime() - rootCreationDate.getTime() < 30000) {
// Ignore templates created within 30 seconds after the root note is created.
// This is useful to prevent predefined templates from being marked
// as 'New' after setting up a new database.
return false;
}
// Determine the difference in days between now and the template's creation date
const age = (new Date().getTime() - creationDate.getTime()) / DAY_LENGTH;
// Return true if the template is at most NEW_TEMPLATE_MAX_AGE days old
return (age <= NEW_TEMPLATE_MAX_AGE);
} else {
return false;
}
}
export default {
getNoteTypeItems
};

View File

@ -192,6 +192,13 @@ samp {
font-family: var(--monospace-font-family) !important;
}
.badge {
--bs-badge-color: var(--muted-text-color);
margin-left: 8px;
background: var(--accented-background-color);
}
.input-group-text {
background-color: var(--accented-background-color) !important;
color: var(--muted-text-color) !important;

View File

@ -178,6 +178,9 @@
--alert-bar-background: #6b6b6b3b;
--badge-background-color: #ffffff1a;
--badge-text-color: var(--muted-text-color);
--promoted-attribute-card-background-color: var(--card-background-color);
--promoted-attribute-card-shadow-color: #000000b3;

View File

@ -171,6 +171,9 @@
--alert-bar-background: #32637b29;
--badge-background-color: #00000011;
--badge-text-color: var(--muted-text-color);
--promoted-attribute-card-background-color: var(--card-background-color);
--promoted-attribute-card-shadow-color: #00000033;

View File

@ -171,6 +171,16 @@ html body .dropdown-item[disabled] {
opacity: var(--menu-item-disabled-opacity);
}
/* Badges */
:root .badge {
--bs-badge-color: var(--badge-text-color);
--bs-badge-font-weight: 500;
background: var(--badge-background-color);
text-transform: uppercase;
letter-spacing: .2pt;
}
/* Menu item icon */
.dropdown-item .bx {
transform: translateY(var(--menu-item-icon-vert-offset));

View File

@ -761,7 +761,8 @@
"book_properties": "Book Properties",
"invalid_view_type": "Invalid view type '{{type}}'",
"calendar": "Calendar",
"table": "Table"
"table": "Table",
"geo-map": "Geo Map"
},
"edited_notes": {
"no_edited_notes_found": "No edited notes on this day yet...",
@ -1627,7 +1628,8 @@
"geo-map": "Geo Map",
"beta-feature": "Beta",
"ai-chat": "AI Chat",
"task-list": "Task List"
"task-list": "Task List",
"new-feature": "New"
},
"protect_note": {
"toggle-on": "Protect the note",
@ -1858,7 +1860,8 @@
},
"geo-map-context": {
"open-location": "Open location",
"remove-from-map": "Remove from map"
"remove-from-map": "Remove from map",
"add-note": "Add a marker at this location"
},
"help-button": {
"title": "Open the relevant help page"

View File

@ -189,7 +189,7 @@ export default class NoteActionsWidget extends NoteContextAwareWidget {
this.toggleDisabled(this.$findInTextButton, ["text", "code", "book", "mindMap"].includes(note.type));
this.toggleDisabled(this.$showAttachmentsButton, !isInOptions);
this.toggleDisabled(this.$showSourceButton, ["text", "code", "relationMap", "mermaid", "canvas", "mindMap", "geoMap"].includes(note.type));
this.toggleDisabled(this.$showSourceButton, ["text", "code", "relationMap", "mermaid", "canvas", "mindMap"].includes(note.type));
const canPrint = ["text", "code"].includes(note.type);
this.toggleDisabled(this.$printActiveNoteButton, canPrint);

View File

@ -154,13 +154,21 @@ export default class NoteTypeChooserDialog extends BasicWidget {
this.$noteTypeDropdown.append($('<h6 class="dropdown-header">').append(t("note_type_chooser.templates")));
} else {
const commandItem = noteType as MenuCommandItem<CommandNames>;
this.$noteTypeDropdown.append(
$('<a class="dropdown-item" tabindex="0">')
const listItem = $('<a class="dropdown-item" tabindex="0">')
.attr("data-note-type", commandItem.type || "")
.attr("data-template-note-id", commandItem.templateNoteId || "")
.append($("<span>").addClass(commandItem.uiIcon || ""))
.append(` ${noteType.title}`)
);
.append(` ${noteType.title}`);
if (commandItem.badges) {
for (let badge of commandItem.badges) {
listItem.append($(`<span class="badge">`)
.addClass(badge.className || "")
.text(badge.title));
}
}
this.$noteTypeDropdown.append(listItem);
}
}

View File

@ -23,7 +23,9 @@ const TPL = /*html*/`\
export default class GeoMapButtons extends NoteContextAwareWidget {
isEnabled() {
return super.isEnabled() && this.note?.type === "geoMap";
return super.isEnabled()
&& this.note?.getLabelValue("viewType") === "geoMap"
&& !this.note.hasLabel("readOnly");
}
doRender() {

View File

@ -17,7 +17,6 @@ export const byNoteType: Record<Exclude<NoteType, "book">, string | null> = {
contentWidget: null,
doc: null,
file: null,
geoMap: "81SGnPGMk7Xc",
image: null,
launcher: null,
mermaid: null,
@ -35,7 +34,8 @@ export const byBookType: Record<ViewTypeOptions, string | null> = {
list: null,
grid: null,
calendar: "xWbu3jpNWapp",
table: "2FvYrpmOXm29"
table: "2FvYrpmOXm29",
geoMap: "81SGnPGMk7Xc"
};
export default class ContextualHelpButton extends NoteContextAwareWidget {

View File

@ -39,10 +39,20 @@ export default class ToggleReadOnlyButton extends OnClickButtonWidget {
}
isEnabled() {
return super.isEnabled()
&& this.note?.type === "mermaid"
&& this.note?.isContentAvailable()
&& this.noteContext?.viewScope?.viewMode === "default";
if (!super.isEnabled()) {
return false;
}
if (!this?.note?.isContentAvailable()) {
return false;
}
if (this.noteContext?.viewScope?.viewMode !== "default") {
return false;
}
return this.note.type === "mermaid" ||
(this.note.getLabelValue("viewType") === "geoMap");
}
}

View File

@ -1,58 +0,0 @@
import type { Map } from "leaflet";
import L from "leaflet";
import "leaflet/dist/leaflet.css";
import NoteContextAwareWidget from "./note_context_aware_widget.js";
const TPL = /*html*/`\
<div class="geo-map-widget">
<style>
.note-detail-geo-map,
.geo-map-widget,
.geo-map-container {
height: 100%;
overflow: hidden;
}
.leaflet-top,
.leaflet-bottom {
z-index: 900;
}
</style>
<div class="geo-map-container"></div>
</div>`;
export type Leaflet = typeof L;
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");
const map = L.map(this.$container[0], {
worldCopyJump: true
});
this.map = map;
if (this.initCallback) {
this.initCallback(L);
}
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
detectRetina: true
}).addTo(map);
}
}

View File

@ -28,7 +28,6 @@ import ContentWidgetTypeWidget from "./type_widgets/content_widget.js";
import AttachmentListTypeWidget from "./type_widgets/attachment_list.js";
import AttachmentDetailTypeWidget from "./type_widgets/attachment_detail.js";
import MindMapWidget from "./type_widgets/mind_map.js";
import GeoMapTypeWidget from "./type_widgets/geo_map.js";
import utils from "../services/utils.js";
import type { NoteType } from "../entities/fnote.js";
import type TypeWidget from "./type_widgets/type_widget.js";
@ -71,7 +70,6 @@ const typeWidgetClasses = {
attachmentDetail: AttachmentDetailTypeWidget,
attachmentList: AttachmentListTypeWidget,
mindMap: MindMapWidget,
geoMap: GeoMapTypeWidget,
aiChat: AiChatTypeWidget,
// Split type editors
@ -197,7 +195,7 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
// https://github.com/zadam/trilium/issues/2522
const isBackendNote = this.noteContext?.noteId === "_backendLog";
const isSqlNote = this.mime === "text/x-sqlite;schema=trilium";
const isFullHeightNoteType = ["canvas", "webView", "noteMap", "mindMap", "geoMap", "mermaid"].includes(this.type ?? "");
const isFullHeightNoteType = ["canvas", "webView", "noteMap", "mindMap", "mermaid"].includes(this.type ?? "");
const isFullHeight = (!this.noteContext?.hasNoteList() && isFullHeightNoteType && !isSqlNote)
|| this.noteContext?.viewScope?.viewMode === "attachments"
|| isBackendNote;

View File

@ -1,7 +1,7 @@
import NoteContextAwareWidget from "./note_context_aware_widget.js";
import NoteListRenderer from "../services/note_list_renderer.js";
import type FNote from "../entities/fnote.js";
import type { CommandListener, CommandListenerData, CommandMappings, CommandNames, EventData } from "../components/app_context.js";
import type { CommandListener, CommandListenerData, CommandMappings, CommandNames, EventData, EventNames } from "../components/app_context.js";
import type ViewMode from "./view_widgets/view_mode.js";
import AttributeDetailWidget from "./attribute_widgets/attribute_detail.js";
import { Attribute } from "../services/attribute_parser.js";
@ -176,4 +176,17 @@ export default class NoteListWidget extends NoteContextAwareWidget {
return super.triggerCommand(name, data);
}
handleEventInChildren<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown[] | unknown> | null {
super.handleEventInChildren(name, data);
if (this.viewMode) {
const ret = this.viewMode.handleEvent(name, data);
if (ret) {
return ret;
}
}
return null;
}
}

View File

@ -186,6 +186,15 @@ interface RefreshContext {
noteIdsToReload: Set<string>;
}
/**
* The information contained within a drag event.
*/
export interface DragData {
noteId: string;
branchId: string;
title: string;
}
export default class NoteTreeWidget extends NoteContextAwareWidget {
private $tree!: JQuery<HTMLElement>;
private $treeActions!: JQuery<HTMLElement>;

View File

@ -1,60 +1,15 @@
import server from "../services/server.js";
import { Dropdown } from "bootstrap";
import { NOTE_TYPES } from "../services/note_types.js";
import { t } from "../services/i18n.js";
import dialogService from "../services/dialog.js";
import mimeTypesService from "../services/mime_types.js";
import NoteContextAwareWidget from "./note_context_aware_widget.js";
import dialogService from "../services/dialog.js";
import { t } from "../services/i18n.js";
import type FNote from "../entities/fnote.js";
import type { NoteType } from "../entities/fnote.js";
import server from "../services/server.js";
import type { EventData } from "../components/app_context.js";
import { Dropdown } from "bootstrap";
import type { NoteType } from "../entities/fnote.js";
import type FNote from "../entities/fnote.js";
interface NoteTypeMapping {
type: NoteType;
mime?: string;
title: string;
isBeta?: boolean;
selectable: boolean;
}
const NOTE_TYPES: NoteTypeMapping[] = [
// The suggested note type ordering method: insert the item into the corresponding group,
// then ensure the items within the group are ordered alphabetically.
// Please keep the order synced with the listing found also in apps/client/src/services/note_types.ts.
// The default note type (always the first item)
{ type: "text", mime: "text/html", title: t("note_types.text"), selectable: true },
// Text notes group
{ type: "book", mime: "", title: t("note_types.book"), selectable: true },
// Graphic notes
{ type: "canvas", mime: "application/json", title: t("note_types.canvas"), selectable: true },
{ type: "mermaid", mime: "text/mermaid", title: t("note_types.mermaid-diagram"), selectable: true },
// Map notes
{ type: "geoMap", mime: "application/json", title: t("note_types.geo-map"), isBeta: true, selectable: true },
{ type: "mindMap", mime: "application/json", title: t("note_types.mind-map"), selectable: true },
{ type: "relationMap", mime: "application/json", title: t("note_types.relation-map"), selectable: true },
// Misc note types
{ type: "render", mime: "", title: t("note_types.render-note"), selectable: true },
{ type: "webView", mime: "", title: t("note_types.web-view"), selectable: true },
// Code notes
{ type: "code", mime: "text/plain", title: t("note_types.code"), selectable: true },
// Reserved types (cannot be created by the user)
{ type: "contentWidget", mime: "", title: t("note_types.widget"), selectable: false },
{ type: "doc", mime: "", title: t("note_types.doc"), selectable: false },
{ type: "file", title: t("note_types.file"), selectable: false },
{ type: "image", title: t("note_types.image"), selectable: false },
{ type: "launcher", mime: "", title: t("note_types.launcher"), selectable: false },
{ type: "noteMap", mime: "", title: t("note_types.note-map"), selectable: false },
{ type: "search", title: t("note_types.saved-search"), selectable: false },
{ type: "aiChat", mime: "application/json", title: t("note_types.ai-chat"), selectable: false }
];
const NOT_SELECTABLE_NOTE_TYPES = NOTE_TYPES.filter((nt) => !nt.selectable).map((nt) => nt.type);
const NOT_SELECTABLE_NOTE_TYPES = NOTE_TYPES.filter((nt) => nt.reserved || nt.static).map((nt) => nt.type);
const TPL = /*html*/`
<div class="dropdown note-type-widget">
@ -64,13 +19,6 @@ const TPL = /*html*/`
overflow-y: auto;
overflow-x: hidden;
}
.note-type-dropdown .badge {
margin-left: 8px;
background: var(--accented-background-color);
font-weight: normal;
color: var(--menu-text-color);
}
</style>
<button type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm dropdown-toggle select-button note-type-button">
<span class="note-type-desc"></span>
@ -117,10 +65,15 @@ export default class NoteTypeWidget extends NoteContextAwareWidget {
return;
}
for (const noteType of NOTE_TYPES.filter((nt) => nt.selectable)) {
for (const noteType of NOTE_TYPES.filter((nt) => !nt.reserved && !nt.static)) {
let $typeLink: JQuery<HTMLElement>;
const $title = $("<span>").text(noteType.title);
if (noteType.isNew) {
$title.append($(`<span class="badge new-note-type-badge">`).text(t("note_types.new-feature")));
}
if (noteType.isBeta) {
$title.append($(`<span class="badge">`).text(t("note_types.beta-feature")));
}

View File

@ -64,7 +64,7 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
}
#isFullWidthNote(note: FNote) {
if (["image", "mermaid", "book", "render", "canvas", "webView", "mindMap", "geoMap"].includes(note.type)) {
if (["image", "mermaid", "book", "render", "canvas", "webView", "mindMap"].includes(note.type)) {
return true;
}

View File

@ -25,6 +25,7 @@ const TPL = /*html*/`
<option value="list">${t("book_properties.list")}</option>
<option value="calendar">${t("book_properties.calendar")}</option>
<option value="table">${t("book_properties.table")}</option>
<option value="geoMap">${t("book_properties.geo-map")}</option>
</select>
</div>
@ -126,7 +127,7 @@ export default class BookPropertiesWidget extends NoteContextAwareWidget {
return;
}
if (!["list", "grid", "calendar", "table"].includes(type)) {
if (!["list", "grid", "calendar", "table", "geoMap"].includes(type)) {
throw new Error(t("book_properties.invalid_view_type", { type }));
}

View File

@ -47,6 +47,7 @@ export default class BookTypeWidget extends TypeWidget {
switch (this.note?.getAttributeValue("label", "viewType")) {
case "calendar":
case "table":
case "geoMap":
return false;
default:
return true;

View File

@ -1,447 +0,0 @@
import { GPX, 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 { CommandListenerData, EventData } from "../../components/app_context.js";
import { t } from "../../services/i18n.js";
import attributes from "../../services/attributes.js";
import openContextMenu from "./geo_map_context_menu.js";
import link from "../../services/link.js";
import note_tooltip from "../../services/note_tooltip.js";
import appContext from "../../components/app_context.js";
import markerIcon from "leaflet/dist/images/marker-icon.png";
import markerIconShadow from "leaflet/dist/images/marker-shadow.png";
import { hasTouchBar } from "../../services/utils.js";
const TPL = /*html*/`\
<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;
};
}
enum State {
Normal,
NewNote
}
export default class GeoMapTypeWidget extends TypeWidget {
private geoMapWidget: GeoMapWidget;
private _state: State;
private L!: Leaflet;
private currentMarkerData: Record<string, Marker>;
private currentTrackData: Record<string, GPX>;
private gpxLoaded?: boolean;
private ignoreNextZoomEvent?: boolean;
static getType() {
return "geoMap";
}
constructor() {
super();
this.geoMapWidget = new GeoMapWidget("type", (L: Leaflet) => this.#onMapInitialized(L));
this.currentMarkerData = {};
this.currentTrackData = {};
this._state = State.Normal;
this.child(this.geoMapWidget);
}
doRender() {
super.doRender();
this.$widget = $(TPL);
this.$widget.append(this.geoMapWidget.render());
}
async #onMapInitialized(L: Leaflet) {
this.L = L;
const map = this.geoMapWidget.map;
if (!map) {
throw new Error(t("geo-map.unable-to-load-map"));
}
this.#restoreViewportAndZoom();
// Restore markers.
await this.#reloadMarkers();
// This fixes an issue with the map appearing cut off at the beginning, due to the container not being properly attached
setTimeout(() => {
map.invalidateSize();
}, 100);
const updateFn = () => this.spacedUpdate.scheduleUpdate();
map.on("moveend", updateFn);
map.on("zoomend", updateFn);
map.on("click", (e) => this.#onMapClicked(e));
if (hasTouchBar) {
map.on("zoom", () => {
if (!this.ignoreNextZoomEvent) {
this.triggerCommand("refreshTouchBar");
}
this.ignoreNextZoomEvent = false;
});
}
}
async #restoreViewportAndZoom() {
const map = this.geoMapWidget.map;
if (!map || !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);
}
async #reloadMarkers() {
if (!this.note) {
return;
}
// Delete all existing markers
for (const marker of Object.values(this.currentMarkerData)) {
marker.remove();
}
// Delete all existing tracks
for (const track of Object.values(this.currentTrackData)) {
track.remove();
}
// Add the new markers.
this.currentMarkerData = {};
const childNotes = await this.note.getChildNotes();
for (const childNote of childNotes) {
if (childNote.mime === "application/gpx+xml") {
this.#processNoteWithGpxTrack(childNote);
continue;
}
const latLng = childNote.getAttributeValue("label", LOCATION_ATTRIBUTE);
if (latLng) {
this.#processNoteWithMarker(childNote, latLng);
}
}
}
async #processNoteWithGpxTrack(note: FNote) {
if (!this.L || !this.geoMapWidget.map) {
return;
}
if (!this.gpxLoaded) {
await import("leaflet-gpx");
this.gpxLoaded = true;
}
const xmlResponse = await server.get<string | Uint8Array>(`notes/${note.noteId}/open`, undefined, true);
let stringResponse: string;
if (xmlResponse instanceof Uint8Array) {
stringResponse = new TextDecoder().decode(xmlResponse);
} else {
stringResponse = xmlResponse;
}
const track = new this.L.GPX(stringResponse, {
markers: {
startIcon: this.#buildIcon(note.getIcon(), note.getColorClass(), note.title),
endIcon: this.#buildIcon("bxs-flag-checkered"),
wptIcons: {
"": this.#buildIcon("bx bx-pin")
}
},
polyline_options: {
color: note.getLabelValue("color") ?? "blue"
}
});
track.addTo(this.geoMapWidget.map);
this.currentTrackData[note.noteId] = track;
}
#processNoteWithMarker(note: FNote, latLng: string) {
const map = this.geoMapWidget.map;
if (!map) {
return;
}
const [lat, lng] = latLng.split(",", 2).map((el) => parseFloat(el));
const L = this.L;
const icon = this.#buildIcon(note.getIcon(), note.getColorClass(), note.title);
const marker = L.marker(L.latLng(lat, lng), {
icon,
draggable: true,
autoPan: true,
autoPanSpeed: 5
})
.addTo(map)
.on("moveend", (e) => {
this.moveMarker(note.noteId, (e.target as Marker).getLatLng());
});
marker.on("mousedown", ({ originalEvent }) => {
// Middle click to open in new tab
if (originalEvent.button === 1) {
const hoistedNoteId = this.hoistedNoteId;
//@ts-ignore, fix once tab manager is ported.
appContext.tabManager.openInNewTab(note.noteId, hoistedNoteId);
return true;
}
});
marker.on("contextmenu", (e) => {
openContextMenu(note.noteId, e.originalEvent);
});
const el = marker.getElement();
if (el) {
const $el = $(el);
$el.attr("data-href", `#${note.noteId}`);
note_tooltip.setupElementTooltip($($el));
}
this.currentMarkerData[note.noteId] = marker;
}
#buildIcon(bxIconClass: string, colorClass?: string, title?: string) {
return this.L.divIcon({
html: /*html*/`\
<img class="icon" src="${markerIcon}" />
<img class="icon-shadow" src="${markerIconShadow}" />
<span class="bx ${bxIconClass} ${colorClass ?? ""}"></span>
<span class="title-label">${title ?? ""}</span>`,
iconSize: [25, 41],
iconAnchor: [12, 41]
});
}
#changeState(newState: State) {
this._state = newState;
this.geoMapWidget.$container.toggleClass("placing-note", newState === State.NewNote);
if (hasTouchBar) {
this.triggerCommand("refreshTouchBar");
}
}
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();
this.#restoreViewportAndZoom();
await this.#reloadMarkers();
}
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
// If any of the children branches are altered.
if (loadResults.getBranchRows().find((branch) => branch.parentNoteId === this.noteId)) {
this.#reloadMarkers();
return;
}
// If any of note has its location attribute changed.
// TODO: Should probably filter by parent here as well.
const attributeRows = loadResults.getAttributeRows();
if (attributeRows.find((at) => [LOCATION_ATTRIBUTE, "color"].includes(at.name ?? ""))) {
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);
}
buildTouchBarCommand({ TouchBar }: CommandListenerData<"buildTouchBar">) {
const map = this.geoMapWidget.map;
const that = this;
if (!map) {
return;
}
return [
new TouchBar.TouchBarSlider({
label: "Zoom",
value: map.getZoom(),
minValue: map.getMinZoom(),
maxValue: map.getMaxZoom(),
change(newValue) {
that.ignoreNextZoomEvent = true;
map.setZoom(newValue);
},
}),
new TouchBar.TouchBarButton({
label: "New geo note",
click: () => this.triggerCommand("geoMapCreateChildNote", { ntxId: this.ntxId }),
enabled: (this._state === State.Normal)
})
];
}
}

View File

@ -1,32 +0,0 @@
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);
}
});
}

View File

@ -0,0 +1,85 @@
import type { LatLng, LeafletMouseEvent } from "leaflet";
import appContext, { type CommandMappings } from "../../../components/app_context.js";
import contextMenu, { type MenuItem } from "../../../menus/context_menu.js";
import linkContextMenu from "../../../menus/link_context_menu.js";
import { t } from "../../../services/i18n.js";
import { createNewNote } from "./editing.js";
import { copyTextWithToast } from "../../../services/clipboard_ext.js";
import link from "../../../services/link.js";
export default function openContextMenu(noteId: string, e: LeafletMouseEvent, isEditable: boolean) {
let items: MenuItem<keyof CommandMappings>[] = [
...buildGeoLocationItem(e),
{ title: "----" },
...linkContextMenu.getItems(),
];
if (isEditable) {
items = [
...items,
{ title: "----" },
{ title: t("geo-map-context.remove-from-map"), command: "deleteFromMap", uiIcon: "bx bx-trash" }
];
}
contextMenu.show({
x: e.originalEvent.pageX,
y: e.originalEvent.pageY,
items,
selectMenuItemHandler: ({ command }, e) => {
if (command === "deleteFromMap") {
appContext.triggerCommand(command, { noteId });
return;
}
// Pass the events to the link context menu
linkContextMenu.handleLinkContextMenuItem(command, noteId);
}
});
}
export function openMapContextMenu(noteId: string, e: LeafletMouseEvent, isEditable: boolean) {
let items: MenuItem<keyof CommandMappings>[] = [
...buildGeoLocationItem(e)
];
if (isEditable) {
items = [
...items,
{ title: "----" },
{
title: t("geo-map-context.add-note"),
handler: () => createNewNote(noteId, e),
uiIcon: "bx bx-plus"
}
]
}
contextMenu.show({
x: e.originalEvent.pageX,
y: e.originalEvent.pageY,
items,
selectMenuItemHandler: () => {
// Nothing to do, as the commands handle themselves.
}
});
}
function buildGeoLocationItem(e: LeafletMouseEvent) {
function formatGeoLocation(latlng: LatLng, precision: number = 6) {
return `${latlng.lat.toFixed(precision)}, ${latlng.lng.toFixed(precision)}`;
}
return [
{
title: formatGeoLocation(e.latlng),
uiIcon: "bx bx-current-location",
handler: () => copyTextWithToast(formatGeoLocation(e.latlng, 15))
},
{
title: t("geo-map-context.open-location"),
uiIcon: "bx bx-map-alt",
handler: () => link.goToLinkExt(null, `geo:${e.latlng.lat},${e.latlng.lng}`)
}
];
}

View File

@ -0,0 +1,80 @@
import { LatLng, LeafletMouseEvent } from "leaflet";
import attributes from "../../../services/attributes";
import { LOCATION_ATTRIBUTE } from "./index.js";
import dialog from "../../../services/dialog";
import server from "../../../services/server";
import { t } from "../../../services/i18n";
import type { Map } from "leaflet";
import type { DragData } from "../../note_tree.js";
import froca from "../../../services/froca.js";
import branches from "../../../services/branches.js";
const CHILD_NOTE_ICON = "bx bx-pin";
// TODO: Deduplicate
interface CreateChildResponse {
note: {
noteId: string;
};
}
export async function moveMarker(noteId: string, latLng: LatLng | null) {
const value = latLng ? [latLng.lat, latLng.lng].join(",") : "";
await attributes.setLabel(noteId, LOCATION_ATTRIBUTE, value);
}
export async function createNewNote(noteId: string, e: LeafletMouseEvent) {
const title = await dialog.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/${noteId}/children?target=into`, {
title,
content: "",
type: "text"
});
attributes.setLabel(note.noteId, "iconClass", CHILD_NOTE_ICON);
moveMarker(note.noteId, e.latlng);
}
}
export function setupDragging($container: JQuery<HTMLElement>, map: Map, mapNoteId: string) {
$container.on("dragover", (e) => {
// Allow drag.
e.preventDefault();
});
$container.on("drop", async (e) => {
if (!e.originalEvent) {
return;
}
const data = e.originalEvent.dataTransfer?.getData('text');
if (!data) {
return;
}
try {
const parsedData = JSON.parse(data) as DragData[];
if (!parsedData.length) {
return;
}
const { noteId } = parsedData[0];
const offset = $container.offset();
const x = e.originalEvent.clientX - (offset?.left ?? 0);
const y = e.originalEvent.clientY - (offset?.top ?? 0);
const latlng = map.containerPointToLatLng([ x, y ]);
const note = await froca.getNote(noteId, true);
const parents = note?.getParentNoteIds();
if (parents?.includes(mapNoteId)) {
await moveMarker(noteId, latlng);
} else {
await branches.cloneNoteToParentNote(noteId, mapNoteId);
await moveMarker(noteId, latlng);
}
} catch (e) {
console.warn(e);
}
});
}

View File

@ -0,0 +1,331 @@
import ViewMode, { ViewModeArgs } from "../view_mode.js";
import L from "leaflet";
import type { GPX, LatLng, LeafletMouseEvent, Map, Marker } from "leaflet";
import "leaflet/dist/leaflet.css";
import SpacedUpdate from "../../../services/spaced_update.js";
import { t } from "../../../services/i18n.js";
import processNoteWithMarker, { processNoteWithGpxTrack } from "./markers.js";
import { hasTouchBar } from "../../../services/utils.js";
import toast from "../../../services/toast.js";
import { CommandListenerData, EventData } from "../../../components/app_context.js";
import { createNewNote, moveMarker, setupDragging } from "./editing.js";
import { openMapContextMenu } from "./context_menu.js";
const TPL = /*html*/`
<div class="geo-view">
<style>
.geo-view {
overflow: hidden;
position: relative;
height: 100%;
}
.geo-map-container {
height: 100%;
overflow: hidden;
}
.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 class="geo-map-container"></div>
</div>`;
interface MapData {
view?: {
center?: LatLng | [number, number];
zoom?: number;
};
}
const DEFAULT_COORDINATES: [number, number] = [3.878638227135724, 446.6630455551659];
const DEFAULT_ZOOM = 2;
export const LOCATION_ATTRIBUTE = "geolocation";
enum State {
Normal,
NewNote
}
export default class GeoView extends ViewMode<MapData> {
private $root: JQuery<HTMLElement>;
private $container!: JQuery<HTMLElement>;
private map?: Map;
private spacedUpdate: SpacedUpdate;
private _state: State;
private ignoreNextZoomEvent?: boolean;
private currentMarkerData: Record<string, Marker>;
private currentTrackData: Record<string, GPX>;
constructor(args: ViewModeArgs) {
super(args, "geoMap");
this.$root = $(TPL);
this.$container = this.$root.find(".geo-map-container");
this.spacedUpdate = new SpacedUpdate(() => this.onSave(), 5_000);
this.currentMarkerData = {};
this.currentTrackData = {};
this._state = State.Normal;
args.$parent.append(this.$root);
}
async renderList() {
this.renderMap();
return this.$root;
}
async renderMap() {
const map = L.map(this.$container[0], {
worldCopyJump: true
});
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
detectRetina: true
}).addTo(map);
this.map = map;
this.#onMapInitialized();
}
async #onMapInitialized() {
const map = this.map;
if (!map) {
throw new Error(t("geo-map.unable-to-load-map"));
}
this.#restoreViewportAndZoom();
const isEditable = !this.isReadOnly;
const updateFn = () => this.spacedUpdate.scheduleUpdate();
map.on("moveend", updateFn);
map.on("zoomend", updateFn);
map.on("click", (e) => this.#onMapClicked(e))
map.on("contextmenu", (e) => openMapContextMenu(this.parentNote.noteId, e, isEditable));
if (isEditable) {
setupDragging(this.$container, map, this.parentNote.noteId);
}
this.#reloadMarkers();
if (hasTouchBar) {
map.on("zoom", () => {
if (!this.ignoreNextZoomEvent) {
this.triggerCommand("refreshTouchBar");
}
this.ignoreNextZoomEvent = false;
});
}
}
async #restoreViewportAndZoom() {
const map = this.map;
if (!map) {
return;
}
const parsedContent = await this.viewStorage.restore();
// Restore viewport position & zoom
const center = parsedContent?.view?.center ?? DEFAULT_COORDINATES;
const zoom = parsedContent?.view?.zoom ?? DEFAULT_ZOOM;
map.setView(center, zoom);
}
private onSave() {
const map = this.map;
let data: MapData = {};
if (map) {
data = {
view: {
center: map.getBounds().getCenter(),
zoom: map.getZoom()
}
};
}
this.viewStorage.store(data);
}
async #reloadMarkers() {
if (!this.map) {
return;
}
// Delete all existing markers
for (const marker of Object.values(this.currentMarkerData)) {
marker.remove();
}
// Delete all existing tracks
for (const track of Object.values(this.currentTrackData)) {
track.remove();
}
// Add the new markers.
this.currentMarkerData = {};
const notes = await this.parentNote.getChildNotes();
const draggable = !this.isReadOnly;
for (const childNote of notes) {
if (childNote.mime === "application/gpx+xml") {
const track = await processNoteWithGpxTrack(this.map, childNote);
this.currentTrackData[childNote.noteId] = track;
continue;
}
const latLng = childNote.getAttributeValue("label", LOCATION_ATTRIBUTE);
if (latLng) {
const marker = processNoteWithMarker(this.map, childNote, latLng, draggable);
this.currentMarkerData[childNote.noteId] = marker;
}
}
}
get isFullHeight(): boolean {
return true;
}
#changeState(newState: State) {
this._state = newState;
this.$container.toggleClass("placing-note", newState === State.NewNote);
if (hasTouchBar) {
this.triggerCommand("refreshTouchBar");
}
}
onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">): boolean | void {
// If any of the children branches are altered.
if (loadResults.getBranchRows().find((branch) => branch.parentNoteId === this.parentNote.noteId)) {
this.#reloadMarkers();
return;
}
// If any of note has its location attribute changed.
// TODO: Should probably filter by parent here as well.
const attributeRows = loadResults.getAttributeRows();
if (attributeRows.find((at) => [LOCATION_ATTRIBUTE, "color"].includes(at.name ?? ""))) {
this.#reloadMarkers();
}
}
async geoMapCreateChildNoteEvent({ ntxId }: EventData<"geoMapCreateChildNote">) {
toast.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);
toast.closePersistent("geo-new-note");
};
window.addEventListener("keydown", globalKeyListener);
}
async #onMapClicked(e: LeafletMouseEvent) {
if (this._state !== State.NewNote) {
return;
}
toast.closePersistent("geo-new-note");
await createNewNote(this.parentNote.noteId, e);
this.#changeState(State.Normal);
}
deleteFromMapEvent({ noteId }: EventData<"deleteFromMap">) {
moveMarker(noteId, null);
}
buildTouchBarCommand({ TouchBar }: CommandListenerData<"buildTouchBar">) {
const map = this.map;
const that = this;
if (!map) {
return;
}
return [
new TouchBar.TouchBarSlider({
label: "Zoom",
value: map.getZoom(),
minValue: map.getMinZoom(),
maxValue: map.getMaxZoom(),
change(newValue) {
that.ignoreNextZoomEvent = true;
map.setZoom(newValue);
},
}),
new TouchBar.TouchBarButton({
label: "New geo note",
click: () => this.triggerCommand("geoMapCreateChildNote"),
enabled: (this._state === State.Normal)
})
];
}
}

View File

@ -0,0 +1,92 @@
import markerIcon from "leaflet/dist/images/marker-icon.png";
import markerIconShadow from "leaflet/dist/images/marker-shadow.png";
import { marker, latLng, divIcon, Map, type Marker } from "leaflet";
import type FNote from "../../../entities/fnote.js";
import openContextMenu from "./context_menu.js";
import server from "../../../services/server.js";
import { moveMarker } from "./editing.js";
import appContext from "../../../components/app_context.js";
import L from "leaflet";
let gpxLoaded = false;
export default function processNoteWithMarker(map: Map, note: FNote, location: string, isEditable: boolean) {
const [lat, lng] = location.split(",", 2).map((el) => parseFloat(el));
const icon = buildIcon(note.getIcon(), note.getColorClass(), note.title, note.noteId);
const newMarker = marker(latLng(lat, lng), {
icon,
draggable: isEditable,
autoPan: true,
autoPanSpeed: 5
}).addTo(map);
if (isEditable) {
newMarker.on("moveend", (e) => {
moveMarker(note.noteId, (e.target as Marker).getLatLng());
});
}
newMarker.on("mousedown", ({ originalEvent }) => {
// Middle click to open in new tab
if (originalEvent.button === 1) {
const hoistedNoteId = appContext.tabManager.getActiveContext()?.hoistedNoteId;
//@ts-ignore, fix once tab manager is ported.
appContext.tabManager.openInNewTab(note.noteId, hoistedNoteId);
return true;
}
});
newMarker.on("contextmenu", (e) => {
openContextMenu(note.noteId, e, isEditable);
});
return newMarker;
}
export async function processNoteWithGpxTrack(map: Map, note: FNote) {
if (!gpxLoaded) {
const GPX = await import("leaflet-gpx");
gpxLoaded = true;
}
const xmlResponse = await server.get<string | Uint8Array>(`notes/${note.noteId}/open`, undefined, true);
let stringResponse: string;
if (xmlResponse instanceof Uint8Array) {
stringResponse = new TextDecoder().decode(xmlResponse);
} else {
stringResponse = xmlResponse;
}
const track = new L.GPX(stringResponse, {
markers: {
startIcon: buildIcon(note.getIcon(), note.getColorClass(), note.title),
endIcon: buildIcon("bxs-flag-checkered"),
wptIcons: {
"": buildIcon("bx bx-pin")
}
},
polyline_options: {
color: note.getLabelValue("color") ?? "blue"
}
});
track.addTo(map);
return track;
}
function buildIcon(bxIconClass: string, colorClass?: string, title?: string, noteIdLink?: string) {
let html = /*html*/`\
<img class="icon" src="${markerIcon}" />
<img class="icon-shadow" src="${markerIconShadow}" />
<span class="bx ${bxIconClass} ${colorClass ?? ""}"></span>
<span class="title-label">${title ?? ""}</span>`;
if (noteIdLink) {
html = `<div data-href="#root/${noteIdLink}">${html}</div>`;
}
return divIcon({
html,
iconSize: [25, 41],
iconAnchor: [12, 41]
});
}

View File

@ -44,12 +44,16 @@ export default abstract class ViewMode<T extends object> extends Component {
return false;
}
get isReadOnly() {
return this.parentNote.hasLabel("readOnly");
}
get viewStorage() {
if (this._viewStorage) {
return this._viewStorage;
}
this._viewStorage = new ViewModeStorage(this.parentNote, this.viewType);
this._viewStorage = new ViewModeStorage<T>(this.parentNote, this.viewType);
return this._viewStorage;
}

View File

@ -26,18 +26,19 @@ export default class ViewModeStorage<T extends object> {
}
async restore() {
const existingAttachments = await this.note.getAttachmentsByRole(ATTACHMENT_ROLE);
const existingAttachments = (await this.note.getAttachmentsByRole(ATTACHMENT_ROLE))
.filter(a => a.title === this.attachmentName);
if (existingAttachments.length === 0) {
return undefined;
}
const attachment = existingAttachments
.find(a => a.title === this.attachmentName);
if (!attachment) {
return undefined;
if (existingAttachments.length > 1) {
// Clean up duplicates.
await Promise.all(existingAttachments.slice(1).map(async a => await server.remove(`attachments/${a.attachmentId}`)));
}
const attachment = existingAttachments[0];
const attachmentData = await server.get<{ content: string } | null>(`attachments/${attachment.attachmentId}/blob`);
return JSON.parse(attachmentData?.content ?? "{}");
return JSON.parse(attachmentData?.content ?? "{}") as T;
}
}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@ -1,5 +1,11 @@
<aside class="admonition important">
<p>Starting with Trilium v0.97.0, the geo map has been converted from a standalone
<a
href="#root/pOsGYCXsbNQG/_help_KSZ04uQ2D1St">note type</a>to a type of view for the&nbsp;<a class="reference-link"
href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/BFs8mudNFgCS/_help_0ESUbbAxVnoK">Note List</a>.&nbsp;</p>
</aside>
<figure class="image image-style-align-center">
<img style="aspect-ratio:892/675;" src="10_Geo Map_image.png" width="892"
<img style="aspect-ratio:892/675;" src="9_Geo Map View_image.png" width="892"
height="675">
</figure>
<p>This note type displays the children notes on a geographical map, based
@ -19,9 +25,9 @@
<tr>
<td>1</td>
<td>
<figure class="image image-style-align-center image_resized" style="width:51.25%;">
<img style="aspect-ratio:1256/1044;" src="7_Geo Map_image.png" width="1256"
height="1044">
<figure class="image">
<img style="aspect-ratio:483/413;" src="15_Geo Map View_image.png" width="483"
height="413">
</figure>
</td>
<td>Right click on any note on the note tree and select <em>Insert child note</em><em>Geo Map (beta)</em>.</td>
@ -30,7 +36,7 @@
<td>2</td>
<td>
<figure class="image image-style-align-center image_resized" style="width:53.44%;">
<img style="aspect-ratio:1720/1396;" src="9_Geo Map_image.png" width="1720"
<img style="aspect-ratio:1720/1396;" src="8_Geo Map View_image.png" width="1720"
height="1396">
</figure>
</td>
@ -39,7 +45,6 @@
</tbody>
</table>
</figure>
<h2>Repositioning the map</h2>
<ul>
<li>Click and drag the map in order to move across the map.</li>
@ -49,6 +54,7 @@
<p>The position on the map and the zoom are saved inside the map note and
restored when visiting again the note.</p>
<h2>Adding a marker using the map</h2>
<h3>Adding a new note using the plus button</h3>
<figure class="table">
<table>
<thead>
@ -63,18 +69,18 @@
<td>1</td>
<td>To create a marker, first navigate to the desired point on the map. Then
press the
<img src="11_Geo Map_image.png">button in the&nbsp;<a href="#root/_help_XpOYSgsLkTJy">Floating buttons</a>&nbsp;(top-right)
<img src="10_Geo Map View_image.png">button in the&nbsp;<a href="#root/_help_XpOYSgsLkTJy">Floating buttons</a>&nbsp;(top-right)
area.&nbsp;
<br>
<br>If the button is not visible, make sure the button section is visible
by pressing the chevron button (
<img src="17_Geo Map_image.png">) in the top-right of the map.</td>
<img src="17_Geo Map View_image.png">) in the top-right of the map.</td>
<td>&nbsp;</td>
</tr>
<tr>
<td>2</td>
<td>
<img class="image_resized" style="aspect-ratio:1730/416;width:100%;" src="2_Geo Map_image.png"
<img class="image_resized" style="aspect-ratio:1730/416;width:100%;" src="2_Geo Map View_image.png"
width="1730" height="416">
</td>
<td>Once pressed, the map will enter in the insert mode, as illustrated by
@ -86,7 +92,7 @@
<tr>
<td>3</td>
<td>
<img class="image_resized" style="aspect-ratio:1586/404;width:100%;" src="8_Geo Map_image.png"
<img class="image_resized" style="aspect-ratio:1586/404;width:100%;" src="7_Geo Map View_image.png"
width="1586" height="404">
</td>
<td>Enter the name of the marker/note to be created.</td>
@ -94,7 +100,7 @@
<tr>
<td>4</td>
<td>
<img class="image_resized" style="aspect-ratio:1696/608;width:100%;" src="16_Geo Map_image.png"
<img class="image_resized" style="aspect-ratio:1696/608;width:100%;" src="16_Geo Map View_image.png"
width="1696" height="608">
</td>
<td>Once confirmed, the marker will show up on the map and it will also be
@ -103,11 +109,34 @@
</tbody>
</table>
</figure>
<h3>Adding a new note using the contextual menu</h3>
<ol>
<li>Right click anywhere on the map, where to place the newly created marker
(and corresponding note).</li>
<li>Select <em>Add a marker at this location</em>.</li>
<li>Enter the name of the newly created note.</li>
<li>The map should be updated with the new marker.</li>
</ol>
<h3>Adding an existing note on note from the note tree</h3>
<ol>
<li>Select the desired note in the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_oPVyFC7WL2Lp">Note Tree</a>.</li>
<li>Hold the mouse on the note and drag it to the map to the desired location.</li>
<li>The map should be updated with the new marker.</li>
</ol>
<p>This works for:</p>
<ul>
<li>Notes that are not part of the geo map, case in which a <a href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/BFs8mudNFgCS/_help_IakOLONlIfGI">clone</a> will
be created.</li>
<li>Notes that are a child of the geo map but not yet positioned on the map.</li>
<li>Notes that are a child of the geo map and also positioned, case in which
the marker will be relocated to the new position.</li>
</ul>
<h2>How the location of the markers is stored</h2>
<p>The location of a marker is stored in the <code>#geolocation</code> attribute
of the child notes:</p>
<img src="18_Geo Map_image.png" width="1288" height="278">
<p>
<img src="18_Geo Map View_image.png" width="1288" height="278">
</p>
<p>This value can be added manually if needed. The value of the attribute
is made up of the latitude and longitude separated by a comma.</p>
<h2>Repositioning markers</h2>
@ -129,18 +158,40 @@
<li>Middle-clicking the marker will open the note in a new tab.</li>
<li>Right-clicking the marker will open a contextual menu allowing:
<ul>
<li>Opening the note in a new tab, split or window.</li>
<li>Opening the location using an external application (if the operating system
supports it).</li>
<li>Removing the marker from the map, which will remove the <code>#geolocation</code> attribute
of the note. To add it back again, the coordinates have to be manually
added back in.</li>
<li>&nbsp;</li>
</ul>
</li>
</ul>
<h2>Contextual menu</h2>
<p>It's possible to press the right mouse button to display a contextual
menu.</p>
<ol>
<li>If right-clicking an empty section of the map (not on a marker), it allows
to:
<ol>
<li>Displays the latitude and longitude. Clicking this option will copy them
to the clipboard.</li>
<li>Open the location using an external application (if the operating system
supports it).</li>
<li>Adding a new marker at that location.</li>
</ol>
</li>
<li>If right-clicking on a marker, it allows to:
<ol>
<li>Displays the latitude and longitude. Clicking this option will copy them
to the clipboard.</li>
<li>Open the location using an external application (if the operating system
supports it).</li>
<li>Open the note in a new tab, split or window.</li>
<li>Remove the marker from the map, which will remove the <code>#geolocation</code> attribute
of the note. To add it back again, the coordinates have to be manually
added back in.</li>
</ol>
</li>
</ol>
<h2>Icon and color of the markers</h2>
<figure class="image image-style-align-center">
<img style="aspect-ratio:523/295;" src="Geo Map_image.jpg" alt="image"
<img style="aspect-ratio:523/295;" src="Geo Map View_image.jpg" alt="image"
width="523" height="295">
</figure>
<p>The markers will have the same icon as the note.</p>
@ -171,7 +222,7 @@
<td>1</td>
<td>
<figure class="image image-style-align-center image_resized" style="width:56.84%;">
<img style="aspect-ratio:732/918;" src="13_Geo Map_image.png" width="732"
<img style="aspect-ratio:732/918;" src="12_Geo Map View_image.png" width="732"
height="918">
</figure>
</td>
@ -188,7 +239,7 @@
<td>2</td>
<td>
<figure class="image image-style-align-center image_resized" style="width:100%;">
<img style="aspect-ratio:518/84;" src="4_Geo Map_image.png" width="518"
<img style="aspect-ratio:518/84;" src="4_Geo Map View_image.png" width="518"
height="84">
</figure>
</td>
@ -198,7 +249,7 @@
<td>3</td>
<td>
<figure class="image image-style-align-center image_resized" style="width:100%;">
<img style="aspect-ratio:1074/276;" src="12_Geo Map_image.png" width="1074"
<img style="aspect-ratio:1074/276;" src="11_Geo Map View_image.png" width="1074"
height="276">
</figure>
</td>
@ -210,7 +261,6 @@
</tbody>
</table>
</figure>
<h3>Adding from OpenStreetMap</h3>
<p>Similarly to the Google Maps approach:</p>
<figure class="table" style="width:100%;">
@ -231,7 +281,7 @@
<tr>
<td>1</td>
<td>
<img class="image_resized" style="aspect-ratio:562/454;width:100%;" src="1_Geo Map_image.png"
<img class="image_resized" style="aspect-ratio:562/454;width:100%;" src="1_Geo Map View_image.png"
width="562" height="454">
</td>
<td>Go to any location on openstreetmap.org and right click to bring up the
@ -240,7 +290,7 @@
<tr>
<td>2</td>
<td>
<img class="image_resized" style="aspect-ratio:696/480;width:100%;" src="Geo Map_image.png"
<img class="image_resized" style="aspect-ratio:696/480;width:100%;" src="Geo Map View_image.png"
width="696" height="480">
</td>
<td>The address will be visible in the top-left of the screen, in the place
@ -251,7 +301,7 @@
<tr>
<td>3</td>
<td>
<img class="image_resized" style="aspect-ratio:640/276;width:100%;" src="5_Geo Map_image.png"
<img class="image_resized" style="aspect-ratio:640/276;width:100%;" src="5_Geo Map View_image.png"
width="640" height="276">
</td>
<td>Simply paste the value inside the text box into the <code>#geolocation</code> attribute
@ -260,7 +310,6 @@
</tbody>
</table>
</figure>
<h2>Adding GPS tracks (.gpx)</h2>
<p>Trilium has basic support for displaying GPS tracks on the geo map.</p>
<figure
@ -283,7 +332,7 @@ class="table" style="width:100%;">
<td>1</td>
<td>
<figure class="image image-style-align-center">
<img style="aspect-ratio:226/74;" src="3_Geo Map_image.png" width="226"
<img style="aspect-ratio:226/74;" src="3_Geo Map View_image.png" width="226"
height="74">
</figure>
</td>
@ -294,7 +343,7 @@ class="table" style="width:100%;">
<td>2</td>
<td>
<figure class="image image-style-align-center">
<img style="aspect-ratio:322/222;" src="15_Geo Map_image.png" width="322"
<img style="aspect-ratio:322/222;" src="14_Geo Map View_image.png" width="322"
height="222">
</figure>
</td>
@ -305,7 +354,7 @@ class="table" style="width:100%;">
<td>3</td>
<td>
<figure class="image image-style-align-center">
<img style="aspect-ratio:620/530;" src="6_Geo Map_image.png" width="620"
<img style="aspect-ratio:620/530;" src="6_Geo Map View_image.png" width="620"
height="530">
</figure>
</td>
@ -324,12 +373,22 @@ class="table" style="width:100%;">
<p>If the GPX contains waypoints, they will also be displayed. If they have
a name, it is displayed when hovering over it with the mouse.</p>
</aside>
<h2>Read-only mode</h2>
<p>When a map is in read-only all editing features will be disabled such
as:</p>
<ul>
<li>The add button in the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_XpOYSgsLkTJy">Floating buttons</a>.</li>
<li>Dragging markers.</li>
<li>Editing from the contextual menu (removing locations or adding new items).</li>
</ul>
<p>To enable read-only mode simply press the <em>Lock</em> icon from the&nbsp;
<a
class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_XpOYSgsLkTJy">Floating buttons</a>. To disable it, press the button again.</p>
<h2>Troubleshooting</h2>
<figure class="image image-style-align-right image_resized" style="width:34.06%;">
<img style="aspect-ratio:678/499;" src="14_Geo Map_image.png" width="678"
<img style="aspect-ratio:678/499;" src="13_Geo Map View_image.png" width="678"
height="499">
</figure>
<h3>Grid-like artifacts on the map</h3>
<p>This occurs if the application is not at 100% zoom which causes the pixels
of the map to not render correctly due to fractional scaling. The only

View File

@ -1,16 +1,16 @@
<figure class="image">
<img style="aspect-ratio:1050/259;" src="Table_image.png" width="1050"
<img style="aspect-ratio:1050/259;" src="Table View_image.png" width="1050"
height="259">
</figure>
<p>The table view displays information in a grid, where the rows are individual
notes and the columns are&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/tC7s2alapj8V/zEY4DaJG4YT5/_help_OFXdgB2nNk1F">Promoted Attributes</a>.
notes and the columns are&nbsp;<a class="reference-link" href="#root/_help_OFXdgB2nNk1F">Promoted Attributes</a>.
In addition, values are editable.</p>
<h2>Interaction</h2>
<h3>Creating a new table</h3>
<p>Right click the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_oPVyFC7WL2Lp">Note Tree</a>&nbsp;and
<p>Right click the&nbsp;<a class="reference-link" href="#root/_help_oPVyFC7WL2Lp">Note Tree</a>&nbsp;and
select <em>Insert child note</em> and look for the <em>Table item</em>.</p>
<h3>Adding columns</h3>
<p>Each column is a <a href="#root/pOsGYCXsbNQG/tC7s2alapj8V/zEY4DaJG4YT5/_help_OFXdgB2nNk1F">promoted attribute</a> that
<p>Each column is a <a href="#root/_help_OFXdgB2nNk1F">promoted attribute</a> that
is defined on the Book note. Ideally, the promoted attributes need to be
inheritable in order to show up in the child notes.</p>
<p>To create a new column, simply press <em>Add new column</em> at the bottom
@ -19,7 +19,7 @@
<ul>
<li>The current item number, identified by the <code>#</code> symbol. This simply
counts the note and is affected by sorting.</li>
<li><a class="reference-link" href="#root/pOsGYCXsbNQG/tC7s2alapj8V/_help_m1lbrzyKDaRB">Note ID</a>,
<li><a class="reference-link" href="#root/_help_m1lbrzyKDaRB">Note ID</a>,
representing the unique ID used internally by Trilium</li>
<li>The title of the note.</li>
</ul>
@ -28,9 +28,7 @@
<p>To create a new note, press <em>Add new row</em> at the bottom of the table.
By default it will try to edit the title of the newly created note.</p>
<p>Alternatively, the note can be created from the<a class="reference-link"
href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_oPVyFC7WL2Lp">Note Tree</a>&nbsp;or
<a
href="#root/pOsGYCXsbNQG/_help_CdNpE2pqjmI6">scripting</a>.</p>
href="#root/_help_oPVyFC7WL2Lp">Note Tree</a>&nbsp;or <a href="#root/_help_CdNpE2pqjmI6">scripting</a>.</p>
<h3>Editing data</h3>
<p>Simply click on a cell within a row to change its value. The change will
not only reflect in the table, but also as an attribute of the corresponding
@ -58,7 +56,7 @@
</ul>
<h3>Reordering rows</h3>
<p>Notes can be dragged around to change their order. This will also change
the order of the note in the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_oPVyFC7WL2Lp">Note Tree</a>.</p>
the order of the note in the&nbsp;<a class="reference-link" href="#root/_help_oPVyFC7WL2Lp">Note Tree</a>.</p>
<p>Currently, it's possible to reorder notes even if sorting is used, but
the result might be inconsistent.</p>
<h2>Limitations</h2>
@ -67,7 +65,7 @@
<ol>
<li>As mentioned previously, the columns of the table are defined as&nbsp;
<a
class="reference-link" href="#root/pOsGYCXsbNQG/tC7s2alapj8V/zEY4DaJG4YT5/_help_OFXdgB2nNk1F">Promoted Attributes</a>.
class="reference-link" href="#root/_help_OFXdgB2nNk1F">Promoted Attributes</a>.
<ol>
<li>But only the promoted attributes that are defined at the level of the
Book note are actually taken into consideration.</li>
@ -77,22 +75,23 @@
<li>Hierarchy is not yet supported, so the table will only show the items
that are direct children of the <em>Book</em> note.</li>
<li>Multiple labels and relations are not supported. If a&nbsp;<a class="reference-link"
href="#root/pOsGYCXsbNQG/tC7s2alapj8V/zEY4DaJG4YT5/_help_OFXdgB2nNk1F">Promoted Attributes</a>&nbsp;is
defined with a <em>Multi value</em> specificity, they will be ignored.</li>
href="#root/_help_OFXdgB2nNk1F">Promoted Attributes</a>&nbsp;is defined
with a <em>Multi value</em> specificity, they will be ignored.</li>
</ol>
<h2>Use in search</h2>
<p>The table view can be used in a&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_m523cpzocqaD">Saved Search</a>&nbsp;by
<p>The table view can be used in a&nbsp;<a class="reference-link" href="#root/_help_m523cpzocqaD">Saved Search</a>&nbsp;by
adding the <code>#viewType=table</code> attribute.</p>
<p>Unlike when used in a book, saved searches are not limited to the sub-hierarchy
of a note and allows for advanced queries thanks to the power of the&nbsp;
<a
class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/wArbEsdSae6g/_help_eIg8jdvaoNNd">Search</a>.</p>
class="reference-link" href="#root/_help_eIg8jdvaoNNd">Search</a>.</p>
<p>However, there are also some limitations:</p>
<ul>
<li>It's not possible to reorder notes.</li>
<li>It's not possible to add a new row.</li>
</ul>
<p>Columns are supported, by being defined as&nbsp;<a class="reference-link"
href="#root/pOsGYCXsbNQG/tC7s2alapj8V/zEY4DaJG4YT5/_help_OFXdgB2nNk1F">Promoted Attributes</a>&nbsp;to
the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_m523cpzocqaD">Saved Search</a>&nbsp;note.</p>
href="#root/_help_OFXdgB2nNk1F">Promoted Attributes</a>&nbsp;to the&nbsp;
<a
class="reference-link" href="#root/_help_m523cpzocqaD">Saved Search</a>&nbsp;note.</p>
<p>Editing is also supported.</p>

View File

@ -54,4 +54,7 @@
hide the Mermaid source code and display the diagram preview in full-size.
In this case, the read-only mode can be easily toggled on or off via a
dedicated button in the&nbsp;<a class="reference-link" href="#root/_help_XpOYSgsLkTJy">Floating buttons</a>&nbsp;area.</li>
<li><a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/BFs8mudNFgCS/0ESUbbAxVnoK/_help_81SGnPGMk7Xc">Geo Map View</a>&nbsp;will
disallow all interaction that would otherwise change the map (dragging
notes, adding new items).</li>
</ul>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

View File

@ -0,0 +1,46 @@
import becca from "../becca/becca";
import becca_loader from "../becca/becca_loader";
import cls from "../services/cls.js";
import hidden_subtree from "../services/hidden_subtree";
export default () => {
cls.init(() => {
becca_loader.load();
// Ensure the geomap template is generated.
hidden_subtree.checkHiddenSubtree(true);
for (const note of Object.values(becca.notes)) {
if (note.type as string !== "geoMap") {
continue;
}
console.log(`Migrating note '${note.noteId}' from geoMap to book type...`);
note.type = "book";
note.mime = "";
note.save();
const content = note.getContent();
if (content) {
const title = "geoMap.json";
const existingAttachment = note.getAttachmentsByRole("viewConfig")
.filter(a => a.title === title)[0];
if (existingAttachment) {
existingAttachment.setContent(content);
} else {
note.saveAttachment({
role: "viewConfig",
title,
mime: "application/json",
content,
position: 0
});
}
}
note.setContent("");
note.setRelation("template", "_template_geo_map");
}
});
};

View File

@ -6,6 +6,11 @@
// Migrations should be kept in descending order, so the latest migration is first.
const MIGRATIONS: (SqlMigration | JsMigration)[] = [
// Migrate geo map to collection
{
version: 233,
module: async () => import("./0233__migrate_geo_map_to_collection.js")
},
// Remove embedding tables since LLM embedding functionality has been removed
{
version: 232,

View File

@ -248,7 +248,7 @@ function register(app: express.Application) {
route(GET, "/api/setup/status", [], setupApiRoute.getStatus, apiResultHandler);
asyncRoute(PST, "/api/setup/new-document", [auth.checkAppNotInitialized], setupApiRoute.setupNewDocument, apiResultHandler);
asyncRoute(PST, "/api/setup/sync-from-server", [auth.checkAppNotInitialized], setupApiRoute.setupSyncFromServer, apiResultHandler);
route(GET, "/api/setup/sync-seed", [auth.checkCredentials], setupApiRoute.getSyncSeed, apiResultHandler);
route(GET, "/api/setup/sync-seed", [loginRateLimiter, auth.checkCredentials], setupApiRoute.getSyncSeed, apiResultHandler);
asyncRoute(PST, "/api/setup/sync-seed", [auth.checkAppNotInitialized], setupApiRoute.saveSyncSeed, apiResultHandler);
apiRoute(GET, "/api/autocomplete", autocompleteApiRoute.getAutocomplete);
@ -263,7 +263,7 @@ function register(app: express.Application) {
apiRoute(PST, "/api/bulk-action/execute", bulkActionRoute.execute);
apiRoute(PST, "/api/bulk-action/affected-notes", bulkActionRoute.getAffectedNoteCount);
route(PST, "/api/login/sync", [], loginApiRoute.loginSync, apiResultHandler);
route(PST, "/api/login/sync", [loginRateLimiter], loginApiRoute.loginSync, apiResultHandler);
// this is for entering protected mode so user has to be already logged-in (that's the reason we don't require username)
apiRoute(PST, "/api/login/protected", loginApiRoute.loginToProtectedSession);
apiRoute(PST, "/api/login/protected/touch", loginApiRoute.touchProtectedSession);

View File

@ -3,7 +3,7 @@ import build from "./build.js";
import packageJson from "../../package.json" with { type: "json" };
import dataDir from "./data_dir.js";
const APP_DB_VERSION = 232;
const APP_DB_VERSION = 233;
const SYNC_VERSION = 36;
const CLIPPER_PROTOCOL_VERSION = "1.0";

View File

@ -102,7 +102,7 @@ eventService.subscribe(eventService.ENTITY_CREATED, ({ entityName, entity }) =>
const content = note.getContent();
if (
["text", "code", "mermaid", "canvas", "relationMap", "mindMap", "geoMap"].includes(note.type) &&
["text", "code", "mermaid", "canvas", "relationMap", "mindMap"].includes(note.type) &&
typeof content === "string" &&
// if the note has already content we're not going to overwrite it with template's one
(!content || content.trim().length === 0) &&

View File

@ -380,7 +380,7 @@ function checkHiddenSubtreeRecursively(parentNoteId: string, item: HiddenSubtree
type: attr.type,
name: attr.name,
value: attr.value,
isInheritable: false
isInheritable: attr.isInheritable
}).save();
} else if (attr.name === "docName" || (existingAttribute.noteId.startsWith("_help") && attr.name === "iconClass")) {
if (existingAttribute.value !== attr.value) {

View File

@ -43,6 +43,33 @@ export default function buildHiddenSubtreeTemplates() {
value: "table"
}
]
},
{
id: "_template_geo_map",
type: "book",
title: "Geo Map",
icon: "bx bx-map-alt",
attributes: [
{
name: "template",
type: "label"
},
{
name: "viewType",
type: "label",
value: "geoMap"
},
{
name: "hidePromotedAttributes",
type: "label"
},
{
name: "label:geolocation",
type: "label",
value: "promoted,alias=Geolocation,single,text",
isInheritable: true
}
]
}
]
};

Binary file not shown.

View File

@ -70,6 +70,19 @@ describe("processNoteContent", () => {
expect(content).toContain(`<a class="reference-link" href="#root/${shopNote.noteId}`);
expect(content).toContain(`<img src="api/images/${bananaNote!.noteId}/banana.jpeg`);
});
it("can import old geomap notes", async () => {
const { importedNote } = await testImport("geomap.zip");
expect(importedNote.type).toBe("book");
expect(importedNote.mime).toBe("");
expect(importedNote.getRelationValue("template")).toBe("_template_geo_map");
const attachment = importedNote.getAttachmentsByRole("viewConfig")[0];
expect(attachment.title).toBe("geoMap.json");
expect(attachment.mime).toBe("application/json");
const content = attachment.getContent();
expect(content).toStrictEqual(`{"view":{"center":{"lat":49.19598332223546,"lng":-2.1414576506668808},"zoom":12}}`);
});
});
function getNoteByTitlePath(parentNote: BNote, ...titlePath: string[]) {

View File

@ -502,6 +502,28 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo
firstNote = firstNote || note;
}
} else {
if (detectedType as string === "geoMap") {
attributes.push({
noteId,
type: "relation",
name: "template",
value: "_template_geo_map"
});
const attachment = new BAttachment({
attachmentId: getNewAttachmentId(newEntityId()),
ownerId: noteId,
title: "geoMap.json",
role: "viewConfig",
mime: "application/json",
position: 0
});
attachment.setContent(content, { forceSave: true });
content = "";
mime = "";
}
({ note } = noteService.createNewNote({
parentNoteId: parentNoteId,
title: noteTitle || "",
@ -656,12 +678,15 @@ export function readZipFile(buffer: Buffer, processEntryCallback: (zipfile: yauz
function resolveNoteType(type: string | undefined): NoteType {
// BC for ZIPs created in Trilium 0.57 and older
if (type === "relation-map") {
switch (type) {
case "relation-map":
return "relationMap";
} else if (type === "note-map") {
case "note-map":
return "noteMap";
} else if (type === "web-view") {
case "web-view":
return "webView";
case "geoMap":
return "book";
}
if (type && (ALLOWED_NOTE_TYPES as readonly string[]).includes(type)) {

View File

@ -15,7 +15,6 @@ const noteTypes = [
{ type: "doc", defaultMime: "" },
{ type: "contentWidget", defaultMime: "" },
{ type: "mindMap", defaultMime: "application/json" },
{ type: "geoMap", defaultMime: "application/json" },
{ type: "aiChat", defaultMime: "application/json" }
];

View File

@ -3177,6 +3177,27 @@
"value": "bx bx-edit-alt",
"isInheritable": false,
"position": 40
},
{
"type": "relation",
"name": "internalLink",
"value": "_optionsTextNotes",
"isInheritable": false,
"position": 80
},
{
"type": "relation",
"name": "internalLink",
"value": "_optionsCodeNotes",
"isInheritable": false,
"position": 90
},
{
"type": "relation",
"name": "internalLink",
"value": "81SGnPGMk7Xc",
"isInheritable": false,
"position": 100
}
],
"format": "markdown",
@ -3431,7 +3452,7 @@
"0ESUbbAxVnoK",
"2FvYrpmOXm29"
],
"title": "Table",
"title": "Table View",
"notePosition": 20,
"prefix": null,
"isExpanded": false,
@ -3439,30 +3460,30 @@
"mime": "text/html",
"attributes": [
{
"type": "label",
"name": "iconClass",
"value": "bx bx-table",
"type": "relation",
"name": "internalLink",
"value": "OFXdgB2nNk1F",
"isInheritable": false,
"position": 10
},
{
"type": "relation",
"name": "internalLink",
"value": "OFXdgB2nNk1F",
"value": "m1lbrzyKDaRB",
"isInheritable": false,
"position": 20
},
{
"type": "relation",
"name": "internalLink",
"value": "oPVyFC7WL2Lp",
"value": "eIg8jdvaoNNd",
"isInheritable": false,
"position": 30
},
{
"type": "relation",
"name": "internalLink",
"value": "m1lbrzyKDaRB",
"value": "oPVyFC7WL2Lp",
"isInheritable": false,
"position": 40
},
@ -3481,15 +3502,15 @@
"position": 60
},
{
"type": "relation",
"name": "internalLink",
"value": "eIg8jdvaoNNd",
"type": "label",
"name": "iconClass",
"value": "bx bx-table",
"isInheritable": false,
"position": 70
"position": 10
}
],
"format": "markdown",
"dataFileName": "Table.md",
"dataFileName": "Table View.md",
"attachments": [
{
"attachmentId": "vJYUG9fLQ2Pd",
@ -3497,7 +3518,232 @@
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "Table_image.png"
"dataFileName": "Table View_image.png"
}
]
},
{
"isClone": false,
"noteId": "81SGnPGMk7Xc",
"notePath": [
"pOsGYCXsbNQG",
"gh7bpGYxajRS",
"BFs8mudNFgCS",
"0ESUbbAxVnoK",
"81SGnPGMk7Xc"
],
"title": "Geo Map View",
"notePosition": 40,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [
{
"type": "relation",
"name": "internalLink",
"value": "XpOYSgsLkTJy",
"isInheritable": false,
"position": 10
},
{
"type": "label",
"name": "iconClass",
"value": "bx bx-map-alt",
"isInheritable": false,
"position": 10
},
{
"type": "relation",
"name": "internalLink",
"value": "oPVyFC7WL2Lp",
"isInheritable": false,
"position": 20
},
{
"type": "relation",
"name": "internalLink",
"value": "IakOLONlIfGI",
"isInheritable": false,
"position": 30
},
{
"type": "relation",
"name": "internalLink",
"value": "KSZ04uQ2D1St",
"isInheritable": false,
"position": 40
},
{
"type": "relation",
"name": "internalLink",
"value": "0ESUbbAxVnoK",
"isInheritable": false,
"position": 50
}
],
"format": "markdown",
"dataFileName": "Geo Map View.md",
"attachments": [
{
"attachmentId": "1f07O0Z25ZRr",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "Geo Map View_image.png"
},
{
"attachmentId": "3oh61qhNLu7D",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "1_Geo Map View_image.png"
},
{
"attachmentId": "aCSNn9QlgHFi",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "2_Geo Map View_image.png"
},
{
"attachmentId": "aCuXZY7WV4li",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "3_Geo Map View_image.png"
},
{
"attachmentId": "agH6yREFgsoU",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "4_Geo Map View_image.png"
},
{
"attachmentId": "AHyDUM6R5HeG",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "5_Geo Map View_image.png"
},
{
"attachmentId": "CcjWLhE3KKfv",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "6_Geo Map View_image.png"
},
{
"attachmentId": "fQy8R1vxKhwN",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "7_Geo Map View_image.png"
},
{
"attachmentId": "gJ4Yz80jxcbn",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "8_Geo Map View_image.png"
},
{
"attachmentId": "I39BinT2gsN9",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "9_Geo Map View_image.png"
},
{
"attachmentId": "IeXU8SLZU7Oz",
"title": "image.jpg",
"role": "image",
"mime": "image/jpg",
"position": 10,
"dataFileName": "Geo Map View_image.jpg"
},
{
"attachmentId": "Mb9kRm63MxjE",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "10_Geo Map View_image.png"
},
{
"attachmentId": "Mx2xwNIk76ZS",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "11_Geo Map View_image.png"
},
{
"attachmentId": "oaahbsMRbqd2",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "12_Geo Map View_image.png"
},
{
"attachmentId": "pGf1p74KKGU4",
"title": "image.png",
"role": "image",
"mime": "image/jpg",
"position": 10,
"dataFileName": "13_Geo Map View_image.png"
},
{
"attachmentId": "tfa1TRUatWEh",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "14_Geo Map View_image.png"
},
{
"attachmentId": "tuNZ7Uk9WfX1",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "15_Geo Map View_image.png"
},
{
"attachmentId": "x6yBLIsY2LSv",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "16_Geo Map View_image.png"
},
{
"attachmentId": "yJMyBRYA3Kwi",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "17_Geo Map View_image.png"
},
{
"attachmentId": "ZvTlu9WMd37z",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "18_Geo Map View_image.png"
}
]
}
@ -7937,201 +8183,6 @@
}
]
},
{
"isClone": false,
"noteId": "81SGnPGMk7Xc",
"notePath": [
"pOsGYCXsbNQG",
"KSZ04uQ2D1St",
"81SGnPGMk7Xc"
],
"title": "Geo Map",
"notePosition": 200,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [
{
"type": "relation",
"name": "internalLink",
"value": "XpOYSgsLkTJy",
"isInheritable": false,
"position": 10
},
{
"type": "label",
"name": "iconClass",
"value": "bx bx-map-alt",
"isInheritable": false,
"position": 10
}
],
"format": "markdown",
"dataFileName": "Geo Map.md",
"attachments": [
{
"attachmentId": "1f07O0Z25ZRr",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "Geo Map_image.png"
},
{
"attachmentId": "3oh61qhNLu7D",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "1_Geo Map_image.png"
},
{
"attachmentId": "aCSNn9QlgHFi",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "2_Geo Map_image.png"
},
{
"attachmentId": "aCuXZY7WV4li",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "3_Geo Map_image.png"
},
{
"attachmentId": "agH6yREFgsoU",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "4_Geo Map_image.png"
},
{
"attachmentId": "AHyDUM6R5HeG",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "5_Geo Map_image.png"
},
{
"attachmentId": "CcjWLhE3KKfv",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "6_Geo Map_image.png"
},
{
"attachmentId": "DapDey8gMiFc",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "7_Geo Map_image.png"
},
{
"attachmentId": "fQy8R1vxKhwN",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "8_Geo Map_image.png"
},
{
"attachmentId": "gJ4Yz80jxcbn",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "9_Geo Map_image.png"
},
{
"attachmentId": "I39BinT2gsN9",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "10_Geo Map_image.png"
},
{
"attachmentId": "IeXU8SLZU7Oz",
"title": "image.jpg",
"role": "image",
"mime": "image/jpg",
"position": 10,
"dataFileName": "Geo Map_image.jpg"
},
{
"attachmentId": "Mb9kRm63MxjE",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "11_Geo Map_image.png"
},
{
"attachmentId": "Mx2xwNIk76ZS",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "12_Geo Map_image.png"
},
{
"attachmentId": "oaahbsMRbqd2",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "13_Geo Map_image.png"
},
{
"attachmentId": "pGf1p74KKGU4",
"title": "image.png",
"role": "image",
"mime": "image/jpg",
"position": 10,
"dataFileName": "14_Geo Map_image.png"
},
{
"attachmentId": "tfa1TRUatWEh",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "15_Geo Map_image.png"
},
{
"attachmentId": "x6yBLIsY2LSv",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "16_Geo Map_image.png"
},
{
"attachmentId": "yJMyBRYA3Kwi",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "17_Geo Map_image.png"
},
{
"attachmentId": "ZvTlu9WMd37z",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "18_Geo Map_image.png"
}
]
},
{
"isClone": false,
"noteId": "W8vYD3Q1zjCR",

File diff suppressed because one or more lines are too long

View File

@ -7,7 +7,7 @@ For example:
* <a class="reference-link" href="../Note%20Types/Text.md">Text</a> notes are represented internally as HTML, using the <a class="reference-link" href="Technologies%20used/CKEditor.md">CKEditor</a> representation. Note that due to the custom plugins, some HTML elements are specific to Trilium only, for example the admonitions.
* <a class="reference-link" href="../Note%20Types/Code.md">Code</a> notes are plain text and are represented internally as-is.
* <a class="reference-link" href="../Note%20Types/Geo%20Map.md">Geo Map</a> notes contain only minimal information (viewport, zoom) as a JSON.
* <a class="reference-link" href="../Basic%20Concepts%20and%20Features/Notes/Note%20List/Geo%20Map%20View.md">Geo Map</a> notes contain only minimal information (viewport, zoom) as a JSON.
* <a class="reference-link" href="../Note%20Types/Canvas.md">Canvas</a> notes are represented as JSON, with Trilium's own information alongside with <a class="reference-link" href="Technologies%20used/Excalidraw.md">Excalidraw</a>'s internal JSON representation format.
* <a class="reference-link" href="../Note%20Types/Mind%20Map.md">Mind Map</a> notes are represented as JSON, with the internal format of <a class="reference-link" href="Technologies%20used/MindElixir.md">MindElixir</a>.

View File

@ -16,7 +16,7 @@ Trilium allows you to share selected notes as **publicly accessible** read-only
### By note type
<figure class="table" style="width:100%;"><table class="ck-table-resized"><colgroup><col style="width:19.92%;"><col style="width:41.66%;"><col style="width:38.42%;"></colgroup><thead><tr><th>&nbsp;</th><th>Supported features</th><th>Limitations</th></tr></thead><tbody><tr><th><a class="reference-link" href="../Note%20Types/Text.md">Text</a></th><td><ul><li>Table of contents.</li><li>Syntax highlight of code blocks, provided a language is selected (does not work if “Auto-detected” is enabled).</li><li>Rendering for math equations.</li></ul></td><td><ul><li>Including notes is not supported.</li><li>Inline Mermaid diagrams are not rendered.</li></ul></td></tr><tr><th><a class="reference-link" href="../Note%20Types/Code.md">Code</a></th><td><ul><li>Basic support (displaying the contents of the note in a monospace font).</li></ul></td><td><ul><li>No syntax highlight.</li></ul></td></tr><tr><th><a class="reference-link" href="../Note%20Types/Saved%20Search.md">Saved Search</a></th><td>Not supported.</td></tr><tr><th><a class="reference-link" href="../Note%20Types/Relation%20Map.md">Relation Map</a></th><td>Not supported.</td></tr><tr><th><a class="reference-link" href="../Note%20Types/Note%20Map.md">Note Map</a></th><td>Not supported.</td></tr><tr><th><a class="reference-link" href="../Note%20Types/Render%20Note.md">Render Note</a></th><td>Not supported.</td></tr><tr><th><a class="reference-link" href="../Note%20Types/Book.md">Book</a></th><td><ul><li>The child notes are displayed in a fixed format.&nbsp;</li></ul></td><td><ul><li>More advanced view types such as the calendar view are not supported.</li></ul></td></tr><tr><th><a class="reference-link" href="../Note%20Types/Mermaid%20Diagrams.md">Mermaid Diagrams</a></th><td><ul><li>The diagram is displayed as a vector image.</li></ul></td><td><ul><li>No further interaction supported.</li></ul></td></tr><tr><th><a class="reference-link" href="../Note%20Types/Canvas.md">Canvas</a></th><td><ul><li>The diagram is displayed as a vector image.</li></ul></td><td><ul><li>No further interaction supported.</li></ul></td></tr><tr><th><a class="reference-link" href="../Note%20Types/Web%20View.md">Web View</a></th><td>Not supported.</td></tr><tr><th><a class="reference-link" href="../Note%20Types/Mind%20Map.md">Mind Map</a></th><td>The diagram is displayed as a vector image.</td><td><ul><li>No further interaction supported.</li></ul></td></tr><tr><th><a class="reference-link" href="../Note%20Types/Geo%20Map.md">Geo Map</a></th><td>Not supported.</td></tr><tr><th><a class="reference-link" href="../Note%20Types/File.md">File</a></th><td>Basic interaction (downloading the file).</td><td><ul><li>No further interaction supported.</li></ul></td></tr></tbody></table></figure>
<figure class="table" style="width:100%;"><table class="ck-table-resized"><colgroup><col style="width:19.92%;"><col style="width:41.66%;"><col style="width:38.42%;"></colgroup><thead><tr><th>&nbsp;</th><th>Supported features</th><th>Limitations</th></tr></thead><tbody><tr><th><a class="reference-link" href="../Note%20Types/Text.md">Text</a></th><td><ul><li>Table of contents.</li><li>Syntax highlight of code blocks, provided a language is selected (does not work if “Auto-detected” is enabled).</li><li>Rendering for math equations.</li></ul></td><td><ul><li>Including notes is not supported.</li><li>Inline Mermaid diagrams are not rendered.</li></ul></td></tr><tr><th><a class="reference-link" href="../Note%20Types/Code.md">Code</a></th><td><ul><li>Basic support (displaying the contents of the note in a monospace font).</li></ul></td><td><ul><li>No syntax highlight.</li></ul></td></tr><tr><th><a class="reference-link" href="../Note%20Types/Saved%20Search.md">Saved Search</a></th><td>Not supported.</td></tr><tr><th><a class="reference-link" href="../Note%20Types/Relation%20Map.md">Relation Map</a></th><td>Not supported.</td></tr><tr><th><a class="reference-link" href="../Note%20Types/Note%20Map.md">Note Map</a></th><td>Not supported.</td></tr><tr><th><a class="reference-link" href="../Note%20Types/Render%20Note.md">Render Note</a></th><td>Not supported.</td></tr><tr><th><a class="reference-link" href="../Note%20Types/Book.md">Book</a></th><td><ul><li>The child notes are displayed in a fixed format.&nbsp;</li></ul></td><td><ul><li>More advanced view types such as the calendar view are not supported.</li></ul></td></tr><tr><th><a class="reference-link" href="../Note%20Types/Mermaid%20Diagrams.md">Mermaid Diagrams</a></th><td><ul><li>The diagram is displayed as a vector image.</li></ul></td><td><ul><li>No further interaction supported.</li></ul></td></tr><tr><th><a class="reference-link" href="../Note%20Types/Canvas.md">Canvas</a></th><td><ul><li>The diagram is displayed as a vector image.</li></ul></td><td><ul><li>No further interaction supported.</li></ul></td></tr><tr><th><a class="reference-link" href="../Note%20Types/Web%20View.md">Web View</a></th><td>Not supported.</td></tr><tr><th><a class="reference-link" href="../Note%20Types/Mind%20Map.md">Mind Map</a></th><td>The diagram is displayed as a vector image.</td><td><ul><li>No further interaction supported.</li></ul></td></tr><tr><th><a class="reference-link" href="../Basic%20Concepts%20and%20Features/Notes/Note%20List/Geo%20Map%20View.md">Geo Map</a></th><td>Not supported.</td></tr><tr><th><a class="reference-link" href="../Note%20Types/File.md">File</a></th><td>Basic interaction (downloading the file).</td><td><ul><li>No further interaction supported.</li></ul></td></tr></tbody></table></figure>
While the sharing feature is powerful, it has some limitations:

View File

@ -1,5 +1,5 @@
# Leaflet
Leaflet is the library behind [Geo map](../../Note%20Types/Geo%20Map.md) notes.
Leaflet is the library behind [Geo map](../../Basic%20Concepts%20and%20Features/Notes/Note%20List/Geo%20Map%20View.md) notes.
## Plugins

View File

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 249 KiB

View File

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 141 KiB

View File

Before

Width:  |  Height:  |  Size: 210 KiB

After

Width:  |  Height:  |  Size: 210 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

Before

Width:  |  Height:  |  Size: 338 KiB

After

Width:  |  Height:  |  Size: 338 KiB

View File

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

View File

@ -0,0 +1,133 @@
# Geo Map View
> [!IMPORTANT]
> Starting with Trilium v0.97.0, the geo map has been converted from a standalone [note type](../../../Note%20Types.md) to a type of view for the <a class="reference-link" href="../Note%20List.md">Note List</a>. 
<figure class="image image-style-align-center"><img style="aspect-ratio:892/675;" src="9_Geo Map View_image.png" width="892" height="675"></figure>
This note type displays the children notes on a geographical map, based on an attribute. It is also possible to add new notes at a specific location using the built-in interface.
## Creating a new geo map
<figure class="table"><table><thead><tr><th>&nbsp;</th><th>&nbsp;</th><th>&nbsp;</th></tr></thead><tbody><tr><td>1</td><td><figure class="image"><img style="aspect-ratio:483/413;" src="15_Geo Map View_image.png" width="483" height="413"></figure></td><td>Right click on any note on the note tree and select <em>Insert child note</em><em>Geo Map (beta)</em>.</td></tr><tr><td>2</td><td><figure class="image image-style-align-center image_resized" style="width:53.44%;"><img style="aspect-ratio:1720/1396;" src="8_Geo Map View_image.png" width="1720" height="1396"></figure></td><td>By default the map will be empty and will show the entire world.</td></tr></tbody></table></figure>
## Repositioning the map
* Click and drag the map in order to move across the map.
* Use the mouse wheel, two-finger gesture on a touchpad or the +/- buttons on the top-left to adjust the zoom.
The position on the map and the zoom are saved inside the map note and restored when visiting again the note.
## Adding a marker using the map
### Adding a new note using the plus button
<figure class="table"><table><thead><tr><th>&nbsp;</th><th>&nbsp;</th><th>&nbsp;</th></tr></thead><tbody><tr><td>1</td><td>To create a marker, first navigate to the desired point on the map. Then press the <img src="10_Geo Map View_image.png"> button in the&nbsp;<a href="../../UI%20Elements/Floating%20buttons.md">Floating buttons</a>&nbsp;(top-right) area.&nbsp;<br><br>If the button is not visible, make sure the button section is visible by pressing the chevron button (<img src="17_Geo Map View_image.png">) in the top-right of the map.</td><td>&nbsp;</td></tr><tr><td>2</td><td><img class="image_resized" style="aspect-ratio:1730/416;width:100%;" src="2_Geo Map View_image.png" width="1730" height="416"></td><td>Once pressed, the map will enter in the insert mode, as illustrated by the notification.&nbsp;&nbsp;&nbsp;&nbsp;<br><br>Simply click the point on the map where to place the marker, or the Escape key to cancel.</td></tr><tr><td>3</td><td><img class="image_resized" style="aspect-ratio:1586/404;width:100%;" src="7_Geo Map View_image.png" width="1586" height="404"></td><td>Enter the name of the marker/note to be created.</td></tr><tr><td>4</td><td><img class="image_resized" style="aspect-ratio:1696/608;width:100%;" src="16_Geo Map View_image.png" width="1696" height="608"></td><td>Once confirmed, the marker will show up on the map and it will also be displayed as a child note of the map.</td></tr></tbody></table></figure>
### Adding a new note using the contextual menu
1. Right click anywhere on the map, where to place the newly created marker (and corresponding note).
2. Select _Add a marker at this location_.
3. Enter the name of the newly created note.
4. The map should be updated with the new marker.
### Adding an existing note on note from the note tree
1. Select the desired note in the <a class="reference-link" href="../../UI%20Elements/Note%20Tree.md">Note Tree</a>.
2. Hold the mouse on the note and drag it to the map to the desired location.
3. The map should be updated with the new marker.
This works for:
* Notes that are not part of the geo map, case in which a [clone](../Cloning%20Notes.md) will be created.
* Notes that are a child of the geo map but not yet positioned on the map.
* Notes that are a child of the geo map and also positioned, case in which the marker will be relocated to the new position.
## How the location of the markers is stored
The location of a marker is stored in the `#geolocation` attribute of the child notes:
<img src="18_Geo Map View_image.png" width="1288" height="278">
This value can be added manually if needed. The value of the attribute is made up of the latitude and longitude separated by a comma.
## Repositioning markers
It's possible to reposition existing markers by simply drag and dropping them to the new destination.
As soon as the mouse is released, the new position is saved.
If moved by mistake, there is currently no way to undo the change. If the mouse was not yet released, it's possible to force a refresh of the page (<kbd>Ctrl</kbd>+<kbd>R</kbd> ) to cancel it.
## Interaction with the markers
* Hovering over a marker will display the content of the note it belongs to.
* Clicking on the note title in the tooltip will navigate to the note in the current view.
* Middle-clicking the marker will open the note in a new tab.
* Right-clicking the marker will open a contextual menu allowing:
## Contextual menu
It's possible to press the right mouse button to display a contextual menu.
1. If right-clicking an empty section of the map (not on a marker), it allows to:
1. Displays the latitude and longitude. Clicking this option will copy them to the clipboard.
2. Open the location using an external application (if the operating system supports it).
3. Adding a new marker at that location.
2. If right-clicking on a marker, it allows to:
1. Displays the latitude and longitude. Clicking this option will copy them to the clipboard.
2. Open the location using an external application (if the operating system supports it).
3. Open the note in a new tab, split or window.
4. Remove the marker from the map, which will remove the `#geolocation` attribute of the note. To add it back again, the coordinates have to be manually added back in.
## Icon and color of the markers
<figure class="image image-style-align-center"><img style="aspect-ratio:523/295;" src="Geo Map View_image.jpg" alt="image" width="523" height="295"></figure>
The markers will have the same icon as the note.
It's possible to add a custom color to a marker by assigning them a `#color` attribute such as `#color=green`.
## Adding the coordinates manually
In a nutshell, create a child note and set the `#geolocation` attribute to the coordinates.
The value of the attribute is made up of the latitude and longitude separated by a comma.
### Adding from Google Maps
<figure class="table" style="width:100%;"><table class="ck-table-resized"><colgroup><col style="width:2.77%;"><col style="width:33.24%;"><col style="width:63.99%;"></colgroup><thead><tr><th>&nbsp;</th><th>&nbsp;</th><th>&nbsp;</th></tr></thead><tbody><tr><td>1</td><td><figure class="image image-style-align-center image_resized" style="width:56.84%;"><img style="aspect-ratio:732/918;" src="12_Geo Map View_image.png" width="732" height="918"></figure></td><td>Go to Google Maps on the web and look for a desired location, right click on it and a context menu will show up.&nbsp;&nbsp;&nbsp;&nbsp;<br><br>Simply click on the first item displaying the coordinates and they will be copied to clipboard.&nbsp;&nbsp;&nbsp;&nbsp;<br><br>Then paste the value inside the text box into the <code>#geolocation</code> attribute of a child note of the map (don't forget to surround the value with a <code>"</code> character).</td></tr><tr><td>2</td><td><figure class="image image-style-align-center image_resized" style="width:100%;"><img style="aspect-ratio:518/84;" src="4_Geo Map View_image.png" width="518" height="84"></figure></td><td>In Trilium, create a child note under the map.</td></tr><tr><td>3</td><td><figure class="image image-style-align-center image_resized" style="width:100%;"><img style="aspect-ratio:1074/276;" src="11_Geo Map View_image.png" width="1074" height="276"></figure></td><td>And then go to Owned Attributes and type <code>#geolocation="</code>, then paste from the clipboard as-is and then add the ending <code>"</code> character. Press Enter to confirm and the map should now be updated to contain the new note.</td></tr></tbody></table></figure>
### Adding from OpenStreetMap
Similarly to the Google Maps approach:
<figure class="table" style="width:100%;"><table class="ck-table-resized"><colgroup><col style="width:2.77%;"><col style="width:33.42%;"><col style="width:63.81%;"></colgroup><thead><tr><th>&nbsp;</th><th>&nbsp;</th><th>&nbsp;</th></tr></thead><tbody><tr><td>1</td><td><img class="image_resized" style="aspect-ratio:562/454;width:100%;" src="1_Geo Map View_image.png" width="562" height="454"></td><td>Go to any location on openstreetmap.org and right click to bring up the context menu. Select the “Show address” item.</td></tr><tr><td>2</td><td><img class="image_resized" style="aspect-ratio:696/480;width:100%;" src="Geo Map View_image.png" width="696" height="480"></td><td>The address will be visible in the top-left of the screen, in the place of the search bar.&nbsp;&nbsp;&nbsp;&nbsp;<br><br>Select the coordinates and copy them into the clipboard.</td></tr><tr><td>3</td><td><img class="image_resized" style="aspect-ratio:640/276;width:100%;" src="5_Geo Map View_image.png" width="640" height="276"></td><td>Simply paste the value inside the text box into the <code>#geolocation</code> attribute of a child note of the map and then it should be displayed on the map.</td></tr></tbody></table></figure>
## Adding GPS tracks (.gpx)
Trilium has basic support for displaying GPS tracks on the geo map.
<figure class="table" style="width:100%;"><table class="ck-table-resized"><colgroup><col style="width:2.77%;"><col style="width:30.22%;"><col style="width:67.01%;"></colgroup><thead><tr><th>&nbsp;</th><th>&nbsp;</th><th>&nbsp;</th></tr></thead><tbody><tr><td>1</td><td><figure class="image image-style-align-center"><img style="aspect-ratio:226/74;" src="3_Geo Map View_image.png" width="226" height="74"></figure></td><td>To add a track, simply drag &amp; drop a .gpx file inside the geo map in the note tree.</td></tr><tr><td>2</td><td><figure class="image image-style-align-center"><img style="aspect-ratio:322/222;" src="14_Geo Map View_image.png" width="322" height="222"></figure></td><td>In order for the file to be recognized as a GPS track, it needs to show up as <code>application/gpx+xml</code> in the <em>File type</em> field.</td></tr><tr><td>3</td><td><figure class="image image-style-align-center"><img style="aspect-ratio:620/530;" src="6_Geo Map View_image.png" width="620" height="530"></figure></td><td>When going back to the map, the track should now be visible.&nbsp;&nbsp;&nbsp;&nbsp;<br><br>The start and end points of the track are indicated by the two blue markers.</td></tr></tbody></table></figure>
> [!NOTE]
> The starting point of the track will be displayed as a marker, with the name of the note underneath. The start marker will also respect the icon and the `color` of the note. The end marker is displayed with a distinct icon.
>
> If the GPX contains waypoints, they will also be displayed. If they have a name, it is displayed when hovering over it with the mouse.
## Read-only mode
When a map is in read-only all editing features will be disabled such as:
* The add button in the <a class="reference-link" href="../../UI%20Elements/Floating%20buttons.md">Floating buttons</a>.
* Dragging markers.
* Editing from the contextual menu (removing locations or adding new items).
To enable read-only mode simply press the _Lock_ icon from the <a class="reference-link" href="../../UI%20Elements/Floating%20buttons.md">Floating buttons</a>. To disable it, press the button again.
## Troubleshooting
<figure class="image image-style-align-right image_resized" style="width:34.06%;"><img style="aspect-ratio:678/499;" src="13_Geo Map View_image.png" width="678" height="499"></figure>
### Grid-like artifacts on the map
This occurs if the application is not at 100% zoom which causes the pixels of the map to not render correctly due to fractional scaling. The only possible solution is to set the UI zoom at 100% (default keyboard shortcut is <kbd>Ctrl</kbd>+<kbd>0</kbd>).

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -1,5 +1,5 @@
# Table
<figure class="image"><img style="aspect-ratio:1050/259;" src="Table_image.png" width="1050" height="259"></figure>
# Table View
<figure class="image"><img style="aspect-ratio:1050/259;" src="Table View_image.png" width="1050" height="259"></figure>
The table view displays information in a grid, where the rows are individual notes and the columns are <a class="reference-link" href="../../../Advanced%20Usage/Attributes/Promoted%20Attributes.md">Promoted Attributes</a>. In addition, values are editable.

View File

@ -40,3 +40,4 @@ When pressed, the note will become editable but will become read-only again afte
Some note types have a special behavior based on whether the read-only mode is enabled:
* <a class="reference-link" href="../../Note%20Types/Mermaid%20Diagrams.md">Mermaid Diagrams</a> will hide the Mermaid source code and display the diagram preview in full-size. In this case, the read-only mode can be easily toggled on or off via a dedicated button in the <a class="reference-link" href="../UI%20Elements/Floating%20buttons.md">Floating buttons</a> area.
* <a class="reference-link" href="Note%20List/Geo%20Map%20View.md">Geo Map View</a> will disallow all interaction that would otherwise change the map (dragging notes, adding new items).

View File

@ -25,4 +25,4 @@ It is possible to change the type of a note after it has been created via the _B
The following note types are supported by Trilium:
<figure class="table" style="width:100%;"><table class="ck-table-resized"><colgroup><col style="width:29.42%;"><col style="width:70.58%;"></colgroup><thead><tr><th>Note Type</th><th>Description</th></tr></thead><tbody><tr><td><a class="reference-link" href="Note%20Types/Text.md">Text</a></td><td>The default note type, which allows for rich text formatting, images, admonitions and right-to-left support.</td></tr><tr><td><a class="reference-link" href="Note%20Types/Code.md">Code</a></td><td>Uses a mono-space font and can be used to store larger chunks of code or plain text than a text note, and has better syntax highlighting.</td></tr><tr><td><a class="reference-link" href="Note%20Types/Saved%20Search.md">Saved Search</a></td><td>Stores the information about a search (the search text, criteria, etc.) for later use. Can be used for quick filtering of a large amount of notes, for example. The search can easily be triggered.</td></tr><tr><td><a class="reference-link" href="Note%20Types/Relation%20Map.md">Relation Map</a></td><td>Allows easy creation of notes and relations between them. Can be used for mainly relational data such as a family tree.</td></tr><tr><td><a class="reference-link" href="Note%20Types/Note%20Map.md">Note Map</a></td><td>Displays the relationships between the notes, whether via relations or their hierarchical structure.</td></tr><tr><td><a class="reference-link" href="Note%20Types/Render%20Note.md">Render Note</a></td><td>Used in&nbsp;<a class="reference-link" href="Scripting.md">Scripting</a>, it displays the HTML content of another note. This allows displaying any kind of content, provided there is a script behind it to generate it.</td></tr><tr><td><a class="reference-link" href="Note%20Types/Book.md">Book</a></td><td><p>Displays the children of the note either as a grid, a list, or for a more specialized case: a calendar.</p><p>Generally useful for easy reading of short notes.</p></td></tr><tr><td><a class="reference-link" href="Note%20Types/Mermaid%20Diagrams.md">Mermaid Diagrams</a></td><td>Displays diagrams such as bar charts, flow charts, state diagrams, etc. Requires a bit of technical knowledge since the diagrams are written in a specialized format.</td></tr><tr><td><a class="reference-link" href="Note%20Types/Canvas.md">Canvas</a></td><td>Allows easy drawing of sketches, diagrams, handwritten content. Uses the same technology behind <a href="https://excalidraw.com">excalidraw.com</a>.</td></tr><tr><td><a class="reference-link" href="Note%20Types/Web%20View.md">Web View</a></td><td>Displays the content of an external web page, similar to a browser.</td></tr><tr><td><a class="reference-link" href="Note%20Types/Mind%20Map.md">Mind Map</a></td><td>Easy for brainstorming ideas, by placing them in a hierarchical layout.</td></tr><tr><td><a class="reference-link" href="Note%20Types/Geo%20Map.md">Geo Map</a></td><td>Displays the children of the note as a geographical map, one use-case would be to plan vacations. It even has basic support for tracks. Notes can also be created from it.</td></tr><tr><td><a class="reference-link" href="Note%20Types/File.md">File</a></td><td>Represents an uploaded file such as PDFs, images, video or audio files.</td></tr></tbody></table></figure>
<figure class="table" style="width:100%;"><table class="ck-table-resized"><colgroup><col style="width:29.42%;"><col style="width:70.58%;"></colgroup><thead><tr><th>Note Type</th><th>Description</th></tr></thead><tbody><tr><td><a class="reference-link" href="Note%20Types/Text.md">Text</a></td><td>The default note type, which allows for rich text formatting, images, admonitions and right-to-left support.</td></tr><tr><td><a class="reference-link" href="Note%20Types/Code.md">Code</a></td><td>Uses a mono-space font and can be used to store larger chunks of code or plain text than a text note, and has better syntax highlighting.</td></tr><tr><td><a class="reference-link" href="Note%20Types/Saved%20Search.md">Saved Search</a></td><td>Stores the information about a search (the search text, criteria, etc.) for later use. Can be used for quick filtering of a large amount of notes, for example. The search can easily be triggered.</td></tr><tr><td><a class="reference-link" href="Note%20Types/Relation%20Map.md">Relation Map</a></td><td>Allows easy creation of notes and relations between them. Can be used for mainly relational data such as a family tree.</td></tr><tr><td><a class="reference-link" href="Note%20Types/Note%20Map.md">Note Map</a></td><td>Displays the relationships between the notes, whether via relations or their hierarchical structure.</td></tr><tr><td><a class="reference-link" href="Note%20Types/Render%20Note.md">Render Note</a></td><td>Used in&nbsp;<a class="reference-link" href="Scripting.md">Scripting</a>, it displays the HTML content of another note. This allows displaying any kind of content, provided there is a script behind it to generate it.</td></tr><tr><td><a class="reference-link" href="Note%20Types/Book.md">Book</a></td><td><p>Displays the children of the note either as a grid, a list, or for a more specialized case: a calendar.</p><p>Generally useful for easy reading of short notes.</p></td></tr><tr><td><a class="reference-link" href="Note%20Types/Mermaid%20Diagrams.md">Mermaid Diagrams</a></td><td>Displays diagrams such as bar charts, flow charts, state diagrams, etc. Requires a bit of technical knowledge since the diagrams are written in a specialized format.</td></tr><tr><td><a class="reference-link" href="Note%20Types/Canvas.md">Canvas</a></td><td>Allows easy drawing of sketches, diagrams, handwritten content. Uses the same technology behind <a href="https://excalidraw.com">excalidraw.com</a>.</td></tr><tr><td><a class="reference-link" href="Note%20Types/Web%20View.md">Web View</a></td><td>Displays the content of an external web page, similar to a browser.</td></tr><tr><td><a class="reference-link" href="Note%20Types/Mind%20Map.md">Mind Map</a></td><td>Easy for brainstorming ideas, by placing them in a hierarchical layout.</td></tr><tr><td><a class="reference-link" href="Basic%20Concepts%20and%20Features/Notes/Note%20List/Geo%20Map%20View.md">Geo Map</a></td><td>Displays the children of the note as a geographical map, one use-case would be to plan vacations. It even has basic support for tracks. Notes can also be created from it.</td></tr><tr><td><a class="reference-link" href="Note%20Types/File.md">File</a></td><td>Represents an uploaded file such as PDFs, images, video or audio files.</td></tr></tbody></table></figure>

Some files were not shown because too many files have changed in this diff Show More