Skip to content
This repository was archived by the owner on Jun 24, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/client/src/components/app_context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ export type CommandMappings = {
buildIcon(name: string): NativeImage;
};
refreshTouchBar: CommandData;
reloadTextEditor: CommandData;
};

type EventMappings = {
Expand Down
4 changes: 4 additions & 0 deletions apps/client/src/services/froca.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,10 @@ class FrocaImpl implements Froca {
}

async getNotes(noteIds: string[] | JQuery<string>, silentNotFoundError = false): Promise<FNote[]> {
if (noteIds.length === 0) {
return [];
}

noteIds = Array.from(new Set(noteIds)); // make unique
const missingNoteIds = noteIds.filter((noteId) => !this.notes[noteId]);

Expand Down
60 changes: 48 additions & 12 deletions apps/client/src/services/note_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { t } from "./i18n.js";
import type { MenuItem } from "../menus/context_menu.js";
import type { TreeCommandNames } from "../menus/tree_context_menu.js";

const SEPARATOR = { title: "----" };

async function getNoteTypeItems(command?: TreeCommandNames) {
const items: MenuItem<TreeCommandNames>[] = [
{ title: t("note_types.text"), command, type: "text", uiIcon: "bx bx-note" },
Expand All @@ -18,25 +20,59 @@ async function getNoteTypeItems(command?: TreeCommandNames) {
{ title: t("note_types.web-view"), command, type: "webView", uiIcon: "bx bx-globe-alt" },
{ title: t("note_types.mind-map"), command, type: "mindMap", uiIcon: "bx bx-sitemap" },
{ title: t("note_types.geo-map"), command, type: "geoMap", uiIcon: "bx bx-map-alt" },
...await getBuiltInTemplates(command),
...await getUserTemplates(command)
];

return items;
}

async function getUserTemplates(command?: TreeCommandNames) {
const templateNoteIds = await server.get<string[]>("search-templates");
const templateNotes = await froca.getNotes(templateNoteIds);
if (templateNotes.length === 0) {
return [];
}

const items: MenuItem<TreeCommandNames>[] = [
SEPARATOR
];
for (const templateNote of templateNotes) {
items.push({
title: templateNote.title,
uiIcon: templateNote.getIcon(),
command: command,
type: templateNote.type,
templateNoteId: templateNote.noteId
});
}
return items;
}

async function getBuiltInTemplates(command?: TreeCommandNames) {
const templatesRoot = await froca.getNote("_templates");
if (!templatesRoot) {
console.warn("Unable to find template root.");
return [];
}

if (templateNotes.length > 0) {
items.push({ title: "----" });

for (const templateNote of templateNotes) {
items.push({
title: templateNote.title,
uiIcon: templateNote.getIcon(),
command: command,
type: templateNote.type,
templateNoteId: templateNote.noteId
});
}
const childNotes = await templatesRoot.getChildNotes();
if (childNotes.length === 0) {
return [];
}

const items: MenuItem<TreeCommandNames>[] = [
SEPARATOR
];
for (const templateNote of childNotes) {
items.push({
title: templateNote.title,
uiIcon: templateNote.getIcon(),
command: command,
type: templateNote.type,
templateNoteId: templateNote.noteId
});
}
return items;
}

Expand Down
9 changes: 6 additions & 3 deletions apps/client/src/stylesheets/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -1280,16 +1280,19 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
padding: 0.5em 1em !important;
}

.ck.ck-slash-command-button__text-part {
.ck.ck-slash-command-button__text-part,
.ck.ck-template-form__text-part {
margin-left: 0.5em;
line-height: 1.2em !important;
}

.ck.ck-slash-command-button__text-part > span {
.ck.ck-slash-command-button__text-part > span,
.ck.ck-template-form__text-part > span {
line-height: inherit !important;
}

.ck.ck-slash-command-button__text-part .ck.ck-slash-command-button__description {
.ck.ck-slash-command-button__text-part .ck.ck-slash-command-button__description,
.ck.ck-template-form__text-part .ck-template-form__description {
display: block;
opacity: 0.8;
}
Expand Down
5 changes: 5 additions & 0 deletions apps/client/src/types-assets.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,9 @@ declare module "*?url" {
export default path;
}

declare module "*?raw" {
var content: string;
export default content;
}

declare module "boxicons/css/boxicons.min.css" { }
8 changes: 7 additions & 1 deletion apps/client/src/widgets/type_widgets/ckeditor/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ import { ensureMimeTypesForHighlighting, isSyntaxHighlightEnabled } from "../../
import utils from "../../../services/utils.js";
import emojiDefinitionsUrl from "@triliumnext/ckeditor5/emoji_definitions/en.json?url";
import { copyTextWithToast } from "../../../services/clipboard_ext.js";
import getTemplates from "./snippets.js";

const TEXT_FORMATTING_GROUP = {
label: "Text formatting",
icon: "text"
};

export function buildConfig(): EditorConfig {
export async function buildConfig(): Promise<EditorConfig> {
return {
image: {
styles: {
Expand Down Expand Up @@ -126,6 +127,9 @@ export function buildConfig(): EditorConfig {
dropdownLimit: Number.MAX_SAFE_INTEGER,
extraCommands: buildExtraCommands()
},
template: {
definitions: await getTemplates()
},
// This value must be kept in sync with the language defined in webpack.config.js.
language: "en"
};
Expand Down Expand Up @@ -206,6 +210,7 @@ export function buildClassicToolbar(multilineToolbar: boolean) {
"outdent",
"indent",
"|",
"insertTemplate",
"markdownImport",
"cuttonote",
"findAndReplace"
Expand Down Expand Up @@ -262,6 +267,7 @@ export function buildFloatingToolbar() {
"outdent",
"indent",
"|",
"insertTemplate",
"imageUpload",
"markdownImport",
"specialCharacters",
Expand Down
105 changes: 105 additions & 0 deletions apps/client/src/widgets/type_widgets/ckeditor/snippets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import debounce from "debounce";
import froca from "../../../services/froca.js";
import type LoadResults from "../../../services/load_results.js";
import search from "../../../services/search.js";
import type { TemplateDefinition } from "@triliumnext/ckeditor5";
import appContext from "../../../components/app_context.js";
import TemplateIcon from "@ckeditor/ckeditor5-icons/theme/icons/template.svg?raw";
import type FNote from "../../../entities/fnote.js";

interface TemplateData {
title: string;
description?: string;
content?: string;
}

let templateCache: Map<string, TemplateData> = new Map();
const debouncedHandleContentUpdate = debounce(handleContentUpdate, 1000);

/**
* Generates the list of snippets based on the user's notes to be passed down to the CKEditor configuration.
*
* @returns the list of templates.
*/
export default async function getTemplates() {
// Build the definitions and populate the cache.
const snippets = await search.searchForNotes("#textSnippet");
const definitions: TemplateDefinition[] = [];
for (const snippet of snippets) {
const { description } = await invalidateCacheFor(snippet);

definitions.push({
title: snippet.title,
data: () => templateCache.get(snippet.noteId)?.content ?? "",
icon: TemplateIcon,
description
});
}
return definitions;
}

async function invalidateCacheFor(snippet: FNote) {
const description = snippet.getLabelValue("textSnippetDescription");
const data: TemplateData = {
title: snippet.title,
description: description ?? undefined,
content: await snippet.getContent()
};
templateCache.set(snippet.noteId, data);
return data;
}

function handleFullReload() {
console.warn("Full text editor reload needed");
appContext.triggerCommand("reloadTextEditor");
}

async function handleContentUpdate(affectedNoteIds: string[]) {
const updatedNoteIds = new Set(affectedNoteIds);
const templateNoteIds = new Set(templateCache.keys());
const affectedTemplateNoteIds = templateNoteIds.intersection(updatedNoteIds);

await froca.getNotes(affectedNoteIds);

let fullReloadNeeded = false;
for (const affectedTemplateNoteId of affectedTemplateNoteIds) {
try {
const template = await froca.getNote(affectedTemplateNoteId);
if (!template) {
console.warn("Unable to obtain template with ID ", affectedTemplateNoteId);
continue;
}

const newTitle = template.title;
if (templateCache.get(affectedTemplateNoteId)?.title !== newTitle) {
fullReloadNeeded = true;
break;
}

await invalidateCacheFor(template);
} catch (e) {
// If a note was not found while updating the cache, it means we need to do a full reload.
fullReloadNeeded = true;
}
}

if (fullReloadNeeded) {
handleFullReload();
}
}

export function updateTemplateCache(loadResults: LoadResults): boolean {
const affectedNoteIds = loadResults.getNoteIds();

// React to creation or deletion of text snippets.
if (loadResults.getAttributeRows().find((attr) =>
attr.type === "label" &&
(attr.name === "textSnippet" || attr.name === "textSnippetDescription"))) {
handleFullReload();
} else if (affectedNoteIds.length > 0) {
// Update content and titles if one of the template notes were updated.
debouncedHandleContentUpdate(affectedNoteIds);
}

return false;
}
27 changes: 22 additions & 5 deletions apps/client/src/widgets/type_widgets/editable_text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { getMermaidConfig } from "../../services/mermaid.js";
import { PopupEditor, ClassicEditor, EditorWatchdog, type CKTextEditor, type MentionFeed, type WatchdogConfig } from "@triliumnext/ckeditor5";
import "@triliumnext/ckeditor5/index.css";
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
import { updateTemplateCache } from "./ckeditor/snippets.js";

const mentionSetup: MentionFeed[] = [
{
Expand Down Expand Up @@ -193,7 +194,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {

const finalConfig = {
...editorConfig,
...buildConfig(),
...(await buildConfig()),
...buildToolbarConfig(isClassicEditor),
htmlSupport: {
allow: JSON.parse(options.get("allowedHtmlTags")),
Expand Down Expand Up @@ -326,7 +327,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
const data = blob?.content || "";
const newContentLanguage = this.note?.getLabelValue("language");
if (this.contentLanguage !== newContentLanguage) {
await this.reinitialize(data);
await this.reinitializeWithData(data);
} else {
this.watchdog.editor?.setData(data);
}
Expand Down Expand Up @@ -562,7 +563,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
this.refreshIncludedNote(this.$editor, noteId);
}

async reinitialize(data: string) {
async reinitializeWithData(data: string) {
if (!this.watchdog) {
return;
}
Expand All @@ -572,9 +573,25 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
this.watchdog.editor?.setData(data);
}

async onLanguageChanged() {
async reinitialize() {
const data = this.watchdog.editor?.getData();
await this.reinitialize(data ?? "");
await this.reinitializeWithData(data ?? "");
}

async reloadTextEditorEvent() {
await this.reinitialize();
}

async onLanguageChanged() {
await this.reinitialize();
}

async entitiesReloadedEvent(e: EventData<"entitiesReloaded">) {
await super.entitiesReloadedEvent(e);

if (updateTemplateCache(e.loadResults)) {
await this.reinitialize();
}
}

buildTouchBarCommand(data: CommandListenerData<"buildTouchBar">) {
Expand Down
4 changes: 3 additions & 1 deletion apps/server/src/services/hidden_subtree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import migrationService from "./migration.js";
import { t } from "i18next";
import { cleanUpHelp, getHelpHiddenSubtreeData } from "./in_app_help.js";
import buildLaunchBarConfig from "./hidden_subtree_launcherbar.js";
import buildHiddenSubtreeTemplates from "./hidden_subtree_templates.js";

const LBTPL_ROOT = "_lbTplRoot";
const LBTPL_BASE = "_lbTplBase";
Expand Down Expand Up @@ -257,7 +258,8 @@ function buildHiddenSubtreeDefinition(helpSubtree: HiddenSubtreeItem[]): HiddenS
icon: "bx-help-circle",
children: helpSubtree,
isExpanded: true
}
},
buildHiddenSubtreeTemplates()
]
};
}
Expand Down
34 changes: 34 additions & 0 deletions apps/server/src/services/hidden_subtree_templates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { HiddenSubtreeItem } from "@triliumnext/commons";

export default function buildHiddenSubtreeTemplates() {
const templates: HiddenSubtreeItem = {
id: "_templates",
title: "Built-in templates",
type: "book",
children: [
{
id: "_template_text_snippet",
type: "text",
title: "Text Snippet",
icon: "bx bx-align-left",
attributes: [
{
name: "template",
type: "label"
},
{
name: "textSnippet",
type: "label"
},
{
name: "label:textSnippetDescription",
type: "label",
value: "promoted,alias=Description,single,text"
}
]
}
]
};

return templates;
}
Loading
Loading