Merge remote-tracking branch 'origin/develop' into feature/client_typescript_port1

; Conflicts:
;	package-lock.json
;	package.json
;	src/public/app/components/app_context.ts
;	src/public/app/services/hoisted_note.ts
;	src/public/app/services/open.ts
;	src/public/app/services/toast.ts
This commit is contained in:
Elian Doran
2024-10-26 10:20:19 +03:00
529 changed files with 19353 additions and 181827 deletions

View File

@@ -14,6 +14,7 @@ import custom from "./routes/custom.js";
import error_handlers from "./routes/error_handlers.js";
import { startScheduledCleanup } from "./services/erase.js";
import sql_init from "./services/sql_init.js";
import { t } from "i18next";
await import('./services/handlers.js');
await import('./becca/becca_loader.js');
@@ -29,6 +30,11 @@ sql_init.initializeDb();
app.set('views', path.join(scriptDir, 'views'));
app.set('view engine', 'ejs');
app.use((req, res, next) => {
res.locals.t = t;
return next();
});
if (!utils.isElectron()) {
app.use(compression()); // HTTP compression
}
@@ -47,8 +53,9 @@ app.use(cookieParser());
app.use(express.static(path.join(scriptDir, 'public/root')));
app.use(`/manifest.webmanifest`, express.static(path.join(scriptDir, 'public/manifest.webmanifest')));
app.use(`/robots.txt`, express.static(path.join(scriptDir, 'public/robots.txt')));
app.use(`/icon.png`, express.static(path.join(scriptDir, 'public/icon.png')));
app.use(sessionParser);
app.use(favicon(`${scriptDir}/../images/app-icons/win/icon.ico`));
app.use(favicon(`${scriptDir}/../images/app-icons/icon.ico`));
assets.register(app);
routes.register(app);

View File

@@ -13,11 +13,13 @@ import cls from "../services/cls.js";
import entityConstructor from "../becca/entity_constructor.js";
import { AttributeRow, BranchRow, EtapiTokenRow, NoteRow, OptionRow } from './entities/rows.js';
import AbstractBeccaEntity from "./entities/abstract_becca_entity.js";
import options_init from "../services/options_init.js";
import ws from "../services/ws.js";
const beccaLoaded = new Promise<void>(async (res, rej) => {
const sqlInit = (await import("../services/sql_init.js")).default;
// We have to import async since options init requires keyboard actions which require translations.
const options_init = (await import("../services/options_init.js")).default;
sqlInit.dbReady.then(() => {
cls.init(() => {
load();

View File

@@ -34,7 +34,7 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
isSynced?: boolean;
blobId?: string;
protected beforeSaving() {
protected beforeSaving(opts?: {}) {
const constructorData = (this.constructor as unknown as ConstructorData<T>);
if (!(this as any)[constructorData.primaryKeyName]) {
(this as any)[constructorData.primaryKeyName] = utils.newEntityId();
@@ -101,7 +101,6 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
/**
* Saves entity - executes SQL, but doesn't commit the transaction on its own
*/
// TODO: opts not used but called a few times, maybe should be used by derived classes or passed to beforeSaving.
save(opts?: {}): this {
const constructorData = (this.constructor as unknown as ConstructorData<T>);
const entityName = constructorData.entityName;
@@ -109,7 +108,7 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
const isNewEntity = !(this as any)[primaryKeyName];
this.beforeSaving();
this.beforeSaving(opts);
const pojo = this.getPojoToSave();

View File

@@ -3,6 +3,8 @@
import protectedSessionService from "../../services/protected_session.js";
import log from "../../services/log.js";
import sql from "../../services/sql.js";
import optionService from "../../services/options.js";
import eraseService from "../../services/erase.js";
import utils from "../../services/utils.js";
import dateUtils from "../../services/date_utils.js";
import AbstractBeccaEntity from "./abstract_becca_entity.js";
@@ -68,7 +70,7 @@ class BNote extends AbstractBeccaEntity<BNote> {
/** set during the deletion operation, before it is completed (removed from becca completely). */
isBeingDeleted!: boolean;
isDecrypted!: boolean;
ownedAttributes!: BAttribute[];
parentBranches!: BBranch[];
parents!: BNote[];
@@ -455,8 +457,8 @@ class BNote extends AbstractBeccaEntity<BNote> {
return this.getAttributes().find(
attr => attr.name.toLowerCase() === name
&& (!value || attr.value.toLowerCase() === value)
&& attr.type === type);
&& (!value || attr.value.toLowerCase() === value)
&& attr.type === type);
}
getRelationTarget(name: string) {
@@ -1107,7 +1109,7 @@ class BNote extends AbstractBeccaEntity<BNote> {
}
getRevisions(): BRevision[] {
return sql.getRows<RevisionRow>("SELECT * FROM revisions WHERE noteId = ?", [this.noteId])
return sql.getRows<RevisionRow>("SELECT * FROM revisions WHERE noteId = ? ORDER BY revisions.utcDateCreated ASC", [this.noteId])
.map(row => new BRevision(row));
}
@@ -1612,10 +1614,31 @@ class BNote extends AbstractBeccaEntity<BNote> {
revision.setContent(noteContent);
this.eraseExcessRevisionSnapshots()
return revision;
});
}
// Limit the number of Snapshots to revisionSnapshotNumberLimit
// Delete older Snapshots that exceed the limit
eraseExcessRevisionSnapshots() {
// lable has a higher priority
let revisionSnapshotNumberLimit = parseInt(this.getLabelValue("versioningLimit") ?? "");
if (!Number.isInteger(revisionSnapshotNumberLimit)) {
revisionSnapshotNumberLimit = parseInt(optionService.getOption('revisionSnapshotNumberLimit'));
}
if (revisionSnapshotNumberLimit >= 0) {
const revisions = this.getRevisions();
if (revisions.length - revisionSnapshotNumberLimit > 0) {
const revisionIds = revisions
.slice(0, revisions.length - revisionSnapshotNumberLimit)
.map(revision => revision.revisionId)
.filter((id): id is string => id !== undefined);
eraseService.eraseRevisions(revisionIds);
}
}
}
/**
* @param matchBy - choose by which property we detect if to update an existing attachment.
* Supported values are either 'attachmentId' (default) or 'title'

View File

@@ -91,7 +91,7 @@ export interface BranchRow {
* end user. Those types should be used only for checking against, they are
* not for direct use.
*/
export const ALLOWED_NOTE_TYPES = [ "file", "image", "search", "noteMap", "launcher", "doc", "contentWidget", "text", "relationMap", "render", "canvas", "mermaid", "book", "webView", "code" ] as const;
export const ALLOWED_NOTE_TYPES = [ "file", "image", "search", "noteMap", "launcher", "doc", "contentWidget", "text", "relationMap", "render", "canvas", "mermaid", "book", "webView", "code", "mindMap" ] as const;
export type NoteType = typeof ALLOWED_NOTE_TYPES[number];
export interface NoteRow {

View File

@@ -48,7 +48,7 @@ paths:
- name: search
in: query
required: true
description: search query string as described in https://github.com/zadam/trilium/wiki/Search
description: search query string as described in https://triliumnext.github.io/Docs/Wiki/search.html
schema:
type: string
examples:

13
src/main.ts Normal file
View File

@@ -0,0 +1,13 @@
/*
* Make sure not to import any modules that depend on localized messages via i18next here, as the initializations
* are loaded later and will result in an empty string.
*/
import { initializeTranslations } from "./services/i18n.js";
async function startApplication() {
await import("./www.js");
}
await initializeTranslations();
await startApplication();

View File

@@ -13,6 +13,7 @@ import MobileScreenSwitcherExecutor from "./mobile_screen_switcher.js";
import MainTreeExecutors from "./main_tree_executors.js";
import toast from "../services/toast.js";
import ShortcutComponent from "./shortcut_component.js";
import { t, initLocale } from "../services/i18n.js";
interface Layout {
getRootWidget: (appContext: AppContext) => RootWidget;
@@ -48,16 +49,20 @@ class AppContext extends Component {
this.beforeUnloadListeners = [];
}
/**
* Must be called as soon as possible, before the creation of any components since this method is in charge of initializing the locale. Any attempts to read translation before this method is called will result in `undefined`.
*/
async earlyInit() {
await options.initializedPromise;
await initLocale();
}
setLayout(layout: Layout) {
this.layout = layout;
}
async start() {
this.initComponents();
// options are often needed for isEnabled()
await options.initializedPromise;
this.renderWidgets();
await froca.initializedPromise;
@@ -172,7 +177,7 @@ $(window).on('beforeunload', () => {
if (!component.beforeUnloadEvent()) {
console.log(`Component ${component.componentId} is not finished saving its state.`);
toast.showMessage("Please wait for a couple of seconds for the save to finish, then you can try again.", 10000);
toast.showMessage(t("app_context.please_wait_for_save"), 10000);
allSaved = false;
}

View File

@@ -9,6 +9,7 @@ import ws from "../services/ws.js";
import bundleService from "../services/bundle.js";
import froca from "../services/froca.js";
import linkService from "../services/link.js";
import { t } from "../services/i18n.js";
export default class Entrypoints extends Component {
constructor() {
@@ -102,7 +103,7 @@ export default class Entrypoints extends Component {
if (utils.isElectron()) {
// standard JS version does not work completely correctly in electron
const webContents = utils.dynamicRequire('@electron/remote').getCurrentWebContents();
const activeIndex = parseInt(webContents.getActiveIndex());
const activeIndex = parseInt(webContents.navigationHistory.getActiveIndex());
webContents.goToIndex(activeIndex - 1);
}
@@ -115,7 +116,7 @@ export default class Entrypoints extends Component {
if (utils.isElectron()) {
// standard JS version does not work completely correctly in electron
const webContents = utils.dynamicRequire('@electron/remote').getCurrentWebContents();
const activeIndex = parseInt(webContents.getActiveIndex());
const activeIndex = parseInt(webContents.navigationHistory.getActiveIndex());
webContents.goToIndex(activeIndex + 1);
}
@@ -172,13 +173,13 @@ export default class Entrypoints extends Component {
const resp = await server.post(`sql/execute/${note.noteId}`);
if (!resp.success) {
toastService.showError(`Error occurred while executing SQL query: ${resp.error}`);
toastService.showError(t("entrypoints.sql-error", { message: resp.error }));
}
await appContext.triggerEvent('sqlQueryResults', {ntxId: ntxId, results: resp.results});
}
toastService.showMessage("Note executed");
toastService.showMessage(t("entrypoints.note-executed"));
}
hideAllPopups() {
@@ -200,6 +201,6 @@ export default class Entrypoints extends Component {
await server.post(`notes/${noteId}/revision`);
toastService.showMessage("Note revision has been created.");
toastService.showMessage(t("entrypoints.note-revision-created"));
}
}

View File

@@ -6,11 +6,15 @@ import toastService from "./services/toast.js";
import noteAutocompleteService from './services/note_autocomplete.js';
import macInit from './services/mac_init.js';
import electronContextMenu from "./menus/electron_context_menu.js";
import DesktopLayout from "./layouts/desktop_layout.js";
import glob from "./services/glob.js";
import { t } from "./services/i18n.js";
bundleService.getWidgetBundlesByParent().then(widgetBundles => {
await appContext.earlyInit();
bundleService.getWidgetBundlesByParent().then(async widgetBundles => {
// A dynamic import is required for layouts since they initialize components which require translations.
const DesktopLayout = (await import("./layouts/desktop_layout.js")).default;
appContext.setLayout(new DesktopLayout(widgetBundles));
appContext.start()
.catch((e) => {

View File

@@ -17,15 +17,16 @@ const NOTE_TYPE_ICONS = {
"code": "bx bx-code",
"render": "bx bx-extension",
"search": "bx bx-file-find",
"relationMap": "bx bx-map-alt",
"relationMap": "bx bxs-network-chart",
"book": "bx bx-book",
"noteMap": "bx bx-map-alt",
"noteMap": "bx bxs-network-chart",
"mermaid": "bx bx-selection",
"canvas": "bx bx-pen",
"webView": "bx bx-globe-alt",
"launcher": "bx bx-link",
"doc": "bx bxs-file-doc",
"contentWidget": "bx bxs-widget"
"contentWidget": "bx bxs-widget",
"mindMap": "bx bx-sitemap"
};
/**
@@ -542,7 +543,7 @@ class FNote {
return workspaceIconClass;
}
else if (this.noteId === 'root') {
return "bx bx-chevrons-right";
return "bx bx-home-alt-2";
}
if (this.noteId === '_share') {
return "bx bx-share-alt";

View File

@@ -38,6 +38,8 @@ import SimilarNotesWidget from "../widgets/ribbon_widgets/similar_notes.js";
import RightPaneContainer from "../widgets/containers/right_pane_container.js";
import EditButton from "../widgets/buttons/edit_button.js";
import EditedNotesWidget from "../widgets/ribbon_widgets/edited_notes.js";
import ShowTocWidgetButton from "../widgets/buttons/show_toc_widget_button.js";
import ShowHighlightsListWidgetButton from "../widgets/buttons/show_highlights_list_widget_button.js";
import MermaidWidget from "../widgets/mermaid.js";
import NoteWrapperWidget from "../widgets/note_wrapper.js";
import BacklinksWidget from "../widgets/floating_buttons/zpetne_odkazy.js";
@@ -69,7 +71,7 @@ import ConfirmDialog from "../widgets/dialogs/confirm.js";
import PromptDialog from "../widgets/dialogs/prompt.js";
import FloatingButtons from "../widgets/floating_buttons/floating_buttons.js";
import RelationMapButtons from "../widgets/floating_buttons/relation_map_buttons.js";
import MermaidExportButton from "../widgets/floating_buttons/mermaid_export_button.js";
import SvgExportButton from "../widgets/floating_buttons/svg_export_button.js";
import LauncherContainer from "../widgets/containers/launcher_container.js";
import RevisionsButton from "../widgets/buttons/revisions_button.js";
import CodeButtonsWidget from "../widgets/floating_buttons/code_buttons.js";
@@ -160,10 +162,12 @@ export default class DesktopLayout {
.child(new WatchedFileUpdateStatusWidget())
.child(new FloatingButtons()
.child(new EditButton())
.child(new ShowTocWidgetButton())
.child(new ShowHighlightsListWidgetButton())
.child(new CodeButtonsWidget())
.child(new RelationMapButtons())
.child(new CopyImageReferenceButton())
.child(new MermaidExportButton())
.child(new SvgExportButton())
.child(new BacklinksWidget())
.child(new HideFloatingButtonsButton())
)

View File

@@ -13,7 +13,7 @@ import FilePropertiesWidget from "../widgets/ribbon_widgets/file_properties.js";
import FloatingButtons from "../widgets/floating_buttons/floating_buttons.js";
import EditButton from "../widgets/buttons/edit_button.js";
import RelationMapButtons from "../widgets/floating_buttons/relation_map_buttons.js";
import MermaidExportButton from "../widgets/floating_buttons/mermaid_export_button.js";
import SvgExportButton from "../widgets/floating_buttons/svg_export_button.js";
import BacklinksWidget from "../widgets/floating_buttons/zpetne_odkazy.js";
import HideFloatingButtonsButton from "../widgets/floating_buttons/hide_floating_buttons_button.js";
import MermaidWidget from "../widgets/mermaid.js";
@@ -150,7 +150,7 @@ export default class MobileLayout {
.child(new FloatingButtons()
.child(new EditButton())
.child(new RelationMapButtons())
.child(new MermaidExportButton())
.child(new SvgExportButton())
.child(new BacklinksWidget())
.child(new HideFloatingButtonsButton())
)

View File

@@ -22,15 +22,31 @@ function setupContextMenu($image) {
command: "copyImageReferenceToClipboard",
uiIcon: "bx bx-empty"
},
{title: "Copy image to clipboard", command: "copyImageToClipboard", uiIcon: "bx bx-empty"},
{ title: "Copy image to clipboard", command: "copyImageToClipboard", uiIcon: "bx bx-empty" },
],
selectMenuItemHandler: ({command}) => {
selectMenuItemHandler: async ({ command }) => {
if (command === 'copyImageReferenceToClipboard') {
imageService.copyImageReferenceToClipboard($image);
} else if (command === 'copyImageToClipboard') {
const webContents = utils.dynamicRequire('@electron/remote').getCurrentWebContents();
utils.dynamicRequire('electron');
webContents.copyImageAt(e.pageX, e.pageY);
try {
const nativeImage = utils.dynamicRequire('electron').nativeImage;
const clipboard = utils.dynamicRequire('electron').clipboard;
const response = await fetch(
$image.attr('src')
);
const blob = await response.blob();
clipboard.writeImage(
nativeImage.createFromBuffer(
Buffer.from(
await blob.arrayBuffer()
)
)
);
} catch (error) {
console.error('Failed to copy image to clipboard:', error);
}
} else {
throw new Error(`Unrecognized command '${command}'`);
}
@@ -41,4 +57,4 @@ function setupContextMenu($image) {
export default {
setupContextMenu
};
};

View File

@@ -3,6 +3,7 @@ import froca from "../services/froca.js";
import contextMenu from "./context_menu.js";
import dialogService from "../services/dialog.js";
import server from "../services/server.js";
import { t } from '../services/i18n.js';
export default class LauncherContextMenu {
/**
@@ -33,29 +34,27 @@ export default class LauncherContextMenu {
const isAvailableItem = parentNoteId === '_lbAvailableLaunchers';
const isItem = isVisibleItem || isAvailableItem;
const canBeDeleted = !note.noteId.startsWith("_"); // fixed notes can't be deleted
const canBeReset = !canBeDeleted && note.isLaunchBarConfig();;
const canBeReset = !canBeDeleted && note.isLaunchBarConfig();
return [
(isVisibleRoot || isAvailableRoot) ? { title: 'Add a note launcher', command: 'addNoteLauncher', uiIcon: "bx bx-plus" } : null,
(isVisibleRoot || isAvailableRoot) ? { title: 'Add a script launcher', command: 'addScriptLauncher', uiIcon: "bx bx-plus" } : null,
(isVisibleRoot || isAvailableRoot) ? { title: 'Add a custom widget', command: 'addWidgetLauncher', uiIcon: "bx bx-plus" } : null,
(isVisibleRoot || isAvailableRoot) ? { title: 'Add spacer', command: 'addSpacerLauncher', uiIcon: "bx bx-plus" } : null,
(isVisibleRoot || isAvailableRoot) ? { title: t("launcher_context_menu.add-note-launcher"), command: 'addNoteLauncher', uiIcon: "bx bx-plus" } : null,
(isVisibleRoot || isAvailableRoot) ? { title: t("launcher_context_menu.add-script-launcher"), command: 'addScriptLauncher', uiIcon: "bx bx-plus" } : null,
(isVisibleRoot || isAvailableRoot) ? { title: t("launcher_context_menu.add-custom-widget"), command: 'addWidgetLauncher', uiIcon: "bx bx-plus" } : null,
(isVisibleRoot || isAvailableRoot) ? { title: t("launcher_context_menu.add-spacer"), command: 'addSpacerLauncher', uiIcon: "bx bx-plus" } : null,
(isVisibleRoot || isAvailableRoot) ? { title: "----" } : null,
{ title: 'Delete <kbd data-command="deleteNotes"></kbd>', command: "deleteNotes", uiIcon: "bx bx-trash", enabled: canBeDeleted },
{ title: 'Reset', command: "resetLauncher", uiIcon: "bx bx-empty", enabled: canBeReset},
{ title: `${t("launcher_context_menu.delete")} <kbd data-command="deleteNotes"></kbd>`, command: "deleteNotes", uiIcon: "bx bx-trash", enabled: canBeDeleted },
{ title: t("launcher_context_menu.reset"), command: "resetLauncher", uiIcon: "bx bx-empty", enabled: canBeReset},
{ title: "----" },
isAvailableItem ? { title: 'Move to visible launchers', command: "moveLauncherToVisible", uiIcon: "bx bx-show", enabled: true } : null,
isVisibleItem ? { title: 'Move to available launchers', command: "moveLauncherToAvailable", uiIcon: "bx bx-hide", enabled: true } : null,
{ title: `Duplicate launcher <kbd data-command="duplicateSubtree">`, command: "duplicateSubtree", uiIcon: "bx bx-empty",
isAvailableItem ? { title: t("launcher_context_menu.move-to-visible-launchers"), command: "moveLauncherToVisible", uiIcon: "bx bx-show", enabled: true } : null,
isVisibleItem ? { title: t("launcher_context_menu.move-to-available-launchers"), command: "moveLauncherToAvailable", uiIcon: "bx bx-hide", enabled: true } : null,
{ title: `${t("launcher_context_menu.duplicate-launcher")} <kbd data-command="duplicateSubtree">`, command: "duplicateSubtree", uiIcon: "bx bx-empty",
enabled: isItem }
].filter(row => row !== null);
}
async selectMenuItemHandler({command}) {
if (command === 'resetLauncher') {
const confirmed = await dialogService.confirm(`Do you really want to reset "${this.node.title}"?
All data / settings in this note (and its children) will be lost
and the launcher will be returned to its original location.`);
const confirmed = await dialogService.confirm(t("launcher_context_menu.reset_launcher_confirm", { title: this.node.title }));
if (confirmed) {
await server.post(`special-notes/launchers/${this.node.data.noteId}/reset`);

View File

@@ -8,6 +8,7 @@ import noteTypesService from "../services/note_types.js";
import server from "../services/server.js";
import toastService from "../services/toast.js";
import dialogService from "../services/dialog.js";
import { t } from "../services/i18n.js";
export default class TreeContextMenu {
/**
@@ -43,59 +44,60 @@ export default class TreeContextMenu {
|| (selNodes.length === 1 && selNodes[0] === this.node);
const notSearch = note.type !== 'search';
const notOptions = !note.noteId.startsWith("_options");
const parentNotSearch = !parentNote || parentNote.type !== 'search';
const insertNoteAfterEnabled = isNotRoot && !isHoisted && parentNotSearch;
return [
{ title: 'Open in a new tab <kbd>Ctrl+Click</kbd>', command: "openInTab", uiIcon: "bx bx-empty", enabled: noSelectedNotes },
{ title: 'Open in a new split', command: "openNoteInSplit", uiIcon: "bx bx-dock-right", enabled: noSelectedNotes },
{ title: 'Insert note after <kbd data-command="createNoteAfter"></kbd>', command: "insertNoteAfter", uiIcon: "bx bx-plus",
{ title: `${t("tree-context-menu.open-in-a-new-tab")} <kbd>Ctrl+Click</kbd>`, command: "openInTab", uiIcon: "bx bx-empty", enabled: noSelectedNotes },
{ title: t("tree-context-menu.open-in-a-new-split"), command: "openNoteInSplit", uiIcon: "bx bx-dock-right", enabled: noSelectedNotes },
{ title: `${t("tree-context-menu.insert-note-after")} <kbd data-command="createNoteAfter"></kbd>`, command: "insertNoteAfter", uiIcon: "bx bx-plus",
items: insertNoteAfterEnabled ? await noteTypesService.getNoteTypeItems("insertNoteAfter") : null,
enabled: insertNoteAfterEnabled && noSelectedNotes },
{ title: 'Insert child note <kbd data-command="createNoteInto"></kbd>', command: "insertChildNote", uiIcon: "bx bx-plus",
enabled: insertNoteAfterEnabled && noSelectedNotes && notOptions },
{ title: `${t("tree-context-menu.insert-child-note")} <kbd data-command="createNoteInto"></kbd>`, command: "insertChildNote", uiIcon: "bx bx-plus",
items: notSearch ? await noteTypesService.getNoteTypeItems("insertChildNote") : null,
enabled: notSearch && noSelectedNotes },
{ title: 'Delete <kbd data-command="deleteNotes"></kbd>', command: "deleteNotes", uiIcon: "bx bx-trash",
enabled: isNotRoot && !isHoisted && parentNotSearch },
enabled: notSearch && noSelectedNotes && notOptions },
{ title: `${t("tree-context-menu.delete")} <kbd data-command="deleteNotes"></kbd>`, command: "deleteNotes", uiIcon: "bx bx-trash",
enabled: isNotRoot && !isHoisted && parentNotSearch && notOptions },
{ title: "----" },
{ title: 'Search in subtree <kbd data-command="searchInSubtree"></kbd>', command: "searchInSubtree", uiIcon: "bx bx-search",
{ title: `${t("tree-context-menu.search-in-subtree")} <kbd data-command="searchInSubtree"></kbd>`, command: "searchInSubtree", uiIcon: "bx bx-search",
enabled: notSearch && noSelectedNotes },
isHoisted ? null : { title: 'Hoist note <kbd data-command="toggleNoteHoisting"></kbd>', command: "toggleNoteHoisting", uiIcon: "bx bx-empty", enabled: noSelectedNotes && notSearch },
!isHoisted || !isNotRoot ? null : { title: 'Unhoist note <kbd data-command="toggleNoteHoisting"></kbd>', command: "toggleNoteHoisting", uiIcon: "bx bx-door-open" },
{ title: 'Edit branch prefix <kbd data-command="editBranchPrefix"></kbd>', command: "editBranchPrefix", uiIcon: "bx bx-empty",
enabled: isNotRoot && parentNotSearch && noSelectedNotes},
{ title: "Advanced", uiIcon: "bx bx-empty", enabled: true, items: [
{ title: 'Expand subtree <kbd data-command="expandSubtree"></kbd>', command: "expandSubtree", uiIcon: "bx bx-expand", enabled: noSelectedNotes },
{ title: 'Collapse subtree <kbd data-command="collapseSubtree"></kbd>', command: "collapseSubtree", uiIcon: "bx bx-collapse", enabled: noSelectedNotes },
{ title: 'Sort by ... <kbd data-command="sortChildNotes"></kbd>', command: "sortChildNotes", uiIcon: "bx bx-empty", enabled: noSelectedNotes && notSearch },
{ title: 'Recent changes in subtree', command: "recentChangesInSubtree", uiIcon: "bx bx-history", enabled: noSelectedNotes },
{ title: 'Convert to attachment', command: "convertNoteToAttachment", uiIcon: "bx bx-empty", enabled: isNotRoot && !isHoisted },
{ title: 'Copy note path to clipboard', command: "copyNotePathToClipboard", uiIcon: "bx bx-empty", enabled: true }
isHoisted ? null : { title: `${t("tree-context-menu.hoist-note")} <kbd data-command="toggleNoteHoisting"></kbd>`, command: "toggleNoteHoisting", uiIcon: "bx bx-empty", enabled: noSelectedNotes && notSearch },
!isHoisted || !isNotRoot ? null : { title: `${t("tree-context-menu.unhoist-note")} <kbd data-command="toggleNoteHoisting"></kbd>`, command: "toggleNoteHoisting", uiIcon: "bx bx-door-open" },
{ title: `${t("tree-context-menu.edit-branch-prefix")} <kbd data-command="editBranchPrefix"></kbd>`, command: "editBranchPrefix", uiIcon: "bx bx-empty",
enabled: isNotRoot && parentNotSearch && noSelectedNotes && notOptions },
{ title: t("tree-context-menu.advanced"), uiIcon: "bx bx-empty", enabled: true, items: [
{ title: `${t("tree-context-menu.expand-subtree")} <kbd data-command="expandSubtree"></kbd>`, command: "expandSubtree", uiIcon: "bx bx-expand", enabled: noSelectedNotes },
{ title: `${t("tree-context-menu.collapse-subtree")} <kbd data-command="collapseSubtree"></kbd>`, command: "collapseSubtree", uiIcon: "bx bx-collapse", enabled: noSelectedNotes },
{ title: `${t("tree-context-menu.sort-by")} <kbd data-command="sortChildNotes"></kbd>`, command: "sortChildNotes", uiIcon: "bx bx-empty", enabled: noSelectedNotes && notSearch },
{ title: t("tree-context-menu.recent-changes-in-subtree"), command: "recentChangesInSubtree", uiIcon: "bx bx-history", enabled: noSelectedNotes && notOptions },
{ title: t("tree-context-menu.convert-to-attachment"), command: "convertNoteToAttachment", uiIcon: "bx bx-empty", enabled: isNotRoot && !isHoisted && notOptions },
{ title: t("tree-context-menu.copy-note-path-to-clipboard"), command: "copyNotePathToClipboard", uiIcon: "bx bx-empty", enabled: true }
] },
{ title: "----" },
{ title: "Protect subtree", command: "protectSubtree", uiIcon: "bx bx-check-shield", enabled: noSelectedNotes },
{ title: "Unprotect subtree", command: "unprotectSubtree", uiIcon: "bx bx-shield", enabled: noSelectedNotes },
{ title: t("tree-context-menu.protect-subtree"), command: "protectSubtree", uiIcon: "bx bx-check-shield", enabled: noSelectedNotes },
{ title: t("tree-context-menu.unprotect-subtree"), command: "unprotectSubtree", uiIcon: "bx bx-shield", enabled: noSelectedNotes },
{ title: "----" },
{ title: 'Copy / clone <kbd data-command="copyNotesToClipboard"></kbd>', command: "copyNotesToClipboard", uiIcon: "bx bx-copy",
{ title: `${t("tree-context-menu.copy-clone")} <kbd data-command="copyNotesToClipboard"></kbd>`, command: "copyNotesToClipboard", uiIcon: "bx bx-copy",
enabled: isNotRoot && !isHoisted },
{ title: 'Clone to ... <kbd data-command="cloneNotesTo"></kbd>', command: "cloneNotesTo", uiIcon: "bx bx-empty",
{ title: `${t("tree-context-menu.clone-to")} <kbd data-command="cloneNotesTo"></kbd>`, command: "cloneNotesTo", uiIcon: "bx bx-empty",
enabled: isNotRoot && !isHoisted },
{ title: 'Cut <kbd data-command="cutNotesToClipboard"></kbd>', command: "cutNotesToClipboard", uiIcon: "bx bx-cut",
{ title: `${t("tree-context-menu.cut")} <kbd data-command="cutNotesToClipboard"></kbd>`, command: "cutNotesToClipboard", uiIcon: "bx bx-cut",
enabled: isNotRoot && !isHoisted && parentNotSearch },
{ title: 'Move to ... <kbd data-command="moveNotesTo"></kbd>', command: "moveNotesTo", uiIcon: "bx bx-empty",
{ title: `${t("tree-context-menu.move-to")} <kbd data-command="moveNotesTo"></kbd>`, command: "moveNotesTo", uiIcon: "bx bx-empty",
enabled: isNotRoot && !isHoisted && parentNotSearch },
{ title: 'Paste into <kbd data-command="pasteNotesFromClipboard"></kbd>', command: "pasteNotesFromClipboard", uiIcon: "bx bx-paste",
{ title: `${t("tree-context-menu.paste-into")} <kbd data-command="pasteNotesFromClipboard"></kbd>`, command: "pasteNotesFromClipboard", uiIcon: "bx bx-paste",
enabled: !clipboard.isClipboardEmpty() && notSearch && noSelectedNotes },
{ title: 'Paste after', command: "pasteNotesAfterFromClipboard", uiIcon: "bx bx-paste",
{ title: t("tree-context-menu.paste-after"), command: "pasteNotesAfterFromClipboard", uiIcon: "bx bx-paste",
enabled: !clipboard.isClipboardEmpty() && isNotRoot && !isHoisted && parentNotSearch && noSelectedNotes },
{ title: `Duplicate subtree <kbd data-command="duplicateSubtree">`, command: "duplicateSubtree", uiIcon: "bx bx-empty",
enabled: parentNotSearch && isNotRoot && !isHoisted },
{ title: `${t("tree-context-menu.duplicate-subtree")} <kbd data-command="duplicateSubtree">`, command: "duplicateSubtree", uiIcon: "bx bx-empty",
enabled: parentNotSearch && isNotRoot && !isHoisted && notOptions },
{ title: "----" },
{ title: "Export", command: "exportNote", uiIcon: "bx bx-empty",
enabled: notSearch && noSelectedNotes },
{ title: "Import into note", command: "importIntoNote", uiIcon: "bx bx-empty",
enabled: notSearch && noSelectedNotes },
{ title: "Apply bulk actions", command: "openBulkActionsDialog", uiIcon: "bx bx-list-plus",
{ title: t("tree-context-menu.export"), command: "exportNote", uiIcon: "bx bx-empty",
enabled: notSearch && noSelectedNotes && notOptions },
{ title: t("tree-context-menu.import-into-note"), command: "importIntoNote", uiIcon: "bx bx-empty",
enabled: notSearch && noSelectedNotes && notOptions },
{ title: t("tree-context-menu.apply-bulk-actions"), command: "openBulkActionsDialog", uiIcon: "bx bx-list-plus",
enabled: true }
].filter(row => row !== null);
}
@@ -134,7 +136,7 @@ export default class TreeContextMenu {
this.treeWidget.triggerCommand("openNewNoteSplit", {ntxId, notePath});
}
else if (command === 'convertNoteToAttachment') {
if (!await dialogService.confirm(`Are you sure you want to convert note selected notes into attachments of their parent notes?`)) {
if (!await dialogService.confirm(t("tree-context-menu.convert-to-attachment-confirm"))) {
return;
}
@@ -152,7 +154,7 @@ export default class TreeContextMenu {
}
}
toastService.showMessage(`${converted} notes have been converted to attachments.`);
toastService.showMessage(t("tree-context-menu.converted-to-attachments", { count: converted }));
}
else if (command === 'copyNotePathToClipboard') {
navigator.clipboard.writeText('#' + notePath);

View File

@@ -1,8 +1,12 @@
import appContext from "./components/app_context.js";
import MobileLayout from "./layouts/mobile_layout.js";
import glob from "./services/glob.js";
glob.setupGlobs();
glob.setupGlobs()
await appContext.earlyInit();
// A dynamic import is required for layouts since they initialize components which require translations.
const MobileLayout = (await import("./layouts/mobile_layout.js")).default;
appContext.setLayout(new MobileLayout());
appContext.start();

View File

@@ -5,6 +5,7 @@ import froca from "./froca.js";
import hoistedNoteService from "./hoisted_note.js";
import ws from "./ws.js";
import appContext from "../components/app_context.js";
import { t } from './i18n.js';
async function moveBeforeBranch(branchIdsToMove, beforeBranchId) {
branchIdsToMove = filterRootNote(branchIdsToMove);
@@ -13,7 +14,7 @@ async function moveBeforeBranch(branchIdsToMove, beforeBranchId) {
const beforeBranch = froca.getBranch(beforeBranchId);
if (['root', '_lbRoot', '_lbAvailableLaunchers', '_lbVisibleLaunchers'].includes(beforeBranch.noteId)) {
toastService.showError('Cannot move notes here.');
toastService.showError(t("branches.cannot-move-notes-here"));
return;
}
@@ -42,7 +43,7 @@ async function moveAfterBranch(branchIdsToMove, afterBranchId) {
];
if (forbiddenNoteIds.includes(afterNote.noteId)) {
toastService.showError('Cannot move notes here.');
toastService.showError(t("branches.cannot-move-notes-here"));
return;
}
@@ -62,7 +63,7 @@ async function moveToParentNote(branchIdsToMove, newParentBranchId) {
const newParentBranch = froca.getBranch(newParentBranchId);
if (newParentBranch.noteId === '_lbRoot') {
toastService.showError('Cannot move notes here.');
toastService.showError(t("branches.cannot-move-notes-here"));
return;
}
@@ -192,7 +193,7 @@ function filterRootNote(branchIds) {
function makeToast(id, message) {
return {
id: id,
title: "Delete status",
title: t("branches.delete-status"),
message: message,
icon: "trash"
};
@@ -207,9 +208,9 @@ ws.subscribeToMessages(async message => {
toastService.closePersistent(message.taskId);
toastService.showError(message.message);
} else if (message.type === 'taskProgressCount') {
toastService.showPersistent(makeToast(message.taskId, `Delete notes in progress: ${message.progressCount}`));
toastService.showPersistent(makeToast(message.taskId, t("branches.delete-notes-in-progress", { count: message.progressCount })));
} else if (message.type === 'taskSucceeded') {
const toast = makeToast(message.taskId, "Delete finished successfully.");
const toast = makeToast(message.taskId, t("branches.delete-finished-successfully"));
toast.closeAfter = 5000;
toastService.showPersistent(toast);
@@ -225,9 +226,9 @@ ws.subscribeToMessages(async message => {
toastService.closePersistent(message.taskId);
toastService.showError(message.message);
} else if (message.type === 'taskProgressCount') {
toastService.showPersistent(makeToast(message.taskId, `Undeleting notes in progress: ${message.progressCount}`));
toastService.showPersistent(makeToast(message.taskId, t("branches.undeleting-notes-in-progress", { count: message.progressCount })));
} else if (message.type === 'taskSucceeded') {
const toast = makeToast(message.taskId, "Undeleting notes finished successfully.");
const toast = makeToast(message.taskId, t("branches.undeleting-notes-finished-successfully"));
toast.closeAfter = 5000;
toastService.showPersistent(toast);

View File

@@ -3,6 +3,7 @@ import server from "./server.js";
import toastService from "./toast.js";
import froca from "./froca.js";
import utils from "./utils.js";
import { t } from "./i18n.js";
async function getAndExecuteBundle(noteId, originEntity = null, script = null, params = null) {
const bundle = await server.post(`script/bundle/${noteId}`, {
@@ -75,9 +76,23 @@ async function getWidgetBundlesByParent() {
try {
widget = await executeBundle(bundle);
widgetsByParent.add(widget);
}
catch (e) {
if (widget) {
widget._noteId = bundle.noteId;
widgetsByParent.add(widget);
}
} catch (e) {
const noteId = bundle.noteId;
const note = await froca.getNote(noteId);
toastService.showPersistent({
title: t("toast.bundle-error.title"),
icon: "alert",
message: t("toast.bundle-error.message", {
id: noteId,
title: note.title,
message: e.message
})
});
logError("Widget initialization failed: ", e);
continue;
}

View File

@@ -78,7 +78,7 @@ async function copy(branchIds) {
clipboard.writeHTML(links.join(', '));
}
toastService.showMessage("Note(s) have been copied into clipboard.");
toastService.showMessage(t("clipboard.copied"));
}
function cut(branchIds) {
@@ -87,7 +87,7 @@ function cut(branchIds) {
if (clipboardBranchIds.length > 0) {
clipboardMode = 'cut';
toastService.showMessage("Note(s) have been cut into clipboard.");
toastService.showMessage(t("clipboard.cut"));
}
}

View File

@@ -34,7 +34,7 @@ async function getRenderedContent(entity, options = {}) {
else if (type === 'code') {
await renderCode(entity, $renderedContent);
}
else if (type === 'image' || type === 'canvas') {
else if (['image', 'canvas', 'mindMap'].includes(type)) {
renderImage(entity, $renderedContent, options);
}
else if (!options.tooltip && ['file', 'pdf', 'audio', 'video'].includes(type)) {

View File

@@ -217,8 +217,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
*/
this.runOnBackend = async (func, params = []) => {
if (func?.constructor.name === "AsyncFunction" || func?.startsWith?.("async ")) {
toastService.showError("You're passing an async function to api.runOnBackend() which will likely not work as you intended. "
+ "Either make the function synchronous (by removing 'async' keyword), or use api.runAsyncOnBackendWithManualTransactionHandling()");
toastService.showError(t("frontend_script_api.async_warning"));
}
return await this.__runOnBackendInner(func, params, true);
@@ -240,8 +239,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
*/
this.runAsyncOnBackendWithManualTransactionHandling = async (func, params = []) => {
if (func?.constructor.name === "Function" || func?.startsWith?.("function")) {
toastService.showError("You're passing a synchronous function to api.runAsyncOnBackendWithManualTransactionHandling(), " +
"while you should likely use api.runOnBackend() instead.");
toastService.showError(t("frontend_script_api.sync_warning"));
}
return await this.__runOnBackendInner(func, params, false);
@@ -249,7 +247,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
/**
* This is a powerful search method - you can search by attributes and their values, e.g.:
* "#dateModified =* MONTH AND #log". See full documentation for all options at: https://github.com/zadam/trilium/wiki/Search
* "#dateModified =* MONTH AND #log". See full documentation for all options at: https://triliumnext.github.io/Docs/Wiki/search.html
*
* @method
* @param {string} searchString
@@ -261,7 +259,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
/**
* This is a powerful search method - you can search by attributes and their values, e.g.:
* "#dateModified =* MONTH AND #log". See full documentation for all options at: https://github.com/zadam/trilium/wiki/Search
* "#dateModified =* MONTH AND #log". See full documentation for all options at: https://triliumnext.github.io/Docs/Wiki/search.html
*
* @method
* @param {string} searchString
@@ -558,7 +556,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
this.getYearNote = dateNotesService.getYearNote;
/**
* Hoist note in the current tab. See https://github.com/zadam/trilium/wiki/Note-hoisting
* Hoist note in the current tab. See https://triliumnext.github.io/Docs/Wiki/note-hoisting.html
*
* @method
* @param {string} noteId - set hoisted note. 'root' will effectively unhoist

View File

@@ -26,21 +26,6 @@ function setupGlobs() {
// for CKEditor integration (button on block toolbar)
window.glob.importMarkdownInline = async () => appContext.triggerCommand("importMarkdownInline");
window.glob.SEARCH_HELP_TEXT = `
<strong>Search tips</strong> - also see <button class="btn btn-sm" type="button" data-help-page="Search">complete help on search</button>
<p>
<ul>
<li>Just enter any text for full text search</li>
<li><code>#abc</code> - returns notes with label abc</li>
<li><code>#year = 2019</code> - matches notes with label <code>year</code> having value <code>2019</code></li>
<li><code>#rock #pop</code> - matches notes which have both <code>rock</code> and <code>pop</code> labels</li>
<li><code>#rock or #pop</code> - only one of the labels must be present</li>
<li><code>#year &lt;= 2000</code> - numerical comparison (also &gt;, &gt;=, &lt;).</li>
<li><code>note.dateCreated >= MONTH-1</code> - notes created in the last month</li>
<li><code>=handler</code> - will execute script defined in <code>handler</code> relation to get results</li>
</ul>
</p>`;
window.onerror = function (msg, url, lineNo, columnNo, error) {
const string = msg.toLowerCase();
@@ -64,6 +49,28 @@ function setupGlobs() {
return false;
};
window.addEventListener("unhandledrejection", (e) => {
const string = e.reason.message.toLowerCase();
let message = "Uncaught error: ";
if (string.includes("script error")) {
message += 'No details available';
} else {
message += [
`Message: ${e.reason.message}`,
`Line: ${e.reason.lineNumber}`,
`Column: ${e.reason.columnNumber}`,
`Error object: ${JSON.stringify(e.reason)}`,
`Stack: ${e.reason && e.reason.stack}`
].join(', ');
}
ws.logError(message);
return false;
});
for (const appCssNoteId of glob.appCssNoteIds || []) {
libraryLoader.requireCss(`api/notes/download/${appCssNoteId}`, false);
}

View File

@@ -3,6 +3,7 @@ import treeService, { Node } from "./tree.js";
import dialogService from "./dialog.js";
import froca from "./froca.js";
import NoteContext from "../components/note_context.js";
import { t } from "./i18n.js";
function getHoistedNoteId() {
const activeNoteContext = appContext.tabManager.getActiveContext();
@@ -58,7 +59,7 @@ async function checkNoteAccess(notePath: string, noteContext: NoteContext) {
const hoistedNote = await froca.getNote(hoistedNoteId);
if ((!hoistedNote?.hasAncestor('_hidden') || resolvedNotePath.includes('_lbBookmarks'))
&& !await dialogService.confirm(`Requested note '${requestedNote?.title}' is outside of hoisted note '${hoistedNote?.title}' subtree and you must unhoist to access the note. Do you want to proceed with unhoisting?`)) {
&& !await dialogService.confirm(t("hoisted_note.confirm_unhoisting", { requestedNote: requestedNote?.title, hoistedNote: hoistedNote?.title }))) {
return false;
}

View File

@@ -1,16 +1,21 @@
import library_loader from "./library_loader.js";
import options from "./options.js";
await library_loader.requireLibrary(library_loader.I18NEXT);
await i18next
.use(i18nextHttpBackend)
.init({
lng: "en",
fallbackLng: "en",
debug: true,
backend: {
loadPath: `/${window.glob.assetPath}/translations/{{lng}}/{{ns}}.json`
}
});
export async function initLocale() {
const locale = options.get("locale") || "en";
export const t = i18next.t;
await i18next
.use(i18nextHttpBackend)
.init({
lng: locale,
fallbackLng: "en",
backend: {
loadPath: `${window.glob.assetPath}/translations/{{lng}}/{{ns}}.json`
},
returnEmptyString: false
});
}
export const t = i18next.t;

View File

@@ -8,9 +8,9 @@ function copyImageReferenceToClipboard($imageWrapper) {
const success = document.execCommand('copy');
if (success) {
toastService.showMessage("Image copied to the clipboard");
toastService.showMessage(t("image.copied-to-clipboard"));
} else {
toastService.showAndLogError("Could not copy the image to clipboard.");
toastService.showAndLogError(t("image.cannot-copy"));
}
}
finally {

View File

@@ -36,7 +36,7 @@ export async function uploadFiles(entityType, parentNoteId, files, options) {
type: 'POST',
timeout: 60 * 60 * 1000,
error: function (xhr) {
toastService.showError(`Import failed: ${xhr.responseText}`);
toastService.showError(t("import.failed", { message: xhr.responseText }));
},
contentType: false, // NEEDED, DON'T REMOVE THIS
processData: false, // NEEDED, DON'T REMOVE THIS

View File

@@ -2,32 +2,31 @@ const CKEDITOR = {"js": ["libraries/ckeditor/ckeditor.js"]};
const CODE_MIRROR = {
js: [
"libraries/codemirror/codemirror.js",
"libraries/codemirror/addon/display/placeholder.js",
"libraries/codemirror/addon/edit/matchbrackets.js",
"libraries/codemirror/addon/edit/matchtags.js",
"libraries/codemirror/addon/fold/xml-fold.js",
"libraries/codemirror/addon/lint/lint.js",
"libraries/codemirror/addon/lint/eslint.js",
"libraries/codemirror/addon/mode/loadmode.js",
"libraries/codemirror/addon/mode/multiplex.js",
"libraries/codemirror/addon/mode/overlay.js",
"libraries/codemirror/addon/mode/simple.js",
"libraries/codemirror/addon/search/match-highlighter.js",
"libraries/codemirror/mode/meta.js",
"libraries/codemirror/keymap/vim.js"
"node_modules/codemirror/lib/codemirror.js",
"node_modules/codemirror/addon/display/placeholder.js",
"node_modules/codemirror/addon/edit/matchbrackets.js",
"node_modules/codemirror/addon/edit/matchtags.js",
"node_modules/codemirror/addon/fold/xml-fold.js",
"node_modules/codemirror/addon/lint/lint.js",
"node_modules/codemirror/addon/mode/loadmode.js",
"node_modules/codemirror/addon/mode/multiplex.js",
"node_modules/codemirror/addon/mode/overlay.js",
"node_modules/codemirror/addon/mode/simple.js",
"node_modules/codemirror/addon/search/match-highlighter.js",
"node_modules/codemirror/mode/meta.js",
"node_modules/codemirror/keymap/vim.js"
],
css: [
"libraries/codemirror/codemirror.css",
"libraries/codemirror/addon/lint/lint.css"
"node_modules/codemirror/lib/codemirror.css",
"node_modules/codemirror/addon/lint/lint.css"
]
};
const ESLINT = {js: ["libraries/eslint.js"]};
const ESLINT = {js: ["node_modules/eslint/bin/eslint.js"]};
const RELATION_MAP = {
js: [
"libraries/jsplumb.js",
"node_modules/jsplumb/dist/js/jsplumb.min.js",
"node_modules/panzoom/dist/panzoom.min.js"
],
css: [
@@ -47,7 +46,7 @@ const KATEX = {
};
const WHEEL_ZOOM = {
js: [ "libraries/wheel-zoom.min.js"]
js: [ "node_modules/vanilla-js-wheel-zoom/dist/wheel-zoom.min.js"]
};
const FORCE_GRAPH = {
@@ -68,7 +67,7 @@ const EXCALIDRAW = {
const MARKJS = {
js: [
"libraries/jquery.mark.es6.min.js"
"node_modules/mark.js/dist/jquery.mark.es6.min.js"
]
};
@@ -79,6 +78,12 @@ const I18NEXT = {
]
};
const MIND_ELIXIR = {
js: [
"node_modules/mind-elixir/dist/MindElixir.iife.js"
]
};
async function requireLibrary(library) {
if (library.css) {
library.css.map(cssUrl => requireCss(cssUrl));
@@ -137,5 +142,6 @@ export default {
MERMAID,
EXCALIDRAW,
MARKJS,
I18NEXT
I18NEXT,
MIND_ELIXIR
}

View File

@@ -105,28 +105,23 @@ function initNoteAutocomplete($el, options) {
$el.addClass("note-autocomplete-input");
const $clearTextButton = $("<a>")
.addClass("input-group-text input-clearer-button bx bxs-tag-x")
.prop("title", "Clear text field");
const $clearTextButton = $("<button>")
.addClass("input-group-text input-clearer-button bx bxs-tag-x")
.prop("title", "Clear text field");
const $showRecentNotesButton = $("<a>")
.addClass("input-group-text show-recent-notes-button bx bx-time")
.prop("title", "Show recent notes");
const $showRecentNotesButton = $("<button>")
.addClass("input-group-text show-recent-notes-button bx bx-time")
.prop("title", "Show recent notes");
const $goToSelectedNoteButton = $("<a>")
const $goToSelectedNoteButton = $("<button>")
.addClass("input-group-text go-to-selected-note-button bx bx-arrow-to-right");
const $sideButtons = $("<div>")
.addClass("input-group-append")
.append($clearTextButton)
.append($showRecentNotesButton);
$el.after($clearTextButton).after($showRecentNotesButton);
if (!options.hideGoToSelectedNoteButton) {
$sideButtons.append($goToSelectedNoteButton);
$el.after($goToSelectedNoteButton);
}
$el.after($sideButtons);
$clearTextButton.on('click', () => clearText($el));
$showRecentNotesButton.on('click', e => {
@@ -180,13 +175,13 @@ function initNoteAutocomplete($el, options) {
}
if (suggestion.action === 'create-note') {
const {success, noteType, templateNoteId} = await noteCreateService.chooseNoteType();
const { success, noteType, templateNoteId } = await noteCreateService.chooseNoteType();
if (!success) {
return;
}
const {note} = await noteCreateService.createNote(suggestion.parentNoteId, {
const { note } = await noteCreateService.createNote(suggestion.parentNoteId, {
title: suggestion.noteTitle,
activate: false,
type: noteType,

View File

@@ -119,7 +119,7 @@ async function duplicateSubtree(noteId, parentNotePath) {
activeNoteContext.setNote(`${parentNotePath}/${note.noteId}`);
const origNote = await froca.getNote(noteId);
toastService.showMessage(`Note "${origNote.title}" has been duplicated`);
toastService.showMessage(t("note_create.duplicated", { title: origNote.title }));
}
export default {

View File

@@ -16,10 +16,14 @@ function setupGlobalTooltip() {
return;
}
$('.note-tooltip').remove();
cleanUpTooltips();
});
}
function cleanUpTooltips() {
$('.note-tooltip').remove();
}
function setupElementTooltip($el) {
$el.on('mouseenter', mouseEnterHandler);
}
@@ -85,22 +89,22 @@ async function mouseEnterHandler() {
customClass: linkId
});
cleanUpTooltips();
$(this).tooltip('show');
// Dismiss the tooltip immediately if a link was clicked inside the tooltip.
$(`.${tooltipClass} a`).on("click", (e) => {
cleanUpTooltips();
});
// the purpose of the code below is to:
// - allow user to go from hovering the link to hovering the tooltip to be able to scroll,
// click on links within tooltip etc. without tooltip disappearing
// - once the user moves the cursor away from both link and the tooltip, hide the tooltip
const checkTooltip = () => {
if (!$(`.${tooltipClass}`).is(':visible')) {
console.log("Not visible anymore");
return;
}
if (!$(this).filter(":hover").length && !$(`.${linkId}:hover`).length) {
// cursor is neither over the link nor over the tooltip, user likely is not interested
$(this).tooltip('dispose');
cleanUpTooltips();
} else {
setTimeout(checkTooltip, 1000);
}

View File

@@ -1,18 +1,20 @@
import server from "./server.js";
import froca from "./froca.js";
import { t } from "./i18n.js";
async function getNoteTypeItems(command) {
const items = [
{ title: "Text", command: command, type: "text", uiIcon: "bx bx-note" },
{ title: "Code", command: command, type: "code", uiIcon: "bx bx-code" },
{ title: "Saved Search", command: command, type: "search", uiIcon: "bx bx-file-find" },
{ title: "Relation Map", command: command, type: "relationMap", uiIcon: "bx bx-map-alt" },
{ title: "Note Map", command: command, type: "noteMap", uiIcon: "bx bx-map-alt" },
{ title: "Render Note", command: command, type: "render", uiIcon: "bx bx-extension" },
{ title: "Book", command: command, type: "book", uiIcon: "bx bx-book" },
{ title: "Mermaid Diagram", command: command, type: "mermaid", uiIcon: "bx bx-selection" },
{ title: "Canvas", command: command, type: "canvas", uiIcon: "bx bx-pen" },
{ title: "Web View", command: command, type: "webView", uiIcon: "bx bx-globe-alt" },
{ title: t("note_types.text"), command: command, type: "text", uiIcon: "bx bx-note" },
{ title: t("note_types.code"), command: command, type: "code", uiIcon: "bx bx-code" },
{ title: t("note_types.saved-search"), command: command, type: "search", uiIcon: "bx bx-file-find" },
{ title: t("note_types.relation-map"), command: command, type: "relationMap", uiIcon: "bx bxs-network-chart" },
{ title: t("note_types.note-map"), command: command, type: "noteMap", uiIcon: "bx bxs-network-chart" },
{ title: t("note_types.render-note"), command: command, type: "render", uiIcon: "bx bx-extension" },
{ title: t("note_types.book"), command: command, type: "book", uiIcon: "bx bx-book" },
{ title: t("note_types.mermaid-diagram"), command: command, type: "mermaid", uiIcon: "bx bx-selection" },
{ title: t("note_types.canvas"), command: command, type: "canvas", uiIcon: "bx bx-pen" },
{ title: t("note_types.web-view"), command: command, type: "webView", uiIcon: "bx bx-globe-alt" },
{ title: t("note_types.mind-map"), command, type: "mindMap", uiIcon: "bx bx-sitemap" }
];
const templateNoteIds = await server.get("search-templates");

View File

@@ -1,7 +1,6 @@
import utils from "./utils.js";
import server from "./server.js";
type OpenType = "notes" | "attachments";
type ExecFunction = (command: string, cb: ((err: string, stdout: string, stderror: string) => void)) => void;
interface TmpResponse {
@@ -79,9 +78,8 @@ async function openCustom(type: string, entityId: string, mime: string) {
const terminal = terminals[index];
if (!terminal) {
console.error('Open Note custom: No terminal found!');
// TODO: This appears to be broken, since getFileUrl expects two arguments, with the first one being the type.
// Also don't know why {url: true} is passed.
(open as any)(getFileUrl(entityId), {url: true});
// TODO: Remove {url: true} if not needed.
(open as any)(getFileUrl(type, entityId), {url: true});
return;
}
exec(`which ${terminal}`, (error, stdout, stderr) => {
@@ -183,6 +181,23 @@ function getHost() {
return `${url.protocol}//${url.hostname}:${url.port}`;
}
async function openDirectory(directory: string) {
try {
if (utils.isElectron()) {
const electron = utils.dynamicRequire('electron');
const res = await electron.shell.openPath(directory);
if (res) {
console.error('Failed to open directory:', res);
}
} else {
console.error('Not running in an Electron environment.');
}
} catch (err: any) {
// Handle file system errors (e.g. path does not exist or is inaccessible)
console.error('Error:', err.message);
}
}
export default {
download,
downloadFileNote,
@@ -193,4 +208,5 @@ export default {
openAttachmentExternally,
openNoteCustom,
openAttachmentCustom,
openDirectory
}

View File

@@ -6,6 +6,7 @@ import appContext from "../components/app_context.js";
import froca from "./froca.js";
import utils from "./utils.js";
import options from "./options.js";
import { t } from './i18n.js';
let protectedSessionDeferred = null;
@@ -50,7 +51,7 @@ async function setupProtectedSession(password) {
const response = await server.post('login/protected', { password: password });
if (!response.success) {
toastService.showError("Wrong password.", 3000);
toastService.showError(t("protected_session.wrong_password"), 3000);
return;
}
@@ -72,7 +73,7 @@ ws.subscribeToMessages(async message => {
protectedSessionDeferred = null;
}
toastService.showMessage("Protected session has been started.");
toastService.showMessage(t("protected_session.started"));
}
else if (message.type === 'protectedSessionLogout') {
utils.reloadFrontendApp(`Protected session logout`);
@@ -85,10 +86,10 @@ async function protectNote(noteId, protect, includingSubtree) {
await server.put(`notes/${noteId}/protect/${protect ? 1 : 0}?subtree=${includingSubtree ? 1 : 0}`);
}
function makeToast(message, protectingLabel, text) {
function makeToast(message, title, text) {
return {
id: message.taskId,
title: `${protectingLabel} status`,
title,
message: text,
icon: message.data.protect ? "check-shield" : "shield"
};
@@ -99,15 +100,19 @@ ws.subscribeToMessages(async message => {
return;
}
const protectingLabel = message.data.protect ? "Protecting" : "Unprotecting";
const isProtecting = message.data.protect;
const title = isProtecting ? t("protected_session.protecting-title") : t("protected_session.unprotecting-title");
if (message.type === 'taskError') {
toastService.closePersistent(message.taskId);
toastService.showError(message.message);
} else if (message.type === 'taskProgressCount') {
toastService.showPersistent(makeToast(message, protectingLabel,`${protectingLabel} in progress: ${message.progressCount}`));
const count = message.progressCount;
const text = ( isProtecting ? t("protected_session.protecting-in-progress", { count }) : t("protected_session.unprotecting-in-progress-count", { count }));
toastService.showPersistent(makeToast(message, title, text));
} else if (message.type === 'taskSucceeded') {
const toast = makeToast(message, protectingLabel, `${protectingLabel} finished successfully.`);
const text = (isProtecting ? t("protected_session.protecting-finished-successfully") : t("protected_session.unprotecting-finished-successfully"))
const toast = makeToast(message, title, text);
toast.closeAfter = 3000;
toastService.showPersistent(toast);

View File

@@ -169,7 +169,11 @@ function ajax(url: string, method: string, data: unknown, headers: Headers, sile
});
},
error: async jqXhr => {
if (silentNotFound && jqXhr.status === 404) {
if (jqXhr.status === 0) {
// don't report requests that are rejected by the browser, usually when the user is refreshing or going to a different page.
rej("rejected by browser");
return;
} else if (silentNotFound && jqXhr.status === 404) {
// report nothing
} else {
await reportError(method, url, jqXhr.status, jqXhr.responseText);

View File

@@ -5,7 +5,7 @@ async function syncNow(ignoreNotConfigured = false) {
const result = await server.post('sync/now');
if (result.success) {
toastService.showMessage("Sync finished successfully.");
toastService.showMessage(t("sync.finished-successfully"));
}
else {
if (result.message.length > 200) {
@@ -13,7 +13,7 @@ async function syncNow(ignoreNotConfigured = false) {
}
if (!ignoreNotConfigured || result.errorCode !== 'NOT_CONFIGURED') {
toastService.showError(`Sync failed: ${result.message}`);
toastService.showError(t("sync.failed", { message: result.message }));
}
}
}

View File

@@ -12,18 +12,21 @@ interface ToastOptions {
}
function toast(options: ToastOptions) {
const $toast = $(`<div class="toast" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header">
<strong class="mr-auto"><span class="bx bx-${options.icon}"></span> <span class="toast-title"></span></strong>
<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="toast-body"></div>
</div>`);
const $toast = $(
`<div class="toast" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header">
<strong class="me-auto">
<span class="bx bx-${options.icon}"></span>
<span class="toast-title"></span>
</strong>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body"></div>
</div>`
);
$toast.find('.toast-title').text(options.title);
$toast.find('.toast-body').text(options.message);
$toast.find('.toast-body').html(options.message);
if (options.id) {
$toast.attr("id", `toast-${options.id}`);

View File

@@ -87,6 +87,9 @@ function now() {
return formatTimeWithSeconds(new Date());
}
/**
* Returns `true` if the client is currently running under Electron, or `false` if running in a web browser.
*/
function isElectron() {
return !!(window && window.process && window.process.type);
}
@@ -201,7 +204,7 @@ function getMimeTypeClass(mime: string) {
function closeActiveDialog() {
if (glob.activeDialog) {
glob.activeDialog.modal('hide');
bootstrap.Modal.getOrCreateInstance(glob.activeDialog).hide();
glob.activeDialog = null;
}
}
@@ -245,8 +248,7 @@ async function openDialog($dialog: JQuery<HTMLElement>, closeActDialog = true) {
}
saveFocusedElement();
$dialog.modal();
bootstrap.Modal.getOrCreateInstance($dialog).show();
$dialog.on('hidden.bs.modal', () => {
$(".aa-input").autocomplete("close");
@@ -335,7 +337,7 @@ function initHelpDropdown($el: JQuery<HTMLElement>) {
initHelpButtons($dropdownMenu);
}
const wikiBaseUrl = "https://github.com/zadam/trilium/wiki/";
const wikiBaseUrl = "https://triliumnext.github.io/Docs/Wiki/";
function openHelp($button: JQuery<HTMLElement>) {
const helpPage = $button.attr("data-help-page");
@@ -512,6 +514,26 @@ function createImageSrcUrl(note: { noteId: string; title: string }) {
return `api/images/${note.noteId}/${encodeURIComponent(note.title)}?timestamp=${Date.now()}`;
}
/**
* Given a string representation of an SVG, triggers a download of the file on the client device.
*
* @param {string} nameWithoutExtension the name of the file. The .svg suffix is automatically added to it.
* @param {string} svgContent the content of the SVG file download.
*/
function downloadSvg(nameWithoutExtension, svgContent) {
const filename = `${nameWithoutExtension}.svg`;
const element = document.createElement('a');
element.setAttribute('href', `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgContent)}`);
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
export default {
reloadFrontendApp,
parseDate,
@@ -551,5 +573,6 @@ export default {
escapeRegExp,
areObjectsEqual,
copyHtmlToClipboard,
createImageSrcUrl
createImageSrcUrl,
downloadSvg
};

View File

@@ -120,10 +120,10 @@ async function handleMessage(event: MessageEvent<any>) {
await executeFrontendUpdate(message.data.entityChanges);
}
else if (message.type === 'sync-hash-check-failed') {
toastService.showError("Sync check failed!", 60000);
toastService.showError(t("ws.sync-check-failed"), 60000);
}
else if (message.type === 'consistency-checks-failed') {
toastService.showError("Consistency checks failed! See logs for details.", 50 * 60000);
toastService.showError(t("ws.consistency-checks-failed"), 50 * 60000);
}
else if (message.type === 'api-log-messages') {
appContext.triggerEvent("apiLogMessages", {noteId: message.noteId, messages: message.messages});
@@ -203,7 +203,7 @@ async function consumeFrontendUpdateData() {
else {
console.log("nonProcessedEntityChanges causing the timeout", nonProcessedEntityChanges);
toastService.showError(`Encountered error "${e.message}", check out the console.`);
toastService.showError(t("ws.encountered-error", { message: e.message }));
}
}

View File

@@ -1,3 +1,4 @@
import { t } from "../services/i18n.js";
import NoteContextAwareWidget from "./note_context_aware_widget.js";
const TPL = `
@@ -32,7 +33,7 @@ const TPL = `
}
</style>
<div class="bx bx-x close-api-log-button" title="Close"></div>
<div class="bx bx-x close-api-log-button" title="${t('api_log.close')}"></div>
<div class="api-log-container"></div>
</div>`;

View File

@@ -1,3 +1,4 @@
import { t } from "../services/i18n.js";
import utils from "../services/utils.js";
import AttachmentActionsWidget from "./buttons/attachments_actions.js";
import BasicWidget from "./basic_widget.js";
@@ -153,19 +154,19 @@ export default class AttachmentDetailWidget extends BasicWidget {
$deletionWarning.show();
if (willBeDeletedInMs >= 60000) {
$deletionWarning.text(`This attachment will be automatically deleted in ${utils.formatTimeInterval(willBeDeletedInMs)}`);
$deletionWarning.text(t('attachment_detail_2.will_be_deleted_in', { time: utils.formatTimeInterval(willBeDeletedInMs) }));
} else {
$deletionWarning.text(`This attachment will be automatically deleted soon`);
$deletionWarning.text(t('attachment_detail_2.will_be_deleted_soon'));
}
$deletionWarning.append(", because the attachment is not linked in the note's content. To prevent deletion, add the attachment link back into the content or convert the attachment into note.");
$deletionWarning.append(t('attachment_detail_2.deletion_reason'));
} else {
this.$wrapper.removeClass("scheduled-for-deletion");
$deletionWarning.hide();
}
this.$wrapper.find('.attachment-details')
.text(`Role: ${this.attachment.role}, Size: ${utils.formatSize(this.attachment.contentLength)}`);
.text(t('attachment_detail_2.role_and_size', { role: this.attachment.role, size: utils.formatSize(this.attachment.contentLength) }));
this.$wrapper.find('.attachment-actions-container').append(this.attachmentActionsWidget.render());
const {$renderedContent} = await contentRenderer.getRenderedContent(this.attachment, { imageHasZoom: this.isFullDetail });
@@ -186,9 +187,9 @@ export default class AttachmentDetailWidget extends BasicWidget {
utils.copyHtmlToClipboard($link[0].outerHTML);
toastService.showMessage("Attachment link copied to clipboard.");
toastService.showMessage(t('attachment_detail_2.link_copied'));
} else {
throw new Error(`Unrecognized attachment role '${this.attachment.role}'.`);
throw new Error(t('attachment_detail_2.unrecognized_role', { role: this.attachment.role }));
}
}

View File

@@ -1,3 +1,4 @@
import { t } from "../../services/i18n.js";
import server from "../../services/server.js";
import froca from "../../services/froca.js";
import linkService from "../../services/link.js";
@@ -68,25 +69,25 @@ const TPL = `
</style>
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
<h5 class="attr-detail-title"></h5>
<h5 class="attr-detail-title">${t('attribute_detail.attr_detail_title')}</h5>
<span class="bx bx-x close-attr-detail-button" title="Cancel changes and close"></span>
<span class="bx bx-x close-attr-detail-button" title="${t('attribute_detail.close_button_title')}"></span>
</div>
<div class="attr-is-owned-by"></div>
<div class="attr-is-owned-by">${t('attribute_detail.attr_is_owned_by')}</div>
<table class="attr-edit-table">
<tr title="Attribute name can be composed of alphanumeric characters, colon and underscore only">
<th>Name:</th>
<tr title="${t('attribute_detail.attr_name_title')}">
<th>${t('attribute_detail.name')}</th>
<td><input type="text" class="attr-input-name form-control" /></td>
</tr>
<tr class="attr-help"></tr>
<tr class="attr-row-value">
<th>Value:</th>
<th>${t('attribute_detail.value')}</th>
<td><input type="text" class="attr-input-value form-control" /></td>
</tr>
<tr class="attr-row-target-note">
<th title="Relation is a named connection between source note and target note.">Target note:</th>
<th title="${t('attribute_detail.target_note_title')}">${t('attribute_detail.target_note')}</th>
<td>
<div class="input-group">
<input type="text" class="attr-input-target-note form-control" />
@@ -94,12 +95,12 @@ const TPL = `
</td>
</tr>
<tr class="attr-row-promoted"
title="Promoted attribute is displayed prominently on the note.">
<th>Promoted:</th>
<td><input type="checkbox" class="attr-input-promoted form-control form-control-sm" /></td>
title="${t('attribute_detail.promoted_title')}">
<th>${t('attribute_detail.promoted')}</th>
<td><input type="checkbox" class="attr-input-promoted form-check" /></td>
</tr>
<tr class="attr-row-promoted-alias">
<th title="The name to be displayed in the promoted attributes UI.">Alias:</th>
<th title="${t('attribute_detail.promoted_alias_title')}">${t('attribute_detail.promoted_alias')}</th>
<td>
<div class="input-group">
<input type="text" class="attr-input-promoted-alias form-control" />
@@ -107,176 +108,159 @@ const TPL = `
</td>
</tr>
<tr class="attr-row-multiplicity">
<th title="Multiplicity defines how many attributes of the same name can be created - at max 1 or more than 1.">Multiplicity:</th>
<th title="${t('attribute_detail.multiplicity_title')}">${t('attribute_detail.multiplicity')}</th>
<td>
<select class="attr-input-multiplicity form-control">
<option value="single">Single value</option>
<option value="multi">Multi value</option>
<option value="single">${t('attribute_detail.single_value')}</option>
<option value="multi">${t('attribute_detail.multi_value')}</option>
</select>
</td>
</tr>
<tr class="attr-row-label-type">
<th title="Type of the label will help Trilium to choose suitable interface to enter the label value.">Type:</th>
<th title="${t('attribute_detail.label_type_title')}">${t('attribute_detail.label_type')}</th>
<td>
<select class="attr-input-label-type form-control">
<option value="text">Text</option>
<option value="number">Number</option>
<option value="boolean">Boolean</option>
<option value="date">Date</option>
<option value="datetime">Date & Time</option>
<option value="url">URL</option>
<option value="text">${t('attribute_detail.text')}</option>
<option value="number">${t('attribute_detail.number')}</option>
<option value="boolean">${t('attribute_detail.boolean')}</option>
<option value="date">${t('attribute_detail.date')}</option>
<option value="datetime">${t('attribute_detail.date_time')}</option>
<option value="url">${t('attribute_detail.url')}</option>
</select>
</td>
</tr>
<tr class="attr-row-number-precision">
<th title="What number of digits after floating point should be available in the value setting interface.">Precision:</th>
<th title="${t('attribute_detail.precision_title')}">${t('attribute_detail.precision')}</th>
<td>
<div class="input-group">
<input type="number" class="form-control attr-input-number-precision" style="text-align: right">
<div class="input-group-append">
<span class="input-group-text">digits</span>
</div>
<span class="input-group-text">${t('attribute_detail.digits')}</span>
</div>
</td>
</tr>
<tr class="attr-row-inverse-relation">
<th title="Optional setting to define to which relation is this one opposite. Example: Father - Son are inverse relations to each other.">Inverse relation:</th>
<th title="${t('attribute_detail.inverse_relation_title')}">${t('attribute_detail.inverse_relation')}</th>
<td>
<div class="input-group">
<input type="text" class="attr-input-inverse-relation form-control" />
</div>
</td>
</tr>
<tr title="Inheritable attribute will be inherited to all descendants under this tree.">
<th>Inheritable:</th>
<td><input type="checkbox" class="attr-input-inheritable form-control form-control-sm" /></td>
<tr title="${t('attribute_detail.inheritable_title')}">
<th>${t('attribute_detail.inheritable')}</th>
<td><input type="checkbox" class="attr-input-inheritable form-check" /></td>
</tr>
</table>
<div class="attr-save-delete-button-container">
<button class="btn btn-primary btn-sm attr-save-changes-and-close-button"
style="flex-grow: 1; margin-right: 20px">
Save & close <kbd>Ctrl+Enter</kbd></button>
${t('attribute_detail.save_and_close')}</button>
<button class="btn btn-secondary btn-sm attr-delete-button">
Delete</button>
${t('attribute_detail.delete')}</button>
</div>
<div class="related-notes-container">
<br/>
<h5 class="related-notes-tile">Other notes with this label</h5>
<h5 class="related-notes-tile">${t('attribute_detail.related_notes_title')}</h5>
<ul class="related-notes-list"></ul>
<div class="related-notes-more-notes"></div>
<div class="related-notes-more-notes">${t('attribute_detail.more_notes')}</div>
</div>
</div>`;
const DISPLAYED_NOTES = 10;
const ATTR_TITLES = {
"label": "Label detail",
"label-definition": "Label definition detail",
"relation": "Relation detail",
"relation-definition": "Relation definition detail"
"label": t('attribute_detail.label'),
"label-definition": t('attribute_detail.label_definition'),
"relation": t('attribute_detail.relation'),
"relation-definition": t('attribute_detail.relation_definition')
};
const ATTR_HELP = {
"label": {
"disableVersioning": "disables auto-versioning. Useful for e.g. large, but unimportant notes - e.g. large JS libraries used for scripting",
"calendarRoot": "marks note which should be used as root for day notes. Only one should be marked as such.",
"archived": "notes with this label won't be visible by default in search results (also in Jump To, Add Link dialogs etc).",
"excludeFromExport": "notes (with their sub-tree) won't be included in any note export",
"run": `defines on which events script should run. Possible values are:
<ul>
<li>frontendStartup - when Trilium frontend starts up (or is refreshed), but not on mobile.</li>
<li>mobileStartup - when Trilium frontend starts up (or is refreshed), on mobile.</li>
<li>backendStartup - when Trilium backend starts up.</li>
<li>hourly - run once an hour. You can use additional label <code>runAtHour</code> to specify at which hour.</li>
<li>daily - run once a day</li>
</ul>`,
"runOnInstance": "Define which trilium instance should run this on. Default to all instances.",
"runAtHour": "On which hour should this run. Should be used together with <code>#run=hourly</code>. Can be defined multiple times for more runs during the day.",
"disableInclusion": "scripts with this label won't be included into parent script execution.",
"sorted": "keeps child notes sorted by title alphabetically",
"sortDirection": "ASC (the default) or DESC",
"sortFoldersFirst": "Folders (notes with children) should be sorted on top",
"top": "keep given note on top in its parent (applies only on sorted parents)",
"hidePromotedAttributes": "Hide promoted attributes on this note",
"readOnly": "editor is in read only mode. Works only for text and code notes.",
"autoReadOnlyDisabled": "text/code notes can be set automatically into read mode when they are too large. You can disable this behavior on per-note basis by adding this label to the note",
"appCss": "marks CSS notes which are loaded into the Trilium application and can thus be used to modify Trilium's looks.",
"appTheme": "marks CSS notes which are full Trilium themes and are thus available in Trilium options.",
"cssClass": "value of this label is then added as CSS class to the node representing given note in the tree. This can be useful for advanced theming. Can be used in template notes.",
"iconClass": "value of this label is added as a CSS class to the icon on the tree which can help visually distinguish the notes in the tree. Example might be bx bx-home - icons are taken from boxicons. Can be used in template notes.",
"pageSize": "number of items per page in note listing",
"customRequestHandler": 'see <a href="javascript:" data-help-page="Custom request handler">Custom request handler</a>',
"customResourceProvider": 'see <a href="javascript:" data-help-page="Custom request handler">Custom request handler</a>',
"widget": "marks this note as a custom widget which will be added to the Trilium component tree",
"workspace": "marks this note as a workspace which allows easy hoisting",
"workspaceIconClass": "defines box icon CSS class which will be used in tab when hoisted to this note",
"workspaceTabBackgroundColor": "CSS color used in the note tab when hoisted to this note",
"workspaceCalendarRoot": "Defines per-workspace calendar root",
"workspaceTemplate": "This note will appear in the selection of available template when creating new note, but only when hoisted into a workspace containing this template",
"searchHome": "new search notes will be created as children of this note",
"workspaceSearchHome": "new search notes will be created as children of this note when hoisted to some ancestor of this workspace note",
"inbox": "default inbox location for new notes - when you create a note using \"new note\" button in the sidebar, notes will be created as child notes in the note marked as with <code>#inbox</code> label.",
"workspaceInbox": "default inbox location for new notes when hoisted to some ancestor of this workspace note",
"sqlConsoleHome": "default location of SQL console notes",
"bookmarkFolder": "note with this label will appear in bookmarks as folder (allowing access to its children)",
"shareHiddenFromTree": "this note is hidden from left navigation tree, but still accessible with its URL",
"shareExternalLink": "note will act as a link to an external website in the share tree",
"shareAlias": "define an alias using which the note will be available under https://your_trilium_host/share/[your_alias]",
"shareOmitDefaultCss": "default share page CSS will be omitted. Use when you make extensive styling changes.",
"shareRoot": "marks note which is served on /share root.",
"shareDescription": "define text to be added to the HTML meta tag for description",
"shareRaw": "note will be served in its raw format, without HTML wrapper",
"shareDisallowRobotIndexing": `will forbid robot indexing of this note via <code>X-Robots-Tag: noindex</code> header`,
"shareCredentials": "require credentials to access this shared note. Value is expected to be in format 'username:password'. Don't forget to make this inheritable to apply to child-notes/images.",
"shareIndex": "note with this this label will list all roots of shared notes",
"displayRelations": "comma delimited names of relations which should be displayed. All other ones will be hidden.",
"hideRelations": "comma delimited names of relations which should be hidden. All other ones will be displayed.",
"titleTemplate": `default title of notes created as children of this note. The value is evaluated as JavaScript string
and thus can be enriched with dynamic content via the injected <code>now</code> and <code>parentNote</code> variables. Examples:
<ul>
<li><code>\${parentNote.getLabelValue('authorName')}'s literary works</code></li>
<li><code>Log for \${now.format('YYYY-MM-DD HH:mm:ss')}</code></li>
</ul>
See <a href="https://github.com/zadam/trilium/wiki/Default-note-title">wiki with details</a>, API docs for <a href="https://zadam.github.io/trilium/backend_api/Note.html">parentNote</a> and <a href="https://day.js.org/docs/en/display/format">now</a> for details.`,
"template": "This note will appear in the selection of available template when creating new note",
"toc": "<code>#toc</code> or <code>#toc=show</code> will force the Table of Contents to be shown, <code>#toc=hide</code> will force hiding it. If the label doesn't exist, the global setting is observed",
"color": "defines color of the note in note tree, links etc. Use any valid CSS color value like 'red' or #a13d5f",
"keyboardShortcut": "Defines a keyboard shortcut which will immediately jump to this note. Example: 'ctrl+alt+e'. Requires frontend reload for the change to take effect.",
"keepCurrentHoisting": "Opening this link won't change hoisting even if the note is not displayable in the current hoisted subtree.",
"executeButton": "Title of the button which will execute the current code note",
"executeDescription": "Longer description of the current code note displayed together with the execute button",
"excludeFromNoteMap": "Notes with this label will be hidden from the Note Map",
"newNotesOnTop": "New notes will be created at the top of the parent note, not on the bottom.",
"hideHighlightWidget": "Hide Hightlight List widget"
"disableVersioning": t('attribute_detail.disable_versioning'),
"calendarRoot": t('attribute_detail.calendar_root'),
"archived": t('attribute_detail.archived'),
"excludeFromExport": t('attribute_detail.exclude_from_export'),
"run": t('attribute_detail.run'),
"runOnInstance": t('attribute_detail.run_on_instance'),
"runAtHour": t('attribute_detail.run_at_hour'),
"disableInclusion": t('attribute_detail.disable_inclusion'),
"sorted": t('attribute_detail.sorted'),
"sortDirection": t('attribute_detail.sort_direction'),
"sortFoldersFirst": t('attribute_detail.sort_folders_first'),
"top": t('attribute_detail.top'),
"hidePromotedAttributes": t('attribute_detail.hide_promoted_attributes'),
"readOnly": t('attribute_detail.read_only'),
"autoReadOnlyDisabled": t('attribute_detail.auto_read_only_disabled'),
"appCss": t('attribute_detail.app_css'),
"appTheme": t('attribute_detail.app_theme'),
"cssClass": t('attribute_detail.css_class'),
"iconClass": t('attribute_detail.icon_class'),
"pageSize": t('attribute_detail.page_size'),
"customRequestHandler": t('attribute_detail.custom_request_handler'),
"customResourceProvider": t('attribute_detail.custom_resource_provider'),
"widget": t('attribute_detail.widget'),
"workspace": t('attribute_detail.workspace'),
"workspaceIconClass": t('attribute_detail.workspace_icon_class'),
"workspaceTabBackgroundColor": t('attribute_detail.workspace_tab_background_color'),
"workspaceCalendarRoot": t('attribute_detail.workspace_calendar_root'),
"workspaceTemplate": t('attribute_detail.workspace_template'),
"searchHome": t('attribute_detail.search_home'),
"workspaceSearchHome": t('attribute_detail.workspace_search_home'),
"inbox": t('attribute_detail.inbox'),
"workspaceInbox": t('attribute_detail.workspace_inbox'),
"sqlConsoleHome": t('attribute_detail.sql_console_home'),
"bookmarkFolder": t('attribute_detail.bookmark_folder'),
"shareHiddenFromTree": t('attribute_detail.share_hidden_from_tree'),
"shareExternalLink": t('attribute_detail.share_external_link'),
"shareAlias": t('attribute_detail.share_alias'),
"shareOmitDefaultCss": t('attribute_detail.share_omit_default_css'),
"shareRoot": t('attribute_detail.share_root'),
"shareDescription": t('attribute_detail.share_description'),
"shareRaw": t('attribute_detail.share_raw'),
"shareDisallowRobotIndexing": t('attribute_detail.share_disallow_robot_indexing'),
"shareCredentials": t('attribute_detail.share_credentials'),
"shareIndex": t('attribute_detail.share_index'),
"displayRelations": t('attribute_detail.display_relations'),
"hideRelations": t('attribute_detail.hide_relations'),
"titleTemplate": t('attribute_detail.title_template'),
"template": t('attribute_detail.template'),
"toc": t('attribute_detail.toc'),
"color": t('attribute_detail.color'),
"keyboardShortcut": t('attribute_detail.keyboard_shortcut'),
"keepCurrentHoisting": t('attribute_detail.keep_current_hoisting'),
"executeButton": t('attribute_detail.execute_button'),
"executeDescription": t('attribute_detail.execute_description'),
"excludeFromNoteMap": t('attribute_detail.exclude_from_note_map'),
"newNotesOnTop": t('attribute_detail.new_notes_on_top'),
"hideHighlightWidget": t('attribute_detail.hide_highlight_widget')
},
"relation": {
"runOnNoteCreation": "executes when note is created on backend. Use this relation if you want to run the script for all notes created under a specific subtree. In that case, create it on the subtree root note and make it inheritable. A new note created within the subtree (any depth) will trigger the script.",
"runOnChildNoteCreation": "executes when new note is created under the note where this relation is defined",
"runOnNoteTitleChange": "executes when note title is changed (includes note creation as well)",
"runOnNoteContentChange": "executes when note content is changed (includes note creation as well).",
"runOnNoteChange": "executes when note is changed (includes note creation as well). Does not include content changes",
"runOnNoteDeletion": "executes when note is being deleted",
"runOnBranchCreation": "executes when a branch is created. Branch is a link between parent note and child note and is created e.g. when cloning or moving note.",
"runOnBranchChange": "executes when a branch is updated.",
"runOnBranchDeletion": "executes when a branch is deleted. Branch is a link between parent note and child note and is deleted e.g. when moving note (old branch/link is deleted).",
"runOnAttributeCreation": "executes when new attribute is created for the note which defines this relation",
"runOnAttributeChange": " executes when the attribute is changed of a note which defines this relation. This is triggered also when the attribute is deleted",
"template": "note's attributes will be inherited even without a parent-child relationship, note's content and subtree will be added to instance notes if empty. See documentation for details.",
"inherit": "note's attributes will be inherited even without a parent-child relationship. See template relation for a similar concept. See attribute inheritance in the documentation.",
"renderNote": 'notes of type "render HTML note" will be rendered using a code note (HTML or script) and it is necessary to point using this relation to which note should be rendered',
"widget": "target of this relation will be executed and rendered as a widget in the sidebar",
"shareCss": "CSS note which will be injected into the share page. CSS note must be in the shared sub-tree as well. Consider using 'shareHiddenFromTree' and 'shareOmitDefaultCss' as well.",
"shareJs": "JavaScript note which will be injected into the share page. JS note must be in the shared sub-tree as well. Consider using 'shareHiddenFromTree'.",
"shareTemplate": "Embedded JavaScript note that will be used as the template for displaying the shared note. Falls back to the default template. Consider using 'shareHiddenFromTree'.",
"shareFavicon": "Favicon note to be set in the shared page. Typically you want to set it to share root and make it inheritable. Favicon note must be in the shared sub-tree as well. Consider using 'shareHiddenFromTree'.",
"runOnNoteCreation": t('attribute_detail.run_on_note_creation'),
"runOnChildNoteCreation": t('attribute_detail.run_on_child_note_creation'),
"runOnNoteTitleChange": t('attribute_detail.run_on_note_title_change'),
"runOnNoteContentChange": t('attribute_detail.run_on_note_content_change'),
"runOnNoteChange": t('attribute_detail.run_on_note_change'),
"runOnNoteDeletion": t('attribute_detail.run_on_note_deletion'),
"runOnBranchCreation": t('attribute_detail.run_on_branch_creation'),
"runOnBranchChange": t('attribute_detail.run_on_branch_change'),
"runOnBranchDeletion": t('attribute_detail.run_on_branch_deletion'),
"runOnAttributeCreation": t('attribute_detail.run_on_attribute_creation'),
"runOnAttributeChange": t('attribute_detail.run_on_attribute_change'),
"template": t('attribute_detail.relation_template'),
"inherit": t('attribute_detail.inherit'),
"renderNote": t('attribute_detail.render_note'),
"widget": t('attribute_detail.widget_relation'),
"shareCss": t('attribute_detail.share_css'),
"shareJs": t('attribute_detail.share_js'),
"shareTemplate": t('attribute_detail.share_template'),
"shareFavicon": t('attribute_detail.share_favicon')
}
};
@@ -363,7 +347,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
this.$rowTargetNote = this.$widget.find('.attr-row-target-note');
this.$inputTargetNote = this.$widget.find('.attr-input-target-note');
noteAutocompleteService.initNoteAutocomplete(this.$inputTargetNote, {allowCreatingNotes: true})
noteAutocompleteService.initNoteAutocomplete(this.$inputTargetNote, { allowCreatingNotes: true })
.on('autocomplete:noteselected', (event, suggestion, dataset) => {
if (!suggestion.notePath) {
return false;
@@ -417,7 +401,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
});
}
async showAttributeDetail({allAttributes, attribute, isOwned, x, y, focus}) {
async showAttributeDetail({ allAttributes, attribute, isOwned, x, y, focus }) {
if (!attribute) {
this.hide();
@@ -454,7 +438,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
.show()
.empty()
.append(attribute.type === 'label' ? 'Label' : 'Relation')
.append(' is owned by note ')
.append(` ${t("attribute_detail.is_owned_by_note")} `)
.append(await linkService.createLink(attribute.noteId))
}
@@ -559,7 +543,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
}
}
return {left, right};
return { left, right };
}
async saveAndClose() {
@@ -603,19 +587,19 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
}
async updateRelatedNotes() {
let {results, count} = await server.post('search-related', this.attribute);
let { results, count } = await server.post('search-related', this.attribute);
for (const res of results) {
res.noteId = res.notePathArray[res.notePathArray.length - 1];
}
results = results.filter(({noteId}) => noteId !== this.noteId);
results = results.filter(({ noteId }) => noteId !== this.noteId);
if (results.length === 0) {
this.$relatedNotesContainer.hide();
} else {
this.$relatedNotesContainer.show();
this.$relatedNotesTitle.text(`Other notes with ${this.attribute.type} name "${this.attribute.name}"`);
this.$relatedNotesTitle.text(t("attribute_detail.other_notes_with_name", { attributeType: this.attribute.type, attributeName: this.attribute.name }));
this.$relatedNotesList.empty();
@@ -625,7 +609,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
for (const note of displayedNotes) {
const notePath = note.getBestNotePathString(hoistedNoteId);
const $noteLink = await linkService.createLink(notePath, {showNotePath: true});
const $noteLink = await linkService.createLink(notePath, { showNotePath: true });
this.$relatedNotesList.append(
$("<li>").append($noteLink)
@@ -633,7 +617,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
}
if (results.length > DISPLAYED_NOTES) {
this.$relatedNotesMoreNotes.show().text(`... and ${count - DISPLAYED_NOTES} more.`);
this.$relatedNotesMoreNotes.show().text(t("attribute_detail.and_more", { count: count - DISPLAYED_NOTES }));
} else {
this.$relatedNotesMoreNotes.hide();
}

View File

@@ -1,3 +1,4 @@
import { t } from "../../services/i18n.js";
import NoteContextAwareWidget from "../note_context_aware_widget.js";
import noteAutocompleteService from "../../services/note_autocomplete.js";
import server from "../../services/server.js";
@@ -11,11 +12,11 @@ import attributeService from "../../services/attributes.js";
import linkService from "../../services/link.js";
const HELP_TEXT = `
<p>To add label, just type e.g. <code>#rock</code> or if you want to add also value then e.g. <code>#year = 2020</code></p>
<p>${t("attribute_editor.help_text_body1")}</p>
<p>For relation, type <code>~author = @</code> which should bring up an autocomplete where you can look up the desired note.</p>
<p>${t("attribute_editor.help_text_body2")}</p>
<p>Alternatively you can add label and relation using the <code>+</code> button on the right side.</p>`;
<p>${t("attribute_editor.help_text_body3")}</p>`;
const TPL = `
<div style="position: relative; padding-top: 10px; padding-bottom: 10px">
@@ -71,8 +72,8 @@ const TPL = `
<div class="attribute-list-editor" tabindex="200"></div>
<div class="bx bx-save save-attributes-button" title="Save attributes <enter>"></div>
<div class="bx bx-plus add-new-attribute-button" title="Add a new attribute"></div>
<div class="bx bx-save save-attributes-button" title="${t("attribute_editor.save_attributes")}"></div>
<div class="bx bx-plus add-new-attribute-button" title="${t("attribute_editor.add_a_new_attribute")}"></div>
<div class="attribute-errors" style="display: none;"></div>
</div>
@@ -174,7 +175,7 @@ const editorConfig = {
toolbar: {
items: []
},
placeholder: "Type the labels and relations here",
placeholder: t("attribute_editor.placeholder"),
mention: mentionSetup
};
@@ -217,13 +218,13 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget {
y: e.pageY,
orientation: 'left',
items: [
{title: `Add new label <kbd data-command="addNewLabel"></kbd>`, command: "addNewLabel", uiIcon: "bx bx-hash"},
{title: `Add new relation <kbd data-command="addNewRelation"></kbd>`, command: "addNewRelation", uiIcon: "bx bx-transfer"},
{title: "----"},
{title: "Add new label definition", command: "addNewLabelDefinition", uiIcon: "bx bx-empty"},
{title: "Add new relation definition", command: "addNewRelationDefinition", uiIcon: "bx bx-empty"},
{ title: t("attribute_editor.add_new_label"), command: "addNewLabel", uiIcon: "bx bx-hash" },
{ title: t("attribute_editor.add_new_relation"), command: "addNewRelation", uiIcon: "bx bx-transfer" },
{ title: "----" },
{ title: t("attribute_editor.add_new_label_definition"), command: "addNewLabelDefinition", uiIcon: "bx bx-empty" },
{ title: t("attribute_editor.add_new_relation_definition"), command: "addNewRelationDefinition", uiIcon: "bx bx-empty" },
],
selectMenuItemHandler: ({command}) => this.handleAddNewAttributeCommand(command)
selectMenuItemHandler: ({ command }) => this.handleAddNewAttributeCommand(command)
});
}

View File

@@ -1,4 +1,5 @@
import Component from "../components/component.js";
import froca from "../services/froca.js";
import { t } from "../services/i18n.js";
import toastService from "../services/toast.js";
@@ -84,15 +85,8 @@ class BasicWidget extends Component {
render() {
try {
this.doRender();
} catch (e) {
toastService.showPersistent({
title: t("toast.widget-error.title"),
icon: "alert",
message: t("toast.widget-error.message", {
title: this.widgetTitle,
message: e.message
})
});
} catch (e) {
this.logRenderingError(e);
}
this.$widget.attr('data-component-id', this.componentId);
@@ -131,6 +125,39 @@ class BasicWidget extends Component {
return this.$widget;
}
logRenderingError(e) {
console.log("Got issue in widget ", this);
console.error(e);
let noteId = this._noteId;
if (this._noteId) {
froca.getNote(noteId, true).then((note) => {
toastService.showPersistent({
title: t("toast.widget-error.title"),
icon: "alert",
message: t("toast.widget-error.message-custom", {
id: noteId,
title: note.title,
message: e.message
})
});
});
return;
}
toastService.showPersistent({
title: t("toast.widget-error.title"),
icon: "alert",
message: t("toast.widget-error.message-unknown", {
message: e.message
})
});
}
/**
* Indicates if the widget is enabled. Widgets are enabled by default. Generally setting this to `false` will cause the widget not to be displayed, however it will still be available on the DOM but hidden.
* @returns
*/
isEnabled() {
return true;
}

View File

@@ -1,6 +1,7 @@
import SwitchWidget from "./switch.js";
import server from "../services/server.js";
import toastService from "../services/toast.js";
import { t } from "../services/i18n.js";
export default class BookmarkSwitchWidget extends SwitchWidget {
isEnabled() {
@@ -12,11 +13,11 @@ export default class BookmarkSwitchWidget extends SwitchWidget {
doRender() {
super.doRender();
this.$switchOnName.text("Bookmark");
this.$switchOnButton.attr("title", "Bookmark this note to the left side panel");
this.$switchOnName.text(t("bookmark_switch.bookmark"));
this.$switchOnButton.attr("title", t("bookmark_switch.bookmark_this_note"));
this.$switchOffName.text("Bookmark");
this.$switchOffButton.attr("title", "Remove bookmark");
this.$switchOffName.text(t("bookmark_switch.bookmark"));
this.$switchOffButton.attr("title", t("bookmark_switch.remove_bookmark"));
}
async toggle(state) {

View File

@@ -1,3 +1,4 @@
import { t } from "../../services/i18n.js";
import server from "../../services/server.js";
import ws from "../../services/ws.js";
import utils from "../../services/utils.js";
@@ -14,7 +15,7 @@ export default class AbstractBulkAction {
$rendered.find('.action-conf-del')
.on('click', () => this.deleteAction())
.attr('title', 'Remove this search action');
.attr('title', t('abstract_bulk_action.remove_this_search_action'));
utils.initHelpDropdown($rendered);

View File

@@ -1,10 +1,11 @@
import { t } from "../../services/i18n.js";
import SpacedUpdate from "../../services/spaced_update.js";
import AbstractBulkAction from "./abstract_bulk_action.js";
const TPL = `
<tr>
<td>
Execute script:
${t('execute_script.execute_script')}
</td>
<td>
<input type="text"
@@ -14,15 +15,15 @@ const TPL = `
<td class="button-column">
<div style="display: flex">
<div class="dropdown help-dropdown">
<span class="bx bx-help-circle icon-action" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
<span class="bx bx-help-circle icon-action" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
<div class="dropdown-menu dropdown-menu-right p-4">
You can execute simple scripts on the matched notes.
${t('execute_script.help_text')}
For example to append a string to a note's title, use this small script:
${t('execute_script.example_1')}
<pre>note.title = note.title + ' - suffix';</pre>
More complex example would be deleting all matched note's attributes:
${t('execute_script.example_2')}
<pre>for (const attr of note.getOwnedAttributes) { attr.markAsDeleted(); }</pre>
</div>
@@ -35,7 +36,7 @@ const TPL = `
export default class ExecuteScriptBulkAction extends AbstractBulkAction {
static get actionName() { return "executeScript"; }
static get actionTitle() { return "Execute script"; }
static get actionTitle() { return t("execute_script.execute_script"); }
doRender() {
const $action = $(TPL);

View File

@@ -1,3 +1,4 @@
import { t } from "../../../services/i18n.js";
import SpacedUpdate from "../../../services/spaced_update.js";
import AbstractBulkAction from "../abstract_bulk_action.js";
@@ -5,31 +6,31 @@ const TPL = `
<tr>
<td colspan="2">
<div style="display: flex; align-items: center">
<div style="margin-right: 10px;" class="text-nowrap">Add label</div>
<div style="margin-right: 10px;" class="text-nowrap">${t("add_label.add_label")}</div>
<input type="text"
class="form-control label-name"
placeholder="label name"
placeholder="${t("add_label.label_name_placeholder")}"
pattern="[\\p{L}\\p{N}_:]+"
title="Alphanumeric characters, underscore and colon are allowed characters."/>
title="${t("add_label.label_name_title")}"/>
<div style="margin-right: 10px; margin-left: 10px;" class="text-nowrap">to value</div>
<div style="margin-right: 10px; margin-left: 10px;" class="text-nowrap">${t("add_label.to_value")}</div>
<input type="text" class="form-control label-value" placeholder="new value"/>
<input type="text" class="form-control label-value" placeholder="${t("add_label.new_value_placeholder")}"/>
</div>
</td>
<td class="button-column">
<div class="dropdown help-dropdown">
<span class="bx bx-help-circle icon-action" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
<span class="bx bx-help-circle icon-action" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
<div class="dropdown-menu dropdown-menu-right p-4">
<p>On all matched notes:</p>
<p>${t("add_label.help_text")}</p>
<ul>
<li>create given label if note doesn't have one yet</li>
<li>or change value of the existing label</li>
<li>${t("add_label.help_text_item1")}</li>
<li>${t("add_label.help_text_item2")}</li>
</ul>
<p>You can also call this method without value, in such case label will be assigned to the note without value.</p>
${t("add_label.help_text_note")}
</div>
</div>
@@ -39,7 +40,7 @@ const TPL = `
export default class AddLabelBulkAction extends AbstractBulkAction {
static get actionName() { return "addLabel"; }
static get actionTitle() { return "Add label"; }
static get actionTitle() { return t("add_label.add_label"); }
doRender() {
const $action = $(TPL);

View File

@@ -1,17 +1,18 @@
import { t } from "../../../services/i18n.js";
import SpacedUpdate from "../../../services/spaced_update.js";
import AbstractBulkAction from "../abstract_bulk_action.js";
const TPL = `
<tr>
<td>
Delete label:
${t("delete_label.delete_label")}
</td>
<td>
<input type="text"
class="form-control label-name"
pattern="[\\p{L}\\p{N}_:]+"
title="Alphanumeric characters, underscore and colon are allowed characters."
placeholder="label name"/>
title="${t("delete_label.label_name_title")}"
placeholder="${t("delete_label.label_name_placeholder")}"/>
</td>
<td class="button-column">
<span class="bx bx-x icon-action action-conf-del"></span>
@@ -20,7 +21,7 @@ const TPL = `
export default class DeleteLabelBulkAction extends AbstractBulkAction {
static get actionName() { return "deleteLabel"; }
static get actionTitle() { return "Delete label"; }
static get actionTitle() { return t("delete_label.delete_label"); }
doRender() {
const $action = $(TPL);

View File

@@ -1,3 +1,4 @@
import { t } from "../../../services/i18n.js";
import SpacedUpdate from "../../../services/spaced_update.js";
import AbstractBulkAction from "../abstract_bulk_action.js";
@@ -5,21 +6,21 @@ const TPL = `
<tr>
<td colspan="2">
<div style="display: flex; align-items: center">
<div style="margin-right: 10px; flex-shrink: 0;">Rename label from:</div>
<div style="margin-right: 10px; flex-shrink: 0;">${t("rename_label.rename_label_from")}</div>
<input type="text"
class="form-control old-label-name"
placeholder="old name"
placeholder="${t("rename_label.old_name_placeholder")}"
pattern="[\\p{L}\\p{N}_:]+"
title="Alphanumeric characters, underscore and colon are allowed characters."/>
title="${t("rename_label.name_title")}"/>
<div style="margin-right: 10px; margin-left: 10px;">To:</div>
<div style="margin-right: 10px; margin-left: 10px;" class="text-nowrap">${t("rename_label.to")}</div>
<input type="text"
class="form-control new-label-name"
placeholder="new name"
placeholder="${t("rename_label.new_name_placeholder")}"
pattern="[\\p{L}\\p{N}_:]+"
title="Alphanumeric characters, underscore and colon are allowed characters."/>
title="${t("rename_label.name_title")}"/>
</div>
</td>
<td class="button-column">
@@ -29,7 +30,7 @@ const TPL = `
export default class RenameLabelBulkAction extends AbstractBulkAction {
static get actionName() { return "renameLabel"; }
static get actionTitle() { return "Rename label"; }
static get actionTitle() { return t("rename_label.rename_label"); }
doRender() {
const $action = $(TPL);

View File

@@ -1,3 +1,4 @@
import { t } from "../../../services/i18n.js";
import SpacedUpdate from "../../../services/spaced_update.js";
import AbstractBulkAction from "../abstract_bulk_action.js";
@@ -5,26 +6,26 @@ const TPL = `
<tr>
<td colspan="2">
<div style="display: flex; align-items: center">
<div style="margin-right: 10px;" class="text-nowrap">Update label value</div>
<div style="margin-right: 10px;" class="text-nowrap">${t("update_label_value.update_label_value")}</div>
<input type="text"
class="form-control label-name"
placeholder="label name"
placeholder="${t("update_label_value.label_name_placeholder")}"
pattern="[\\p{L}\\p{N}_:]+"
title="Alphanumeric characters, underscore and colon are allowed characters."/>
title="${t("update_label_value.label_name_title")}"/>
<div style="margin-right: 10px; margin-left: 10px;" class="text-nowrap">to value</div>
<div style="margin-right: 10px; margin-left: 10px;" class="text-nowrap">${t("update_label_value.to_value")}</div>
<input type="text" class="form-control label-value" placeholder="new value"/>
<input type="text" class="form-control label-value" placeholder="${t("update_label_value.new_value_placeholder")}"/>
</div>
</td>
<td class="button-column">
<div class="dropdown help-dropdown">
<span class="bx bx-help-circle icon-action" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
<span class="bx bx-help-circle icon-action" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
<div class="dropdown-menu dropdown-menu-right p-4">
<p>On all matched notes, change value of the existing label.</p>
<p>${t("update_label_value.help_text")}</p>
<p>You can also call this method without value, in such case label will be assigned to the note without value.</p>
${t("update_label_value.help_text_note")}
</div>
</div>
@@ -34,7 +35,7 @@ const TPL = `
export default class UpdateLabelValueBulkAction extends AbstractBulkAction {
static get actionName() { return "updateLabelValue"; }
static get actionTitle() { return "Update label value"; }
static get actionTitle() { return t("update_label_value.update_label_value"); }
doRender() {
const $action = $(TPL);

View File

@@ -1,3 +1,4 @@
import { t } from "../../../services/i18n.js";
import AbstractBulkAction from "../abstract_bulk_action.js";
const TPL = `
@@ -5,17 +6,17 @@ const TPL = `
<td colspan="2">
<span class="bx bx-trash"></span>
Delete matched notes
${t("delete_note.delete_matched_notes")}
</td>
<td class="button-column">
<div class="dropdown help-dropdown">
<span class="bx bx-help-circle icon-action" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
<span class="bx bx-help-circle icon-action" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
<div class="dropdown-menu dropdown-menu-right p-4">
<p>This will delete matched notes.</p>
<p>${t("delete_note.delete_matched_notes_description")}</p>
<p>After the deletion, it's possible to undelete them from <span class="bx bx-history"></span> Recent Notes dialog.</p>
<p>${t("delete_note.undelete_notes_instruction")}</p>
<p>To erase notes permanently, you can go after the deletion to the Option -> Other and click the "Erase deleted notes now" button.</p>
${t("delete_note.erase_notes_instruction")}
</div>
</div>
@@ -25,7 +26,7 @@ const TPL = `
export default class DeleteNoteBulkAction extends AbstractBulkAction {
static get actionName() { return "deleteNote"; }
static get actionTitle() { return "Delete note"; }
static get actionTitle() { return t("delete_note.delete_note"); }
doRender() {
return $(TPL);

View File

@@ -1,27 +1,26 @@
import { t } from "../../../services/i18n.js";
import AbstractBulkAction from "../abstract_bulk_action.js";
const TPL = `
<tr>
<td colspan="2">
<span class="bx bx-trash"></span>
Delete note revisions
${t('delete_revisions.delete_note_revisions')}
</td>
<td class="button-column">
<div class="dropdown help-dropdown">
<span class="bx bx-help-circle icon-action" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
<span class="bx bx-help-circle icon-action" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
<div class="dropdown-menu dropdown-menu-right p-4">
All past note revisions of matched notes will be deleted. Note itself will be fully preserved. In other terms, note's history will be removed.
${t('delete_revisions.all_past_note_revisions')}
</div>
</div>
<span class="bx bx-x icon-action action-conf-del"></span>
</td>
</tr>`;
export default class DeleteRevisionsBulkAction extends AbstractBulkAction {
static get actionName() { return "deleteRevisions"; }
static get actionTitle() { return "Delete note revisions"; }
static get actionTitle() { return t('delete_revisions.delete_note_revisions'); }
doRender() {
return $(TPL);

View File

@@ -1,3 +1,4 @@
import { t } from "../../../services/i18n.js";
import SpacedUpdate from "../../../services/spaced_update.js";
import AbstractBulkAction from "../abstract_bulk_action.js";
import noteAutocompleteService from "../../../services/note_autocomplete.js";
@@ -6,25 +7,25 @@ const TPL = `
<tr>
<td colspan="2">
<div style="display: flex; align-items: center">
<div style="margin-right: 10px;" class="text-nowrap">Move note</div>
<div style="margin-right: 10px;" class="text-nowrap">${t('move_note.move_note')}</div>
<div style="margin-right: 10px;" class="text-nowrap">to</div>
<div style="margin-right: 10px;" class="text-nowrap">${t('move_note.to')}</div>
<div class="input-group">
<input type="text" class="form-control target-parent-note" placeholder="target parent note"/>
<input type="text" class="form-control target-parent-note" placeholder="${t('move_note.target_parent_note')}"/>
</div>
</div>
</td>
<td class="button-column">
<div class="dropdown help-dropdown">
<span class="bx bx-help-circle icon-action" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
<span class="bx bx-help-circle icon-action" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
<div class="dropdown-menu dropdown-menu-right p-4">
<p>On all matched notes:</p>
<p>${t('move_note.on_all_matched_notes')}:</p>
<ul>
<li>move note to the new parent if note has only one parent (i.e. the old placement is removed and new placement into the new parent is created)</li>
<li>clone note to the new parent if note has multiple clones/placements (it's not clear which placement should be removed)</li>
<li>nothing will happen if note cannot be moved to the target note (i.e. this would create a tree cycle)</li>
<ul style="margin-bottom: 0;">
<li>${t('move_note.move_note_new_parent')}</li>
<li>${t('move_note.clone_note_new_parent')}</li>
<li>${t('move_note.nothing_will_happen')}</li>
</ul>
</div>
</div>
@@ -35,7 +36,7 @@ const TPL = `
export default class MoveNoteBulkAction extends AbstractBulkAction {
static get actionName() { return "moveNote"; }
static get actionTitle() { return "Move note"; }
static get actionTitle() { return t('move_note.move_note'); }
doRender() {
const $action = $(TPL);

View File

@@ -1,31 +1,32 @@
import SpacedUpdate from "../../../services/spaced_update.js";
import AbstractBulkAction from "../abstract_bulk_action.js";
import { t } from "../../../services/i18n.js";
const TPL = `
<tr>
<td colspan="2">
<div style="display: flex; align-items: center">
<div style="margin-right: 10px; flex-shrink: 0;">Rename note title to:</div>
<div style="margin-right: 10px; flex-shrink: 0;">${t('rename_note.rename_note_title_to')}</div>
<input type="text"
class="form-control new-title"
placeholder="new note title"
title="Click help icon on the right to see all the options"/>
placeholder="${t('rename_note.new_note_title')}"
title="${t('rename_note.click_help_icon')}"/>
</div>
</td>
<td class="button-column">
<div class="dropdown help-dropdown">
<span class="bx bx-help-circle icon-action" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
<span class="bx bx-help-circle icon-action" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
<div class="dropdown-menu dropdown-menu-right p-4">
<p>The given value is evaluated as JavaScript string and thus can be enriched with dynamic content via the injected <code>note</code> variable (note being renamed). Examples:</p>
<p>${t('rename_note.evaluated_as_js_string')}</p>
<ul>
<li><code>Note</code> - all matched notes are renamed to "Note"</li>
<li><code>NEW: \${note.title}</code> - matched notes titles are prefixed with "NEW: "</li>
<li><code>\${note.dateCreatedObj.format('MM-DD:')}: \${note.title}</code> - matched notes are prefixed with note's creation month-date</li>
<li>${t('rename_note.example_note')}</li>
<li>${t('rename_note.example_new_title')}</li>
<li>${t('rename_note.example_date_prefix')}</li>
</ul>
See API docs for <a href="https://zadam.github.io/trilium/backend_api/Note.html">note</a> and its <a href="https://day.js.org/docs/en/display/format">dateCreatedObj / utcDateCreatedObj properties</a> for details.
${t('rename_note.api_docs')}
</div>
</div>
@@ -35,7 +36,7 @@ const TPL = `
export default class RenameNoteBulkAction extends AbstractBulkAction {
static get actionName() { return "renameNote"; }
static get actionTitle() { return "Rename note"; }
static get actionTitle() { return t('rename_note.rename_note'); }
doRender() {
const $action = $(TPL);

View File

@@ -1,32 +1,33 @@
import SpacedUpdate from "../../../services/spaced_update.js";
import AbstractBulkAction from "../abstract_bulk_action.js";
import noteAutocompleteService from "../../../services/note_autocomplete.js";
import { t } from "../../../services/i18n.js";
const TPL = `
<tr>
<td colspan="2">
<div style="display: flex; align-items: center">
<div style="margin-right: 10px;" class="text-nowrap">Add relation</div>
<div style="margin-right: 10px;" class="text-nowrap">${t('add_relation.add_relation')}</div>
<input type="text"
class="form-control relation-name"
placeholder="relation name"
placeholder="${t('add_relation.relation_name')}"
pattern="[\\p{L}\\p{N}_:]+"
style="flex-shrink: 3"
title="Alphanumeric characters, underscore and colon are allowed characters."/>
title="${t('add_relation.allowed_characters')}"/>
<div style="margin-right: 10px; margin-left: 10px;" class="text-nowrap">to</div>
<div style="margin-right: 10px; margin-left: 10px;" class="text-nowrap">${t('add_relation.to')}</div>
<div class="input-group" style="flex-shrink: 2">
<input type="text" class="form-control target-note" placeholder="target note"/>
<input type="text" class="form-control target-note" placeholder="${t('add_relation.target_note')}"/>
</div>
</div>
</td>
<td class="button-column">
<div class="dropdown help-dropdown">
<span class="bx bx-help-circle icon-action" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
<span class="bx bx-help-circle icon-action" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
<div class="dropdown-menu dropdown-menu-right p-4">
<p>On all matched notes create given relation.</p>
${t('add_relation.create_relation_on_all_matched_notes')}
</div>
</div>
@@ -36,7 +37,7 @@ const TPL = `
export default class AddRelationBulkAction extends AbstractBulkAction {
static get actionName() { return "addRelation"; }
static get actionTitle() { return "Add relation"; }
static get actionTitle() { return t('add_relation.add_relation'); }
doRender() {
const $action = $(TPL);
@@ -55,7 +56,7 @@ export default class AddRelationBulkAction extends AbstractBulkAction {
relationName: $relationName.val(),
targetNoteId: $targetNote.getSelectedNoteId()
});
}, 1000)
}, 1000);
$relationName.on('input', () => spacedUpdate.scheduleUpdate());
$targetNote.on('input', () => spacedUpdate.scheduleUpdate());

View File

@@ -1,18 +1,19 @@
import SpacedUpdate from "../../../services/spaced_update.js";
import AbstractBulkAction from "../abstract_bulk_action.js";
import { t } from "../../../services/i18n.js";
const TPL = `
<tr>
<td>
Delete relation:
${t('delete_relation.delete_relation')}
</td>
<td>
<div style="display: flex; align-items: center">
<input type="text"
class="form-control relation-name"
pattern="[\\p{L}\\p{N}_:]+"
placeholder="relation name"
title="Alphanumeric characters, underscore and colon are allowed characters."/>
placeholder="${t('delete_relation.relation_name')}"
title="${t('delete_relation.allowed_characters')}"/>
</div>
</td>
<td class="button-column">
@@ -22,7 +23,7 @@ const TPL = `
export default class DeleteRelationBulkAction extends AbstractBulkAction {
static get actionName() { return "deleteRelation"; }
static get actionTitle() { return "Delete relation"; }
static get actionTitle() { return t('delete_relation.delete_relation'); }
doRender() {
const $action = $(TPL);
@@ -31,7 +32,7 @@ export default class DeleteRelationBulkAction extends AbstractBulkAction {
const spacedUpdate = new SpacedUpdate(async () => {
await this.saveAction({ relationName: $relationName.val() });
}, 1000)
}, 1000);
$relationName.on('input', () => spacedUpdate.scheduleUpdate());

View File

@@ -1,25 +1,26 @@
import SpacedUpdate from "../../../services/spaced_update.js";
import AbstractBulkAction from "../abstract_bulk_action.js";
import { t } from "../../../services/i18n.js";
const TPL = `
<tr>
<td colspan="2">
<div style="display: flex; align-items: center">
<div style="margin-right: 10px; flex-shrink: 0;">Rename relation from:</div>
<div style="margin-right: 10px; flex-shrink: 0;">${t('rename_relation.rename_relation_from')}</div>
<input type="text"
class="form-control old-relation-name"
placeholder="old name"
placeholder="${t('rename_relation.old_name')}"
pattern="[\\p{L}\\p{N}_:]+"
title="Alphanumeric characters, underscore and colon are allowed characters."/>
title="${t('rename_relation.allowed_characters')}"/>
<div style="margin-right: 10px; margin-left: 10px;">To:</div>
<div style="margin-right: 10px; margin-left: 10px;" class="text-nowrap">${t('rename_relation.to')}</div>
<input type="text"
class="form-control new-relation-name"
placeholder="new name"
placeholder="${t('rename_relation.new_name')}"
pattern="[\\p{L}\\p{N}_:]+"
title="Alphanumeric characters, underscore and colon are allowed characters."/>
title="${t('rename_relation.allowed_characters')}"/>
</div>
</td>
<td class="button-column">
@@ -29,7 +30,7 @@ const TPL = `
export default class RenameRelationBulkAction extends AbstractBulkAction {
static get actionName() { return "renameRelation"; }
static get actionTitle() { return "Rename relation"; }
static get actionTitle() { return t('rename_relation.rename_relation'); }
doRender() {
const $action = $(TPL);
@@ -45,7 +46,7 @@ export default class RenameRelationBulkAction extends AbstractBulkAction {
oldRelationName: $oldRelationName.val(),
newRelationName: $newRelationName.val()
});
}, 1000)
}, 1000);
$oldRelationName.on('input', () => spacedUpdate.scheduleUpdate());
$newRelationName.on('input', () => spacedUpdate.scheduleUpdate());

View File

@@ -1,36 +1,37 @@
import SpacedUpdate from "../../../services/spaced_update.js";
import AbstractBulkAction from "../abstract_bulk_action.js";
import noteAutocompleteService from "../../../services/note_autocomplete.js";
import { t } from "../../../services/i18n.js";
const TPL = `
<tr>
<td colspan="2">
<div style="display: flex; align-items: center">
<div style="margin-right: 10px;" class="text-nowrap">Update relation</div>
<div style="margin-right: 10px;" class="text-nowrap">${t('update_relation_target.update_relation')}</div>
<input type="text"
class="form-control relation-name"
placeholder="relation name"
placeholder="${t('update_relation_target.relation_name')}"
pattern="[\\p{L}\\p{N}_:]+"
style="flex-shrink: 3"
title="Alphanumeric characters, underscore and colon are allowed characters."/>
title="${t('update_relation_target.allowed_characters')}"/>
<div style="margin-right: 10px; margin-left: 10px;" class="text-nowrap">to</div>
<div style="margin-right: 10px; margin-left: 10px;" class="text-nowrap">${t('update_relation_target.to')}</div>
<div class="input-group" style="flex-shrink: 2">
<input type="text" class="form-control target-note" placeholder="target note"/>
<input type="text" class="form-control target-note" placeholder="${t('update_relation_target.target_note')}"/>
</div>
</div>
</td>
<td class="button-column">
<div class="dropdown help-dropdown">
<span class="bx bx-help-circle icon-action" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
<span class="bx bx-help-circle icon-action" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
<div class="dropdown-menu dropdown-menu-right p-4">
<p>On all matched notes:</p>
<p>${t('update_relation_target.on_all_matched_notes')}:</p>
<ul>
<li>create given relation if note doesn't have one yet</li>
<li>or change target note of the existing relation</li>
<ul style="margin-bottom: 0;">
<li>${t('update_relation_target.create_given_relation')}</li>
<li>${t('update_relation_target.change_target_note')}</li>
</ul>
</div>
</div>
@@ -41,7 +42,7 @@ const TPL = `
export default class UpdateRelationTargetBulkAction extends AbstractBulkAction {
static get actionName() { return "updateRelationTarget"; }
static get actionTitle() { return "Update relation target"; }
static get actionTitle() { return t('update_relation_target.update_relation_target'); }
doRender() {
const $action = $(TPL);
@@ -60,7 +61,7 @@ export default class UpdateRelationTargetBulkAction extends AbstractBulkAction {
relationName: $relationName.val(),
targetNoteId: $targetNote.getSelectedNoteId()
});
}, 1000)
}, 1000);
$relationName.on('input', () => spacedUpdate.scheduleUpdate());
$targetNote.on('input', () => spacedUpdate.scheduleUpdate());

View File

@@ -1,7 +1,7 @@
import NoteContextAwareWidget from "../note_context_aware_widget.js";
const TPL = `<button class="button-widget bx"
data-toggle="tooltip"
data-bs-toggle="tooltip"
title=""></button>`;
export default class AbstractButtonWidget extends NoteContextAwareWidget {
@@ -22,10 +22,13 @@ export default class AbstractButtonWidget extends NoteContextAwareWidget {
doRender() {
this.$widget = $(TPL);
this.tooltip = new bootstrap.Tooltip(this.$widget, {
html: true, title: () => this.getTitle(), trigger: 'hover'
})
if (this.settings.onContextMenu) {
this.$widget.on("contextmenu", e => {
this.$widget.tooltip("hide");
this.tooltip.hide();
this.settings.onContextMenu(e);
@@ -35,12 +38,6 @@ export default class AbstractButtonWidget extends NoteContextAwareWidget {
this.$widget.attr("data-placement", this.settings.titlePlacement);
this.$widget.tooltip({
html: true,
title: () => this.getTitle(),
trigger: "hover"
});
super.doRender();
}

View File

@@ -1,3 +1,4 @@
import { t } from "../../services/i18n.js";
import BasicWidget from "../basic_widget.js";
import server from "../../services/server.js";
import dialogService from "../../services/dialog.js";
@@ -26,21 +27,21 @@ const TPL = `
}
</style>
<button type="button" data-toggle="dropdown" aria-haspopup="true"
<button type="button" data-bs-toggle="dropdown" aria-haspopup="true"
aria-expanded="false" class="icon-action icon-action-always-border bx bx-dots-vertical-rounded"
style="position: relative; top: 3px;"></button>
<div class="dropdown-menu dropdown-menu-right">
<a data-trigger-command="openAttachment" class="dropdown-item"
title="File will be open in an external application and watched for changes. You'll then be able to upload the modified version back to Trilium.">Open externally</a>
title="${t('attachments_actions.open_externally_title')}">${t('attachments_actions.open_externally')}</a>
<a data-trigger-command="openAttachmentCustom" class="dropdown-item"
title="File will be open in an external application and watched for changes. You'll then be able to upload the modified version back to Trilium.">Open custom</a>
<a data-trigger-command="downloadAttachment" class="dropdown-item">Download</a>
<a data-trigger-command="renameAttachment" class="dropdown-item">Rename attachment</a>
<a data-trigger-command="uploadNewAttachmentRevision" class="dropdown-item">Upload new revision</a>
<a data-trigger-command="copyAttachmentLinkToClipboard" class="dropdown-item">Copy link to clipboard</a>
<a data-trigger-command="convertAttachmentIntoNote" class="dropdown-item">Convert attachment into note</a>
<a data-trigger-command="deleteAttachment" class="dropdown-item">Delete attachment</a>
title="${t('attachments_actions.open_custom_title')}">${t('attachments_actions.open_custom')}</a>
<a data-trigger-command="downloadAttachment" class="dropdown-item">${t('attachments_actions.download')}</a>
<a data-trigger-command="renameAttachment" class="dropdown-item">${t('attachments_actions.rename_attachment')}</a>
<a data-trigger-command="uploadNewAttachmentRevision" class="dropdown-item">${t('attachments_actions.upload_new_revision')}</a>
<a data-trigger-command="copyAttachmentLinkToClipboard" class="dropdown-item">${t('attachments_actions.copy_link_to_clipboard')}</a>
<a data-trigger-command="convertAttachmentIntoNote" class="dropdown-item">${t('attachments_actions.convert_attachment_into_note')}</a>
<a data-trigger-command="deleteAttachment" class="dropdown-item">${t('attachments_actions.delete_attachment')}</a>
</div>
<input type="file" class="attachment-upload-new-revision-input" style="display: none">
@@ -60,7 +61,8 @@ export default class AttachmentActionsWidget extends BasicWidget {
doRender() {
this.$widget = $(TPL);
this.$widget.on('click', '.dropdown-item', () => this.$widget.find("[data-toggle='dropdown']").dropdown('toggle'));
this.dropdown = bootstrap.Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']"));
this.$widget.on('click', '.dropdown-item', () => this.dropdown.toggle());
this.$uploadNewRevisionInput = this.$widget.find(".attachment-upload-new-revision-input");
this.$uploadNewRevisionInput.on('change', async () => {
@@ -70,34 +72,35 @@ export default class AttachmentActionsWidget extends BasicWidget {
const result = await server.upload(`attachments/${this.attachmentId}/file`, fileToUpload);
if (result.uploaded) {
toastService.showMessage("New attachment revision has been uploaded.");
toastService.showMessage(t('attachments_actions.upload_success'));
} else {
toastService.showError("Upload of a new attachment revision failed.");
toastService.showError(t('attachments_actions.upload_failed'));
}
});
const isElectron = utils.isElectron();
if (!this.isFullDetail) {
// we deactivate this button because the WatchedFileUpdateStatusWidget assumes only one visible attachment
// in a note context, so it doesn't work in a list
const $openAttachmentButton = this.$widget.find("[data-trigger-command='openAttachment']");
$openAttachmentButton
.addClass("disabled")
.append($('<span class="disabled-tooltip"> (?)</span>')
.attr("title", "Opening attachment externally is available only from the detail page, please first click on the attachment detail first and repeat the action.")
);
const $openAttachmentCustomButton = this.$widget.find("[data-trigger-command='openAttachmentCustom']");
$openAttachmentCustomButton
.addClass("disabled")
.append($('<span class="disabled-tooltip"> (?)</span>')
.attr("title", "Opening attachment externally is available only from the detail page, please first click on the attachment detail first and repeat the action.")
.attr("title", t('attachments_actions.open_externally_detail_page'))
);
if (isElectron) {
const $openAttachmentCustomButton = this.$widget.find("[data-trigger-command='openAttachmentCustom']");
$openAttachmentCustomButton
.addClass("disabled")
.append($('<span class="disabled-tooltip"> (?)</span>')
.attr("title", t('attachments_actions.open_externally_detail_page'))
);
}
}
if (!utils.isElectron()){
if (!isElectron) {
const $openAttachmentCustomButton = this.$widget.find("[data-trigger-command='openAttachmentCustom']");
$openAttachmentCustomButton
.addClass("disabled")
.append($('<span class="disabled-tooltip"> (?)</span>')
.attr("title", "Custom opening of attachments can only be done from the client.")
.attr("title", t('attachments_actions.open_custom_client_only'))
);
}
}
@@ -123,29 +126,29 @@ export default class AttachmentActionsWidget extends BasicWidget {
}
async deleteAttachmentCommand() {
if (!await dialogService.confirm(`Are you sure you want to delete attachment '${this.attachment.title}'?`)) {
if (!await dialogService.confirm(t('attachments_actions.delete_confirm', { title: this.attachment.title }))) {
return;
}
await server.remove(`attachments/${this.attachmentId}`);
toastService.showMessage(`Attachment '${this.attachment.title}' has been deleted.`);
toastService.showMessage(t('attachments_actions.delete_success', { title: this.attachment.title }));
}
async convertAttachmentIntoNoteCommand() {
if (!await dialogService.confirm(`Are you sure you want to convert attachment '${this.attachment.title}' into a separate note?`)) {
if (!await dialogService.confirm(t('attachments_actions.convert_confirm', { title: this.attachment.title }))) {
return;
}
const {note: newNote} = await server.post(`attachments/${this.attachmentId}/convert-to-note`)
toastService.showMessage(`Attachment '${this.attachment.title}' has been converted to note.`);
const { note: newNote } = await server.post(`attachments/${this.attachmentId}/convert-to-note`)
toastService.showMessage(t('attachments_actions.convert_success', { title: this.attachment.title }));
await ws.waitForMaxKnownEntityChangeId();
await appContext.tabManager.getActiveContext().setNote(newNote.noteId);
}
async renameAttachmentCommand() {
const attachmentTitle = await dialogService.prompt({
title: "Rename attachment",
message: "Please enter new attachment's name",
title: t('attachments_actions.rename_attachment'),
message: t('attachments_actions.enter_new_name'),
defaultValue: this.attachment.title
});
@@ -153,6 +156,6 @@ export default class AttachmentActionsWidget extends BasicWidget {
return;
}
await server.put(`attachments/${this.attachmentId}/rename`, {title: attachmentTitle});
await server.put(`attachments/${this.attachmentId}/rename`, { title: attachmentTitle });
}
}

View File

@@ -1,3 +1,4 @@
import { t } from "../../services/i18n.js";
import libraryLoader from "../../services/library_loader.js";
import utils from "../../services/utils.js";
import dateNoteService from "../../services/date_notes.js";
@@ -5,29 +6,67 @@ import server from "../../services/server.js";
import appContext from "../../components/app_context.js";
import RightDropdownButtonWidget from "./right_dropdown_button.js";
import toastService from "../../services/toast.js";
import options from "../../services/options.js";
const MONTHS = [
t("calendar.january"),
t("calendar.febuary"),
t("calendar.march"),
t("calendar.april"),
t("calendar.may"),
t("calendar.june"),
t("calendar.july"),
t("calendar.august"),
t("calendar.september"),
t("calendar.october"),
t("calendar.november"),
t("calendar.december")
];
const DROPDOWN_TPL = `
<div class="calendar-dropdown-widget">
<style>
.calendar-dropdown-widget {
width: 350px;
}
</style>
<style>
.calendar-dropdown-widget {
width: 350px;
}
</style>
<div class="calendar-header">
<button class="calendar-btn bx bx-left-arrow-alt" data-calendar-toggle="previous"></button>
<div class="calendar-header">
<div class="calendar-month-selector">
<button class="calendar-btn bx bx-left-arrow-alt" data-calendar-toggle="previous"></button>
<div class="calendar-header-label" data-calendar-label="month"></div>
<select data-calendar-input="month">
${Object.entries(MONTHS).map(([i, month]) => `<option value=${i}>${month}</option>`)}
</select>
<button class="calendar-btn bx bx-right-arrow-alt" data-calendar-toggle="next"></button>
</div>
<button class="calendar-btn bx bx-right-arrow-alt" data-calendar-toggle="next"></button>
</div>
<div class="calendar-week">
<span>Mon</span> <span>Tue</span><span>Wed</span> <span>Thu</span> <span>Fri</span> <span>Sat</span> <span>Sun</span>
</div>
<div class="calendar-body" data-calendar-area="month"></div>
<div class="calendar-year-selector">
<button class="calendar-btn bx bx-left-arrow-alt" data-calendar-toggle="previousYear"></button>
<input type="number" min="1900" max="2999" step="1" data-calendar-input="year" />
<button class="calendar-btn bx bx-right-arrow-alt" data-calendar-toggle="nextYear"></button>
</div>
</div>
<div class="calendar-week">
</div>
<div class="calendar-body" data-calendar-area="month"></div>
</div>`;
const DAYS_OF_WEEK = [
t("calendar.sun"),
t("calendar.mon"),
t("calendar.tue"),
t("calendar.wed"),
t("calendar.thu"),
t("calendar.fri"),
t("calendar.sat")
];
export default class CalendarWidget extends RightDropdownButtonWidget {
constructor(title, icon) {
super(title, icon, DROPDOWN_TPL);
@@ -37,20 +76,44 @@ export default class CalendarWidget extends RightDropdownButtonWidget {
super.doRender();
this.$month = this.$dropdownContent.find('[data-calendar-area="month"]');
this.$next = this.$dropdownContent.find('[data-calendar-toggle="next"]');
this.$previous = this.$dropdownContent.find('[data-calendar-toggle="previous"]');
this.$label = this.$dropdownContent.find('[data-calendar-label="month"]');
this.$weekHeader = this.$dropdownContent.find(".calendar-week");
this.manageFirstDayOfWeek();
// Month navigation
this.$monthSelect = this.$dropdownContent.find('[data-calendar-input="month"]');
this.$monthSelect.on("input", (e) => {
this.date.setMonth(e.target.value);
this.createMonth();
});
this.$next = this.$dropdownContent.find('[data-calendar-toggle="next"]');
this.$next.on('click', () => {
this.date.setMonth(this.date.getMonth() + 1);
this.createMonth();
});
this.$previous = this.$dropdownContent.find('[data-calendar-toggle="previous"]');
this.$previous.on('click', e => {
this.date.setMonth(this.date.getMonth() - 1);
this.createMonth();
});
// Year navigation
this.$yearSelect = this.$dropdownContent.find('[data-calendar-input="year"]');
this.$yearSelect.on("input", (e) => {
this.date.setFullYear(e.target.value);
this.createMonth();
});
this.$nextYear = this.$dropdownContent.find('[data-calendar-toggle="nextYear"]');
this.$nextYear.on('click', () => {
this.date.setFullYear(this.date.getFullYear() + 1);
this.createMonth();
});
this.$previousYear = this.$dropdownContent.find('[data-calendar-toggle="previousYear"]');
this.$previousYear.on('click', e => {
this.date.setFullYear(this.date.getFullYear() - 1);
this.createMonth();
});
this.$dropdownContent.find('.calendar-header').on("click", e => e.stopPropagation());
this.$dropdownContent.on('click', '.calendar-date', async ev => {
@@ -60,12 +123,27 @@ export default class CalendarWidget extends RightDropdownButtonWidget {
if (note) {
appContext.tabManager.getActiveContext().setNote(note.noteId);
this.hideDropdown();
this.dropdown.hide();
}
else {
toastService.showError("Cannot find day note");
toastService.showError(t("calendar.cannot_find_day_note"));
}
});
ev.stopPropagation();
});
// Prevent dismissing the calendar popup by clicking on an empty space inside it.
this.$dropdownContent.on("click", (e) => e.stopPropagation());
}
manageFirstDayOfWeek() {
this.firstDayOfWeek = options.getInt("firstDayOfWeek");
// Generate the list of days of the week taking into consideration the user's selected first day of week.
let localeDaysOfWeek = [...DAYS_OF_WEEK];
const daysToBeAddedAtEnd = localeDaysOfWeek.splice(0, this.firstDayOfWeek);
localeDaysOfWeek = [...localeDaysOfWeek, ...daysToBeAddedAtEnd];
this.$weekHeader.html(localeDaysOfWeek.map((el) => `<span>${el}</span>`));
}
async dropdownShown() {
@@ -94,18 +172,21 @@ export default class CalendarWidget extends RightDropdownButtonWidget {
// if it's the first day of the month
if (num === 1) {
if (day === 0) {
$newDay.css("marginLeft", (6 * 14.28) + '%');
} else {
$newDay.css("marginLeft", `${(day - 1) * 14.28}%`);
}
// 0 1 2 3 4 5 6
// Su Mo Tu We Th Fr Sa
// 1 2 3 4 5 6 0
// Mo Tu We Th Fr Sa Su
let dayOffset = day - this.firstDayOfWeek;
if (dayOffset < 0)
dayOffset = 7 + dayOffset;
$newDay.css("marginLeft", (dayOffset * 14.28) + '%');
}
const dateNoteId = dateNotesForMonth[utils.formatDateISO(this.date)];
if (dateNoteId) {
$newDay.addClass('calendar-date-exists');
$newDay.attr("href", `#root/${dateNoteId}`);
$newDay.attr("data-href", `#root/${dateNoteId}`);
}
if (this.isEqual(this.date, this.activeDate)) {
@@ -153,23 +234,17 @@ export default class CalendarWidget extends RightDropdownButtonWidget {
this.date.setDate(1);
this.date.setMonth(this.date.getMonth() - 1);
this.$label.html(`${this.monthsAsString(this.date.getMonth())} ${this.date.getFullYear()}`);
this.$monthSelect.val(this.date.getMonth());
this.$yearSelect.val(this.date.getFullYear());
}
monthsAsString(monthIndex) {
return [
'January',
'Febuary',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
][monthIndex];
async entitiesReloadedEvent({ loadResults }) {
if (!loadResults.getOptionNames().includes("firstDayOfWeek")) {
return;
}
this.manageFirstDayOfWeek();
this.createMonth();
}
}

View File

@@ -1,3 +1,4 @@
import { t } from "../../services/i18n.js";
import OnClickButtonWidget from "./onclick_button.js";
export default class ClosePaneButton extends OnClickButtonWidget {
@@ -15,7 +16,7 @@ export default class ClosePaneButton extends OnClickButtonWidget {
super();
this.icon("bx-x")
.title("Close this pane")
.title(t("close_pane_button.close_this_pane"))
.titlePlacement("bottom")
.onClick((widget, e) => {
// to avoid split pane container detecting click within the pane which would try to activate this

View File

@@ -11,7 +11,7 @@ export default class CommandButtonWidget extends AbstractButtonWidget {
if (this.settings.command) {
this.$widget.on("click", () => {
this.$widget.tooltip("hide");
this.tooltip.hide();
this.triggerCommand(this._command);
});

View File

@@ -1,3 +1,4 @@
import { t } from "../../services/i18n.js";
import OnClickButtonWidget from "./onclick_button.js";
export default class CreatePaneButton extends OnClickButtonWidget {
@@ -5,7 +6,7 @@ export default class CreatePaneButton extends OnClickButtonWidget {
super();
this.icon("bx-dock-right")
.title("Create new split")
.title(t("create_pane_button.create_new_split"))
.titlePlacement("bottom")
.onClick(widget => widget.triggerCommand("openNewNoteSplit", { ntxId: widget.getClosestNtxId() }))
.class("icon-action");

View File

@@ -2,6 +2,7 @@ import OnClickButtonWidget from "./onclick_button.js";
import appContext from "../../components/app_context.js";
import attributeService from "../../services/attributes.js";
import protectedSessionHolder from "../../services/protected_session_holder.js";
import { t } from "../../services/i18n.js";
export default class EditButton extends OnClickButtonWidget {
isEnabled() {
@@ -14,7 +15,7 @@ export default class EditButton extends OnClickButtonWidget {
super();
this.icon("bx-edit-alt")
.title("Edit this note")
.title(t("edit_button.edit_this_note"))
.titlePlacement("bottom")
.onClick(widget => {
this.noteContext.viewScope.readOnlyTemporarilyDisabled = true;

View File

@@ -1,10 +1,11 @@
import { t } from "../../services/i18n.js";
import BasicWidget from "../basic_widget.js";
import utils from "../../services/utils.js";
import UpdateAvailableWidget from "./update_available.js";
import options from "../../services/options.js";
const TPL = `
<div class="dropdown global-menu dropright">
<div class="dropdown global-menu dropend">
<style>
.global-menu {
width: 53px;
@@ -15,20 +16,31 @@ const TPL = `
min-width: 20em;
}
.global-menu-button {
background-image: url("${window.glob.assetPath}/images/icon-black.svg");
background-repeat: no-repeat;
background-position: 40% 50%;
background-size: 45px;
.global-menu-button {
width: 100%;
height: 100%;
position: relative;
}
.global-menu-button:hover {
background-image: url("${window.glob.assetPath}/images/icon-color.svg");
padding: 6px;
border: 0;
}
.global-menu-button > svg path {
fill: var(--launcher-pane-text-color);
}
.global-menu-button:hover { border: 0; }
.global-menu-button:hover > svg path {
transition: 200ms ease-in-out fill;
}
.global-menu-button:hover > svg path.st0 { fill:#95C980; }
.global-menu-button:hover > svg path.st1 { fill:#72B755; }
.global-menu-button:hover > svg path.st2 { fill:#4FA52B; }
.global-menu-button:hover > svg path.st3 { fill:#EE8C89; }
.global-menu-button:hover > svg path.st4 { fill:#E96562; }
.global-menu-button:hover > svg path.st5 { fill:#E33F3B; }
.global-menu-button:hover > svg path.st6 { fill:#EFB075; }
.global-menu-button:hover > svg path.st7 { fill:#E99547; }
.global-menu-button:hover > svg path.st8 { fill:#E47B19; }
.global-menu-button-update-available {
position: absolute;
@@ -58,6 +70,7 @@ const TPL = `
background-color: var(--button-background-color);
padding: 3px;
margin-left: 3px;
text-decoration: none;
}
.global-menu .zoom-buttons a:hover {
@@ -88,123 +101,138 @@ const TPL = `
left: 0;
top: 5px;
}
</style>
</style>
<button type="button" data-bs-toggle="dropdown" aria-haspopup="true"
aria-expanded="false" class="icon-action global-menu-button">
<svg viewBox="0 0 256 256" data-bs-toggle="tooltip" title="${t('global_menu.menu')}">
<g>
<path class="st0" d="m202.9 112.7c-22.5 16.1-54.5 12.8-74.9 6.3l14.8-11.8 14.1-11.3 49.1-39.3-51.2 35.9-14.3 10-14.9 10.5c0.7-21.2 7-49.9 28.6-65.4 1.8-1.3 3.9-2.6 6.1-3.8 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.4 2.8-4.9 5.4-7.4 7.8-3.4 3.5-6.8 6.4-10.1 8.8z"/>
<path class="st1" d="m213.1 104c-22.2 12.6-51.4 9.3-70.3 3.2l14.1-11.3 49.1-39.3-51.2 35.9-14.3 10c0.5-18.1 4.9-42.1 19.7-58.6 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.3 2.8-4.8 5.4-7.2 7.8z"/>
<path class="st2" d="m220.5 96.2c-21.1 8.6-46.6 5.3-63.7-0.2l49.2-39.4-51.2 35.9c0.3-15.8 3.5-36.6 14.3-52.8 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.8 66z"/>
<path class="st3" d="m106.7 179c-5.8-21 5.2-43.8 15.5-57.2l4.8 14.2 4.5 13.4 15.9 47-12.8-47.6-3.6-13.2-3.7-13.9c15.5 6.2 35.1 18.6 40.7 38.8 0.5 1.7 0.9 3.6 1.2 5.5 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.1-3.8-7.6-1.6-3.5-2.9-6.8-3.8-10z"/>
<path class="st4" d="m110.4 188.9c-3.4-19.8 6.9-40.5 16.6-52.9l4.5 13.4 15.9 47-12.8-47.6-3.6-13.2c13.3 5.2 29.9 15 38.1 30.4 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.2-3.8-7.7z"/>
<path class="st5" d="m114.2 196.5c-0.7-18 8.6-35.9 17.3-47.1l15.9 47-12.8-47.6c11.6 4.4 26.1 12.4 35.2 24.8 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8z"/>
<path class="st6" d="m86.3 59.1c21.7 10.9 32.4 36.6 35.8 54.9l-15.2-6.6-14.5-6.3-50.6-22 48.8 24.9 13.6 6.9 14.3 7.3c-16.6 7.9-41.3 14.5-62.1 4.1-1.8-0.9-3.6-1.9-5.4-3.2-2.3-1.5-4.5-3.2-6.8-5.1-19.9-16.4-40.3-46.4-42.7-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.2 0.8 6.2 1.6 9.1 2.5 4 1.3 7.6 2.8 10.9 4.4z"/>
<path class="st7" d="m75.4 54.8c18.9 12 28.4 35.6 31.6 52.6l-14.5-6.3-50.6-22 48.7 24.9 13.6 6.9c-14.1 6.8-34.5 13-53.3 8.2-2.3-1.5-4.5-3.2-6.8-5.1-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.1 0.8 6.2 1.6 9.1 2.6z"/>
<path class="st8" d="m66.3 52.2c15.3 12.8 23.3 33.6 26.1 48.9l-50.6-22 48.8 24.9c-12.2 6-29.6 11.8-46.5 10-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3z"/>
</g>
</svg>
<button type="button" data-toggle="dropdown" data-placement="right"
aria-haspopup="true" aria-expanded="false"
class="icon-action global-menu-button" title="Menu">
<div class="global-menu-button-update-available"></div>
</button>
<ul class="dropdown-menu dropdown-menu-right">
<li class="dropdown-item" data-trigger-command="showOptions">
<span class="bx bx-slider"></span>
Options
<span class="bx bx-cog"></span>
${t('global_menu.options')}
</li>
<li class="dropdown-item" data-trigger-command="openNewWindow">
<span class="bx bx-window-open"></span>
Open New Window
${t('global_menu.open_new_window')}
<kbd data-command="openNewWindow"></kbd>
</li>
<li class="dropdown-item switch-to-mobile-version-button" data-trigger-command="switchToMobileVersion">
<span class="bx bx-mobile"></span>
Switch to Mobile Version
${t('global_menu.switch_to_mobile_version')}
</li>
<li class="dropdown-item switch-to-desktop-version-button" data-trigger-command="switchToDesktopVersion">
<span class="bx bx-desktop"></span>
Switch to Desktop Version
${t('global_menu.switch_to_desktop_version')}
</li>
<span class="zoom-container dropdown-item">
<div>
<span class="bx bx-empty"></span>
Zoom
${t('global_menu.zoom')}
</div>
<div class="zoom-buttons">
<a data-trigger-command="toggleFullscreen" title="Toggle Fullscreen" class="bx bx-expand-alt"></a>
<a data-trigger-command="toggleFullscreen" title="${t('global_menu.toggle_fullscreen')}" class="bx bx-expand-alt"></a>
&nbsp;
<a data-trigger-command="zoomOut" title="Zoom Out" class="bx bx-minus"></a>
<a data-trigger-command="zoomOut" title="${t('global_menu.zoom_out')}" class="bx bx-minus"></a>
<span data-trigger-command="zoomReset" title="Reset Zoom Level" class="zoom-state"></span>
<span data-trigger-command="zoomReset" title="${t('global_menu.reset_zoom_level')}" class="zoom-state"></span>
<a data-trigger-command="zoomIn" title="Zoom In" class="bx bx-plus"></a>
<a data-trigger-command="zoomIn" title="${t('global_menu.zoom_in')}" class="bx bx-plus"></a>
</div>
</span>
<li class="dropdown-item" data-trigger-command="showLaunchBarSubtree">
<span class="bx bx-sidebar"></span>
Configure Launchbar
${t('global_menu.configure_launchbar')}
</li>
<li class="dropdown-item" data-trigger-command="showShareSubtree">
<span class="bx bx-share-alt"></span>
Show Shared Notes Subtree
${t('global_menu.show_shared_notes_subtree')}
</li>
<li class="dropdown-item dropdown-submenu">
<span class="dropdown-toggle">
<span class="bx bx-empty"></span>
Advanced
<span class="bx bx-chip"></span>
${t('global_menu.advanced')}
</span>
<ul class="dropdown-menu">
<li class="dropdown-item open-dev-tools-button" data-trigger-command="openDevTools">
<span class="bx bx-bug-alt"></span>
Open Dev Tools
${t('global_menu.open_dev_tools')}
<kbd data-command="openDevTools"></kbd>
</li>
<li class="dropdown-item" data-trigger-command="showSQLConsole">
<span class="bx bx-data"></span>
Open SQL Console
${t('global_menu.open_sql_console')}
<kbd data-command="showSQLConsole"></kbd>
</li>
<li class="dropdown-item" data-trigger-command="showSQLConsoleHistory">
<span class="bx bx-empty"></span>
Open SQL Console History
<span class="bx bx-data"></span>
${t('global_menu.open_sql_console_history')}
</li>
<li class="dropdown-item" data-trigger-command="showSearchHistory">
<span class="bx bx-empty"></span>
Open Search History
<span class="bx bx-search-alt"></span>
${t('global_menu.open_search_history')}
</li>
<li class="dropdown-item" data-trigger-command="showBackendLog">
<span class="bx bx-empty"></span>
Show Backend Log
<span class="bx bx-detail"></span>
${t('global_menu.show_backend_log')}
<kbd data-command="showBackendLog"></kbd>
</li>
<li class="dropdown-item" data-trigger-command="reloadFrontendApp"
title="Reload can help with some visual glitches without restarting the whole app.">
<span class="bx bx-empty"></span>
Reload Frontend
title="${t('global_menu.reload_hint')}">
<span class="bx bx-refresh"></span>
${t('global_menu.reload_frontend')}
<kbd data-command="reloadFrontendApp"></kbd>
</li>
<li class="dropdown-item" data-trigger-command="showHiddenSubtree">
<span class="bx bx-empty"></span>
Show Hidden Subtree
<span class="bx bx-hide"></span>
${t('global_menu.show_hidden_subtree')}
</li>
</ul>
</li>
<li class="dropdown-item show-help-button" data-trigger-command="showHelp">
<span class="bx bx-info-circle"></span>
Show Help
<span class="bx bx-help-circle"></span>
${t('global_menu.show_help')}
<kbd data-command="showHelp"></kbd>
</li>
<li class="dropdown-item show-about-dialog-button">
<span class="bx bx-empty"></span>
About TriliumNext Notes
<span class="bx bx-info-circle"></span>
${t('global_menu.about')}
</li>
<li class="dropdown-item update-to-latest-version-button" data-trigger-command="downloadLatestVersion">
@@ -215,7 +243,7 @@ const TPL = `
<li class="dropdown-item logout-button" data-trigger-command="logout">
<span class="bx bx-log-out"></span>
Logout
${t('global_menu.logout')}
</li>
</ul>
</div>
@@ -231,10 +259,9 @@ export default class GlobalMenuWidget extends BasicWidget {
doRender() {
this.$widget = $(TPL);
this.$dropdown = this.$widget.find("[data-toggle='dropdown']");
const $button = this.$widget.find(".global-menu-button");
$button.tooltip({ trigger: "hover" });
$button.on("click", () => $button.tooltip("hide"));
this.dropdown = bootstrap.Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']"));
this.tooltip = new bootstrap.Tooltip(this.$widget.find("[data-bs-toggle='tooltip']"), { trigger: "hover" });
this.$widget.find(".show-about-dialog-button").on('click', () => this.triggerCommand("openAboutDialog"));
@@ -250,8 +277,13 @@ export default class GlobalMenuWidget extends BasicWidget {
return;
}
this.$dropdown.dropdown('toggle');
this.dropdown.toggle();
});
this.$widget.on('click', '.dropdown-submenu', e => {
if ($(e.target).children(".dropdown-menu").length === 1 || $(e.target).hasClass('dropdown-toggle')) {
e.stopPropagation();
}
})
this.$widget.find(".global-menu-button-update-available").append(
this.updateAvailableWidget.render()
@@ -264,7 +296,12 @@ export default class GlobalMenuWidget extends BasicWidget {
}
this.$zoomState = this.$widget.find(".zoom-state");
this.$widget.on('show.bs.dropdown', () => this.updateZoomState());
this.$widget.on('show.bs.dropdown', () => {
this.updateZoomState();
this.tooltip.hide();
this.tooltip.disable();
});
this.$widget.on('hide.bs.dropdown', () => this.tooltip.enable());
this.$widget.find(".zoom-buttons").on("click",
// delay to wait for the actual zoom change
@@ -310,14 +347,14 @@ export default class GlobalMenuWidget extends BasicWidget {
}
downloadLatestVersionCommand() {
window.open("https://github.com/zadam/trilium/releases/latest");
window.open("https://github.com/TriliumNext/Notes/releases/latest");
}
activeContextChangedEvent() {
this.$dropdown.dropdown('hide');
this.dropdown.hide();
}
noteSwitchedEvent() {
this.$dropdown.dropdown('hide');
this.dropdown.hide();
}
}

View File

@@ -1,3 +1,4 @@
import { t } from "../../../services/i18n.js";
import AbstractLauncher from "./abstract_launcher.js";
import dialogService from "../../../services/dialog.js";
import appContext from "../../../components/app_context.js";
@@ -58,7 +59,7 @@ export default class NoteLauncher extends AbstractLauncher {
const targetNoteId = this.launcherNote.getRelationValue('target');
if (!targetNoteId) {
dialogService.info("This launcher doesn't define target note.");
dialogService.info(t("note_launcher.this_launcher_doesnt_define_target_note"));
return;
}

View File

@@ -1,6 +1,7 @@
import options from "../../services/options.js";
import splitService from "../../services/resizer.js";
import CommandButtonWidget from "./command_button.js";
import { t } from "../../services/i18n.js";
export default class LeftPaneToggleWidget extends CommandButtonWidget {
constructor() {
@@ -13,8 +14,8 @@ export default class LeftPaneToggleWidget extends CommandButtonWidget {
: "bx-chevrons-right";
this.settings.title = () => options.is('leftPaneVisible')
? "Hide panel"
: "Open panel";
? t("left_pane_toggle.hide_panel")
: t("left_pane_toggle.show_panel");
this.settings.command = () => options.is('leftPaneVisible')
? "hideLeftPane"

View File

@@ -1,5 +1,6 @@
import OnClickButtonWidget from "./onclick_button.js";
import appContext from "../../components/app_context.js";
import { t } from "../../services/i18n.js";
export default class MovePaneButton extends OnClickButtonWidget {
constructor(isMovingLeft) {
@@ -8,7 +9,7 @@ export default class MovePaneButton extends OnClickButtonWidget {
this.isMovingLeft = isMovingLeft;
this.icon(isMovingLeft ? "bx-chevron-left" : "bx-chevron-right")
.title(isMovingLeft ? "Move left" : "Move right")
.title(isMovingLeft ? t("move_pane_button.move_left") : t("move_pane_button.move_right"))
.titlePlacement("bottom")
.onClick(async (widget, e) => {
e.stopPropagation();

View File

@@ -6,6 +6,7 @@ import server from "../../services/server.js";
import toastService from "../../services/toast.js";
import ws from "../../services/ws.js";
import appContext from "../../components/app_context.js";
import { t } from "../../services/i18n.js";
const TPL = `
<div class="dropdown note-actions">
@@ -16,7 +17,7 @@ const TPL = `
}
.note-actions .dropdown-menu {
width: 15em;
min-width: 15em;
}
.note-actions .dropdown-item[disabled], .note-actions .dropdown-item[disabled]:hover {
@@ -26,26 +27,26 @@ const TPL = `
}
</style>
<button type="button" data-toggle="dropdown" aria-haspopup="true"
<button type="button" data-bs-toggle="dropdown" aria-haspopup="true"
aria-expanded="false" class="icon-action bx bx-dots-vertical-rounded"></button>
<div class="dropdown-menu dropdown-menu-right">
<a data-trigger-command="convertNoteIntoAttachment" class="dropdown-item">Convert into attachment</a>
<a data-trigger-command="renderActiveNote" class="dropdown-item render-note-button"><kbd data-command="renderActiveNote"></kbd> Re-render note</a>
<a data-trigger-command="findInText" class="dropdown-item find-in-text-button">Search in note <kbd data-command="findInText"></a>
<a data-trigger-command="showNoteSource" class="dropdown-item show-source-button"><kbd data-command="showNoteSource"></kbd> Note source</a>
<a data-trigger-command="showAttachments" class="dropdown-item"><kbd data-command="showAttachments"></kbd> Note attachments</a>
<a data-trigger-command="convertNoteIntoAttachment" class="dropdown-item">${t('note_actions.convert_into_attachment')}</a>
<a data-trigger-command="renderActiveNote" class="dropdown-item render-note-button"><kbd data-command="renderActiveNote"></kbd> ${t('note_actions.re_render_note')}</a>
<a data-trigger-command="findInText" class="dropdown-item find-in-text-button">${t('note_actions.search_in_note')} <kbd data-command="findInText"></kbd></a>
<a data-trigger-command="showNoteSource" class="dropdown-item show-source-button"><kbd data-command="showNoteSource"></kbd> ${t('note_actions.note_source')}</a>
<a data-trigger-command="showAttachments" class="dropdown-item show-attachments-button"><kbd data-command="showAttachments"></kbd> ${t('note_actions.note_attachments')}</a>
<a data-trigger-command="openNoteExternally" class="dropdown-item open-note-externally-button"
title="File will be open in an external application and watched for changes. You'll then be able to upload the modified version back to Trilium.">
title="${t('note_actions.open_note_externally_title')}">
<kbd data-command="openNoteExternally"></kbd>
Open note externally
${t('note_actions.open_note_externally')}
</a>
<a data-trigger-command="openNoteCustom" class="dropdown-item open-note-custom-button"><kbd data-command="openNoteCustom"></kbd> Open note custom</a>
<a class="dropdown-item import-files-button">Import files</a>
<a class="dropdown-item export-note-button">Export note</a>
<a class="dropdown-item delete-note-button">Delete note</a>
<a data-trigger-command="printActiveNote" class="dropdown-item print-active-note-button"><kbd data-command="printActiveNote"></kbd> Print note</a>
<a data-trigger-command="forceSaveRevision" class="dropdown-item save-revision-button"><kbd data-command="forceSaveRevision"></kbd> Save revision</a>
<a data-trigger-command="openNoteCustom" class="dropdown-item open-note-custom-button"><kbd data-command="openNoteCustom"></kbd> ${t('note_actions.open_note_custom')}</a>
<a class="dropdown-item import-files-button">${t('note_actions.import_files')}</a>
<a class="dropdown-item export-note-button">${t('note_actions.export_note')}</a>
<a class="dropdown-item delete-note-button">${t('note_actions.delete_note')}</a>
<a data-trigger-command="printActiveNote" class="dropdown-item print-active-note-button"><kbd data-command="printActiveNote"></kbd> ${t('note_actions.print_note')}</a>
<a data-trigger-command="forceSaveRevision" class="dropdown-item save-revision-button"><kbd data-command="forceSaveRevision"></kbd> ${t('note_actions.save_revision')}</a>
</div>
</div>`;
@@ -62,7 +63,9 @@ export default class NoteActionsWidget extends NoteContextAwareWidget {
this.$findInTextButton = this.$widget.find('.find-in-text-button');
this.$printActiveNoteButton = this.$widget.find('.print-active-note-button');
this.$showSourceButton = this.$widget.find('.show-source-button');
this.$showAttachmentsButton = this.$widget.find('.show-attachments-button');
this.$renderNoteButton = this.$widget.find('.render-note-button');
this.$saveRevisionButton = this.$widget.find(".save-revision-button");
this.$exportNoteButton = this.$widget.find('.export-note-button');
this.$exportNoteButton.on("click", () => {
@@ -79,7 +82,7 @@ export default class NoteActionsWidget extends NoteContextAwareWidget {
this.$importNoteButton = this.$widget.find('.import-files-button');
this.$importNoteButton.on("click", () => this.triggerCommand("showImportDialog", {noteId: this.noteId}));
this.$widget.on('click', '.dropdown-item', () => this.$widget.find("[data-toggle='dropdown']").dropdown('toggle'));
this.$widget.on('click', '.dropdown-item', () => this.$widget.find("[data-bs-toggle='dropdown']").dropdown('toggle'));
this.$openNoteExternallyButton = this.$widget.find(".open-note-externally-button");
this.$openNoteCustomButton = this.$widget.find(".open-note-custom-button");
@@ -95,11 +98,14 @@ export default class NoteActionsWidget extends NoteContextAwareWidget {
}
async refreshVisibility(note) {
const isInOptions = note.noteId.startsWith("_options");
this.$convertNoteIntoAttachmentButton.toggle(note.isEligibleForConversionToAttachment());
this.toggleDisabled(this.$findInTextButton, ['text', 'code', 'book'].includes(note.type));
this.toggleDisabled(this.$showSourceButton, ['text', 'code', 'relationMap', 'mermaid', 'canvas'].includes(note.type));
this.toggleDisabled(this.$showAttachmentsButton, !isInOptions);
this.toggleDisabled(this.$showSourceButton, ['text', 'code', 'relationMap', 'mermaid', 'canvas', 'mindMap'].includes(note.type));
this.toggleDisabled(this.$printActiveNoteButton, ['text', 'code'].includes(note.type));
@@ -113,24 +119,26 @@ export default class NoteActionsWidget extends NoteContextAwareWidget {
);
// I don't want to handle all special notes like this, but intuitively user might want to export content of backend log
this.toggleDisabled(this.$exportNoteButton, !['_backendLog'].includes(note.noteId));
this.toggleDisabled(this.$exportNoteButton, !['_backendLog'].includes(note.noteId) && !isInOptions);
this.toggleDisabled(this.$importNoteButton, !['search'].includes(note.type));
this.toggleDisabled(this.$importNoteButton, !['search'].includes(note.type) && !isInOptions);
this.toggleDisabled(this.$deleteNoteButton, !isInOptions);
this.toggleDisabled(this.$saveRevisionButton, !isInOptions);
}
async convertNoteIntoAttachmentCommand() {
if (!await dialogService.confirm(`Are you sure you want to convert note '${this.note.title}' into an attachment of the parent note?`)) {
if (!await dialogService.confirm(t("note_actions.convert_into_attachment_prompt", { title: this.note.title }))) {
return;
}
const {attachment: newAttachment} = await server.post(`notes/${this.noteId}/convert-to-attachment`);
if (!newAttachment) {
toastService.showMessage(`Converting note '${this.note.title}' failed.`);
toastService.showMessage(t("note_actions.convert_into_attachment_failed", { title: this.note.title }));
return;
}
toastService.showMessage(`Note '${newAttachment.title}' has been converted to attachment.`);
toastService.showMessage(t("note_actions.convert_into_attachment_successful", { title: newAttachment.title }));
await ws.waitForMaxKnownEntityChangeId();
await appContext.tabManager.getActiveContext().setNote(newAttachment.ownerId, {
viewScope: {

View File

@@ -1,4 +1,5 @@
import AbstractButtonWidget from "./abstract_button.js";
import { t } from "../../services/i18n.js";
export default class OnClickButtonWidget extends AbstractButtonWidget {
doRender() {
@@ -6,13 +7,12 @@ export default class OnClickButtonWidget extends AbstractButtonWidget {
if (this.settings.onClick) {
this.$widget.on("click", e => {
e.stopPropagation();
this.$widget.tooltip("hide");
this.settings.onClick(this, e);
});
} else {
console.warn(`Button widget '${this.componentId}' has no defined click handler`, this.settings);
console.warn(t("onclick_button.no_click_handler", { componentId: this.componentId }), this.settings);
}
if (this.settings.onAuxClick) {

View File

@@ -1,3 +1,4 @@
import { t } from "../../services/i18n.js";
import protectedSessionHolder from "../../services/protected_session_holder.js";
import CommandButtonWidget from "./command_button.js";
@@ -12,8 +13,8 @@ export default class ProtectedSessionStatusWidget extends CommandButtonWidget {
: "bx-shield-quarter";
this.settings.title = () => protectedSessionHolder.isProtectedSessionAvailable()
? "Protected session is active. Click to leave protected session."
: "Click to enter protected session";
? t("protected_session_status.active")
: t("protected_session_status.inactive");
this.settings.command = () => protectedSessionHolder.isProtectedSessionAvailable()
? "leaveProtectedSession"

View File

@@ -1,3 +1,4 @@
import { t } from "../../services/i18n.js";
import CommandButtonWidget from "./command_button.js";
export default class RevisionsButton extends CommandButtonWidget {
@@ -5,7 +6,7 @@ export default class RevisionsButton extends CommandButtonWidget {
super();
this.icon('bx-history')
.title("Note Revisions")
.title(t("revisions_button.note_revisions"))
.command("showRevisions")
.titlePlacement("bottom")
.class("icon-action");

View File

@@ -1,17 +1,19 @@
import BasicWidget from "../basic_widget.js";
const TPL = `
<div class="dropdown right-dropdown-widget dropright">
<div class="dropdown right-dropdown-widget dropend">
<style>
.right-dropdown-widget {
height: 53px;
}
</style>
<button type="button" data-toggle="dropdown" data-placement="right"
<button type="button" data-bs-toggle="dropdown" data-placement="right"
aria-haspopup="true" aria-expanded="false"
class="bx right-dropdown-button launcher-button"></button>
<div class="tooltip-trigger"></div>
<div class="dropdown-menu dropdown-menu-right"></div>
</div>
`;
@@ -28,12 +30,16 @@ export default class RightDropdownButtonWidget extends BasicWidget {
doRender() {
this.$widget = $(TPL);
this.$dropdownMenu = this.$widget.find(".dropdown-menu");
this.dropdown = bootstrap.Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']"));
const $button = this.$widget.find(".right-dropdown-button")
this.$tooltip = this.$widget.find(".tooltip-trigger").attr("title", this.title);
this.tooltip = new bootstrap.Tooltip(this.$tooltip);
this.$widget.find(".right-dropdown-button")
.addClass(this.iconClass)
.attr("title", this.title)
.tooltip({ trigger: "hover" })
.on("click", () => $button.tooltip("hide"));
.on("click", () => this.tooltip.hide())
.on('mouseenter', () => this.tooltip.show())
.on('mouseleave', () => this.tooltip.hide());
this.$widget.on('show.bs.dropdown', async () => {
await this.dropdownShown();
@@ -51,10 +57,5 @@ export default class RightDropdownButtonWidget extends BasicWidget {
}
// to be overridden
async dropdownShow() {}
hideDropdown() {
this.$widget.dropdown("hide");
this.$dropdownMenu.removeClass("show");
}
async dropdownShow() { }
}

View File

@@ -0,0 +1,50 @@
import OnClickButtonWidget from "./onclick_button.js";
import appContext from "../../components/app_context.js";
import attributeService from "../../services/attributes.js";
import { t } from "../../services/i18n.js";
export default class ShowHighlightsListWidgetButton extends OnClickButtonWidget {
isEnabled() {
return super.isEnabled()
&& this.note
&& this.note.type === 'text'
&& this.noteContext.viewScope.viewMode === 'default';
}
constructor() {
super();
this.icon("bx-highlight")
.title(t("show_highlights_list_widget_button.show_highlights_list"))
.titlePlacement("bottom")
.onClick(widget => {
this.noteContext.viewScope.highlightsListTemporarilyHidden = false;
appContext.triggerEvent("showHighlightsListWidget", { noteId: this.noteId });
this.toggleInt(false);
});
}
async refreshWithNote(note) {
this.toggleInt(this.noteContext.viewScope.highlightsListTemporarilyHidden);
}
async reEvaluateHighlightsListWidgetVisibilityEvent({ noteId }) {
if (noteId === this.noteId) {
await this.refresh();
}
}
async entitiesReloadedEvent({ loadResults }) {
if (loadResults.isNoteContentReloaded(this.noteId)) {
await this.refresh();
} else if (loadResults.getAttributeRows().find(attr => attr.type === 'label'
&& (attr.name.toLowerCase().includes('readonly') || attr.name === 'hideHighlightWidget')
&& attributeService.isAffecting(attr, this.note))) {
await this.refresh();
}
}
async noteTypeMimeChangedEvent({ noteId }) {
if (this.isNote(noteId)) {
await this.refresh();
}
}
}

View File

@@ -0,0 +1,50 @@
import OnClickButtonWidget from "./onclick_button.js";
import appContext from "../../components/app_context.js";
import attributeService from "../../services/attributes.js";
import { t } from "../../services/i18n.js";
export default class ShowTocWidgetButton extends OnClickButtonWidget {
isEnabled() {
return super.isEnabled()
&& this.note
&& this.note.type === 'text'
&& this.noteContext.viewScope.viewMode === 'default';
}
constructor() {
super();
this.icon("bx-objects-horizontal-left")
.title(t("show_toc_widget_button.show_toc"))
.titlePlacement("bottom")
.onClick(widget => {
this.noteContext.viewScope.tocTemporarilyHidden = false;
appContext.triggerEvent("showTocWidget", { noteId: this.noteId });
this.toggleInt(false);
});
}
async refreshWithNote(note) {
this.toggleInt(this.noteContext.viewScope.tocTemporarilyHidden);
}
async reEvaluateTocWidgetVisibilityEvent({ noteId }) {
if (noteId === this.noteId) {
await this.refresh();
}
}
async entitiesReloadedEvent({ loadResults }) {
if (loadResults.isNoteContentReloaded(this.noteId)) {
await this.refresh();
} else if (loadResults.getAttributeRows().find(attr => attr.type === 'label'
&& (attr.name.toLowerCase().includes('readonly') || attr.name === 'toc')
&& attributeService.isAffecting(attr, this.note))) {
await this.refresh();
}
}
async noteTypeMimeChangedEvent({ noteId }) {
if (this.isNote(noteId)) {
await this.refresh();
}
}
}

View File

@@ -1,3 +1,4 @@
import { t } from "../../services/i18n.js";
import BasicWidget from "../basic_widget.js";
const TPL = `
@@ -23,7 +24,7 @@ const TPL = `
}
</style>
<span class="bx bx-sync global-menu-button-update-available-button" title="Update available"></span>
<span class="bx bx-sync global-menu-button-update-available-button" title="${t('update_available.update_available')}"></span>
</div>
`;

View File

@@ -8,7 +8,11 @@ export default class Container extends BasicWidget {
renderChildren() {
for (const widget of this.children) {
this.$widget.append(widget.render());
try {
this.$widget.append(widget.render());
} catch (e) {
widget.logRenderingError(e);
}
}
}
}

View File

@@ -2,20 +2,19 @@ import server from "../../services/server.js";
import utils from "../../services/utils.js";
import { t } from "../../services/i18n.js";
import BasicWidget from "../basic_widget.js";
import openService from "../../services/open.js";
const TPL = `
<div class="about-dialog modal fade mx-auto" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title mr-auto">${t("about.title")}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" style="margin-left: 0;">
<span aria-hidden="true">&times;</span>
</button>
<h5 class="modal-title">${t("about.title")}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<table class="table table-borderless">
<table class="table table-borderless text-nowrap">
<tr>
<th>${t("about.homepage")}</th>
<td><a href="https://github.com/TriliumNext/Notes" class="external">https://github.com/TriliumNext/Notes</a></td>
@@ -72,7 +71,18 @@ export default class AboutDialog extends BasicWidget {
this.$buildDate.text(appInfo.buildDate);
this.$buildRevision.text(appInfo.buildRevision);
this.$buildRevision.attr('href', `https://github.com/TriliumNext/Notes/commit/${appInfo.buildRevision}`);
this.$dataDirectory.text(appInfo.dataDirectory);
if (utils.isElectron()) {
this.$dataDirectory.html($('<a></a>', {
href: '#',
text: appInfo.dataDirectory,
}));
this.$dataDirectory.find("a").on('click', (event) => {
event.preventDefault();
openService.openDirectory(appInfo.dataDirectory);
})
} else {
this.$dataDirectory.text(appInfo.dataDirectory);
}
}
async openAboutDialogEvent() {

View File

@@ -1,3 +1,4 @@
import { t } from "../../services/i18n.js";
import treeService from '../../services/tree.js';
import noteAutocompleteService from "../../services/note_autocomplete.js";
import utils from "../../services/utils.js";
@@ -8,21 +9,17 @@ const TPL = `
<div class="modal-dialog modal-lg" style="max-width: 1000px" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title mr-auto">Add link</h5>
<button type="button" class="help-button" title="Help on links" data-help-page="Links">?</button>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" style="margin-left: 0 !important;">
<span aria-hidden="true">&times;</span>
</button>
<h5 class="modal-title flex-grow-1">${t('add_link.add_link')}</h5>
<button type="button" class="help-button" title="${t('add_link.help_on_links')}" data-help-page="links.html">?</button>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${t('add_link.close')}"></button>
</div>
<form class="add-link-form">
<div class="modal-body">
<div class="form-group">
<label for="add-link-note-autocomplete">Note</label>
<label for="add-link-note-autocomplete">${t('add_link.note')}</label>
<div class="input-group">
<input class="add-link-note-autocomplete form-control" placeholder="search for note by its name">
<input class="add-link-note-autocomplete form-control" placeholder="${t('add_link.search_note')}">
</div>
</div>
@@ -30,20 +27,20 @@ const TPL = `
<div class="add-link-title-radios form-check">
<label class="form-check-label">
<input class="form-check-input" type="radio" name="link-type" value="reference-link" checked>
link title mirrors the note's current title
${t('add_link.link_title_mirrors')}
</label>
</div>
<div class="add-link-title-radios form-check">
<label class="form-check-label">
<input class="form-check-input" type="radio" name="link-type" value="hyper-link">
link title can be changed arbitrarily
${t('add_link.link_title_arbitrary')}
</label>
</div>
<div class="add-link-title-form-group form-group">
<br/>
<label>
Link title
${t('add_link.link_title')}
<input class="link-title form-control" style="width: 100%;">
</label>
@@ -51,7 +48,7 @@ const TPL = `
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">Add link <kbd>enter</kbd></button>
<button type="submit" class="btn btn-primary">${t('add_link.add_link')} <kbd>enter</kbd></button>
</div>
</form>
</div>

View File

@@ -5,6 +5,7 @@ import toastService from "../../services/toast.js";
import utils from "../../services/utils.js";
import BasicWidget from "../basic_widget.js";
import appContext from "../../components/app_context.js";
import { t } from "../../services/i18n.js";
let branchId;
@@ -13,29 +14,22 @@ const TPL = `<div class="branch-prefix-dialog modal fade mx-auto" tabindex="-1"
<form class="branch-prefix-form">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title mr-auto">Edit branch prefix</h5>
<button class="help-button" type="button" data-help-page="Tree-concepts#prefix" title="Help on Tree prefix">?</button>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" style="margin-left: 0;">
<span aria-hidden="true">&times;</span>
</button>
<h5 class="modal-title flex-grow-1">${t('branch_prefix.edit_branch_prefix')}</h5>
<button class="help-button" type="button" data-help-page="tree-concepts.html#prefix" title="${t('branch_prefix.help_on_tree_prefix')}">?</button>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${t('branch_prefix.close')}"></button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="branch-prefix-input">Prefix: </label> &nbsp;
<label for="branch-prefix-input">${t('branch_prefix.prefix')}</label> &nbsp;
<div class="input-group">
<input class="branch-prefix-input form-control">
<div class="input-group-append">
<div class="branch-prefix-note-title input-group-text"></div>
</div>
<div class="branch-prefix-note-title input-group-text"></div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary btn-sm">Save</button>
<button class="btn btn-primary btn-sm">${t('branch_prefix.save')}</button>
</div>
</div>
</form>
@@ -45,6 +39,7 @@ const TPL = `<div class="branch-prefix-dialog modal fade mx-auto" tabindex="-1"
export default class BranchPrefixDialog extends BasicWidget {
doRender() {
this.$widget = $(TPL);
this.modal = bootstrap.Modal.getOrCreateInstance(this.$widget);
this.$form = this.$widget.find(".branch-prefix-form");
this.$treePrefixInput = this.$widget.find(".branch-prefix-input");
this.$noteTitle = this.$widget.find('.branch-prefix-note-title');
@@ -59,7 +54,7 @@ export default class BranchPrefixDialog extends BasicWidget {
}
async refresh(notePath) {
const {noteId, parentNoteId} = treeService.getNoteIdAndParentIdFromUrl(notePath);
const { noteId, parentNoteId } = treeService.getNoteIdAndParentIdFromUrl(notePath);
if (!noteId || !parentNoteId) {
return;
@@ -96,10 +91,10 @@ export default class BranchPrefixDialog extends BasicWidget {
async savePrefix() {
const prefix = this.$treePrefixInput.val();
await server.put(`branches/${branchId}/set-prefix`, {prefix: prefix});
await server.put(`branches/${branchId}/set-prefix`, { prefix: prefix });
this.$widget.modal('hide');
this.modal.hide();
toastService.showMessage("Branch prefix has been saved.");
toastService.showMessage(t('branch_prefix.branch_prefix_saved'));
}
}

View File

@@ -4,6 +4,7 @@ import bulkActionService from "../../services/bulk_action.js";
import utils from "../../services/utils.js";
import server from "../../services/server.js";
import toastService from "../../services/toast.js";
import { t } from "../../services/i18n.js";
const TPL = `
<div class="bulk-actions-dialog modal mx-auto" tabindex="-1" role="dialog">
@@ -37,32 +38,27 @@ const TPL = `
<div class="modal-dialog modal-xl" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title mr-auto">Bulk actions</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" style="margin-left: 0 !important;">
<span aria-hidden="true">&times;</span>
</button>
<h5 class="modal-title">${t('bulk_actions.bulk_actions')}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${t('bulk_actions.close')}"></button>
</div>
<div class="modal-body">
<h4>Affected notes: <span class="affected-note-count">0</span></h4>
<h4>${t('bulk_actions.affected_notes')}: <span class="affected-note-count">0</span></h4>
<div class="form-check">
<label class="form-check-label">
<input class="include-descendants form-check-input" type="checkbox" value="">
Include descendants of the selected notes
</label>
<input class="include-descendants form-check-input" type="checkbox" value="">
<label class="form-check-label">${t('bulk_actions.include_descendants')}</label>
</div>
<h4>Available actions</h4>
<h4>${t('bulk_actions.available_actions')}</h4>
<table class="bulk-available-action-list"></table>
<h4>Chosen actions</h4>
<h4>${t('bulk_actions.chosen_actions')}</h4>
<table class="bulk-existing-action-list"></table>
</div>
<div class="modal-footer">
<button type="submit" class="execute-bulk-actions btn btn-primary">Execute bulk actions</button>
<button type="submit" class="execute-bulk-actions btn btn-primary">${t('bulk_actions.execute_bulk_actions')}</button>
</div>
</div>
</div>
@@ -71,7 +67,6 @@ const TPL = `
export default class BulkActionsDialog extends BasicWidget {
doRender() {
this.$widget = $(TPL);
this.$includeDescendants = this.$widget.find(".include-descendants");
this.$includeDescendants.on("change", () => this.refresh());
@@ -95,7 +90,7 @@ export default class BulkActionsDialog extends BasicWidget {
includeDescendants: this.$includeDescendants.is(":checked")
});
toastService.showMessage("Bulk actions have been executed successfully.", 3000);
toastService.showMessage(t('bulk_actions.bulk_actions_executed'), 3000);
utils.closeActiveDialog();
});
@@ -120,7 +115,7 @@ export default class BulkActionsDialog extends BasicWidget {
if (actions.length > 0) {
this.$existingActionList.append(...actions.map(action => action.render()));
} else {
this.$existingActionList.append($("<p>None yet ... add an action by clicking one of the available ones above.</p>"))
this.$existingActionList.append($("<p>").text(t('bulk_actions.none_yet')))
}
}

View File

@@ -6,44 +6,41 @@ import froca from "../../services/froca.js";
import branchService from "../../services/branches.js";
import appContext from "../../components/app_context.js";
import BasicWidget from "../basic_widget.js";
import { t } from "../../services/i18n.js";
const TPL = `
<div class="clone-to-dialog modal mx-auto" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" style="max-width: 1000px" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title mr-auto">Clone notes to ...</h5>
<button type="button" class="help-button" title="Help on links" data-help-page="Cloning-notes">?</button>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" style="margin-left: 0 !important;">
<span aria-hidden="true">&times;</span>
</button>
<h5 class="modal-title flex-grow-1">${t('clone_to.clone_notes_to')}</h5>
<button type="button" class="help-button" title="${t('clone_to.help_on_links')}" data-help-page="cloning-notes.html">?</button>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form class="clone-to-form">
<div class="modal-body">
<h5>Notes to clone</h5>
<h5>${t('clone_to.notes_to_clone')}</h5>
<ul class="clone-to-note-list" style="max-height: 200px; overflow: auto;"></ul>
<div class="form-group">
<label style="width: 100%">
Target parent note
${t('clone_to.target_parent_note')}
<div class="input-group">
<input class="clone-to-note-autocomplete form-control" placeholder="search for note by its name">
<input class="clone-to-note-autocomplete form-control" placeholder="${t('clone_to.search_for_note_by_its_name')}">
</div>
</label>
</div>
<div class="form-group" title="Cloned note will be shown in note tree with given prefix">
<div class="form-group" title="${t('clone_to.cloned_note_prefix_title')}">
<label style="width: 100%">
Prefix (optional)
${t('clone_to.prefix_optional')}
<input class="clone-prefix form-control" style="width: 100%;">
</label>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">Clone to selected note <kbd>enter</kbd></button>
<button type="submit" class="btn btn-primary">${t('clone_to.clone_to_selected_note')}</button>
</div>
</form>
</div>
@@ -73,16 +70,16 @@ export default class CloneToDialog extends BasicWidget {
this.cloneNotesTo(notePath);
}
else {
logError("No path to clone to.");
logError(t('clone_to.no_path_to_clone_to'));
}
return false;
});
}
async cloneNoteIdsToEvent({noteIds}) {
async cloneNoteIdsToEvent({ noteIds }) {
if (!noteIds || noteIds.length === 0) {
noteIds = [ appContext.tabManager.getActiveContextNoteId() ];
noteIds = [appContext.tabManager.getActiveContextNoteId()];
}
this.clonedNoteIds = [];
@@ -110,7 +107,7 @@ export default class CloneToDialog extends BasicWidget {
}
async cloneNotesTo(notePath) {
const {noteId, parentNoteId} = treeService.getNoteIdAndParentIdFromUrl(notePath);
const { noteId, parentNoteId } = treeService.getNoteIdAndParentIdFromUrl(notePath);
const targetBranchId = await froca.getBranchId(parentNoteId, noteId);
for (const cloneNoteId of this.clonedNoteIds) {
@@ -119,7 +116,7 @@ export default class CloneToDialog extends BasicWidget {
const clonedNote = await froca.getNote(cloneNoteId);
const targetNote = await froca.getBranch(targetBranchId).getNote();
toastService.showMessage(`Note "${clonedNote.title}" has been cloned into ${targetNote.title}`);
toastService.showMessage(t('clone_to.note_cloned', { clonedTitle: clonedNote.title, targetTitle: targetNote.title }));
}
}
}

View File

@@ -1,4 +1,5 @@
import BasicWidget from "../basic_widget.js";
import { t } from "../../services/i18n.js";
const DELETE_NOTE_BUTTON_CLASS = "confirm-dialog-delete-note";
@@ -7,11 +8,8 @@ const TPL = `
<div class="modal-dialog modal-dialog-scrollable" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title mr-auto">Confirmation</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h5 class="modal-title">${t('confirm.confirmation')}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="confirm-dialog-content"></div>
@@ -19,11 +17,11 @@ const TPL = `
<div class="confirm-dialog-custom"></div>
</div>
<div class="modal-footer">
<button class="confirm-dialog-cancel-button btn btn-sm">Cancel</button>
<button class="confirm-dialog-cancel-button btn btn-sm">${t('confirm.cancel')}</button>
&nbsp;
<button class="confirm-dialog-ok-button btn btn-primary btn-sm">OK</button>
<button class="confirm-dialog-ok-button btn btn-primary btn-sm">${t('confirm.ok')}</button>
</div>
</div>
</div>
@@ -39,6 +37,7 @@ export default class ConfirmDialog extends BasicWidget {
doRender() {
this.$widget = $(TPL);
this.modal = bootstrap.Modal.getOrCreateInstance(this.$widget);
this.$confirmContent = this.$widget.find(".confirm-dialog-content");
this.$okButton = this.$widget.find(".confirm-dialog-ok-button");
this.$cancelButton = this.$widget.find(".confirm-dialog-cancel-button");
@@ -61,7 +60,7 @@ export default class ConfirmDialog extends BasicWidget {
this.$okButton.on('click', () => this.doResolve(true));
}
showConfirmDialogEvent({message, callback}) {
showConfirmDialogEvent({ message, callback }) {
this.$originallyFocused = $(':focus');
this.$custom.hide();
@@ -74,15 +73,15 @@ export default class ConfirmDialog extends BasicWidget {
this.$confirmContent.empty().append(message);
this.$widget.modal();
this.modal.show();
this.resolve = callback;
}
showConfirmDeleteNoteBoxWithNoteDialogEvent({title, callback}) {
showConfirmDeleteNoteBoxWithNoteDialogEvent({ title, callback }) {
glob.activeDialog = this.$widget;
this.$confirmContent.text(`Are you sure you want to remove the note "${title}" from relation map?`);
this.$confirmContent.text(`${t('confirm.are_you_sure_remove_note', { title: title })}`);
this.$custom.empty()
.append("<br/>")
@@ -92,18 +91,18 @@ export default class ConfirmDialog extends BasicWidget {
$("<label>")
.addClass("form-check-label")
.attr("style", "text-decoration: underline dotted var(--main-text-color)")
.attr("title", "If you don't check this, the note will be only removed from the relation map.")
.attr("title", `${t('confirm.if_you_dont_check')}`)
.append(
$("<input>")
.attr("type", "checkbox")
.addClass(`form-check-input ${DELETE_NOTE_BUTTON_CLASS}`)
)
.append("Also delete the note")
.append(`${t('confirm.also_delete_note')}`)
));
this.$custom.show();
this.$widget.modal();
this.modal.show();
this.resolve = callback;
}
@@ -116,6 +115,6 @@ export default class ConfirmDialog extends BasicWidget {
this.resolve = null;
this.$widget.modal("hide");
this.modal.hide();
}
}

View File

@@ -3,59 +3,51 @@ import froca from "../../services/froca.js";
import linkService from "../../services/link.js";
import utils from "../../services/utils.js";
import BasicWidget from "../basic_widget.js";
import { t } from "../../services/i18n.js";
const TPL = `
<div class="delete-notes-dialog modal mx-auto" tabindex="-1" role="dialog">
<div class="modal-dialog modal-dialog-scrollable modal-xl" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title mr-auto">Delete notes preview</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">${t('delete_notes.delete_notes_preview')}</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="checkbox">
<label>
<input class="delete-all-clones" value="1" type="checkbox">
delete also all clones (can be undone in recent changes)
</label>
<div class="form-checkbox">
<input class="delete-all-clones form-check-input" value="1" type="checkbox">
<label class="form-check-label">${t('delete_notes.delete_all_clones_description')}</label>
</div>
<div class="checkbox">
<label title="Normal (soft) deletion only marks the notes as deleted and they can be undeleted (in recent changes dialog) within a period of time. Checking this option will erase the notes immediately and it won't be possible to undelete the notes.">
<input class="erase-notes" value="1" type="checkbox">
erase notes permanently (can't be undone), including all clones. This will force application reload.
</label>
<div class="form-checkbox" style="margin-bottom: 1rem">
<input class="erase-notes form-check-input" value="1" type="checkbox">
<label class="form-check-label">${t('delete_notes.erase_notes_warning')}</label>
</div>
<div class="delete-notes-list-wrapper">
<h4>Following notes will be deleted (<span class="deleted-notes-count"></span>)</h4>
<h4>${t('delete_notes.notes_to_be_deleted')} (<span class="deleted-notes-count"></span>)</h4>
<ul class="delete-notes-list" style="max-height: 200px; overflow: auto;"></ul>
</div>
<div class="no-note-to-delete-wrapper alert alert-info">
No note will be deleted (only clones).
${t('delete_notes.no_note_to_delete')}
</div>
<div class="broken-relations-wrapper">
<div class="alert alert-danger">
<h4>Following relations will be broken and deleted (<span class="broke-relations-count"></span>)</h4>
<h4>${t('delete_notes.broken_relations_to_be_deleted')} (<span class="broke-relations-count"></span>)</h4>
<ul class="broken-relations-list" style="max-height: 200px; overflow: auto;"></ul>
</div>
</div>
</div>
<div class="modal-footer">
<button class="delete-notes-dialog-cancel-button btn btn-sm">Cancel</button>
<button class="delete-notes-dialog-cancel-button btn btn-sm">${t('delete_notes.cancel')}</button>
&nbsp;
<button class="delete-notes-dialog-ok-button btn btn-primary btn-sm">OK</button>
<button class="delete-notes-dialog-ok-button btn btn-primary btn-sm">${t('delete_notes.ok')}</button>
</div>
</div>
</div>
@@ -135,9 +127,9 @@ export default class DeleteNotesDialog extends BasicWidget {
for (const attr of response.brokenRelations) {
this.$brokenRelationsList.append(
$("<li>")
.append(`Note `)
.append(`${t('delete_notes.note')} `)
.append(await linkService.createLink(attr.value))
.append(` (to be deleted) is referenced by relation <code>${attr.name}</code> originating from `)
.append(` ${t('delete_notes.to_be_deleted', {attrName: attr.name})} `)
.append(await linkService.createLink(attr.noteId))
);
}

View File

@@ -5,6 +5,7 @@ import toastService from "../../services/toast.js";
import froca from "../../services/froca.js";
import openService from "../../services/open.js";
import BasicWidget from "../basic_widget.js";
import { t } from "../../services/i18n.js";
const TPL = `
<div class="export-dialog modal fade mx-auto" tabindex="-1" role="dialog">
@@ -32,17 +33,15 @@ const TPL = `
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Export note "<span class="export-note-title"></span>"</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h5 class="modal-title">${t('export.export_note_title')} <span class="export-note-title"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${t('export.close')}"></button>
</div>
<form class="export-form">
<div class="modal-body">
<div class="form-check">
<label class="form-check-label">
<input class="export-type-subtree form-check-input" type="radio" name="export-type" value="subtree">
this note and all of its descendants
${t('export.export_type_subtree')}
</label>
</div>
@@ -50,21 +49,21 @@ const TPL = `
<div class="form-check">
<label class="form-check-label">
<input class="form-check-input" type="radio" name="export-subtree-format" value="html">
HTML in ZIP archive - this is recommended since this preserves all the formatting.
${t('export.format_html')}
</label>
</div>
<div class="form-check">
<label class="form-check-label">
<input class="form-check-input" type="radio" name="export-subtree-format" value="markdown">
Markdown - this preserves most of the formatting.
${t('export.format_markdown')}
</label>
</div>
<div class="form-check">
<label class="form-check-label">
<input class="form-check-input" type="radio" name="export-subtree-format" value="opml">
OPML - outliner interchange format for text only. Formatting, images and files are not included.
${t('export.format_opml')}
</label>
</div>
@@ -72,14 +71,14 @@ const TPL = `
<div class="form-check">
<label class="form-check-label">
<input class="form-check-input" type="radio" name="opml-version" value="1.0">
OPML v1.0 - plain text only
${t('export.opml_version_1')}
</label>
</div>
<div class="form-check">
<label class="form-check-label">
<input class="form-check-input" type="radio" name="opml-version" value="2.0">
OMPL v2.0 - allows also HTML
${t('export.opml_version_2')}
</label>
</div>
</div>
@@ -88,7 +87,7 @@ const TPL = `
<div class="form-check">
<label class="form-check-label">
<input class="form-check-input" type="radio" name="export-type" value="single">
only this note without its descendants
${t('export.export_type_single')}
</label>
</div>
@@ -96,20 +95,20 @@ const TPL = `
<div class="form-check">
<label class="form-check-label">
<input class="form-check-input" type="radio" name="export-single-format" value="html">
HTML - this is recommended since this preserves all the formatting.
${t('export.format_html')}
</label>
</div>
<div class="form-check">
<label class="form-check-label">
<input class="form-check-input" type="radio" name="export-single-format" value="markdown">
Markdown - this preserves most of the formatting.
<input class="form-check-input" type="radio" name="export-single-format" value="markdown">
${t('export.format_markdown')}
</label>
</div>
</div>
</div>
<div class="modal-footer">
<button class="export-button btn btn-primary">Export</button>
<button class="export-button btn btn-primary">${t('export.export')}</button>
</div>
</form>
</div>
@@ -126,6 +125,7 @@ export default class ExportDialog extends BasicWidget {
doRender() {
this.$widget = $(TPL);
this.modal = bootstrap.Modal.getOrCreateInstance(this.$widget);
this.$form = this.$widget.find(".export-form");
this.$noteTitle = this.$widget.find(".export-note-title");
this.$subtreeFormats = this.$widget.find(".export-subtree-formats");
@@ -136,13 +136,12 @@ export default class ExportDialog extends BasicWidget {
this.$opmlVersions = this.$widget.find(".opml-versions");
this.$form.on('submit', () => {
this.$widget.modal('hide');
this.modal.hide();
const exportType = this.$widget.find("input[name='export-type']:checked").val();
if (!exportType) {
// this shouldn't happen as we always choose a default export type
toastService.showError("Choose export type first please");
toastService.showError(t('export.choose_export_type'));
return;
}
@@ -188,15 +187,13 @@ export default class ExportDialog extends BasicWidget {
});
}
async showExportDialogEvent({notePath, defaultType}) {
// each opening of the dialog resets the taskId, so we don't associate it with previous exports anymore
async showExportDialogEvent({ notePath, defaultType }) {
this.taskId = '';
this.$exportButton.removeAttr("disabled");
if (defaultType === 'subtree') {
this.$subtreeType.prop("checked", true).trigger('change');
// to show/hide OPML versions
this.$widget.find("input[name=export-subtree-format]:checked").trigger('change');
}
else if (defaultType === 'single') {
@@ -210,7 +207,7 @@ export default class ExportDialog extends BasicWidget {
utils.openDialog(this.$widget);
const {noteId, parentNoteId} = treeService.getNoteIdAndParentIdFromUrl(notePath);
const { noteId, parentNoteId } = treeService.getNoteIdAndParentIdFromUrl(notePath);
this.branchId = await froca.getBranchId(parentNoteId, noteId);
this.$noteTitle.text(await treeService.getNoteTitle(noteId));
@@ -228,7 +225,7 @@ export default class ExportDialog extends BasicWidget {
ws.subscribeToMessages(async message => {
const makeToast = (id, message) => ({
id: id,
title: "Export status",
title: t('export.export_status'),
message: message,
icon: "arrow-square-up-right"
});
@@ -242,10 +239,10 @@ ws.subscribeToMessages(async message => {
toastService.showError(message.message);
}
else if (message.type === 'taskProgressCount') {
toastService.showPersistent(makeToast(message.taskId, `Export in progress: ${message.progressCount}`));
toastService.showPersistent(makeToast(message.taskId, t('export.export_in_progress', { progressCount: message.progressCount })));
}
else if (message.type === 'taskSucceeded') {
const toast = makeToast(message.taskId, "Export finished successfully.");
const toast = makeToast(message.taskId, t('export.export_finished_successfully'));
toast.closeAfter = 5000;
toastService.showPersistent(toast);

View File

@@ -1,33 +1,31 @@
import utils from "../../services/utils.js";
import BasicWidget from "../basic_widget.js";
import { t } from "../../services/i18n.js";
const TPL = `
<div class="help-dialog modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document" style="min-width: 100%; height: 100%; margin: 0;">
<div class="modal-content" style="height: auto;">
<div class="modal-header">
<h5 class="modal-title mr-auto">Help (full documentation is available <a class="external" href="https://github.com/zadam/trilium/wiki">online</a>)</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h5 class="modal-title">${t('help.fullDocumentation')}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${t('help.close')}"></button>
</div>
<div class="modal-body" style="overflow: auto; height: calc(100vh - 70px);">
<div class="card-columns help-cards">
<div class="help-cards row row-cols-3 g-3">
<div class="card">
<div class="card-body">
<h5 class="card-title">Note navigation</h5>
<h5 class="card-title">${t('help.noteNavigation')}</h5>
<p class="card-text">
<ul>
<li><kbd>UP</kbd>, <kbd>DOWN</kbd> - go up/down in the list of notes</li>
<li><kbd>LEFT</kbd>, <kbd>RIGHT</kbd> - collapse/expand node</li>
<li><kbd data-command="backInNoteHistory">not set</kbd>, <kbd data-command="forwardInNoteHistory">not set</kbd> - go back / forwards in the history</li>
<li><kbd data-command="jumpToNote">not set</kbd> - show <a class="external" href="https://github.com/zadam/trilium/wiki/Note-navigation#jump-to-note">"Jump to" dialog</a></li>
<li><kbd data-command="scrollToActiveNote">not set</kbd> - scroll to active note</li>
<li><kbd>Backspace</kbd> - jump to parent note</li>
<li><kbd data-command="collapseTree">not set</kbd> - collapse whole note tree</li>
<li><kbd data-command="collapseSubtree">not set</kbd> - collapse sub-tree</li>
<li><kbd>UP</kbd>, <kbd>DOWN</kbd> - ${t('help.goUpDown')}</li>
<li><kbd>LEFT</kbd>, <kbd>RIGHT</kbd> - ${t('help.collapseExpand')}</li>
<li><kbd data-command="backInNoteHistory">${t('help.notSet')}</kbd>, <kbd data-command="forwardInNoteHistory">${t('help.notSet')}</kbd> - ${t('help.goBackForwards')}</li>
<li><kbd data-command="jumpToNote">${t('help.notSet')}</kbd> - ${t('help.showJumpToNoteDialog')}</li>
<li><kbd data-command="scrollToActiveNote">${t('help.notSet')}</kbd> - ${t('help.scrollToActiveNote')}</li>
<li><kbd>Backspace</kbd> - ${t('help.jumpToParentNote')}</li>
<li><kbd data-command="collapseTree">${t('help.notSet')}</kbd> - ${t('help.collapseWholeTree')}</li>
<li><kbd data-command="collapseSubtree">${t('help.notSet')}</kbd> - ${t('help.collapseSubTree')}</li>
</ul>
</p>
</div>
@@ -35,19 +33,19 @@ const TPL = `
<div class="card">
<div class="card-body">
<h5 class="card-title">Tab shortcuts</h5>
<h5 class="card-title">${t('help.tabShortcuts')}</h5>
<p class="card-text">
<ul>
<li><kbd>CTRL+click</kbd> (or middle mouse click) on note link opens note in a new tab</li>
<li><kbd>CTRL+click</kbd> ${t('help.newTabNoteLink')}</li>
</ul>
Only in desktop (electron build):
${t('help.onlyInDesktop')}:
<ul>
<li><kbd data-command="openNewTab">not set</kbd> open empty tab</li>
<li><kbd data-command="closeActiveTab">not set</kbd> close active tab</li>
<li><kbd data-command="activateNextTab">not set</kbd> activate next tab</li>
<li><kbd data-command="activatePreviousTab">not set</kbd> activate previous tab</li>
<li><kbd data-command="openNewTab">${t('help.notSet')}</kbd> ${t('help.openEmptyTab')}</li>
<li><kbd data-command="closeActiveTab">${t('help.notSet')}</kbd> ${t('help.closeActiveTab')}</li>
<li><kbd data-command="activateNextTab">${t('help.notSet')}</kbd> ${t('help.activateNextTab')}</li>
<li><kbd data-command="activatePreviousTab">${t('help.notSet')}</kbd> ${t('help.activatePreviousTab')}</li>
</ul>
</p>
</div>
@@ -55,13 +53,13 @@ const TPL = `
<div class="card">
<div class="card-body">
<h5 class="card-title">Creating notes</h5>
<h5 class="card-title">${t('help.creatingNotes')}</h5>
<p class="card-text">
<ul>
<li><kbd data-command="createNoteAfter">not set</kbd> - create new note after the active note</li>
<li><kbd data-command="createNoteInto">not set</kbd> - create new sub-note into active note</li>
<li><kbd data-command="editBranchPrefix">not set</kbd> - edit <a class="external" href="https://github.com/zadam/trilium/wiki/Tree concepts#prefix">prefix</a> of active note clone</li>
<li><kbd data-command="createNoteAfter">${t('help.notSet')}</kbd> - ${t('help.createNoteAfter')}</li>
<li><kbd data-command="createNoteInto">${t('help.notSet')}</kbd> - ${t('help.createNoteInto')}</li>
<li><kbd data-command="editBranchPrefix">${t('help.notSet')}</kbd> - ${t('help.editBranchPrefix')}</li>
</ul>
</p>
</div>
@@ -69,19 +67,19 @@ const TPL = `
<div class="card">
<div class="card-body">
<h5 class="card-title">Moving / cloning notes</h5>
<h5 class="card-title">${t('help.movingCloningNotes')}</h5>
<p class="card-text">
<ul>
<li><kbd data-command="moveNoteUp">not set</kbd>, <kbd data-command="moveNoteDown">not set</kbd> - move note up/down in the note list</li>
<li><kbd data-command="moveNoteUpInHierarchy">not set</kbd>, <kbd data-command="moveNoteDownInHierarchy">not set</kbd> - move note up in the hierarchy</li>
<li><kbd data-command="addNoteAboveToSelection">not set</kbd>, <kbd data-command="addNoteBelowToSelection">not set</kbd> - multi-select note above/below</li>
<li><kbd data-command="selectAllNotesInParent">not set</kbd> - select all notes in the current level</li>
<li><kbd>Shift+click</kbd> - select note</li>
<li><kbd data-command="copyNotesToClipboard">not set</kbd> - copy active note (or current selection) into clipboard (used for <a class="external" href="https://github.com/zadam/trilium/wiki/Cloning notes">cloning</a>)</li>
<li><kbd data-command="cutNotesToClipboard">not set</kbd> - cut current (or current selection) note into clipboard (used for moving notes)</li>
<li><kbd data-command="pasteNotesFromClipboard">not set</kbd> - paste note(s) as sub-note into active note (which is either move or clone depending on whether it was copied or cut into clipboard)</li>
<li><kbd data-command="deleteNotes">not set</kbd> - delete note / sub-tree</li>
<li><kbd data-command="moveNoteUp">${t('help.notSet')}</kbd>, <kbd data-command="moveNoteDown">${t('help.notSet')}</kbd> - ${t('help.moveNoteUpDown')}</li>
<li><kbd data-command="moveNoteUpInHierarchy">${t('help.notSet')}</kbd>, <kbd data-command="moveNoteDownInHierarchy">${t('help.notSet')}</kbd> - ${t('help.moveNoteUpHierarchy')}</li>
<li><kbd data-command="addNoteAboveToSelection">${t('help.notSet')}</kbd>, <kbd data-command="addNoteBelowToSelection">${t('help.notSet')}</kbd> - ${t('help.multiSelectNote')}</li>
<li><kbd data-command="selectAllNotesInParent">${t('help.notSet')}</kbd> - ${t('help.selectAllNotes')}</li>
<li><kbd>Shift+click</kbd> - ${t('help.selectNote')}</li>
<li><kbd data-command="copyNotesToClipboard">${t('help.notSet')}</kbd> - ${t('help.copyNotes')}</li>
<li><kbd data-command="cutNotesToClipboard">${t('help.notSet')}</kbd> - ${t('help.cutNotes')}</li>
<li><kbd data-command="pasteNotesFromClipboard">${t('help.notSet')}</kbd> - ${t('help.pasteNotes')}</li>
<li><kbd data-command="deleteNotes">${t('help.notSet')}</kbd> - ${t('help.deleteNotes')}</li>
</ul>
</p>
</div>
@@ -89,17 +87,16 @@ const TPL = `
<div class="card">
<div class="card-body">
<h5 class="card-title">Editing notes</h5>
<h5 class="card-title">${t('help.editingNotes')}</h5>
<p class="card-text">
<ul>
<li><kbd data-command="editNoteTitle">not set</kbd> in tree pane will switch from tree pane into note title. Enter from note title will switch focus to text editor.
<kbd data-command="scrollToActiveNote">not set</kbd> will switch back from editor to tree pane.</li>
<li><kbd>Ctrl+K</kbd> - create / edit external link</li>
<li><kbd data-command="addLinkToText">not set</kbd> - create internal link</li>
<li><kbd data-command="followLinkUnderCursor">not set</kbd> - follow link under cursor</li>
<li><kbd data-command="insertDateTimeToText">not set</kbd> - insert current date and time at caret position</li>
<li><kbd data-command="scrollToActiveNote">not set</kbd> - jump away to the tree pane and scroll to active note</li>
<li><kbd data-command="editNoteTitle">${t('help.notSet')}</kbd> ${t('help.editNoteTitle')}</li>
<li><kbd>Ctrl+K</kbd> - ${t('help.createEditLink')}</li>
<li><kbd data-command="addLinkToText">${t('help.notSet')}</kbd> - ${t('help.createInternalLink')}</li>
<li><kbd data-command="followLinkUnderCursor">${t('help.notSet')}</kbd> - ${t('help.followLink')}</li>
<li><kbd data-command="insertDateTimeToText">${t('help.notSet')}</kbd> - ${t('help.insertDateTime')}</li>
<li><kbd data-command="scrollToActiveNote">${t('help.notSet')}</kbd> - ${t('help.jumpToTreePane')}</li>
</ul>
</p>
</div>
@@ -107,14 +104,14 @@ const TPL = `
<div class="card">
<div class="card-body">
<h5 class="card-title"><a class="external" href="https://github.com/zadam/trilium/wiki/Text-notes#autoformat">Markdown-like autoformatting</a></h5>
<h5 class="card-title"><a class="external" href="https://triliumnext.github.io/Docs/Wiki/text-notes.html#markdown--autoformat">${t('help.markdownAutoformat')}</a></h5>
<p class="card-text">
<ul>
<li><kbd>##</kbd>, <kbd>###</kbd>, <kbd>####</kbd> etc. followed by space for headings</li>
<li><kbd>*</kbd> or <kbd>-</kbd> followed by space for bullet list</li>
<li><kbd>1.</kbd> or <kbd>1)</kbd> followed by space for numbered list</li>
<li>start a line with <kbd>&gt;</kbd> followed by space for block quote</li>
<li><kbd>##</kbd>, <kbd>###</kbd>, <kbd>####</kbd> ${t('help.headings')}</li>
<li>${t('help.bulletList')}</li>
<li>${t('help.numberedList')}</li>
<li>${t('help.blockQuote')}</li>
</ul>
</p>
</div>
@@ -122,13 +119,13 @@ const TPL = `
<div class="card">
<div class="card-body">
<h5 class="card-title">Troubleshooting</h5>
<h5 class="card-title">${t('help.troubleshooting')}</h5>
<p class="card-text">
<ul>
<li><kbd data-command="reloadFrontendApp">not set</kbd> - reload Trilium frontend</li>
<li><kbd data-command="openDevTools">not set</kbd> - show developer tools</li>
<li><kbd data-command="showSQLConsole">not set</kbd> - show SQL console</li>
<li><kbd data-command="reloadFrontendApp">${t('help.notSet')}</kbd> - ${t('help.reloadFrontend')}</li>
<li><kbd data-command="openDevTools">${t('help.notSet')}</kbd> - ${t('help.showDevTools')}</li>
<li><kbd data-command="showSQLConsole">${t('help.notSet')}</kbd> - ${t('help.showSQLConsole')}</li>
</ul>
</p>
</div>
@@ -136,12 +133,12 @@ const TPL = `
<div class="card">
<div class="card-body">
<h5 class="card-title">Other</h5>
<h5 class="card-title">${t('help.other')}</h5>
<p class="card-text">
<ul>
<li><kbd data-command="quickSearch">not set</kbd> - focus on quick search input</li>
<li><kbd data-command="findInText">not set</kbd> - in page search</li>
<li><kbd data-command="quickSearch">${t('help.notSet')}</kbd> - ${t('help.quickSearch')}</li>
<li><kbd data-command="findInText">${t('help.notSet')}</kbd> - ${t('help.inPageSearch')}</li>
</ul>
</p>
</div>

View File

@@ -3,75 +3,72 @@ import treeService from "../../services/tree.js";
import importService from "../../services/import.js";
import options from "../../services/options.js";
import BasicWidget from "../basic_widget.js";
import { t } from "../../services/i18n.js";
const TPL = `
<div class="import-dialog modal fade mx-auto" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Import into note</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h5 class="modal-title">${t('import.importIntoNote')}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${t('import.close')}"></button>
</div>
<form class="import-form">
<div class="modal-body">
<div class="form-group">
<label for="import-file-upload-input"><strong>Choose import file</strong></label>
<label for="import-file-upload-input"><strong>${t('import.chooseImportFile')}</strong></label>
<input type="file" class="import-file-upload-input form-control-file" multiple />
<p>Content of the selected file(s) will be imported as child note(s) into <strong class="import-note-title"></strong>.
<p>${t('import.importDescription')} <strong class="import-note-title"></strong>.
</div>
<div class="form-group">
<strong>Options:</strong>
<strong>${t('import.options')}:</strong>
<div class="checkbox">
<label data-toggle="tooltip" title="Trilium <code>.zip</code> export files can contain executable scripts which may contain harmful behavior. Safe import will deactivate automatic execution of all imported scripts. Uncheck &quot;Safe import&quot; only if the imported tar archive is supposed to contain executable scripts and you completely trust the contents of the import file.">
<label data-bs-toggle="tooltip" title="${t('import.safeImportTooltip')}">
<input class="safe-import-checkbox" value="1" type="checkbox" checked>
<span>Safe import</span>
<span>${t('import.safeImport')}</span>
</label>
</div>
<div class="checkbox">
<label data-toggle="tooltip" title="If this is checked then Trilium will read <code>.zip</code>, <code>.enex</code> and <code>.opml</code> files and create notes from files insides those archives. If unchecked, then Trilium will attach the archives themselves to the note.">
<label data-bs-toggle="tooltip" title="${t('import.explodeArchivesTooltip')}">
<input class="explode-archives-checkbox" value="1" type="checkbox" checked>
<span>Read contents of <code>.zip</code>, <code>.enex</code> and <code>.opml</code> archives.</span>
<span>${t('import.explodeArchives')}</span>
</label>
</div>
<div class="checkbox">
<label data-toggle="tooltip" title="<p>If you check this option, Trilium will attempt to shrink the imported images by scaling and optimization which may affect the perceived image quality. If unchecked, images will be imported without changes.</p><p>This doesn't apply to <code>.zip</code> imports with metadata since it is assumed these files are already optimized.</p>">
<input class="shrink-images-checkbox" value="1" type="checkbox" checked> <span>Shrink images</span>
<label data-bs-toggle="tooltip" title="${t('import.shrinkImagesTooltip')}">
<input class="shrink-images-checkbox" value="1" type="checkbox" checked> <span>${t('import.shrinkImages')}</span>
</label>
</div>
<div class="checkbox">
<label>
<input class="text-imported-as-text-checkbox" value="1" type="checkbox" checked>
Import HTML, Markdown and TXT as text notes if it's unclear from metadata
${t('import.textImportedAsText')}
</label>
</div>
<div class="checkbox">
<label>
<input class="code-imported-as-code-checkbox" value="1" type="checkbox" checked> Import recognized code files (e.g. <code>.json</code>) as code notes if it's unclear from metadata
<input class="code-imported-as-code-checkbox" value="1" type="checkbox" checked> ${t('import.codeImportedAsCode')}
</label>
</div>
<div class="checkbox">
<label>
<input class="replace-underscores-with-spaces-checkbox" value="1" type="checkbox" checked>
Replace underscores with spaces in imported note names
${t('import.replaceUnderscoresWithSpaces')}
</label>
</div>
</div>
</div>
<div class="modal-footer">
<button class="import-button btn btn-primary">Import</button>
<button class="import-button btn btn-primary">${t('import.import')}</button>
</div>
</form>
</div>
@@ -87,6 +84,8 @@ export default class ImportDialog extends BasicWidget {
doRender() {
this.$widget = $(TPL);
bootstrap.Modal.getOrCreateInstance(this.$widget);
this.$form = this.$widget.find(".import-form");
this.$noteTitle = this.$widget.find(".import-note-title");
this.$fileUploadInput = this.$widget.find(".import-file-upload-input");
@@ -116,12 +115,14 @@ export default class ImportDialog extends BasicWidget {
}
});
this.$widget.find('[data-toggle="tooltip"]').tooltip({
html: true
let _ = [...this.$widget.find('[data-bs-toggle="tooltip"]')].forEach(element => {
bootstrap.Tooltip.getOrCreateInstance(element, {
html: true
});
});
}
async showImportDialogEvent({noteId}) {
async showImportDialogEvent({ noteId }) {
this.parentNoteId = noteId;
this.$fileUploadInput.val('').trigger('change'); // to trigger Import button disabling listener below

View File

@@ -1,3 +1,4 @@
import { t } from "../../services/i18n.js";
import treeService from '../../services/tree.js';
import noteAutocompleteService from '../../services/note_autocomplete.js';
import utils from "../../services/utils.js";
@@ -9,43 +10,35 @@ const TPL = `
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Include note</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h5 class="modal-title">${t('include_note.dialog_title')}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form class="include-note-form">
<div class="modal-body">
<div class="form-group">
<label for="include-note-autocomplete">Note</label>
<label for="include-note-autocomplete">${t('include_note.label_note')}</label>
<div class="input-group">
<input class="include-note-autocomplete form-control" placeholder="search for note by its name">
<input class="include-note-autocomplete form-control" placeholder="${t('include_note.placeholder_search')}">
</div>
</div>
Box size of the included note:
${t('include_note.box_size_prompt')}
<div class="form-check">
<label class="form-check-label">
<input class="form-check-input" type="radio" name="include-note-box-size" value="small">
small (~ 10 lines)
</label>
<input class="form-check-input" type="radio" name="include-note-box-size" value="small">
<label class="form-check-label">${t('include_note.box_size_small')}</label>
</div>
<div class="form-check">
<label class="form-check-label">
<input class="form-check-input" type="radio" name="include-note-box-size" value="medium" checked>
medium (~ 30 lines)
</label>
<input class="form-check-input" type="radio" name="include-note-box-size" value="medium" checked>
<label class="form-check-label">${t('include_note.box_size_medium')}</label>
</div>
<div class="form-check">
<label class="form-check-label">
<input class="form-check-input" type="radio" name="include-note-box-size" value="full">
full (box shows complete text)
</label>
<input class="form-check-input" type="radio" name="include-note-box-size" value="full">
<label class="form-check-label">${t('include_note.box_size_full')}</label>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">Include note <kbd>enter</kbd></button>
<button type="submit" class="btn btn-primary">${t('include_note.button_include')} <kbd>enter</kbd></button>
</div>
</form>
</div>
@@ -55,17 +48,16 @@ const TPL = `
export default class IncludeNoteDialog extends BasicWidget {
doRender() {
this.$widget = $(TPL);
this.modal = bootstrap.Modal.getOrCreateInstance(this.$widget);
this.$form = this.$widget.find(".include-note-form");
this.$autoComplete = this.$widget.find(".include-note-autocomplete");
this.$form.on('submit', () => {
const notePath = this.$autoComplete.getSelectedNotePath();
if (notePath) {
this.$widget.modal('hide');
this.modal.hide();
this.includeNote(notePath);
}
else {
} else {
logError("No noteId to include.");
}
@@ -73,17 +65,18 @@ export default class IncludeNoteDialog extends BasicWidget {
})
}
async showIncludeNoteDialogEvent({textTypeWidget}) {
async showIncludeNoteDialogEvent({ textTypeWidget }) {
this.textTypeWidget = textTypeWidget;
await this.refresh();
utils.openDialog(this.$widget);
this.$autoComplete
.trigger('focus')
.trigger('select'); // to be able to quickly remove entered text
}
async refresh(widget) {
this.$autoComplete.val('');
noteAutocompleteService.initNoteAutocomplete(this.$autoComplete, {
hideGoToSelectedNoteButton: true,
allowCreatingNotes: true
@@ -94,15 +87,13 @@ export default class IncludeNoteDialog extends BasicWidget {
async includeNote(notePath) {
const noteId = treeService.getNoteIdFromUrl(notePath);
const note = await froca.getNote(noteId);
const boxSize = $("input[name='include-note-box-size']:checked").val();
if (['image', 'canvas', 'mermaid'].includes(note.type)) {
// there's no benefit to use insert note functionlity for images,
// so we'll just add an IMG tag
this.textTypeWidget.addImage(noteId);
}
else {
} else {
this.textTypeWidget.addIncludeNote(noteId, boxSize);
}
}

View File

@@ -1,3 +1,4 @@
import { t } from "../../services/i18n.js";
import utils from "../../services/utils.js";
import BasicWidget from "../basic_widget.js";
@@ -6,17 +7,14 @@ const TPL = `
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title mr-auto">Info message</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h5 class="modal-title">${t("info.modalTitle")}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${t("info.closeButton")}"></button>
</div>
<div class="modal-body">
<div class="info-dialog-content"></div>
</div>
<div class="modal-footer">
<button class="info-dialog-ok-button btn btn-primary btn-sm">OK</button>
<button class="info-dialog-ok-button btn btn-primary btn-sm">${t("info.okButton")}</button>
</div>
</div>
</div>
@@ -32,6 +30,7 @@ export default class InfoDialog extends BasicWidget {
doRender() {
this.$widget = $(TPL);
this.modal = bootstrap.Modal.getOrCreateInstance(this.$widget);
this.$infoContent = this.$widget.find(".info-dialog-content");
this.$okButton = this.$widget.find(".info-dialog-ok-button");
@@ -48,10 +47,10 @@ export default class InfoDialog extends BasicWidget {
}
});
this.$okButton.on('click', () => this.$widget.modal("hide"));
this.$okButton.on('click', () => this.modal.hide());
}
showInfoDialogEvent({message, callback}) {
showInfoDialogEvent({ message, callback }) {
this.$originallyFocused = $(':focus');
this.$infoContent.text(message);

View File

@@ -1,3 +1,4 @@
import { t } from "../../services/i18n.js";
import noteAutocompleteService from '../../services/note_autocomplete.js';
import utils from "../../services/utils.js";
import appContext from "../../components/app_context.js";
@@ -9,18 +10,15 @@ const TPL = `<div class="jump-to-note-dialog modal mx-auto" tabindex="-1" role="
<div class="modal-content">
<div class="modal-header">
<div class="input-group">
<input class="jump-to-note-autocomplete form-control" placeholder="search for note by its name">
<input class="jump-to-note-autocomplete form-control" placeholder="${t('jump_to_note.search_placeholder')}">
</div>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="algolia-autocomplete-container jump-to-note-results"></div>
</div>
<div class="modal-footer">
<button class="show-in-full-text-button btn btn-sm">Search in full text <kbd>Ctrl+Enter</kbd></button>
<button class="show-in-full-text-button btn btn-sm">${t('jump_to_note.search_button')}</button>
</div>
</div>
</div>
@@ -37,6 +35,8 @@ export default class JumpToNoteDialog extends BasicWidget {
doRender() {
this.$widget = $(TPL);
this.modal = bootstrap.Modal.getOrCreateInstance(this.$widget);
this.$autoComplete = this.$widget.find(".jump-to-note-autocomplete");
this.$results = this.$widget.find(".jump-to-note-results");
this.$showInFullTextButton = this.$widget.find(".show-in-full-text-button");
@@ -93,8 +93,8 @@ export default class JumpToNoteDialog extends BasicWidget {
const searchString = this.$autoComplete.val();
this.triggerCommand('searchNotes', {searchString});
this.triggerCommand('searchNotes', { searchString });
this.$widget.modal('hide');
this.modal.hide();
}
}

View File

@@ -1,3 +1,4 @@
import { t } from "../../services/i18n.js";
import toastService from "../../services/toast.js";
import utils from "../../services/utils.js";
import appContext from "../../components/app_context.js";
@@ -10,18 +11,16 @@ const TPL = `
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Markdown import</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h5 class="modal-title">${t("markdown_import.dialog_title")}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>Because of browser sandbox it's not possible to directly read clipboard from JavaScript. Please paste the Markdown to import to textarea below and click on Import button</p>
<p>${t("markdown_import.modal_body_text")}</p>
<textarea class="markdown-import-textarea" style="height: 340px; width: 100%"></textarea>
</div>
<div class="modal-footer">
<button class="markdown-import-button btn btn-primary">Import <kbd>Ctrl+Enter</kbd></button>
<button class="markdown-import-button btn btn-primary">${t("markdown_import.import_button")}</button>
</div>
</div>
</div>
@@ -36,6 +35,7 @@ export default class MarkdownImportDialog extends BasicWidget {
doRender() {
this.$widget = $(TPL);
this.modal = bootstrap.Modal.getOrCreateInstance(this.$widget);
this.$importTextarea = this.$widget.find('.markdown-import-textarea');
this.$importButton = this.$widget.find('.markdown-import-button');
@@ -47,7 +47,7 @@ export default class MarkdownImportDialog extends BasicWidget {
}
async convertMarkdownToHtml(markdownContent) {
const {htmlContent} = await server.post('other/render-markdown', { markdownContent });
const { htmlContent } = await server.post('other/render-markdown', { markdownContent });
const textEditor = await appContext.tabManager.getActiveContext().getTextEditor();
@@ -56,7 +56,7 @@ export default class MarkdownImportDialog extends BasicWidget {
textEditor.model.insertContent(modelFragment, textEditor.model.document.selection);
toastService.showMessage("Markdown content has been imported into the document.");
toastService.showMessage(t("markdown_import.import_success"));
}
async pasteMarkdownIntoTextEvent() {
@@ -69,7 +69,7 @@ export default class MarkdownImportDialog extends BasicWidget {
}
if (utils.isElectron()) {
const {clipboard} = utils.dynamicRequire('electron');
const { clipboard } = utils.dynamicRequire('electron');
const text = clipboard.readText();
this.convertMarkdownToHtml(text);
@@ -82,7 +82,7 @@ export default class MarkdownImportDialog extends BasicWidget {
async sendForm() {
const text = this.$importTextarea.val();
this.$widget.modal('hide');
this.modal.hide();
await this.convertMarkdownToHtml(text);

View File

@@ -5,35 +5,33 @@ import froca from "../../services/froca.js";
import branchService from "../../services/branches.js";
import treeService from "../../services/tree.js";
import BasicWidget from "../basic_widget.js";
import { t } from "../../services/i18n.js";
const TPL = `
<div class="move-to-dialog modal mx-auto" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" style="max-width: 1000px" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title mr-auto">Move notes to ...</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" style="margin-left: 0 !important;">
<span aria-hidden="true">&times;</span>
</button>
<h5 class="modal-title me-auto">${t("move_to.dialog_title")}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form class="move-to-form">
<div class="modal-body">
<h5>Notes to move</h5>
<h5>${t("move_to.notes_to_move")}</h5>
<ul class="move-to-note-list" style="max-height: 200px; overflow: auto;"></ul>
<div class="form-group">
<label style="width: 100%">
Target parent note
${t("move_to.target_parent_note")}
<div class="input-group">
<input class="move-to-note-autocomplete form-control" placeholder="search for note by its name">
<input class="move-to-note-autocomplete form-control" placeholder="${t("move_to.search_placeholder")}">
</div>
</label>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">Move to selected note <kbd>enter</kbd></button>
<button type="submit" class="btn btn-primary">${t("move_to.move_button")}</button>
</div>
</form>
</div>
@@ -63,7 +61,7 @@ export default class MoveToDialog extends BasicWidget {
froca.getBranchId(parentNoteId, noteId).then(branchId => this.moveNotesTo(branchId));
}
else {
logError("No path to move to.");
logError(t("move_to.error_no_path"));
}
return false;
@@ -96,6 +94,6 @@ export default class MoveToDialog extends BasicWidget {
const parentBranch = froca.getBranch(parentBranchId);
const parentNote = await parentBranch.getNote();
toastService.showMessage(`Selected notes have been moved into ${parentNote.title}`);
toastService.showMessage(`${t("move_to.move_success_message")} ${parentNote.title}`);
}
}

View File

@@ -1,3 +1,4 @@
import { t } from "../../services/i18n.js";
import noteTypesService from "../../services/note_types.js";
import BasicWidget from "../basic_widget.js";
@@ -22,17 +23,16 @@ const TPL = `
<div class="modal-dialog" style="max-width: 500px;" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title mr-auto">Choose note type</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" style="margin-left: 0 !important;">
<span aria-hidden="true">&times;</span>
</button>
<h5 class="modal-title">${t("note_type_chooser.modal_title")}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
Choose note type / template of the new note:
${t("note_type_chooser.modal_body")}
<div class="dropdown">
<button class="note-type-dropdown-trigger" type="button" style="display: none;" data-toggle="dropdown">Dropdown trigger</button>
<div class="dropdown" style="display: flex;">
<button class="note-type-dropdown-trigger" type="button" style="display: none;"
data-bs-toggle="dropdown" data-bs-display="static">
</button>
<div class="note-type-dropdown dropdown-menu"></div>
</div>
@@ -52,13 +52,14 @@ export default class NoteTypeChooserDialog extends BasicWidget {
doRender() {
this.$widget = $(TPL);
this.modal = bootstrap.Modal.getOrCreateInstance(this.$widget);
this.$noteTypeDropdown = this.$widget.find(".note-type-dropdown");
this.$noteTypeDropdownTrigger = this.$widget.find(".note-type-dropdown-trigger");
this.$noteTypeDropdownTrigger.dropdown();
this.dropdown = bootstrap.Dropdown.getOrCreateInstance(this.$widget.find(".note-type-dropdown-trigger"));
this.$widget.on("hidden.bs.modal", () => {
if (this.resolve) {
this.resolve({success: false});
this.resolve({ success: false });
}
if (this.$originalFocused) {
@@ -93,7 +94,7 @@ export default class NoteTypeChooserDialog extends BasicWidget {
});
}
async chooseNoteTypeEvent({callback}) {
async chooseNoteTypeEvent({ callback }) {
this.$originalFocused = $(':focus');
const noteTypes = await noteTypesService.getNoteTypeItems();
@@ -102,7 +103,7 @@ export default class NoteTypeChooserDialog extends BasicWidget {
for (const noteType of noteTypes) {
if (noteType.title === '----') {
this.$noteTypeDropdown.append($('<h6 class="dropdown-header">').append("Templates:"));
this.$noteTypeDropdown.append($('<h6 class="dropdown-header">').append(t("note_type_chooser.templates")));
}
else {
this.$noteTypeDropdown.append(
@@ -115,11 +116,11 @@ export default class NoteTypeChooserDialog extends BasicWidget {
}
}
this.$noteTypeDropdownTrigger.dropdown('show');
this.dropdown.show();
this.$originalDialog = glob.activeDialog;
glob.activeDialog = this.$widget;
this.$widget.modal();
glob.activeDialog = this.modal;
this.modal.show();
this.$noteTypeDropdown.find(".dropdown-item:first").focus();
@@ -138,6 +139,6 @@ export default class NoteTypeChooserDialog extends BasicWidget {
});
this.resolve = null;
this.$widget.modal("hide");
this.modal.hide();
}
}

View File

@@ -1,3 +1,4 @@
import { t } from "../../services/i18n.js";
import utils from "../../services/utils.js";
import BasicWidget from "../basic_widget.js";
@@ -6,17 +7,13 @@ const TPL = `
<div class="modal-dialog modal-md" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title mr-auto">Password is not set</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" style="margin-left: 0;">
<span aria-hidden="true">&times;</span>
</button>
<h5 class="modal-title">${t("password_not_set.title")}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
Protected notes are encrypted using a user password, but password has not been set yet.
To be able to protect notes, <a class="open-password-options-button" href="javascript:">
click here to open the Options dialog</a> and set your password.
${t("password_not_set.body1")}
${t("password_not_set.body2")}
</div>
</div>
</div>
@@ -26,8 +23,10 @@ const TPL = `
export default class PasswordNoteSetDialog extends BasicWidget {
doRender() {
this.$widget = $(TPL);
this.modal = bootstrap.Modal.getOrCreateInstance(this.$widget);
this.$openPasswordOptionsButton = this.$widget.find(".open-password-options-button");
this.$openPasswordOptionsButton.on("click", () => {
this.modal.hide();
this.triggerCommand("showOptions", { section: '_optionsPassword' });
});
}

View File

@@ -1,3 +1,4 @@
import { t } from "../../services/i18n.js";
import utils from "../../services/utils.js";
import BasicWidget from "../basic_widget.js";
@@ -7,16 +8,12 @@ const TPL = `
<div class="modal-content">
<form class="prompt-dialog-form">
<div class="modal-header">
<h5 class="prompt-title modal-title mr-auto">Prompt</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<h5 class="prompt-title modal-title">${t("prompt.title")}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body"></div>
<div class="modal-footer">
<button class="prompt-dialog-ok-button btn btn-primary btn-sm">OK <kbd>enter</kbd></button>
<button class="prompt-dialog-ok-button btn btn-primary btn-sm">${t("prompt.ok")}</button>
</div>
</form>
</div>
@@ -33,6 +30,7 @@ export default class PromptDialog extends BasicWidget {
doRender() {
this.$widget = $(TPL);
this.modal = bootstrap.Modal.getOrCreateInstance(this.$widget);
this.$dialogBody = this.$widget.find(".modal-body");
this.$form = this.$widget.find(".prompt-dialog-form");
this.$question = null;
@@ -61,7 +59,7 @@ export default class PromptDialog extends BasicWidget {
e.preventDefault();
this.resolve(this.$answer.val());
this.$widget.modal('hide');
this.modal.hide();
});
}
@@ -69,7 +67,7 @@ export default class PromptDialog extends BasicWidget {
this.shownCb = shown;
this.resolve = callback;
this.$widget.find(".prompt-title").text(title || "Prompt");
this.$widget.find(".prompt-title").text(title || t("prompt.defaultTitle"));
this.$question = $("<label>")
.prop("for", "prompt-dialog-answer")

View File

@@ -1,3 +1,4 @@
import { t } from "../../services/i18n.js";
import protectedSessionService from "../../services/protected_session.js";
import utils from "../../services/utils.js";
import BasicWidget from "../basic_widget.js";
@@ -7,25 +8,17 @@ const TPL = `
<div class="modal-dialog modal-md" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title mr-auto">Protected session</h5>
<button class="help-button" type="button" data-help-page="Protected-notes" title="Help on Protected notes">?</button>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" style="margin-left: 0;">
<span aria-hidden="true">&times;</span>
</button>
<h5 class="modal-title flex-grow-1">${t("protected_session_password.modal_title")}</h5>
<button class="help-button" type="button" data-help-page="protected-notes.html" title="${t("protected_session_password.help_title")}">?</button>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${t("protected_session_password.close_label")}"></button>
</div>
<form class="protected-session-password-form">
<div class="modal-body">
<div class="form-group">
<label>
To proceed with requested action you need to start protected session by entering password:
<input class="form-control protected-session-password" type="password">
</label>
</div>
<label class="col-form-label">${t("protected_session_password.form_label")}</label>
<input class="form-control protected-session-password" type="password">
</div>
<div class="modal-footer">
<button class="btn btn-primary">Start protected session <kbd>enter</kbd></button>
<button class="btn btn-primary">${t("protected_session_password.start_button")}</button>
</div>
</form>
</div>
@@ -35,6 +28,8 @@ const TPL = `
export default class ProtectedSessionPasswordDialog extends BasicWidget {
doRender() {
this.$widget = $(TPL);
this.modal = bootstrap.Modal.getOrCreateInstance(this.$widget);
this.$passwordForm = this.$widget.find(".protected-session-password-form");
this.$passwordInput = this.$widget.find(".protected-session-password");
this.$passwordForm.on('submit', () => {
@@ -54,6 +49,6 @@ export default class ProtectedSessionPasswordDialog extends BasicWidget {
}
closeProtectedSessionPasswordDialogEvent() {
this.$widget.modal('hide');
this.modal.hide();
}
}

View File

@@ -1,3 +1,4 @@
import { t } from "../../services/i18n.js";
import linkService from '../../services/link.js';
import utils from '../../services/utils.js';
import server from '../../services/server.js';
@@ -14,14 +15,9 @@ const TPL = `
<div class="modal-dialog modal-lg modal-dialog-scrollable" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title mr-auto">Recent changes</h5>
<button class="erase-deleted-notes-now-button btn btn-sm" style="padding: 0 10px">
Erase deleted notes now</button>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" style="margin-left: 0 !important;">
<span aria-hidden="true">&times;</span>
</button>
<h5 class="modal-title flex-grow-1">${t('recent_changes.title')}</h5>
<button class="erase-deleted-notes-now-button btn btn-sm" style="padding: 0 10px">${t('recent_changes.erase_notes_button')}</button>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="recent-changes-content"></div>
@@ -33,18 +29,20 @@ const TPL = `
export default class RecentChangesDialog extends BasicWidget {
doRender() {
this.$widget = $(TPL);
this.modal = bootstrap.Modal.getOrCreateInstance(this.$widget);
this.$content = this.$widget.find(".recent-changes-content");
this.$eraseDeletedNotesNow = this.$widget.find(".erase-deleted-notes-now-button");
this.$eraseDeletedNotesNow.on("click", () => {
server.post('notes/erase-deleted-notes-now').then(() => {
this.refresh();
toastService.showMessage("Deleted notes have been erased.");
toastService.showMessage(t('recent_changes.deleted_notes_message'));
});
});
}
async showRecentChangesEvent({ancestorNoteId}) {
async showRecentChangesEvent({ ancestorNoteId }) {
this.ancestorNoteId = ancestorNoteId;
await this.refresh();
@@ -65,7 +63,7 @@ export default class RecentChangesDialog extends BasicWidget {
this.$content.empty();
if (recentChangesRows.length === 0) {
this.$content.append("No changes yet ...");
this.$content.append(t('recent_changes.no_changes_message'));
}
const groupedByDate = this.groupByDate(recentChangesRows);
@@ -85,14 +83,14 @@ export default class RecentChangesDialog extends BasicWidget {
if (change.canBeUndeleted) {
const $undeleteLink = $(`<a href="javascript:">`)
.text("undelete")
.text(t('recent_changes.undelete_link'))
.on('click', async () => {
const text = 'Do you want to undelete this note and its sub-notes?';
const text = t('recent_changes.confirm_undelete');
if (await dialogService.confirm(text)) {
await server.put(`notes/${change.noteId}/undelete`);
this.$widget.modal('hide');
this.modal.hide();
await ws.waitForMaxKnownEntityChangeId();

View File

@@ -1,3 +1,4 @@
import { t } from "../../services/i18n.js";
import utils from '../../services/utils.js';
import server from '../../services/server.js';
import toastService from "../../services/toast.js";
@@ -7,6 +8,7 @@ import openService from "../../services/open.js";
import protectedSessionHolder from "../../services/protected_session_holder.js";
import BasicWidget from "../basic_widget.js";
import dialogService from "../../services/dialog.js";
import options from "../../services/options.js";
const TPL = `
<div class="revisions-dialog modal fade mx-auto" tabindex="-1" role="dialog">
@@ -39,21 +41,18 @@ const TPL = `
<div class="modal-dialog modal-xl" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title mr-auto">Note revisions</h5>
<h5 class="modal-title flex-grow-1">${t("revisions.note_revisions")}</h5>
<button class="revisions-erase-all-revisions-button btn btn-sm"
title="Delete all revisions of this note"
style="padding: 0 10px 0 10px;" type="button">Delete all revisions</button>
<button class="help-button" type="button" data-help-page="Note-revisions" title="Help on Note revisions">?</button>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" style="margin-left: 0 !important;">
<span aria-hidden="true">&times;</span>
</button>
title="${t("revisions.delete_all_revisions")}"
style="padding: 0 10px 0 10px;" type="button">${t("revisions.delete_all_button")}</button>
<button class="help-button" type="button" data-help-page="note-revisions.html" title="${t("revisions.help_title")}">?</button>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" style="display: flex; height: 80vh;">
<div class="dropdown">
<button class="revision-list-dropdown" type="button" style="display: none;" data-toggle="dropdown">Dropdown trigger</button>
<button class="revision-list-dropdown" type="button" style="display: none;"
data-bs-toggle="dropdown" data-bs-display="static">
</button>
<div class="revision-list dropdown-menu" style="position: static; height: 100%; overflow: auto;"></div>
</div>
@@ -68,6 +67,11 @@ const TPL = `
<div class="revision-content"></div>
</div>
</div>
<div class="modal-footer py-0">
<span class="revisions-snapshot-interval flex-grow-1 my-0 py-0"></span>
<span class="maximum-revisions-for-current-note flex-grow-1 my-0 py-0"></span>
<button class="revision-settings-button icon-action bx bx-cog my-0 py-0" title="${t("revisions.settings")}"></button>
</div>
</div>
</div>
</div>`;
@@ -83,44 +87,50 @@ export default class RevisionsDialog extends BasicWidget {
doRender() {
this.$widget = $(TPL);
this.modal = bootstrap.Modal.getOrCreateInstance(this.$widget);
this.$list = this.$widget.find(".revision-list");
this.$listDropdown = this.$widget.find(".revision-list-dropdown");
this.listDropdown = bootstrap.Dropdown.getOrCreateInstance(this.$listDropdown);
this.$content = this.$widget.find(".revision-content");
this.$title = this.$widget.find(".revision-title");
this.$titleButtons = this.$widget.find(".revision-title-buttons");
this.$eraseAllRevisionsButton = this.$widget.find(".revisions-erase-all-revisions-button");
this.$listDropdown.dropdown();
this.$snapshotInterval = this.$widget.find(".revisions-snapshot-interval");
this.$maximumRevisions = this.$widget.find(".maximum-revisions-for-current-note");
this.$revisionSettingsButton = this.$widget.find(".revision-settings-button")
this.listDropdown.show();
this.$listDropdown.parent().on('hide.bs.dropdown', e => {
// prevent closing dropdown by clicking outside
if (e.clickEvent) {
e.preventDefault();
}
// Prevent closing dropdown by pressing ESC and clicking outside
e.preventDefault();
});
document.addEventListener('keydown', e => {
// Close the revision dialog when revision element is focused and ESC is pressed
if (e.key === 'Escape' ||
e.target.classList.contains(['dropdown-item', 'active'])) {
this.modal.hide();
}
}, true)
this.$widget.on('shown.bs.modal', () => {
this.$list.find(`[data-revision-id="${this.revisionId}"]`)
.trigger('focus');
});
this.$eraseAllRevisionsButton.on('click', async () => {
const text = 'Do you want to delete all revisions of this note? This action will erase revision title and content, but still preserve revision metadata.';
const text = t("revisions.confirm_delete_all");
if (await dialogService.confirm(text)) {
await server.remove(`notes/${this.note.noteId}/revisions`);
this.$widget.modal('hide');
this.modal.hide();
toastService.showMessage('Note revisions has been deleted.');
toastService.showMessage(t("revisions.revisions_deleted"));
}
});
this.$list.on('click', '.dropdown-item', e => {
e.preventDefault();
return false;
});
this.$list.on('focus', '.dropdown-item', e => {
this.$list.find('.dropdown-item').each((i, el) => {
$(el).toggleClass('active', el === e.target);
@@ -128,9 +138,13 @@ export default class RevisionsDialog extends BasicWidget {
this.setContentPane();
});
this.$revisionSettingsButton.on('click', async () => {
appContext.tabManager.openContextWithNote('_optionsOther', { activate: true });
});
}
async showRevisionsEvent({noteId = appContext.tabManager.getActiveContextNoteId()}) {
async showRevisionsEvent({ noteId = appContext.tabManager.getActiveContextNoteId() }) {
utils.openDialog(this.$widget);
await this.loadRevisions(noteId);
@@ -149,22 +163,33 @@ export default class RevisionsDialog extends BasicWidget {
$('<a class="dropdown-item" tabindex="0">')
.text(`${item.dateLastEdited.substr(0, 16)} (${utils.formatSize(item.contentLength)})`)
.attr('data-revision-id', item.revisionId)
.attr('title', `This revision was last edited on ${item.dateLastEdited}`)
.attr('title', t("revisions.revision_last_edited", { date: item.dateLastEdited }))
);
}
this.$listDropdown.dropdown('show');
this.listDropdown.show();
if (this.revisionItems.length > 0) {
if (!this.revisionId) {
this.revisionId = this.revisionItems[0].revisionId;
}
} else {
this.$title.text("No revisions for this note yet...");
this.$title.text(t("revisions.no_revisions"));
this.revisionId = null;
}
this.$eraseAllRevisionsButton.toggle(this.revisionItems.length > 0);
// Show the footer of the revisions dialog
this.$snapshotInterval.text(t("revisions.snapshot_interval", { seconds: options.getInt('revisionSnapshotTimeInterval') }))
let revisionsNumberLimit = parseInt(this.note.getLabelValue("versioningLimit") ?? "");
if (!Number.isInteger(revisionsNumberLimit)) {
revisionsNumberLimit = parseInt(options.getInt('revisionSnapshotNumberLimit'));
}
if (revisionsNumberLimit === -1) {
revisionsNumberLimit = "∞"
}
this.$maximumRevisions.text(t("revisions.maximum_revisions", { number: revisionsNumberLimit }))
}
async setContentPane() {
@@ -182,31 +207,31 @@ export default class RevisionsDialog extends BasicWidget {
renderContentButtons(revisionItem) {
this.$titleButtons.empty();
const $restoreRevisionButton = $('<button class="btn btn-sm" type="button">Restore this revision</button>');
const $restoreRevisionButton = $(`<button class="btn btn-sm" type="button">${t("revisions.restore_button")}</button>`);
$restoreRevisionButton.on('click', async () => {
const text = 'Do you want to restore this revision? This will overwrite current title/content of the note with this revision.';
const text = t("revisions.confirm_restore");
if (await dialogService.confirm(text)) {
await server.post(`revisions/${revisionItem.revisionId}/restore`);
this.$widget.modal('hide');
this.modal.hide();
toastService.showMessage('Note revision has been restored.');
toastService.showMessage(t("revisions.revision_restored"));
}
});
const $eraseRevisionButton = $('<button class="btn btn-sm" type="button">Delete this revision</button>');
const $eraseRevisionButton = $(`<button class="btn btn-sm" type="button">${t("revisions.delete_button")}</button>`);
$eraseRevisionButton.on('click', async () => {
const text = 'Do you want to delete this revision? This action will delete revision title and content, but still preserve revision metadata.';
const text = t("revisions.confirm_delete");
if (await dialogService.confirm(text)) {
await server.remove(`revisions/${revisionItem.revisionId}`);
this.loadRevisions(revisionItem.noteId);
toastService.showMessage('Note revision has been deleted.');
toastService.showMessage(t("revisions.revision_deleted"));
}
});
@@ -220,7 +245,7 @@ export default class RevisionsDialog extends BasicWidget {
.append($eraseRevisionButton)
.append(' &nbsp; ');
const $downloadButton = $('<button class="btn btn-sm btn-primary" type="button">Download</button>');
const $downloadButton = $(`<button class="btn btn-sm btn-primary" type="button">${t("revisions.download_button")}</button>`);
$downloadButton.on('click', () => openService.downloadRevision(revisionItem.noteId, revisionItem.revisionId));
@@ -240,32 +265,40 @@ export default class RevisionsDialog extends BasicWidget {
if (this.$content.find('span.math-tex').length > 0) {
await libraryLoader.requireLibrary(libraryLoader.KATEX);
renderMathInElement(this.$content[0], {trust: true});
renderMathInElement(this.$content[0], { trust: true });
}
} else if (revisionItem.type === 'code') {
this.$content.html($("<pre>").text(fullRevision.content));
} else if (revisionItem.type === 'image') {
this.$content.html($("<img>")
// the reason why we put this inline as base64 is that we do not want to let user copy this
// as a URL to be used in a note. Instead, if they copy and paste it into a note, it will be uploaded as a new note
.attr("src", `data:${fullRevision.mime};base64,${fullRevision.content}`)
.css("max-width", "100%")
.css("max-height", "100%"));
if (fullRevision.mime === "image/svg+xml") {
let encodedSVG = encodeURIComponent(fullRevision.content); //Base64 of other format images may be embedded in svg
this.$content.html($("<img>")
.attr("src", `data:${fullRevision.mime};utf8,${encodedSVG}`)
.css("max-width", "100%")
.css("max-height", "100%"));
} else {
this.$content.html($("<img>")
// the reason why we put this inline as base64 is that we do not want to let user copy this
// as a URL to be used in a note. Instead, if they copy and paste it into a note, it will be uploaded as a new note
.attr("src", `data:${fullRevision.mime};base64,${fullRevision.content}`)
.css("max-width", "100%")
.css("max-height", "100%"));
}
} else if (revisionItem.type === 'file') {
const $table = $("<table cellpadding='10'>")
.append($("<tr>").append(
$("<th>").text("MIME: "),
$("<th>").text(t("revisions.mime")),
$("<td>").text(revisionItem.mime)
))
.append($("<tr>").append(
$("<th>").text("File size:"),
$("<th>").text(t("revisions.file_size")),
$("<td>").text(utils.formatSize(revisionItem.contentLength))
));
if (fullRevision.content) {
$table.append($("<tr>").append(
$('<td colspan="2">').append(
$('<div style="font-weight: bold;">').text("Preview:"),
$('<div style="font-weight: bold;">').text(t("revisions.preview")),
$('<pre class="file-preview-content"></pre>')
.text(fullRevision.content)
)
@@ -273,7 +306,7 @@ export default class RevisionsDialog extends BasicWidget {
}
this.$content.html($table);
} else if (revisionItem.type === 'canvas') {
} else if (["canvas", "mindMap"].includes(revisionItem.type)) {
const encodedTitle = encodeURIComponent(revisionItem.title);
this.$content.html($("<img>")
@@ -288,7 +321,7 @@ export default class RevisionsDialog extends BasicWidget {
this.$content.append($("<pre>").text(fullRevision.content));
} else {
this.$content.text("Preview isn't available for this note type.");
this.$content.text(t("revisions.preview_not_available"));
}
}
}

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