mirror of
https://github.com/zadam/trilium.git
synced 2025-11-06 13:26:01 +01:00
Compare commits
219 Commits
v0.98.1
...
feat/quick
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
912bc61730 | ||
|
|
93e8459d4b | ||
|
|
6c26fa709e | ||
|
|
f3416fa03e | ||
|
|
3e5ab2b1e1 | ||
|
|
5c0bc9a7c2 | ||
|
|
2adfa55acd | ||
|
|
8125e8afcd | ||
|
|
e328f18558 | ||
|
|
cbdfa9079c | ||
|
|
e08c4515a7 | ||
|
|
650aa16b89 | ||
|
|
11d908218b | ||
|
|
3627a7dc93 | ||
|
|
1e1c8cc4ff | ||
|
|
d616bc09c9 | ||
|
|
f1cef44d5d | ||
|
|
3795be4750 | ||
|
|
2bb66a7526 | ||
|
|
28a472782f | ||
|
|
a4da002352 | ||
|
|
94fdc2beee | ||
|
|
dd4a01d9f8 | ||
|
|
a2a6c67350 | ||
|
|
2152ca7ba6 | ||
|
|
40e4d236f4 | ||
|
|
0450cd080d | ||
|
|
1eaac79d63 | ||
|
|
19c0305ed9 | ||
|
|
f0d14a966a | ||
|
|
37e6ccdc1a | ||
|
|
06cea99b40 | ||
|
|
1851336862 | ||
|
|
461eb273d9 | ||
|
|
470edc4d70 | ||
|
|
26132a2a56 | ||
|
|
d92bd16042 | ||
|
|
1c7dfa6c91 | ||
|
|
3a3fed4314 | ||
|
|
82bdb76d75 | ||
|
|
066f3ea078 | ||
|
|
9d760a21d5 | ||
|
|
976c795ac6 | ||
|
|
ed320e4e24 | ||
|
|
3111738700 | ||
|
|
1fa0bada23 | ||
|
|
11eca7e58b | ||
|
|
4b50e2f14d | ||
|
|
63faba9603 | ||
|
|
3e213699e0 | ||
|
|
c88ff07691 | ||
|
|
b53aa5cf6e | ||
|
|
2641b9b3fe | ||
|
|
3a2a73992c | ||
|
|
b82b17a701 | ||
|
|
a6202edcd1 | ||
|
|
6eac0cb75d | ||
|
|
83672d6138 | ||
|
|
51dadf72d0 | ||
|
|
0cbf61acb3 | ||
|
|
399c7435ac | ||
|
|
d51fae7878 | ||
|
|
9750e25ad5 | ||
|
|
2f9b2f0e8f | ||
|
|
8deaf22544 | ||
|
|
c38bf09af0 | ||
|
|
4dbc76790a | ||
|
|
917ea3e401 | ||
|
|
5a54dd666f | ||
|
|
733ec2c145 | ||
|
|
e386b03b90 | ||
|
|
25d5d51085 | ||
|
|
50c4301a34 | ||
|
|
0f60c0696b | ||
|
|
3ec2947c4f | ||
|
|
e89162838e | ||
|
|
72181090a5 | ||
|
|
72b2a5cc0d | ||
|
|
1eaeec8100 | ||
|
|
660db3b3ab | ||
|
|
89d2fcb81e | ||
|
|
ccda623840 | ||
|
|
36fb097d1d | ||
|
|
35ef5fd0d3 | ||
|
|
885dd2053b | ||
|
|
6f6f280bdd | ||
|
|
a3e8fd374f | ||
|
|
f91c1f4180 | ||
|
|
d85746c1b9 | ||
|
|
5a17075eef | ||
|
|
6cab47fb55 | ||
|
|
f2db7baeba | ||
|
|
a507991808 | ||
|
|
7c86f90ac6 | ||
|
|
1e9b772692 | ||
|
|
096ab52216 | ||
|
|
88c3cd5cdd | ||
|
|
99a911a220 | ||
|
|
3218ab971b | ||
|
|
274e3c1f7f | ||
|
|
f8916a6e35 | ||
|
|
73f20d01e4 | ||
|
|
2fd3a875b6 | ||
|
|
68cba8d3b2 | ||
|
|
6b28fd405e | ||
|
|
3bccbabe53 | ||
|
|
4b212232c8 | ||
|
|
ac3a8edf2b | ||
|
|
04fbc82d7c | ||
|
|
3f105f7b8b | ||
|
|
b9193a5562 | ||
|
|
e1fa188244 | ||
|
|
80ad87671a | ||
|
|
b6d5a6ec2e | ||
|
|
759398d804 | ||
|
|
c1b30db3d1 | ||
|
|
0c8bfc39ef | ||
|
|
3815fddb27 | ||
|
|
b585a64a38 | ||
|
|
ad85ee3531 | ||
|
|
b607d1e628 | ||
|
|
d7e36bdf93 | ||
|
|
2b8b185b5b | ||
|
|
927ebcbec9 | ||
|
|
ea1397de63 | ||
|
|
ce1f5c6204 | ||
|
|
652114c7b5 | ||
|
|
17cd2128fd | ||
|
|
bc4378cb3e | ||
|
|
9f217b88e4 | ||
|
|
d53faa8c01 | ||
|
|
a934760960 | ||
|
|
82914fc2aa | ||
|
|
db687197de | ||
|
|
efd713dc61 | ||
|
|
3f3c7cfe88 | ||
|
|
73ca285b7a | ||
|
|
168d25c020 | ||
|
|
e8ae5486c8 | ||
|
|
f049b8b915 | ||
|
|
12053e75bb | ||
|
|
62372ed4c5 | ||
|
|
e5caf37697 | ||
|
|
befc5a9530 | ||
|
|
1e00407864 | ||
|
|
73038efccf | ||
|
|
6d37e19b40 | ||
|
|
2c33ef2b0d | ||
|
|
6c30e0836f | ||
|
|
5f77ca31bd | ||
|
|
f7c82d6b09 | ||
|
|
86dd9aa42a | ||
|
|
a85141ace2 | ||
|
|
c33280bbb2 | ||
|
|
df3aa04787 | ||
|
|
4bd25a0d4a | ||
|
|
7fadf4c6e1 | ||
|
|
b24d786933 | ||
|
|
8f69b87dd1 | ||
|
|
8287063aab | ||
|
|
21683db0b8 | ||
|
|
978d829150 | ||
|
|
cc05572a35 | ||
|
|
c5bb310613 | ||
|
|
77551b1fed | ||
|
|
70728c274e | ||
|
|
cee4714665 | ||
|
|
c3eca3b626 | ||
|
|
01e4cd2e78 | ||
|
|
b99d01ad7b | ||
|
|
bf0213907e | ||
|
|
eff5b6459d | ||
|
|
8e29b5eed6 | ||
|
|
c91748da15 | ||
|
|
f04f9dc262 | ||
|
|
e873cdab7e | ||
|
|
f9b6fd6ac5 | ||
|
|
da4810672d | ||
|
|
f772f59d7c | ||
|
|
1964fb90d5 | ||
|
|
5945f2860a | ||
|
|
f45da049b9 | ||
|
|
c0beab8a5d | ||
|
|
cabeb13adb | ||
|
|
e2e9721d5f | ||
|
|
4e9deab605 | ||
|
|
9bb048fb01 | ||
|
|
6849f80506 | ||
|
|
5597f4e2e0 | ||
|
|
45fbcec805 | ||
|
|
d76d50f30e | ||
|
|
4685aef88d | ||
|
|
a106510924 | ||
|
|
9d54503ef7 | ||
|
|
b1449eebf3 | ||
|
|
b213453062 | ||
|
|
076c0321cf | ||
|
|
4d71b73f38 | ||
|
|
b20ffdf7db | ||
|
|
ef018e22d6 | ||
|
|
3fa290a257 | ||
|
|
cdde530b60 | ||
|
|
aa608510d0 | ||
|
|
009fd63ce9 | ||
|
|
bea352855a | ||
|
|
51e8a80ca3 | ||
|
|
8a543d4513 | ||
|
|
945e180a6f | ||
|
|
b93fa332d3 | ||
|
|
9e947f742d | ||
|
|
033e90f8b7 | ||
|
|
be576176c5 | ||
|
|
4da3e8a4d8 | ||
|
|
db2bf537ea | ||
|
|
9a4fdcaef2 | ||
|
|
ca40360f7d | ||
|
|
799e705ff8 | ||
|
|
59486cd55d | ||
|
|
afe3904ea3 |
@@ -1,6 +1,6 @@
|
|||||||
root = true
|
root = true
|
||||||
|
|
||||||
[*.{js,ts}]
|
[*.{js,ts,.tsx}]
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
|
|||||||
5
.vscode/snippets.code-snippets
vendored
5
.vscode/snippets.code-snippets
vendored
@@ -20,5 +20,10 @@
|
|||||||
"scope": "typescript",
|
"scope": "typescript",
|
||||||
"prefix": "jqf",
|
"prefix": "jqf",
|
||||||
"body": ["private $${1:name}!: JQuery<HTMLElement>;"]
|
"body": ["private $${1:name}!: JQuery<HTMLElement>;"]
|
||||||
|
},
|
||||||
|
"region": {
|
||||||
|
"scope": "css",
|
||||||
|
"prefix": "region",
|
||||||
|
"body": ["/* #region ${1:name} */\n$0\n/* #endregion */"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
"@playwright/test": "1.55.0",
|
"@playwright/test": "1.55.0",
|
||||||
"@stylistic/eslint-plugin": "5.2.3",
|
"@stylistic/eslint-plugin": "5.2.3",
|
||||||
"@types/express": "5.0.3",
|
"@types/express": "5.0.3",
|
||||||
"@types/node": "22.17.2",
|
"@types/node": "22.18.0",
|
||||||
"@types/yargs": "17.0.33",
|
"@types/yargs": "17.0.33",
|
||||||
"@vitest/coverage-v8": "3.2.4",
|
"@vitest/coverage-v8": "3.2.4",
|
||||||
"eslint": "9.34.0",
|
"eslint": "9.34.0",
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
"rcedit": "4.0.1",
|
"rcedit": "4.0.1",
|
||||||
"rimraf": "6.0.1",
|
"rimraf": "6.0.1",
|
||||||
"tslib": "2.8.1",
|
"tslib": "2.8.1",
|
||||||
"typedoc": "0.28.10",
|
"typedoc": "0.28.11",
|
||||||
"typedoc-plugin-missing-exports": "4.1.0"
|
"typedoc-plugin-missing-exports": "4.1.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
"@triliumnext/highlightjs": "workspace:*",
|
"@triliumnext/highlightjs": "workspace:*",
|
||||||
"@triliumnext/share-theme": "workspace:*",
|
"@triliumnext/share-theme": "workspace:*",
|
||||||
"autocomplete.js": "0.38.1",
|
"autocomplete.js": "0.38.1",
|
||||||
"bootstrap": "5.3.7",
|
"bootstrap": "5.3.8",
|
||||||
"boxicons": "2.1.4",
|
"boxicons": "2.1.4",
|
||||||
"dayjs": "1.11.13",
|
"dayjs": "1.11.13",
|
||||||
"dayjs-plugin-utc": "0.1.2",
|
"dayjs-plugin-utc": "0.1.2",
|
||||||
|
|||||||
@@ -31,16 +31,13 @@ import { StartupChecks } from "./startup_checks.js";
|
|||||||
import type { CreateNoteOpts } from "../services/note_create.js";
|
import type { CreateNoteOpts } from "../services/note_create.js";
|
||||||
import { ColumnComponent } from "tabulator-tables";
|
import { ColumnComponent } from "tabulator-tables";
|
||||||
import { ChooseNoteTypeCallback } from "../widgets/dialogs/note_type_chooser.jsx";
|
import { ChooseNoteTypeCallback } from "../widgets/dialogs/note_type_chooser.jsx";
|
||||||
|
import type RootContainer from "../widgets/containers/root_container.js";
|
||||||
|
|
||||||
interface Layout {
|
interface Layout {
|
||||||
getRootWidget: (appContext: AppContext) => RootWidget;
|
getRootWidget: (appContext: AppContext) => RootContainer;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RootWidget extends Component {
|
export interface BeforeUploadListener extends Component {
|
||||||
render: () => JQuery<HTMLElement>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BeforeUploadListener extends Component {
|
|
||||||
beforeUnloadEvent(): boolean;
|
beforeUnloadEvent(): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +82,6 @@ export type CommandMappings = {
|
|||||||
focusTree: CommandData;
|
focusTree: CommandData;
|
||||||
focusOnTitle: CommandData;
|
focusOnTitle: CommandData;
|
||||||
focusOnDetail: CommandData;
|
focusOnDetail: CommandData;
|
||||||
focusOnSearchDefinition: Required<CommandData>;
|
|
||||||
searchNotes: CommandData & {
|
searchNotes: CommandData & {
|
||||||
searchString?: string;
|
searchString?: string;
|
||||||
ancestorNoteId?: string | null;
|
ancestorNoteId?: string | null;
|
||||||
@@ -323,6 +319,7 @@ export type CommandMappings = {
|
|||||||
printActiveNote: CommandData;
|
printActiveNote: CommandData;
|
||||||
exportAsPdf: CommandData;
|
exportAsPdf: CommandData;
|
||||||
openNoteExternally: CommandData;
|
openNoteExternally: CommandData;
|
||||||
|
openNoteCustom: CommandData;
|
||||||
renderActiveNote: CommandData;
|
renderActiveNote: CommandData;
|
||||||
unhoist: CommandData;
|
unhoist: CommandData;
|
||||||
reloadFrontendApp: CommandData;
|
reloadFrontendApp: CommandData;
|
||||||
@@ -526,7 +523,7 @@ export type FilteredCommandNames<T extends CommandData> = keyof Pick<CommandMapp
|
|||||||
export class AppContext extends Component {
|
export class AppContext extends Component {
|
||||||
isMainWindow: boolean;
|
isMainWindow: boolean;
|
||||||
components: Component[];
|
components: Component[];
|
||||||
beforeUnloadListeners: WeakRef<BeforeUploadListener>[];
|
beforeUnloadListeners: (WeakRef<BeforeUploadListener> | (() => boolean))[];
|
||||||
tabManager!: TabManager;
|
tabManager!: TabManager;
|
||||||
layout?: Layout;
|
layout?: Layout;
|
||||||
noteTreeWidget?: NoteTreeWidget;
|
noteTreeWidget?: NoteTreeWidget;
|
||||||
@@ -619,7 +616,7 @@ export class AppContext extends Component {
|
|||||||
component.triggerCommand(commandName, { $el: $(this) });
|
component.triggerCommand(commandName, { $el: $(this) });
|
||||||
});
|
});
|
||||||
|
|
||||||
this.child(rootWidget);
|
this.child(rootWidget as Component);
|
||||||
|
|
||||||
this.triggerEvent("initialRenderComplete", {});
|
this.triggerEvent("initialRenderComplete", {});
|
||||||
}
|
}
|
||||||
@@ -649,13 +646,17 @@ export class AppContext extends Component {
|
|||||||
return $(el).closest(".component").prop("component");
|
return $(el).closest(".component").prop("component");
|
||||||
}
|
}
|
||||||
|
|
||||||
addBeforeUnloadListener(obj: BeforeUploadListener) {
|
addBeforeUnloadListener(obj: BeforeUploadListener | (() => boolean)) {
|
||||||
if (typeof WeakRef !== "function") {
|
if (typeof WeakRef !== "function") {
|
||||||
// older browsers don't support WeakRef
|
// older browsers don't support WeakRef
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof obj === "object") {
|
||||||
this.beforeUnloadListeners.push(new WeakRef<BeforeUploadListener>(obj));
|
this.beforeUnloadListeners.push(new WeakRef<BeforeUploadListener>(obj));
|
||||||
|
} else {
|
||||||
|
this.beforeUnloadListeners.push(obj);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -665,10 +666,11 @@ const appContext = new AppContext(window.glob.isMainWindow);
|
|||||||
$(window).on("beforeunload", () => {
|
$(window).on("beforeunload", () => {
|
||||||
let allSaved = true;
|
let allSaved = true;
|
||||||
|
|
||||||
appContext.beforeUnloadListeners = appContext.beforeUnloadListeners.filter((wr) => !!wr.deref());
|
appContext.beforeUnloadListeners = appContext.beforeUnloadListeners.filter((wr) => typeof wr === "function" || !!wr.deref());
|
||||||
|
|
||||||
for (const weakRef of appContext.beforeUnloadListeners) {
|
for (const listener of appContext.beforeUnloadListeners) {
|
||||||
const component = weakRef.deref();
|
if (typeof listener === "object") {
|
||||||
|
const component = listener.deref();
|
||||||
|
|
||||||
if (!component) {
|
if (!component) {
|
||||||
continue;
|
continue;
|
||||||
@@ -676,14 +678,17 @@ $(window).on("beforeunload", () => {
|
|||||||
|
|
||||||
if (!component.beforeUnloadEvent()) {
|
if (!component.beforeUnloadEvent()) {
|
||||||
console.log(`Component ${component.componentId} is not finished saving its state.`);
|
console.log(`Component ${component.componentId} is not finished saving its state.`);
|
||||||
|
|
||||||
toast.showMessage(t("app_context.please_wait_for_save"), 10000);
|
|
||||||
|
|
||||||
allSaved = false;
|
allSaved = false;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (!listener()) {
|
||||||
|
allSaved = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!allSaved) {
|
if (!allSaved) {
|
||||||
|
toast.showMessage(t("app_context.please_wait_for_save"), 10000);
|
||||||
return "some string";
|
return "some string";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import utils from "../services/utils.js";
|
import utils from "../services/utils.js";
|
||||||
import type { CommandMappings, CommandNames, EventData, EventNames } from "./app_context.js";
|
import type { CommandMappings, CommandNames, EventData, EventNames } from "./app_context.js";
|
||||||
|
|
||||||
|
type EventHandler = ((data: any) => void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract class for all components in the Trilium's frontend.
|
* Abstract class for all components in the Trilium's frontend.
|
||||||
*
|
*
|
||||||
@@ -19,6 +21,7 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
|
|||||||
initialized: Promise<void> | null;
|
initialized: Promise<void> | null;
|
||||||
parent?: TypedComponent<any>;
|
parent?: TypedComponent<any>;
|
||||||
_position!: number;
|
_position!: number;
|
||||||
|
private listeners: Record<string, EventHandler[]> | null = {};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.componentId = `${this.sanitizedClassName}-${utils.randomString(8)}`;
|
this.componentId = `${this.sanitizedClassName}-${utils.randomString(8)}`;
|
||||||
@@ -76,6 +79,14 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
|
|||||||
handleEventInChildren<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown[] | unknown> | null {
|
handleEventInChildren<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown[] | unknown> | null {
|
||||||
const promises: Promise<unknown>[] = [];
|
const promises: Promise<unknown>[] = [];
|
||||||
|
|
||||||
|
// Handle React children.
|
||||||
|
if (this.listeners?.[name]) {
|
||||||
|
for (const listener of this.listeners[name]) {
|
||||||
|
listener(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle legacy children.
|
||||||
for (const child of this.children) {
|
for (const child of this.children) {
|
||||||
const ret = child.handleEvent(name, data) as Promise<void>;
|
const ret = child.handleEvent(name, data) as Promise<void>;
|
||||||
|
|
||||||
@@ -120,6 +131,35 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
|
|||||||
|
|
||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
registerHandler<T extends EventNames>(name: T, handler: EventHandler) {
|
||||||
|
if (!this.listeners) {
|
||||||
|
this.listeners = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.listeners[name]) {
|
||||||
|
this.listeners[name] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.listeners[name].includes(handler)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.listeners[name].push(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeHandler<T extends EventNames>(name: T, handler: EventHandler) {
|
||||||
|
if (!this.listeners?.[name]?.includes(handler)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.listeners[name] = this.listeners[name]
|
||||||
|
.filter(listener => listener !== handler);
|
||||||
|
|
||||||
|
if (!this.listeners[name].length) {
|
||||||
|
delete this.listeners[name];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Component extends TypedComponent<Component> {}
|
export default class Component extends TypedComponent<Component> {}
|
||||||
|
|||||||
@@ -43,8 +43,6 @@ export default class RootCommandExecutor extends Component {
|
|||||||
const noteContext = await appContext.tabManager.openTabWithNoteWithHoisting(searchNote.noteId, {
|
const noteContext = await appContext.tabManager.openTabWithNoteWithHoisting(searchNote.noteId, {
|
||||||
activate: true
|
activate: true
|
||||||
});
|
});
|
||||||
|
|
||||||
appContext.triggerCommand("focusOnSearchDefinition", { ntxId: noteContext.ntxId });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchInSubtreeCommand({ notePath }: CommandListenerData<"searchInSubtree">) {
|
async searchInSubtreeCommand({ notePath }: CommandListenerData<"searchInSubtree">) {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import electronContextMenu from "./menus/electron_context_menu.js";
|
|||||||
import glob from "./services/glob.js";
|
import glob from "./services/glob.js";
|
||||||
import { t } from "./services/i18n.js";
|
import { t } from "./services/i18n.js";
|
||||||
import options from "./services/options.js";
|
import options from "./services/options.js";
|
||||||
import server from "./services/server.js";
|
|
||||||
import type ElectronRemote from "@electron/remote";
|
import type ElectronRemote from "@electron/remote";
|
||||||
import type Electron from "electron";
|
import type Electron from "electron";
|
||||||
import "./stylesheets/bootstrap.scss";
|
import "./stylesheets/bootstrap.scss";
|
||||||
|
|||||||
@@ -1020,6 +1020,14 @@ class FNote {
|
|||||||
return this.noteId.startsWith("_options");
|
return this.noteId.startsWith("_options");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isTriliumSqlite() {
|
||||||
|
return this.mime === "text/x-sqlite;schema=trilium";
|
||||||
|
}
|
||||||
|
|
||||||
|
isTriliumScript() {
|
||||||
|
return this.mime.startsWith("application/javascript");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides note's date metadata.
|
* Provides note's date metadata.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -4,21 +4,13 @@ import TabRowWidget from "../widgets/tab_row.js";
|
|||||||
import TitleBarButtonsWidget from "../widgets/title_bar_buttons.js";
|
import TitleBarButtonsWidget from "../widgets/title_bar_buttons.js";
|
||||||
import LeftPaneContainer from "../widgets/containers/left_pane_container.js";
|
import LeftPaneContainer from "../widgets/containers/left_pane_container.js";
|
||||||
import NoteTreeWidget from "../widgets/note_tree.js";
|
import NoteTreeWidget from "../widgets/note_tree.js";
|
||||||
import NoteTitleWidget from "../widgets/note_title.js";
|
import NoteTitleWidget from "../widgets/note_title.jsx";
|
||||||
import OwnedAttributeListWidget from "../widgets/ribbon_widgets/owned_attribute_list.js";
|
|
||||||
import NoteActionsWidget from "../widgets/buttons/note_actions.js";
|
|
||||||
import NoteDetailWidget from "../widgets/note_detail.js";
|
import NoteDetailWidget from "../widgets/note_detail.js";
|
||||||
import RibbonContainer from "../widgets/containers/ribbon_container.js";
|
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
|
||||||
import PromotedAttributesWidget from "../widgets/ribbon_widgets/promoted_attributes.js";
|
|
||||||
import InheritedAttributesWidget from "../widgets/ribbon_widgets/inherited_attribute_list.js";
|
|
||||||
import NoteListWidget from "../widgets/note_list.js";
|
import NoteListWidget from "../widgets/note_list.js";
|
||||||
import SearchDefinitionWidget from "../widgets/ribbon_widgets/search_definition.js";
|
|
||||||
import SqlResultWidget from "../widgets/sql_result.js";
|
import SqlResultWidget from "../widgets/sql_result.js";
|
||||||
import SqlTableSchemasWidget from "../widgets/sql_table_schemas.js";
|
import SqlTableSchemasWidget from "../widgets/sql_table_schemas.js";
|
||||||
import FilePropertiesWidget from "../widgets/ribbon_widgets/file_properties.js";
|
import NoteIconWidget from "../widgets/note_icon.jsx";
|
||||||
import ImagePropertiesWidget from "../widgets/ribbon_widgets/image_properties.js";
|
|
||||||
import NotePropertiesWidget from "../widgets/ribbon_widgets/note_properties.js";
|
|
||||||
import NoteIconWidget from "../widgets/note_icon.js";
|
|
||||||
import SearchResultWidget from "../widgets/search_result.js";
|
import SearchResultWidget from "../widgets/search_result.js";
|
||||||
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
||||||
import RootContainer from "../widgets/containers/root_container.js";
|
import RootContainer from "../widgets/containers/root_container.js";
|
||||||
@@ -29,15 +21,8 @@ import SplitNoteContainer from "../widgets/containers/split_note_container.js";
|
|||||||
import LeftPaneToggleWidget from "../widgets/buttons/left_pane_toggle.js";
|
import LeftPaneToggleWidget from "../widgets/buttons/left_pane_toggle.js";
|
||||||
import CreatePaneButton from "../widgets/buttons/create_pane_button.js";
|
import CreatePaneButton from "../widgets/buttons/create_pane_button.js";
|
||||||
import ClosePaneButton from "../widgets/buttons/close_pane_button.js";
|
import ClosePaneButton from "../widgets/buttons/close_pane_button.js";
|
||||||
import BasicPropertiesWidget from "../widgets/ribbon_widgets/basic_properties.js";
|
|
||||||
import NoteInfoWidget from "../widgets/ribbon_widgets/note_info_widget.js";
|
|
||||||
import BookPropertiesWidget from "../widgets/ribbon_widgets/book_properties.js";
|
|
||||||
import NoteMapRibbonWidget from "../widgets/ribbon_widgets/note_map.js";
|
|
||||||
import NotePathsWidget from "../widgets/ribbon_widgets/note_paths.js";
|
|
||||||
import SimilarNotesWidget from "../widgets/ribbon_widgets/similar_notes.js";
|
|
||||||
import RightPaneContainer from "../widgets/containers/right_pane_container.js";
|
import RightPaneContainer from "../widgets/containers/right_pane_container.js";
|
||||||
import EditButton from "../widgets/floating_buttons/edit_button.js";
|
import EditButton from "../widgets/floating_buttons/edit_button.js";
|
||||||
import EditedNotesWidget from "../widgets/ribbon_widgets/edited_notes.js";
|
|
||||||
import ShowTocWidgetButton from "../widgets/buttons/show_toc_widget_button.js";
|
import ShowTocWidgetButton from "../widgets/buttons/show_toc_widget_button.js";
|
||||||
import ShowHighlightsListWidgetButton from "../widgets/buttons/show_highlights_list_widget_button.js";
|
import ShowHighlightsListWidgetButton from "../widgets/buttons/show_highlights_list_widget_button.js";
|
||||||
import NoteWrapperWidget from "../widgets/note_wrapper.js";
|
import NoteWrapperWidget from "../widgets/note_wrapper.js";
|
||||||
@@ -51,16 +36,13 @@ import FloatingButtons from "../widgets/floating_buttons/floating_buttons.js";
|
|||||||
import RelationMapButtons from "../widgets/floating_buttons/relation_map_buttons.js";
|
import RelationMapButtons from "../widgets/floating_buttons/relation_map_buttons.js";
|
||||||
import SvgExportButton from "../widgets/floating_buttons/svg_export_button.js";
|
import SvgExportButton from "../widgets/floating_buttons/svg_export_button.js";
|
||||||
import LauncherContainer from "../widgets/containers/launcher_container.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";
|
import CodeButtonsWidget from "../widgets/floating_buttons/code_buttons.js";
|
||||||
import ApiLogWidget from "../widgets/api_log.js";
|
import ApiLogWidget from "../widgets/api_log.js";
|
||||||
import HideFloatingButtonsButton from "../widgets/floating_buttons/hide_floating_buttons_button.js";
|
import HideFloatingButtonsButton from "../widgets/floating_buttons/hide_floating_buttons_button.js";
|
||||||
import ScriptExecutorWidget from "../widgets/ribbon_widgets/script_executor.js";
|
|
||||||
import MovePaneButton from "../widgets/buttons/move_pane_button.js";
|
import MovePaneButton from "../widgets/buttons/move_pane_button.js";
|
||||||
import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
|
import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
|
||||||
import CopyImageReferenceButton from "../widgets/floating_buttons/copy_image_reference_button.js";
|
import CopyImageReferenceButton from "../widgets/floating_buttons/copy_image_reference_button.js";
|
||||||
import ScrollPaddingWidget from "../widgets/scroll_padding.js";
|
import ScrollPaddingWidget from "../widgets/scroll_padding.js";
|
||||||
import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js";
|
|
||||||
import options from "../services/options.js";
|
import options from "../services/options.js";
|
||||||
import utils from "../services/utils.js";
|
import utils from "../services/utils.js";
|
||||||
import GeoMapButtons from "../widgets/floating_buttons/geo_map_button.js";
|
import GeoMapButtons from "../widgets/floating_buttons/geo_map_button.js";
|
||||||
@@ -73,6 +55,7 @@ import ToggleReadOnlyButton from "../widgets/floating_buttons/toggle_read_only_b
|
|||||||
import PngExportButton from "../widgets/floating_buttons/png_export_button.js";
|
import PngExportButton from "../widgets/floating_buttons/png_export_button.js";
|
||||||
import RefreshButton from "../widgets/floating_buttons/refresh_button.js";
|
import RefreshButton from "../widgets/floating_buttons/refresh_button.js";
|
||||||
import { applyModals } from "./layout_commons.js";
|
import { applyModals } from "./layout_commons.js";
|
||||||
|
import Ribbon from "../widgets/ribbon/Ribbon.jsx";
|
||||||
|
|
||||||
export default class DesktopLayout {
|
export default class DesktopLayout {
|
||||||
|
|
||||||
@@ -151,37 +134,15 @@ export default class DesktopLayout {
|
|||||||
.css("min-height", "50px")
|
.css("min-height", "50px")
|
||||||
.css("align-items", "center")
|
.css("align-items", "center")
|
||||||
.cssBlock(".title-row > * { margin: 5px; }")
|
.cssBlock(".title-row > * { margin: 5px; }")
|
||||||
.child(new NoteIconWidget())
|
.child(<NoteIconWidget />)
|
||||||
.child(new NoteTitleWidget())
|
.child(<NoteTitleWidget />)
|
||||||
.child(new SpacerWidget(0, 1))
|
.child(new SpacerWidget(0, 1))
|
||||||
.child(new MovePaneButton(true))
|
.child(new MovePaneButton(true))
|
||||||
.child(new MovePaneButton(false))
|
.child(new MovePaneButton(false))
|
||||||
.child(new ClosePaneButton())
|
.child(new ClosePaneButton())
|
||||||
.child(new CreatePaneButton())
|
.child(new CreatePaneButton())
|
||||||
)
|
)
|
||||||
.child(
|
.child(<Ribbon />)
|
||||||
new RibbonContainer()
|
|
||||||
// the order of the widgets matter. Some of these want to "activate" themselves
|
|
||||||
// when visible. When this happens to multiple of them, the first one "wins".
|
|
||||||
// promoted attributes should always win.
|
|
||||||
.ribbon(new ClassicEditorToolbar())
|
|
||||||
.ribbon(new ScriptExecutorWidget())
|
|
||||||
.ribbon(new SearchDefinitionWidget())
|
|
||||||
.ribbon(new EditedNotesWidget())
|
|
||||||
.ribbon(new BookPropertiesWidget())
|
|
||||||
.ribbon(new NotePropertiesWidget())
|
|
||||||
.ribbon(new FilePropertiesWidget())
|
|
||||||
.ribbon(new ImagePropertiesWidget())
|
|
||||||
.ribbon(new BasicPropertiesWidget())
|
|
||||||
.ribbon(new OwnedAttributeListWidget())
|
|
||||||
.ribbon(new InheritedAttributesWidget())
|
|
||||||
.ribbon(new NotePathsWidget())
|
|
||||||
.ribbon(new NoteMapRibbonWidget())
|
|
||||||
.ribbon(new SimilarNotesWidget())
|
|
||||||
.ribbon(new NoteInfoWidget())
|
|
||||||
.button(new RevisionsButton())
|
|
||||||
.button(new NoteActionsWidget())
|
|
||||||
)
|
|
||||||
.child(new SharedInfoWidget())
|
.child(new SharedInfoWidget())
|
||||||
.child(new WatchedFileUpdateStatusWidget())
|
.child(new WatchedFileUpdateStatusWidget())
|
||||||
.child(
|
.child(
|
||||||
@@ -235,8 +196,8 @@ export default class DesktopLayout {
|
|||||||
.child(new CloseZenButton())
|
.child(new CloseZenButton())
|
||||||
|
|
||||||
// Desktop-specific dialogs.
|
// Desktop-specific dialogs.
|
||||||
.child(new PasswordNoteSetDialog())
|
.child(<PasswordNoteSetDialog />)
|
||||||
.child(new UploadAttachmentsDialog());
|
.child(<UploadAttachmentsDialog />);
|
||||||
|
|
||||||
applyModals(rootContainer);
|
applyModals(rootContainer);
|
||||||
return rootContainer;
|
return rootContainer;
|
||||||
@@ -24,48 +24,48 @@ import InfoDialog from "../widgets/dialogs/info.js";
|
|||||||
import IncorrectCpuArchDialog from "../widgets/dialogs/incorrect_cpu_arch.js";
|
import IncorrectCpuArchDialog from "../widgets/dialogs/incorrect_cpu_arch.js";
|
||||||
import PopupEditorDialog from "../widgets/dialogs/popup_editor.js";
|
import PopupEditorDialog from "../widgets/dialogs/popup_editor.js";
|
||||||
import FlexContainer from "../widgets/containers/flex_container.js";
|
import FlexContainer from "../widgets/containers/flex_container.js";
|
||||||
import NoteIconWidget from "../widgets/note_icon.js";
|
import NoteIconWidget from "../widgets/note_icon";
|
||||||
import NoteTitleWidget from "../widgets/note_title.js";
|
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
|
||||||
import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js";
|
|
||||||
import PromotedAttributesWidget from "../widgets/ribbon_widgets/promoted_attributes.js";
|
|
||||||
import NoteDetailWidget from "../widgets/note_detail.js";
|
import NoteDetailWidget from "../widgets/note_detail.js";
|
||||||
import NoteListWidget from "../widgets/note_list.js";
|
import NoteListWidget from "../widgets/note_list.js";
|
||||||
import { CallToActionDialog } from "../widgets/dialogs/call_to_action.jsx";
|
import CallToActionDialog from "../widgets/dialogs/call_to_action.jsx";
|
||||||
|
import NoteTitleWidget from "../widgets/note_title.jsx";
|
||||||
|
import { PopupEditorFormattingToolbar } from "../widgets/ribbon/FormattingToolbar.js";
|
||||||
|
|
||||||
export function applyModals(rootContainer: RootContainer) {
|
export function applyModals(rootContainer: RootContainer) {
|
||||||
rootContainer
|
rootContainer
|
||||||
.child(new BulkActionsDialog())
|
.child(<BulkActionsDialog />)
|
||||||
.child(new AboutDialog())
|
.child(<AboutDialog />)
|
||||||
.child(new HelpDialog())
|
.child(<HelpDialog />)
|
||||||
.child(new RecentChangesDialog())
|
.child(<RecentChangesDialog />)
|
||||||
.child(new BranchPrefixDialog())
|
.child(<BranchPrefixDialog />)
|
||||||
.child(new SortChildNotesDialog())
|
.child(<SortChildNotesDialog />)
|
||||||
.child(new IncludeNoteDialog())
|
.child(<IncludeNoteDialog />)
|
||||||
.child(new NoteTypeChooserDialog())
|
.child(<NoteTypeChooserDialog />)
|
||||||
.child(new JumpToNoteDialog())
|
.child(<JumpToNoteDialog />)
|
||||||
.child(new AddLinkDialog())
|
.child(<AddLinkDialog />)
|
||||||
.child(new CloneToDialog())
|
.child(<CloneToDialog />)
|
||||||
.child(new MoveToDialog())
|
.child(<MoveToDialog />)
|
||||||
.child(new ImportDialog())
|
.child(<ImportDialog />)
|
||||||
.child(new ExportDialog())
|
.child(<ExportDialog />)
|
||||||
.child(new MarkdownImportDialog())
|
.child(<MarkdownImportDialog />)
|
||||||
.child(new ProtectedSessionPasswordDialog())
|
.child(<ProtectedSessionPasswordDialog />)
|
||||||
.child(new RevisionsDialog())
|
.child(<RevisionsDialog />)
|
||||||
.child(new DeleteNotesDialog())
|
.child(<DeleteNotesDialog />)
|
||||||
.child(new InfoDialog())
|
.child(<InfoDialog />)
|
||||||
.child(new ConfirmDialog())
|
.child(<ConfirmDialog />)
|
||||||
.child(new PromptDialog())
|
.child(<PromptDialog />)
|
||||||
.child(new IncorrectCpuArchDialog())
|
.child(<IncorrectCpuArchDialog />)
|
||||||
.child(new PopupEditorDialog()
|
.child(new PopupEditorDialog()
|
||||||
.child(new FlexContainer("row")
|
.child(new FlexContainer("row")
|
||||||
.class("title-row")
|
.class("title-row")
|
||||||
.css("align-items", "center")
|
.css("align-items", "center")
|
||||||
.cssBlock(".title-row > * { margin: 5px; }")
|
.cssBlock(".title-row > * { margin: 5px; }")
|
||||||
.child(new NoteIconWidget())
|
.child(<NoteIconWidget />)
|
||||||
.child(new NoteTitleWidget()))
|
.child(<NoteTitleWidget />))
|
||||||
.child(new ClassicEditorToolbar())
|
.child(<PopupEditorFormattingToolbar />)
|
||||||
.child(new PromotedAttributesWidget())
|
.child(new PromotedAttributesWidget())
|
||||||
.child(new NoteDetailWidget())
|
.child(new NoteDetailWidget())
|
||||||
.child(new NoteListWidget(true)))
|
.child(new NoteListWidget(true)))
|
||||||
.child(new CallToActionDialog());
|
.child(<CallToActionDialog />);
|
||||||
}
|
}
|
||||||
@@ -7,7 +7,6 @@ import ToggleSidebarButtonWidget from "../widgets/mobile_widgets/toggle_sidebar_
|
|||||||
import MobileDetailMenuWidget from "../widgets/mobile_widgets/mobile_detail_menu.js";
|
import MobileDetailMenuWidget from "../widgets/mobile_widgets/mobile_detail_menu.js";
|
||||||
import ScreenContainer from "../widgets/mobile_widgets/screen_container.js";
|
import ScreenContainer from "../widgets/mobile_widgets/screen_container.js";
|
||||||
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
||||||
import FilePropertiesWidget from "../widgets/ribbon_widgets/file_properties.js";
|
|
||||||
import FloatingButtons from "../widgets/floating_buttons/floating_buttons.js";
|
import FloatingButtons from "../widgets/floating_buttons/floating_buttons.js";
|
||||||
import EditButton from "../widgets/floating_buttons/edit_button.js";
|
import EditButton from "../widgets/floating_buttons/edit_button.js";
|
||||||
import RelationMapButtons from "../widgets/floating_buttons/relation_map_buttons.js";
|
import RelationMapButtons from "../widgets/floating_buttons/relation_map_buttons.js";
|
||||||
@@ -19,14 +18,18 @@ import GlobalMenuWidget from "../widgets/buttons/global_menu.js";
|
|||||||
import LauncherContainer from "../widgets/containers/launcher_container.js";
|
import LauncherContainer from "../widgets/containers/launcher_container.js";
|
||||||
import RootContainer from "../widgets/containers/root_container.js";
|
import RootContainer from "../widgets/containers/root_container.js";
|
||||||
import SharedInfoWidget from "../widgets/shared_info.js";
|
import SharedInfoWidget from "../widgets/shared_info.js";
|
||||||
import PromotedAttributesWidget from "../widgets/ribbon_widgets/promoted_attributes.js";
|
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
|
||||||
import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js";
|
import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js";
|
||||||
import type AppContext from "../components/app_context.js";
|
import type AppContext from "../components/app_context.js";
|
||||||
import TabRowWidget from "../widgets/tab_row.js";
|
import TabRowWidget from "../widgets/tab_row.js";
|
||||||
import RefreshButton from "../widgets/floating_buttons/refresh_button.js";
|
import RefreshButton from "../widgets/floating_buttons/refresh_button.js";
|
||||||
import MobileEditorToolbar from "../widgets/ribbon_widgets/mobile_editor_toolbar.js";
|
import MobileEditorToolbar from "../widgets/type_widgets/ckeditor/mobile_editor_toolbar.js";
|
||||||
import { applyModals } from "./layout_commons.js";
|
import { applyModals } from "./layout_commons.js";
|
||||||
import CloseZenButton from "../widgets/close_zen_button.js";
|
import CloseZenButton from "../widgets/close_zen_button.js";
|
||||||
|
import FilePropertiesTab from "../widgets/ribbon/FilePropertiesTab.jsx";
|
||||||
|
import { useNoteContext } from "../widgets/react/hooks.jsx";
|
||||||
|
import { useContext } from "preact/hooks";
|
||||||
|
import { ParentComponent } from "../widgets/react/react_utils.jsx";
|
||||||
|
|
||||||
const MOBILE_CSS = `
|
const MOBILE_CSS = `
|
||||||
<style>
|
<style>
|
||||||
@@ -144,7 +147,7 @@ export default class MobileLayout {
|
|||||||
.css("font-size", "larger")
|
.css("font-size", "larger")
|
||||||
.css("align-items", "center")
|
.css("align-items", "center")
|
||||||
.child(new ToggleSidebarButtonWidget().contentSized())
|
.child(new ToggleSidebarButtonWidget().contentSized())
|
||||||
.child(new NoteTitleWidget().contentSized().css("position", "relative").css("padding-left", "0.5em"))
|
.child(<NoteTitleWidget />)
|
||||||
.child(new MobileDetailMenuWidget(true).contentSized())
|
.child(new MobileDetailMenuWidget(true).contentSized())
|
||||||
)
|
)
|
||||||
.child(new SharedInfoWidget())
|
.child(new SharedInfoWidget())
|
||||||
@@ -164,7 +167,7 @@ export default class MobileLayout {
|
|||||||
.contentSized()
|
.contentSized()
|
||||||
.child(new NoteDetailWidget())
|
.child(new NoteDetailWidget())
|
||||||
.child(new NoteListWidget(false))
|
.child(new NoteListWidget(false))
|
||||||
.child(new FilePropertiesWidget().css("font-size", "smaller"))
|
.child(<FilePropertiesWrapper />)
|
||||||
)
|
)
|
||||||
.child(new MobileEditorToolbar())
|
.child(new MobileEditorToolbar())
|
||||||
)
|
)
|
||||||
@@ -181,3 +184,13 @@ export default class MobileLayout {
|
|||||||
return rootContainer;
|
return rootContainer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function FilePropertiesWrapper() {
|
||||||
|
const { note } = useNoteContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{note?.type === "file" && <FilePropertiesTab note={note} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import server from "./server.js";
|
|||||||
import froca from "./froca.js";
|
import froca from "./froca.js";
|
||||||
import type FNote from "../entities/fnote.js";
|
import type FNote from "../entities/fnote.js";
|
||||||
import type { AttributeRow } from "./load_results.js";
|
import type { AttributeRow } from "./load_results.js";
|
||||||
|
import { AttributeType } from "@triliumnext/commons";
|
||||||
|
|
||||||
async function addLabel(noteId: string, name: string, value: string = "", isInheritable = false) {
|
async function addLabel(noteId: string, name: string, value: string = "", isInheritable = false) {
|
||||||
await server.put(`notes/${noteId}/attribute`, {
|
await server.put(`notes/${noteId}/attribute`, {
|
||||||
@@ -25,6 +26,14 @@ async function removeAttributeById(noteId: string, attributeId: string) {
|
|||||||
await server.remove(`notes/${noteId}/attributes/${attributeId}`);
|
await server.remove(`notes/${noteId}/attributes/${attributeId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function removeOwnedAttributesByNameOrType(note: FNote, type: AttributeType, name: string) {
|
||||||
|
for (const attr of note.getOwnedAttributes()) {
|
||||||
|
if (attr.type === type && attr.name === name) {
|
||||||
|
await server.remove(`notes/${note.noteId}/attributes/${attr.attributeId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes a label identified by its name from the given note, if it exists. Note that the label must be owned, i.e.
|
* Removes a label identified by its name from the given note, if it exists. Note that the label must be owned, i.e.
|
||||||
* it will not remove inherited attributes.
|
* it will not remove inherited attributes.
|
||||||
@@ -52,7 +61,7 @@ function removeOwnedLabelByName(note: FNote, labelName: string) {
|
|||||||
* @param value the value of the attribute to set.
|
* @param value the value of the attribute to set.
|
||||||
*/
|
*/
|
||||||
export async function setAttribute(note: FNote, type: "label" | "relation", name: string, value: string | null | undefined) {
|
export async function setAttribute(note: FNote, type: "label" | "relation", name: string, value: string | null | undefined) {
|
||||||
if (value) {
|
if (value !== null && value !== undefined) {
|
||||||
// Create or update the attribute.
|
// Create or update the attribute.
|
||||||
await server.put(`notes/${note.noteId}/set-attribute`, { type, name, value });
|
await server.put(`notes/${note.noteId}/set-attribute`, { type, name, value });
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import type FNote from "../entities/fnote.js";
|
|||||||
import toast from "./toast.js";
|
import toast from "./toast.js";
|
||||||
import { BulkAction } from "@triliumnext/commons";
|
import { BulkAction } from "@triliumnext/commons";
|
||||||
|
|
||||||
const ACTION_GROUPS = [
|
export const ACTION_GROUPS = [
|
||||||
{
|
{
|
||||||
title: t("bulk_actions.labels"),
|
title: t("bulk_actions.labels"),
|
||||||
actions: [AddLabelBulkAction, UpdateLabelValueBulkAction, RenameLabelBulkAction, DeleteLabelBulkAction]
|
actions: [AddLabelBulkAction, UpdateLabelValueBulkAction, RenameLabelBulkAction, DeleteLabelBulkAction]
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ function download(url: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function downloadFileNote(noteId: string) {
|
export function downloadFileNote(noteId: string) {
|
||||||
const url = `${getFileUrl("notes", noteId)}?${Date.now()}`; // don't use cache
|
const url = `${getFileUrl("notes", noteId)}?${Date.now()}`; // don't use cache
|
||||||
|
|
||||||
download(url);
|
download(url);
|
||||||
@@ -163,7 +163,7 @@ async function openExternally(type: string, entityId: string, mime: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const openNoteExternally = async (noteId: string, mime: string) => await openExternally("notes", noteId, mime);
|
export const openNoteExternally = async (noteId: string, mime: string) => await openExternally("notes", noteId, mime);
|
||||||
const openAttachmentExternally = async (attachmentId: string, mime: string) => await openExternally("attachments", attachmentId, mime);
|
const openAttachmentExternally = async (attachmentId: string, mime: string) => await openExternally("attachments", attachmentId, mime);
|
||||||
|
|
||||||
function getHost() {
|
function getHost() {
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ export function isElectron() {
|
|||||||
return !!(window && window.process && window.process.type);
|
return !!(window && window.process && window.process.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isMac() {
|
export function isMac() {
|
||||||
return navigator.platform.indexOf("Mac") > -1;
|
return navigator.platform.indexOf("Mac") > -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +185,11 @@ export function escapeQuotes(value: string) {
|
|||||||
return value.replaceAll('"', """);
|
return value.replaceAll('"', """);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatSize(size: number) {
|
export function formatSize(size: number | null | undefined) {
|
||||||
|
if (size === null || size === undefined) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
size = Math.max(Math.round(size / 1024), 1);
|
size = Math.max(Math.round(size / 1024), 1);
|
||||||
|
|
||||||
if (size < 1024) {
|
if (size < 1024) {
|
||||||
@@ -292,7 +296,7 @@ function isHtmlEmpty(html: string) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function clearBrowserCache() {
|
export async function clearBrowserCache() {
|
||||||
if (isElectron()) {
|
if (isElectron()) {
|
||||||
const win = dynamicRequire("@electron/remote").getCurrentWindow();
|
const win = dynamicRequire("@electron/remote").getCurrentWindow();
|
||||||
await win.webContents.session.clearCache();
|
await win.webContents.session.clearCache();
|
||||||
@@ -740,7 +744,7 @@ function isUpdateAvailable(latestVersion: string | null | undefined, currentVers
|
|||||||
return compareVersions(latestVersion, currentVersion) > 0;
|
return compareVersions(latestVersion, currentVersion) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isLaunchBarConfig(noteId: string) {
|
export function isLaunchBarConfig(noteId: string) {
|
||||||
return ["_lbRoot", "_lbAvailableLaunchers", "_lbVisibleLaunchers", "_lbMobileRoot", "_lbMobileAvailableLaunchers", "_lbMobileVisibleLaunchers"].includes(noteId);
|
return ["_lbRoot", "_lbAvailableLaunchers", "_lbVisibleLaunchers", "_lbMobileRoot", "_lbMobileAvailableLaunchers", "_lbMobileVisibleLaunchers"].includes(noteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -788,6 +792,38 @@ export function arrayEqual<T>(a: T[], b: T[]) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Indexed<T extends object> = T & { index: number };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an object array, alters every object in the array to have an index field assigned to it.
|
||||||
|
*
|
||||||
|
* @param items the objects to be numbered.
|
||||||
|
* @returns the same object for convenience, with the type changed to indicate the new index field.
|
||||||
|
*/
|
||||||
|
export function numberObjectsInPlace<T extends object>(items: T[]): Indexed<T>[] {
|
||||||
|
let index = 0;
|
||||||
|
for (const item of items) {
|
||||||
|
(item as Indexed<T>).index = index++;
|
||||||
|
}
|
||||||
|
return items as Indexed<T>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapToKeyValueArray<K extends string | number | symbol, V>(map: Record<K, V>) {
|
||||||
|
const values: { key: K, value: V }[] = [];
|
||||||
|
for (const [ key, value ] of Object.entries(map)) {
|
||||||
|
values.push({ key: key as K, value: value as V });
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getErrorMessage(e: unknown) {
|
||||||
|
if (e && typeof e === "object" && "message" in e && typeof e.message === "string") {
|
||||||
|
return e.message;
|
||||||
|
} else {
|
||||||
|
return "Unknown error";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
reloadFrontendApp,
|
reloadFrontendApp,
|
||||||
restartDesktopApp,
|
restartDesktopApp,
|
||||||
|
|||||||
@@ -848,7 +848,7 @@
|
|||||||
"debug": "调试",
|
"debug": "调试",
|
||||||
"debug_description": "调试将打印额外的调试信息到控制台,以帮助调试复杂查询",
|
"debug_description": "调试将打印额外的调试信息到控制台,以帮助调试复杂查询",
|
||||||
"action": "操作",
|
"action": "操作",
|
||||||
"search_button": "搜索 <kbd>回车</kbd>",
|
"search_button": "搜索",
|
||||||
"search_execute": "搜索并执行操作",
|
"search_execute": "搜索并执行操作",
|
||||||
"save_to_note": "保存到笔记",
|
"save_to_note": "保存到笔记",
|
||||||
"search_parameters": "搜索参数",
|
"search_parameters": "搜索参数",
|
||||||
|
|||||||
@@ -207,7 +207,7 @@
|
|||||||
},
|
},
|
||||||
"markdown_import": {
|
"markdown_import": {
|
||||||
"dialog_title": "Markdown-Import",
|
"dialog_title": "Markdown-Import",
|
||||||
"modal_body_text": "Aufgrund der Browser-Sandbox ist es nicht möglich, die Zwischenablage direkt aus JavaScript zu lesen. Bitte füge den zu importierenden Markdown in den Textbereich unten ein und klicke auf die Schaltfläche „Importieren“.",
|
"modal_body_text": "Aufgrund der Browser-Sandbox ist es nicht möglich, die Zwischenablage direkt aus JavaScript zu lesen. Bitte füge den zu importierenden Markdown in den Textbereich unten ein und klicke auf die Schaltfläche „Importieren“",
|
||||||
"import_button": "Importieren",
|
"import_button": "Importieren",
|
||||||
"import_success": "Markdown-Inhalt wurde in das Dokument importiert."
|
"import_success": "Markdown-Inhalt wurde in das Dokument importiert."
|
||||||
},
|
},
|
||||||
@@ -356,7 +356,7 @@
|
|||||||
"sorted": "Hält untergeordnete Notizen alphabetisch nach Titel sortiert",
|
"sorted": "Hält untergeordnete Notizen alphabetisch nach Titel sortiert",
|
||||||
"sort_direction": "ASC (Standard) oder DESC",
|
"sort_direction": "ASC (Standard) oder DESC",
|
||||||
"sort_folders_first": "Ordner (Notizen mit Unternotizen) sollten oben sortiert werden",
|
"sort_folders_first": "Ordner (Notizen mit Unternotizen) sollten oben sortiert werden",
|
||||||
"top": "Behalte die angegebene Notiz oben in der übergeordneten Notiz (gilt nur für sortierte übergeordnete Notizen).",
|
"top": "Behalte die angegebene Notiz oben in der übergeordneten Notiz (gilt nur für sortierte übergeordnete Notizen)",
|
||||||
"hide_promoted_attributes": "Heraufgestufte Attribute für diese Notiz ausblenden",
|
"hide_promoted_attributes": "Heraufgestufte Attribute für diese Notiz ausblenden",
|
||||||
"read_only": "Der Editor befindet sich im schreibgeschützten Modus. Funktioniert nur für Text- und Codenotizen.",
|
"read_only": "Der Editor befindet sich im schreibgeschützten Modus. Funktioniert nur für Text- und Codenotizen.",
|
||||||
"auto_read_only_disabled": "Text-/Codenotizen können automatisch in den Lesemodus versetzt werden, wenn sie zu groß sind. Du kannst dieses Verhalten für jede einzelne Notiz deaktivieren, indem du diese Beschriftung zur Notiz hinzufügst",
|
"auto_read_only_disabled": "Text-/Codenotizen können automatisch in den Lesemodus versetzt werden, wenn sie zu groß sind. Du kannst dieses Verhalten für jede einzelne Notiz deaktivieren, indem du diese Beschriftung zur Notiz hinzufügst",
|
||||||
@@ -379,10 +379,10 @@
|
|||||||
"inbox": "Standard-Inbox-Position für neue Notizen – wenn du eine Notiz über den \"Neue Notiz\"-Button in der Seitenleiste erstellst, wird die Notiz als untergeordnete Notiz der Notiz erstellt, die mit dem <code>#inbox</code>-Label markiert ist.",
|
"inbox": "Standard-Inbox-Position für neue Notizen – wenn du eine Notiz über den \"Neue Notiz\"-Button in der Seitenleiste erstellst, wird die Notiz als untergeordnete Notiz der Notiz erstellt, die mit dem <code>#inbox</code>-Label markiert ist.",
|
||||||
"workspace_inbox": "Standard-Posteingangsspeicherort für neue Notizen, wenn sie zu einem Vorgänger dieser Arbeitsbereichsnotiz verschoben werden",
|
"workspace_inbox": "Standard-Posteingangsspeicherort für neue Notizen, wenn sie zu einem Vorgänger dieser Arbeitsbereichsnotiz verschoben werden",
|
||||||
"sql_console_home": "Standardspeicherort der SQL-Konsolennotizen",
|
"sql_console_home": "Standardspeicherort der SQL-Konsolennotizen",
|
||||||
"bookmark_folder": "Notizen mit dieser Bezeichnung werden in den Lesezeichen als Ordner angezeigt (und ermöglichen den Zugriff auf ihre untergeordneten Ordner).",
|
"bookmark_folder": "Notizen mit dieser Bezeichnung werden in den Lesezeichen als Ordner angezeigt (und ermöglichen den Zugriff auf ihre untergeordneten Ordner)",
|
||||||
"share_hidden_from_tree": "Diese Notiz ist im linken Navigationsbaum ausgeblendet, kann aber weiterhin über ihre URL aufgerufen werden",
|
"share_hidden_from_tree": "Diese Notiz ist im linken Navigationsbaum ausgeblendet, kann aber weiterhin über ihre URL aufgerufen werden",
|
||||||
"share_external_link": "Die Notiz dient als Link zu einer externen Website im Freigabebaum",
|
"share_external_link": "Die Notiz dient als Link zu einer externen Website im Freigabebaum",
|
||||||
"share_alias": "Lege einen Alias fest, mit dem die Notiz unter https://your_trilium_host/share/[dein_alias] verfügbar sein wird.",
|
"share_alias": "Lege einen Alias fest, mit dem die Notiz unter https://your_trilium_host/share/[dein_alias] verfügbar sein wird",
|
||||||
"share_omit_default_css": "Das Standard-CSS für die Freigabeseite wird weggelassen. Verwende es, wenn du umfangreiche Stylingänderungen vornimmst.",
|
"share_omit_default_css": "Das Standard-CSS für die Freigabeseite wird weggelassen. Verwende es, wenn du umfangreiche Stylingänderungen vornimmst.",
|
||||||
"share_root": "Markiert eine Notiz, die im /share-Root bereitgestellt wird.",
|
"share_root": "Markiert eine Notiz, die im /share-Root bereitgestellt wird.",
|
||||||
"share_description": "Definiere Text, der dem HTML-Meta-Tag zur Beschreibung hinzugefügt werden soll",
|
"share_description": "Definiere Text, der dem HTML-Meta-Tag zur Beschreibung hinzugefügt werden soll",
|
||||||
@@ -416,7 +416,7 @@
|
|||||||
"run_on_attribute_change": " wird ausgeführt, wenn das Attribut einer Notiz geändert wird, die diese Beziehung definiert. Dies wird auch ausgelöst, wenn das Attribut gelöscht wird",
|
"run_on_attribute_change": " wird ausgeführt, wenn das Attribut einer Notiz geändert wird, die diese Beziehung definiert. Dies wird auch ausgelöst, wenn das Attribut gelöscht wird",
|
||||||
"relation_template": "Die Attribute der Notiz werden auch ohne eine Eltern-Kind-Beziehung vererbt. Der Inhalt und der Unterbaum der Notiz werden den Instanznotizen hinzugefügt, wenn sie leer sind. Einzelheiten findest du in der Dokumentation.",
|
"relation_template": "Die Attribute der Notiz werden auch ohne eine Eltern-Kind-Beziehung vererbt. Der Inhalt und der Unterbaum der Notiz werden den Instanznotizen hinzugefügt, wenn sie leer sind. Einzelheiten findest du in der Dokumentation.",
|
||||||
"inherit": "Die Attribute einer Notiz werden auch ohne eine Eltern-Kind-Beziehung vererbt. Ein ähnliches Konzept findest du unter Vorlagenbeziehung. Siehe Attributvererbung in der Dokumentation.",
|
"inherit": "Die Attribute einer Notiz werden auch ohne eine Eltern-Kind-Beziehung vererbt. Ein ähnliches Konzept findest du unter Vorlagenbeziehung. Siehe Attributvererbung in der Dokumentation.",
|
||||||
"render_note": "Notizen vom Typ \"HTML-Notiz rendern\" werden mit einer Code-Notiz (HTML oder Skript) gerendert, und es ist notwendig, über diese Beziehung anzugeben, welche Notiz gerendert werden soll.",
|
"render_note": "Notizen vom Typ \"HTML-Notiz rendern\" werden mit einer Code-Notiz (HTML oder Skript) gerendert, und es ist notwendig, über diese Beziehung anzugeben, welche Notiz gerendert werden soll",
|
||||||
"widget_relation": "Das Ziel dieser Beziehung wird ausgeführt und als Widget in der Seitenleiste gerendert",
|
"widget_relation": "Das Ziel dieser Beziehung wird ausgeführt und als Widget in der Seitenleiste gerendert",
|
||||||
"share_css": "CSS-Hinweis, der in die Freigabeseite eingefügt wird. Die CSS-Notiz muss sich ebenfalls im gemeinsamen Unterbaum befinden. Erwäge auch die Verwendung von „share_hidden_from_tree“ und „share_omit_default_css“.",
|
"share_css": "CSS-Hinweis, der in die Freigabeseite eingefügt wird. Die CSS-Notiz muss sich ebenfalls im gemeinsamen Unterbaum befinden. Erwäge auch die Verwendung von „share_hidden_from_tree“ und „share_omit_default_css“.",
|
||||||
"share_js": "JavaScript-Hinweis, der in die Freigabeseite eingefügt wird. Die JS-Notiz muss sich ebenfalls im gemeinsamen Unterbaum befinden. Erwäge die Verwendung von „share_hidden_from_tree“.",
|
"share_js": "JavaScript-Hinweis, der in die Freigabeseite eingefügt wird. Die JS-Notiz muss sich ebenfalls im gemeinsamen Unterbaum befinden. Erwäge die Verwendung von „share_hidden_from_tree“.",
|
||||||
@@ -499,9 +499,9 @@
|
|||||||
"to": "nach",
|
"to": "nach",
|
||||||
"target_parent_note": "Ziel-Übergeordnetenotiz",
|
"target_parent_note": "Ziel-Übergeordnetenotiz",
|
||||||
"on_all_matched_notes": "Auf allen übereinstimmenden Notizen",
|
"on_all_matched_notes": "Auf allen übereinstimmenden Notizen",
|
||||||
"move_note_new_parent": "Verschiebe die Notiz in die neue übergeordnete Notiz, wenn die Notiz nur eine übergeordnete Notiz hat (d. h. der alte Zweig wird entfernt und ein neuer Zweig in die neue übergeordnete Notiz erstellt).",
|
"move_note_new_parent": "Verschiebe die Notiz in die neue übergeordnete Notiz, wenn die Notiz nur eine übergeordnete Notiz hat (d. h. der alte Zweig wird entfernt und ein neuer Zweig in die neue übergeordnete Notiz erstellt)",
|
||||||
"clone_note_new_parent": "Notiz auf die neue übergeordnete Notiz klonen, wenn die Notiz mehrere Klone/Zweige hat (es ist nicht klar, welcher Zweig entfernt werden soll)",
|
"clone_note_new_parent": "Notiz auf die neue übergeordnete Notiz klonen, wenn die Notiz mehrere Klone/Zweige hat (es ist nicht klar, welcher Zweig entfernt werden soll)",
|
||||||
"nothing_will_happen": "Es passiert nichts, wenn die Notiz nicht zur Zielnotiz verschoben werden kann (z.B. wenn dies einen Kreislauf in der Baumstruktur erzeugen würde)."
|
"nothing_will_happen": "Es passiert nichts, wenn die Notiz nicht zur Zielnotiz verschoben werden kann (z.B. wenn dies einen Kreislauf in der Baumstruktur erzeugen würde)"
|
||||||
},
|
},
|
||||||
"rename_note": {
|
"rename_note": {
|
||||||
"rename_note": "Notiz umbenennen",
|
"rename_note": "Notiz umbenennen",
|
||||||
@@ -509,7 +509,7 @@
|
|||||||
"new_note_title": "neuer Notiztitel",
|
"new_note_title": "neuer Notiztitel",
|
||||||
"click_help_icon": "Klicke rechts auf das Hilfesymbol, um alle Optionen anzuzeigen",
|
"click_help_icon": "Klicke rechts auf das Hilfesymbol, um alle Optionen anzuzeigen",
|
||||||
"evaluated_as_js_string": "Der angegebene Wert wird als JavaScript-String ausgewertet und kann somit über die injizierte <code>note</code>-Variable mit dynamischem Inhalt angereichert werden (Notiz wird umbenannt). Beispiele:",
|
"evaluated_as_js_string": "Der angegebene Wert wird als JavaScript-String ausgewertet und kann somit über die injizierte <code>note</code>-Variable mit dynamischem Inhalt angereichert werden (Notiz wird umbenannt). Beispiele:",
|
||||||
"example_note": "<code>Notiz</code> – alle übereinstimmenden Notizen werden in „Notiz“ umbenannt.",
|
"example_note": "<code>Notiz</code> – alle übereinstimmenden Notizen werden in „Notiz“ umbenannt",
|
||||||
"example_new_title": "<code>NEU: ${note.title}</code> – Übereinstimmende Notiztitel erhalten das Präfix „NEU:“",
|
"example_new_title": "<code>NEU: ${note.title}</code> – Übereinstimmende Notiztitel erhalten das Präfix „NEU:“",
|
||||||
"example_date_prefix": "<code>${note.dateCreatedObj.format('MM-DD:')}: ${note.title}</code> – übereinstimmende Notizen werden mit dem Erstellungsmonat und -datum der Notiz vorangestellt",
|
"example_date_prefix": "<code>${note.dateCreatedObj.format('MM-DD:')}: ${note.title}</code> – übereinstimmende Notizen werden mit dem Erstellungsmonat und -datum der Notiz vorangestellt",
|
||||||
"api_docs": "Siehe API-Dokumente für <a href='https://zadam.github.io/trilium/backend_api/Note.html'>Notiz</a> und seinen <a href='https://day.js.org/ docs/en/display/format'>dateCreatedObj / utcDateCreatedObj properties</a> für Details."
|
"api_docs": "Siehe API-Dokumente für <a href='https://zadam.github.io/trilium/backend_api/Note.html'>Notiz</a> und seinen <a href='https://day.js.org/ docs/en/display/format'>dateCreatedObj / utcDateCreatedObj properties</a> für Details."
|
||||||
@@ -716,7 +716,7 @@
|
|||||||
"mobile_detail_menu": {
|
"mobile_detail_menu": {
|
||||||
"insert_child_note": "Untergeordnete Notiz einfügen",
|
"insert_child_note": "Untergeordnete Notiz einfügen",
|
||||||
"delete_this_note": "Diese Notiz löschen",
|
"delete_this_note": "Diese Notiz löschen",
|
||||||
"error_cannot_get_branch_id": "BranchId für notePath „{{notePath}}“ kann nicht abgerufen werden.",
|
"error_cannot_get_branch_id": "BranchId für notePath „{{notePath}}“ kann nicht abgerufen werden",
|
||||||
"error_unrecognized_command": "Unbekannter Befehl {{command}}"
|
"error_unrecognized_command": "Unbekannter Befehl {{command}}"
|
||||||
},
|
},
|
||||||
"note_icon": {
|
"note_icon": {
|
||||||
@@ -845,7 +845,7 @@
|
|||||||
"debug": "debuggen",
|
"debug": "debuggen",
|
||||||
"debug_description": "Debug gibt zusätzliche Debuginformationen in die Konsole aus, um das Debuggen komplexer Abfragen zu erleichtern",
|
"debug_description": "Debug gibt zusätzliche Debuginformationen in die Konsole aus, um das Debuggen komplexer Abfragen zu erleichtern",
|
||||||
"action": "Aktion",
|
"action": "Aktion",
|
||||||
"search_button": "Suchen <kbd>Eingabetaste</kbd>",
|
"search_button": "Suchen",
|
||||||
"search_execute": "Aktionen suchen und ausführen",
|
"search_execute": "Aktionen suchen und ausführen",
|
||||||
"save_to_note": "Als Notiz speichern",
|
"save_to_note": "Als Notiz speichern",
|
||||||
"search_parameters": "Suchparameter",
|
"search_parameters": "Suchparameter",
|
||||||
@@ -884,7 +884,7 @@
|
|||||||
"include_archived_notes": "Füge archivierte Notizen hinzu"
|
"include_archived_notes": "Füge archivierte Notizen hinzu"
|
||||||
},
|
},
|
||||||
"limit": {
|
"limit": {
|
||||||
"limit": "Limit",
|
"limit": "Limitierung",
|
||||||
"take_first_x_results": "Nehmen Sie nur die ersten X angegebenen Ergebnisse."
|
"take_first_x_results": "Nehmen Sie nur die ersten X angegebenen Ergebnisse."
|
||||||
},
|
},
|
||||||
"order_by": {
|
"order_by": {
|
||||||
@@ -1046,7 +1046,7 @@
|
|||||||
"failed": "Synchronisierung fehlgeschlagen: {{message}}"
|
"failed": "Synchronisierung fehlgeschlagen: {{message}}"
|
||||||
},
|
},
|
||||||
"vacuum_database": {
|
"vacuum_database": {
|
||||||
"title": "Vakuumdatenbank",
|
"title": "Datenbank aufräumen",
|
||||||
"description": "Dadurch wird die Datenbank neu erstellt, was normalerweise zu einer kleineren Datenbankdatei führt. Es werden keine Daten tatsächlich geändert.",
|
"description": "Dadurch wird die Datenbank neu erstellt, was normalerweise zu einer kleineren Datenbankdatei führt. Es werden keine Daten tatsächlich geändert.",
|
||||||
"button_text": "Vakuumdatenbank",
|
"button_text": "Vakuumdatenbank",
|
||||||
"vacuuming_database": "Datenbank wird geleert...",
|
"vacuuming_database": "Datenbank wird geleert...",
|
||||||
@@ -1117,7 +1117,8 @@
|
|||||||
"code_auto_read_only_size": {
|
"code_auto_read_only_size": {
|
||||||
"title": "Automatische schreibgeschützte Größe",
|
"title": "Automatische schreibgeschützte Größe",
|
||||||
"description": "Die automatische schreibgeschützte Notizgröße ist die Größe, ab der Notizen im schreibgeschützten Modus angezeigt werden (aus Leistungsgründen).",
|
"description": "Die automatische schreibgeschützte Notizgröße ist die Größe, ab der Notizen im schreibgeschützten Modus angezeigt werden (aus Leistungsgründen).",
|
||||||
"label": "Automatische schreibgeschützte Größe (Codenotizen)"
|
"label": "Automatische schreibgeschützte Größe (Codenotizen)",
|
||||||
|
"unit": "Zeichen"
|
||||||
},
|
},
|
||||||
"code_mime_types": {
|
"code_mime_types": {
|
||||||
"title": "Verfügbare MIME-Typen im Dropdown-Menü"
|
"title": "Verfügbare MIME-Typen im Dropdown-Menü"
|
||||||
@@ -1136,7 +1137,8 @@
|
|||||||
"download_images_description": "Eingefügter HTML-Code kann Verweise auf Online-Bilder enthalten. Trilium findet diese Verweise und lädt die Bilder herunter, sodass sie offline verfügbar sind.",
|
"download_images_description": "Eingefügter HTML-Code kann Verweise auf Online-Bilder enthalten. Trilium findet diese Verweise und lädt die Bilder herunter, sodass sie offline verfügbar sind.",
|
||||||
"enable_image_compression": "Bildkomprimierung aktivieren",
|
"enable_image_compression": "Bildkomprimierung aktivieren",
|
||||||
"max_image_dimensions": "Maximale Breite/Höhe eines Bildes in Pixel (die Größe des Bildes wird geändert, wenn es diese Einstellung überschreitet).",
|
"max_image_dimensions": "Maximale Breite/Höhe eines Bildes in Pixel (die Größe des Bildes wird geändert, wenn es diese Einstellung überschreitet).",
|
||||||
"jpeg_quality_description": "JPEG-Qualität (10 – schlechteste Qualität, 100 – beste Qualität, 50 – 85 wird empfohlen)"
|
"jpeg_quality_description": "JPEG-Qualität (10 – schlechteste Qualität, 100 – beste Qualität, 50 – 85 wird empfohlen)",
|
||||||
|
"max_image_dimensions_unit": "Pixel"
|
||||||
},
|
},
|
||||||
"attachment_erasure_timeout": {
|
"attachment_erasure_timeout": {
|
||||||
"attachment_erasure_timeout": "Zeitüberschreitung beim Löschen von Anhängen",
|
"attachment_erasure_timeout": "Zeitüberschreitung beim Löschen von Anhängen",
|
||||||
@@ -1168,7 +1170,8 @@
|
|||||||
"note_revisions_snapshot_limit_description": "Das Limit für Notizrevision-Snapshots bezieht sich auf die maximale Anzahl von Revisionen, die für jede Notiz gespeichert werden können. Dabei bedeutet -1, dass es kein Limit gibt, und 0 bedeutet, dass alle Revisionen gelöscht werden. Du kannst das maximale Limit für Revisionen einer einzelnen Notiz über das Label #versioningLimit festlegen.",
|
"note_revisions_snapshot_limit_description": "Das Limit für Notizrevision-Snapshots bezieht sich auf die maximale Anzahl von Revisionen, die für jede Notiz gespeichert werden können. Dabei bedeutet -1, dass es kein Limit gibt, und 0 bedeutet, dass alle Revisionen gelöscht werden. Du kannst das maximale Limit für Revisionen einer einzelnen Notiz über das Label #versioningLimit festlegen.",
|
||||||
"snapshot_number_limit_label": "Limit der Notizrevision-Snapshots:",
|
"snapshot_number_limit_label": "Limit der Notizrevision-Snapshots:",
|
||||||
"erase_excess_revision_snapshots": "Überschüssige Revision-Snapshots jetzt löschen",
|
"erase_excess_revision_snapshots": "Überschüssige Revision-Snapshots jetzt löschen",
|
||||||
"erase_excess_revision_snapshots_prompt": "Überschüssige Revision-Snapshots wurden gelöscht."
|
"erase_excess_revision_snapshots_prompt": "Überschüssige Revision-Snapshots wurden gelöscht.",
|
||||||
|
"snapshot_number_limit_unit": "Momentaufnahmen"
|
||||||
},
|
},
|
||||||
"search_engine": {
|
"search_engine": {
|
||||||
"title": "Suchmaschine",
|
"title": "Suchmaschine",
|
||||||
@@ -1210,19 +1213,29 @@
|
|||||||
"title": "Inhaltsverzeichnis",
|
"title": "Inhaltsverzeichnis",
|
||||||
"description": "Das Inhaltsverzeichnis wird in Textnotizen angezeigt, wenn die Notiz mehr als eine definierte Anzahl von Überschriften enthält. Du kannst diese Nummer anpassen:",
|
"description": "Das Inhaltsverzeichnis wird in Textnotizen angezeigt, wenn die Notiz mehr als eine definierte Anzahl von Überschriften enthält. Du kannst diese Nummer anpassen:",
|
||||||
"disable_info": "Du kannst diese Option auch verwenden, um TOC effektiv zu deaktivieren, indem du eine sehr hohe Zahl festlegst.",
|
"disable_info": "Du kannst diese Option auch verwenden, um TOC effektiv zu deaktivieren, indem du eine sehr hohe Zahl festlegst.",
|
||||||
"shortcut_info": "Du kannst eine Tastenkombination zum schnellen Umschalten des rechten Bereichs (einschließlich Inhaltsverzeichnis) unter Optionen -> Tastenkombinationen konfigurieren (Name „toggleRightPane“)."
|
"shortcut_info": "Du kannst eine Tastenkombination zum schnellen Umschalten des rechten Bereichs (einschließlich Inhaltsverzeichnis) unter Optionen -> Tastenkombinationen konfigurieren (Name „toggleRightPane“).",
|
||||||
|
"unit": "Überschriften"
|
||||||
},
|
},
|
||||||
"text_auto_read_only_size": {
|
"text_auto_read_only_size": {
|
||||||
"title": "Automatische schreibgeschützte Größe",
|
"title": "Automatische schreibgeschützte Größe",
|
||||||
"description": "Die automatische schreibgeschützte Notizgröße ist die Größe, ab der Notizen im schreibgeschützten Modus angezeigt werden (aus Leistungsgründen).",
|
"description": "Die automatische schreibgeschützte Notizgröße ist die Größe, ab der Notizen im schreibgeschützten Modus angezeigt werden (aus Leistungsgründen).",
|
||||||
"label": "Automatische schreibgeschützte Größe (Textnotizen)"
|
"label": "Automatische schreibgeschützte Größe (Textnotizen)",
|
||||||
|
"unit": "Zeichen"
|
||||||
},
|
},
|
||||||
"i18n": {
|
"i18n": {
|
||||||
"title": "Lokalisierung",
|
"title": "Lokalisierung",
|
||||||
"language": "Sprache",
|
"language": "Sprache",
|
||||||
"first-day-of-the-week": "Erster Tag der Woche",
|
"first-day-of-the-week": "Erster Tag der Woche",
|
||||||
"sunday": "Sonntag",
|
"sunday": "Sonntag",
|
||||||
"monday": "Montag"
|
"monday": "Montag",
|
||||||
|
"first-week-of-the-year": "Erste Woche des Jahres",
|
||||||
|
"first-week-contains-first-day": "Erste Woche enthält den ersten Tag des Jahres",
|
||||||
|
"first-week-contains-first-thursday": "Erste Woche enthält den ersten Donnerstag des Jahres",
|
||||||
|
"first-week-has-minimum-days": "Erste Woche hat Mindestanzahl an Tagen",
|
||||||
|
"min-days-in-first-week": "Mindestanzahl an Tagen in erster Woche",
|
||||||
|
"first-week-info": "Die erste Woche, die den ersten Donnerstag des Jahres enthält, basiert auf dem Standard <a href=\"https://en.wikipedia.org/wiki/ISO_week_date#First_week\">ISO 8601</a>.",
|
||||||
|
"first-week-warning": "Das Ändern der Optionen für die erste Woche kann zu Duplikaten mit bestehenden Wochen-Notizen führen. Bestehende Wochen-Notizen werden nicht entsprechend aktualisiert.",
|
||||||
|
"formatting-locale": "Datums- und Zahlenformat"
|
||||||
},
|
},
|
||||||
"backup": {
|
"backup": {
|
||||||
"automatic_backup": "Automatische Sicherung",
|
"automatic_backup": "Automatische Sicherung",
|
||||||
@@ -1687,7 +1700,119 @@
|
|||||||
"openai_settings": "OpenAI Einstellungen",
|
"openai_settings": "OpenAI Einstellungen",
|
||||||
"api_key": "API Schlüssel",
|
"api_key": "API Schlüssel",
|
||||||
"url": "Basis-URL",
|
"url": "Basis-URL",
|
||||||
"model": "Modell"
|
"model": "Modell",
|
||||||
|
"anthropic_settings": "Anthropic Einstellungen",
|
||||||
|
"partial": "{{ percentage }}% verarbeitet",
|
||||||
|
"anthropic_api_key_description": "Dein Anthropic API-Key für den Zugriff auf Claude Modelle",
|
||||||
|
"anthropic_model_description": "Anthropic Claude Modell für Chat-Vervollständigung",
|
||||||
|
"voyage_settings": "Einstellungen für Voyage AI",
|
||||||
|
"ollama_url_description": "URL für die Ollama API (Standard: http://localhost:11434)",
|
||||||
|
"ollama_model_description": "Ollama Modell für Chat-Vervollständigung",
|
||||||
|
"anthropic_configuration": "Anthropic Konfiguration",
|
||||||
|
"voyage_configuration": "Voyage AI Konfiguration",
|
||||||
|
"voyage_url_description": "Standard: https://api.voyageai.com/v1",
|
||||||
|
"ollama_configuration": "Ollama Konfiguration",
|
||||||
|
"enable_ollama": "Aktiviere Ollama",
|
||||||
|
"enable_ollama_description": "Aktiviere Ollama für lokale KI Modell Nutzung",
|
||||||
|
"ollama_url": "Ollama URL",
|
||||||
|
"ollama_model": "Ollama Modell",
|
||||||
|
"refresh_models": "Aktualisiere Modelle",
|
||||||
|
"refreshing_models": "Aktualisiere...",
|
||||||
|
"enable_automatic_indexing": "Aktiviere automatische Indizierung",
|
||||||
|
"rebuild_index": "Index neu aufbauen",
|
||||||
|
"rebuild_index_error": "Fehler beim Neuaufbau des Index. Prüfe Log für mehr Informationen.",
|
||||||
|
"retry_failed": "Fehler: Notiz konnte nicht erneut eingereiht werden",
|
||||||
|
"max_notes_per_llm_query": "Max. Notizen je Abfrage",
|
||||||
|
"max_notes_per_llm_query_description": "Maximale Anzahl ähnlicher Notizen zum Einbinden als KI Kontext",
|
||||||
|
"active_providers": "Aktive Anbieter",
|
||||||
|
"disabled_providers": "Inaktive Anbieter",
|
||||||
|
"remove_provider": "Entferne Anbieter von Suche",
|
||||||
|
"restore_provider": "Anbieter zur Suche wiederherstellen",
|
||||||
|
"similarity_threshold": "Ähnlichkeitsschwelle",
|
||||||
|
"similarity_threshold_description": "Mindestähnlichkeitswert (0-1) für Notizen, die im Kontext für LLM-Abfragen berücksichtigt werden sollen",
|
||||||
|
"reprocess_index": "Suchindex neu erstellen",
|
||||||
|
"reprocessing_index": "Neuerstellung...",
|
||||||
|
"reprocess_index_started": "Suchindex-Optimierung wurde im Hintergrund gestartet",
|
||||||
|
"reprocess_index_error": "Fehler beim Wiederaufbau des Suchindex",
|
||||||
|
"index_rebuild_progress": "Fortschritt der Index-Neuerstellung",
|
||||||
|
"index_rebuilding": "Optimierung Index ({{percentage}}%)",
|
||||||
|
"index_rebuild_complete": "Index Optimierung abgeschlossen",
|
||||||
|
"index_rebuild_status_error": "Fehler bei Überprüfung Status Index Neuerstellung",
|
||||||
|
"never": "Niemals",
|
||||||
|
"processing": "Verarbeitung ({{percentage}}%)",
|
||||||
|
"refreshing": "Aktualisiere...",
|
||||||
|
"incomplete": "Unvollständig ({{percentage}}%)",
|
||||||
|
"complete": "Abgeschlossen (100%)",
|
||||||
|
"auto_refresh_notice": "Auto-Aktualisierung alle {{seconds}} Sekunden",
|
||||||
|
"note_queued_for_retry": "Notiz in Warteschlange für erneuten Versuch hinzugefügt",
|
||||||
|
"failed_to_retry_note": "Wiederholungsversuch fehlgeschlagen für Notiz",
|
||||||
|
"ai_settings": "KI Einstellungen",
|
||||||
|
"agent": {
|
||||||
|
"processing": "Verarbeite...",
|
||||||
|
"thinking": "Nachdenken...",
|
||||||
|
"loading": "Lade...",
|
||||||
|
"generating": "Generiere..."
|
||||||
|
},
|
||||||
|
"name": "KI",
|
||||||
|
"openai": "OpenAI",
|
||||||
|
"use_enhanced_context": "Benutze verbesserten Kontext",
|
||||||
|
"openai_api_key_description": "Dein OpenAPI-Key für den Zugriff auf den KI-Dienst",
|
||||||
|
"default_model": "Standardmodell",
|
||||||
|
"openai_model_description": "Beispiele: gpt-4o, gpt-4-turbo, gpt-3.5-turbo",
|
||||||
|
"base_url": "Basis URL",
|
||||||
|
"openai_url_description": "Standard: https://api.openai.com/v1",
|
||||||
|
"anthropic_url_description": "Basis URL für Anthropic API (Standard: https://api.anthropic.com)",
|
||||||
|
"ollama_settings": "Ollama Einstellungen",
|
||||||
|
"note_title": "Notiz Titel",
|
||||||
|
"error": "Fehler",
|
||||||
|
"last_attempt": "Letzter Versuch",
|
||||||
|
"actions": "Aktionen",
|
||||||
|
"retry": "Erneut versuchen",
|
||||||
|
"retry_queued": "Notiz für weiteren Versuch eingereiht",
|
||||||
|
"empty_key_warning": {
|
||||||
|
"anthropic": "Anthropic API-Key ist leer. Bitte gültigen API-Key eingeben.",
|
||||||
|
"openai": "OpenAI API-Key ist leer. Bitte gültigen API-Key eingeben.",
|
||||||
|
"voyage": "Voyage API-Key ist leer. Bitte gültigen API-Key eingeben.",
|
||||||
|
"ollama": "Ollama API-Key ist leer. Bitte gültigen API-Key eingeben."
|
||||||
|
},
|
||||||
|
"api_key_tooltip": "API-Key für den Zugriff auf den Dienst",
|
||||||
|
"failed_to_retry_all": "Wiederholungsversuch für Notizen fehlgeschlagen",
|
||||||
|
"all_notes_queued_for_retry": "Alle fehlgeschlagenen Notizen wurden zur Wiederholung in die Warteschlange gestellt",
|
||||||
|
"enhanced_context_description": "Versorgt die KI mit mehr Kontext aus der Notiz und den zugehörigen Notizen, um bessere Antworten zu ermöglichen",
|
||||||
|
"show_thinking": "Zeige Denkprozess",
|
||||||
|
"show_thinking_description": "Zeige den Denkprozess der KI",
|
||||||
|
"enter_message": "Geben Sie Ihre Nachricht ein...",
|
||||||
|
"error_contacting_provider": "Fehler beim Kontaktieren des KI-Anbieters. Bitte überprüfe die Einstellungen und die Internetverbindung.",
|
||||||
|
"error_generating_response": "Fehler beim Generieren der KI Antwort",
|
||||||
|
"index_all_notes": "Indiziere alle Notizen",
|
||||||
|
"index_status": "Indizierungsstatus",
|
||||||
|
"indexed_notes": "Indizierte Notizen",
|
||||||
|
"indexing_stopped": "Indizierung gestoppt",
|
||||||
|
"indexing_in_progress": "Indizierung in Bearbeitung...",
|
||||||
|
"last_indexed": "Zuletzt Indiziert",
|
||||||
|
"note_chat": "Notizen-Chat",
|
||||||
|
"sources": "Quellen",
|
||||||
|
"start_indexing": "Starte Indizierung",
|
||||||
|
"use_advanced_context": "Benutze erweiterten Kontext",
|
||||||
|
"ollama_no_url": "Ollama ist nicht konfiguriert. Bitte trage eine gültige URL ein.",
|
||||||
|
"chat": {
|
||||||
|
"root_note_title": "KI Chats",
|
||||||
|
"root_note_content": "Diese Notiz enthält gespeicherte KI-Chat-Unterhaltungen.",
|
||||||
|
"new_chat_title": "Neuer Chat",
|
||||||
|
"create_new_ai_chat": "Erstelle neuen KI Chat"
|
||||||
|
},
|
||||||
|
"create_new_ai_chat": "Erstelle neuen KI Chat",
|
||||||
|
"configuration_warnings": "Es wurden Probleme mit der KI Konfiguration festgestellt. Bitte überprüfe die Einstellungen.",
|
||||||
|
"experimental_warning": "Die LLM-Funktionen sind aktuell experimentell - sei an dieser Stelle gewarnt.",
|
||||||
|
"selected_provider": "Ausgewählter Anbieter",
|
||||||
|
"selected_provider_description": "Wähle einen KI-Anbieter für Chat- und Vervollständigungsfunktionen",
|
||||||
|
"select_model": "Wähle Modell...",
|
||||||
|
"select_provider": "Wähle Anbieter...",
|
||||||
|
"ai_enabled": "KI Funktionen aktiviert",
|
||||||
|
"ai_disabled": "KI Funktionen deaktiviert",
|
||||||
|
"no_models_found_online": "Keine Modelle gefunden. Bitte überprüfe den API-Key und die Einstellungen.",
|
||||||
|
"no_models_found_ollama": "Kein Ollama Modell gefunden. Bitte prüfe, ob Ollama gerade läuft.",
|
||||||
|
"error_fetching": "Fehler beim Abrufen der Modelle: {{error}}"
|
||||||
},
|
},
|
||||||
"zen_mode": {
|
"zen_mode": {
|
||||||
"button_exit": "Verlasse Zen Modus"
|
"button_exit": "Verlasse Zen Modus"
|
||||||
@@ -1697,5 +1822,32 @@
|
|||||||
"enable-motion": "Aktiviere Übergänge und Animationen",
|
"enable-motion": "Aktiviere Übergänge und Animationen",
|
||||||
"enable-shadows": "Aktiviere Schatten",
|
"enable-shadows": "Aktiviere Schatten",
|
||||||
"enable-backdrop-effects": "Aktiviere Hintergrundeffekte für Menüs, Pop-up Fenster und Panele"
|
"enable-backdrop-effects": "Aktiviere Hintergrundeffekte für Menüs, Pop-up Fenster und Panele"
|
||||||
|
},
|
||||||
|
"code-editor-options": {
|
||||||
|
"title": "Editor"
|
||||||
|
},
|
||||||
|
"custom_date_time_format": {
|
||||||
|
"title": "Benutzerdefiniertes Datums-/Zeitformat",
|
||||||
|
"description": "Passe das Format des Datums und der Uhrzeit an, die über <shortcut /> oder die Symbolleiste eingefügt werden. Die verfügbaren Format-Tokens sind unter <doc>Day.js docs</doc> zu finden.",
|
||||||
|
"format_string": "Format Zeichenfolge:",
|
||||||
|
"formatted_time": "Formatiertes Datum/Uhrzeit:"
|
||||||
|
},
|
||||||
|
"multi_factor_authentication": {
|
||||||
|
"title": "Multi-Faktor-Authentifizierung",
|
||||||
|
"description": "Die Multi-Faktor-Authentifizierung (MFA) bietet Ihrem Konto eine zusätzliche Sicherheitsebene. Anstatt sich lediglich mit einem Passwort anzumelden, müssen bei der MFA ein oder mehrere zusätzliche Nachweise erbracht werden, um die Identität zu bestätigen. Auf diese Weise kann selbst bei Bekanntwerden des Passworts, ohne die zweite Information nicht auf Ihr Konto zugegriffen werden. Das ist so, als würden Sie ein zusätzliches Schloss an einer Tür anbringen, wodurch es für andere viel schwieriger wird, einzubrechen.<br><br>Befolgen Sie bitte die nachstehenden Anweisungen, um MFA zu aktivieren. Wenn Sie die Konfiguration nicht korrekt vornehmen, erfolgt die Anmeldung weiterhin nur mit dem Passwort.",
|
||||||
|
"mfa_enabled": "Aktiviere Multi-Faktor-Authentifizierung",
|
||||||
|
"mfa_method": "MFA Methode",
|
||||||
|
"electron_disabled": "Multi-Faktor-Authentifizierung wird aktuell nicht in der Desktop-Version unterstützt.",
|
||||||
|
"totp_title": "Zeitbasiertes Einmalpasswort (TOTP)",
|
||||||
|
"totp_description": "TOTP (Zeitbasiertes Einmalpasswort) ist eine Sicherheitsfunktion, die einen einzigartigen, temporären Code generiert, der sich alle 30 Sekunden ändert. Sie verwenden diesen Code zusammen mit Ihrem Passwort, um sich bei Ihrem Konto anzumelden, wodurch es für andere Personen wesentlich schwieriger wird, darauf unbefugt zuzugreifen.",
|
||||||
|
"totp_secret_title": "Generiere TOTP Geheimnis",
|
||||||
|
"totp_secret_generate": "Generiere TOTP Geheimnis",
|
||||||
|
"totp_secret_regenerate": "TOTP-Geheimnis neu generieren",
|
||||||
|
"no_totp_secret_warning": "Um TOTP zu aktivieren, muss zunächst ein TOTP Geheimnis generiert werden.",
|
||||||
|
"totp_secret_description_warning": "Nach der Generierung des TOTP Geheimnisses ist eine Neuanmeldung mit dem TOTP Geheimnis erforderlich.",
|
||||||
|
"totp_secret_generated": "TOTP Geheimnis generiert",
|
||||||
|
"totp_secret_warning": "Bitte speichere das TOTP Geheimnis an einem sicheren Ort. Es wird nicht noch einmal angezeigt.",
|
||||||
|
"totp_secret_regenerate_confirm": "Möchten Sie das TOTP-Geheimnis wirklich neu generieren? Dadurch werden das bisherige TOTP-Geheimnis und alle vorhandenen Wiederherstellungscodes ungültig.",
|
||||||
|
"recovery_keys_title": "Einmalige Wiederherstellungsschlüssel"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -732,7 +732,8 @@
|
|||||||
"note_type": "Note type",
|
"note_type": "Note type",
|
||||||
"editable": "Editable",
|
"editable": "Editable",
|
||||||
"basic_properties": "Basic Properties",
|
"basic_properties": "Basic Properties",
|
||||||
"language": "Language"
|
"language": "Language",
|
||||||
|
"configure_code_notes": "Configure code notes..."
|
||||||
},
|
},
|
||||||
"book_properties": {
|
"book_properties": {
|
||||||
"view_type": "View type",
|
"view_type": "View type",
|
||||||
@@ -848,7 +849,7 @@
|
|||||||
"debug": "debug",
|
"debug": "debug",
|
||||||
"debug_description": "Debug will print extra debugging information into the console to aid in debugging complex queries",
|
"debug_description": "Debug will print extra debugging information into the console to aid in debugging complex queries",
|
||||||
"action": "action",
|
"action": "action",
|
||||||
"search_button": "Search <kbd>enter</kbd>",
|
"search_button": "Search",
|
||||||
"search_execute": "Search & Execute actions",
|
"search_execute": "Search & Execute actions",
|
||||||
"save_to_note": "Save to note",
|
"save_to_note": "Save to note",
|
||||||
"search_parameters": "Search Parameters",
|
"search_parameters": "Search Parameters",
|
||||||
|
|||||||
@@ -848,7 +848,7 @@
|
|||||||
"debug": "depurar",
|
"debug": "depurar",
|
||||||
"debug_description": "La depuración imprimirá información de depuración adicional en la consola para ayudar a depurar consultas complejas",
|
"debug_description": "La depuración imprimirá información de depuración adicional en la consola para ayudar a depurar consultas complejas",
|
||||||
"action": "acción",
|
"action": "acción",
|
||||||
"search_button": "Buscar <kbd>Enter</kbd>",
|
"search_button": "Buscar",
|
||||||
"search_execute": "Buscar y ejecutar acciones",
|
"search_execute": "Buscar y ejecutar acciones",
|
||||||
"save_to_note": "Guardar en nota",
|
"save_to_note": "Guardar en nota",
|
||||||
"search_parameters": "Parámetros de búsqueda",
|
"search_parameters": "Parámetros de búsqueda",
|
||||||
|
|||||||
@@ -848,7 +848,7 @@
|
|||||||
"debug": "debug",
|
"debug": "debug",
|
||||||
"debug_description": "Debug imprimera des informations supplémentaires dans la console pour faciliter le débogage des requêtes complexes",
|
"debug_description": "Debug imprimera des informations supplémentaires dans la console pour faciliter le débogage des requêtes complexes",
|
||||||
"action": "action",
|
"action": "action",
|
||||||
"search_button": "Recherche <kbd>Entrée</kbd>",
|
"search_button": "Recherche",
|
||||||
"search_execute": "Rechercher et exécuter des actions",
|
"search_execute": "Rechercher et exécuter des actions",
|
||||||
"save_to_note": "Enregistrer dans la note",
|
"save_to_note": "Enregistrer dans la note",
|
||||||
"search_parameters": "Paramètres de recherche",
|
"search_parameters": "Paramètres de recherche",
|
||||||
|
|||||||
@@ -185,7 +185,7 @@
|
|||||||
"debug": "デバッグ",
|
"debug": "デバッグ",
|
||||||
"debug_description": "デバッグは複雑なクエリのデバッグを支援するために、追加のデバッグ情報をコンソールに出力します",
|
"debug_description": "デバッグは複雑なクエリのデバッグを支援するために、追加のデバッグ情報をコンソールに出力します",
|
||||||
"action": "アクション",
|
"action": "アクション",
|
||||||
"search_button": "検索 <kbd>Enter</kbd>",
|
"search_button": "検索",
|
||||||
"search_execute": "検索とアクションの実行",
|
"search_execute": "検索とアクションの実行",
|
||||||
"save_to_note": "ノートに保存",
|
"save_to_note": "ノートに保存",
|
||||||
"search_parameters": "検索パラメータ",
|
"search_parameters": "検索パラメータ",
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
"code_block": {
|
"code_block": {
|
||||||
"theme_none": "Sem destaque de sintaxe",
|
"theme_none": "Sem destaque de sintaxe",
|
||||||
"theme_group_light": "Temas claros",
|
"theme_group_light": "Temas claros",
|
||||||
"theme_group_dark": "Temas escuros"
|
"theme_group_dark": "Temas escuros",
|
||||||
|
"word_wrapping": "Quebra automática de linhas",
|
||||||
|
"copy_title": "Copiar para a área de transferência"
|
||||||
},
|
},
|
||||||
"about": {
|
"about": {
|
||||||
"title": "Sobre o Trilium Notes",
|
"title": "Sobre o Trilium Notes",
|
||||||
@@ -192,7 +194,34 @@
|
|||||||
"ai_disabled": "Recursos de IA desabilitados",
|
"ai_disabled": "Recursos de IA desabilitados",
|
||||||
"no_models_found_online": "Nenhum modelo encontrado. Por favor, verifique sua chave de API e as configurações.",
|
"no_models_found_online": "Nenhum modelo encontrado. Por favor, verifique sua chave de API e as configurações.",
|
||||||
"no_models_found_ollama": "Nenhum modelo Ollama encontrado. Por favor, verifique se o Ollama está em execução.",
|
"no_models_found_ollama": "Nenhum modelo Ollama encontrado. Por favor, verifique se o Ollama está em execução.",
|
||||||
"error_fetching": "Erro ao obter modelos: {{error}}"
|
"error_fetching": "Erro ao obter modelos: {{error}}",
|
||||||
|
"ollama_tab": "Ollama",
|
||||||
|
"enable_ai_desc": "Habilitar recursos de IA como sumarização de notas, geração de conteúdo, e outras capacidades de LLM",
|
||||||
|
"provider_precedence": "Prioridade de provedor",
|
||||||
|
"provider_precedence_description": "Lista de provedores em ordem de prioridade, separados por vírgula (por exemplo, 'openai, anthropic, ollama')",
|
||||||
|
"temperature_description": "Controla a aleatoriedade em respostas (0 = determinística, 2 = aleatoriedade máxima)",
|
||||||
|
"ollama_settings": "Configurações do Ollama",
|
||||||
|
"ollama_url_description": "URL para a API Ollama (padrão: http://localhost:11434)",
|
||||||
|
"ollama_model_description": "Modelo Ollama usado para complementação de chat",
|
||||||
|
"anthropic_configuration": "Configuração da Anthropic",
|
||||||
|
"voyage_configuration": "Configuração da Voyage IA",
|
||||||
|
"voyage_url_description": "Padrão: https://api.voyageai.com/v1",
|
||||||
|
"ollama_configuration": "Configuração da Ollama",
|
||||||
|
"enable_ollama": "Habilitar Ollama",
|
||||||
|
"enable_ollama_description": "Habilitar Ollama para uso do modelo local de IA",
|
||||||
|
"ollama_url": "URL da Ollama",
|
||||||
|
"ollama_model": "Modelo do Ollama",
|
||||||
|
"refresh_models": "Atualizar Modelos",
|
||||||
|
"refreshing_models": "Atualizando…",
|
||||||
|
"enable_automatic_indexing": "Habilitar indexação automática",
|
||||||
|
"rebuild_index": "Reconstruir Índice",
|
||||||
|
"rebuild_index_error": "Ocorreu um erro ao iniciar a reconstrução do índice. Verifique os logs para obter detalhes.",
|
||||||
|
"note_title": "Título da nota",
|
||||||
|
"error": "Erro",
|
||||||
|
"last_attempt": "Última Tentativa",
|
||||||
|
"actions": "Ações",
|
||||||
|
"partial": "{{ percentage }}% concluído",
|
||||||
|
"show_thinking_description": "Exibir o processo de linha de raciocínio da AI"
|
||||||
},
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"confirmation": "Confirmação",
|
"confirmation": "Confirmação",
|
||||||
@@ -544,7 +573,17 @@
|
|||||||
"run_on_branch_change": "executa quando uma remificação é atualizada.",
|
"run_on_branch_change": "executa quando uma remificação é atualizada.",
|
||||||
"run_on_attribute_creation": "executa quando um novo atributo é criado para a nota que define esta relação",
|
"run_on_attribute_creation": "executa quando um novo atributo é criado para a nota que define esta relação",
|
||||||
"run_on_attribute_change": " executa quando o atributo é alterado na nota que define esta relação. Também é disparado quando o atributo é excluído",
|
"run_on_attribute_change": " executa quando o atributo é alterado na nota que define esta relação. Também é disparado quando o atributo é excluído",
|
||||||
"widget_relation": "o destino desta relação será executado e renderizado como um widget na barra lateral"
|
"widget_relation": "o destino desta relação será executado e renderizado como um widget na barra lateral",
|
||||||
|
"run_on_branch_deletion": "executa quando uma ramificação é excluída. Ramificação é um link entre a nota pai e a nota filha e é excluído, por exemplo, ao mover a nota (a ramificação/link antiga é excluída).",
|
||||||
|
"relation_template": "os atributos da nota serão herdados mesmo sem um relacionamento pai-filho, o conteúdo e subárvore da nota serão adicionados às notas da instância se vazias. Veja a documentação para detalhes.",
|
||||||
|
"inherit": "os atributos da nota serão herdados mesmo sem um relacionamento pai-filho. Veja relação de modelos para um conceito semelhante. Veja a herança de atributos na documentação.",
|
||||||
|
"render_note": "notas do tipo \"nota de renderização HTML\" serão renderizadas usando uma nota de código (HTML ou script) e é necessário apontar usando esta relação qual nota deve ser renderizada",
|
||||||
|
"share_css": "Nota CSS que será injetada na página de compartilhamento. A nota CSS também deve estar na subárvore compartilhada. Considere usar também 'share_hidden_from_tree' e 'share_omit_default_css'.",
|
||||||
|
"share_js": "Nota JavaScript que será injetada na página de compartilhamento. A nota JS também deve estar na subárvore compartilhada. Considere usar 'share_hidden_from_tree'.",
|
||||||
|
"share_template": "Nota JavaScript incorporada que será usada como modelo para exibir a nota compartilhada. Retorna ao modelo padrão. Considere usar 'share_hidden_from_tree'.",
|
||||||
|
"share_favicon": "Nota Favicon que será usada na página compartilhada. Tipicamente você quer defini-la na raiz do compartilhamento e torná-lo herdável. A nota de Favicon também deve estar na subárvore compartilhada. Considere usar 'share_hidden_from_tree'.",
|
||||||
|
"is_owned_by_note": "é propriedade da nota",
|
||||||
|
"print_landscape": "Ao exportar para PDF, muda a orientação da página para paisagem em vez de retrato."
|
||||||
},
|
},
|
||||||
"attachments_actions": {
|
"attachments_actions": {
|
||||||
"delete_attachment": "Excluir anexo",
|
"delete_attachment": "Excluir anexo",
|
||||||
@@ -732,7 +771,10 @@
|
|||||||
"move_note": "Mover nota",
|
"move_note": "Mover nota",
|
||||||
"to": "para",
|
"to": "para",
|
||||||
"target_parent_note": "nota pai destino",
|
"target_parent_note": "nota pai destino",
|
||||||
"on_all_matched_notes": "Em todas as notas correspondentes"
|
"on_all_matched_notes": "Em todas as notas correspondentes",
|
||||||
|
"move_note_new_parent": "move a nota para o novo pai se a nota tem apenas um pai (ou seja, a antiga ramificação é removida e uma nova ramificação é criada para o novo pai)",
|
||||||
|
"clone_note_new_parent": "clona a nota para o novo pai se a nota tem vários clones / ramificações (não é claro qual ramificação deve ser removida)",
|
||||||
|
"nothing_will_happen": "nada acontecerá se a nota não puder ser movida para a nota de destino (por exemplo, se criaria um ciclo de árvore)"
|
||||||
},
|
},
|
||||||
"rename_note": {
|
"rename_note": {
|
||||||
"rename_note": "Renomear nota",
|
"rename_note": "Renomear nota",
|
||||||
@@ -742,7 +784,8 @@
|
|||||||
"example_note": "<code>Nota</code> - todas as notas correspondentes serão renomeadas para 'Nota'",
|
"example_note": "<code>Nota</code> - todas as notas correspondentes serão renomeadas para 'Nota'",
|
||||||
"example_new_title": "<code>NOVO: ${note.title}</code> - o título das notas correspondentes receberá o prefixo 'NOVO: '",
|
"example_new_title": "<code>NOVO: ${note.title}</code> - o título das notas correspondentes receberá o prefixo 'NOVO: '",
|
||||||
"example_date_prefix": "<code>${note.dateCreatedObj.format('MM-DD:')}: ${note.title}</code> - notas correspondentes receberão um prefixo com o mês-dia da data de criação da nota",
|
"example_date_prefix": "<code>${note.dateCreatedObj.format('MM-DD:')}: ${note.title}</code> - notas correspondentes receberão um prefixo com o mês-dia da data de criação da nota",
|
||||||
"api_docs": "Veja da documentação da API para <a href='https://zadam.github.io/trilium/backend_api/Note.html'>nota</a> e suas <a href='https://day.js.org/docs/en/display/format'>propriedades dateCreatedObj / utcDateCreatedObj</a> para detalhes."
|
"api_docs": "Veja da documentação da API para <a href='https://zadam.github.io/trilium/backend_api/Note.html'>nota</a> e suas <a href='https://day.js.org/docs/en/display/format'>propriedades dateCreatedObj / utcDateCreatedObj</a> para detalhes.",
|
||||||
|
"evaluated_as_js_string": "O valor digitado é avaliado como string JavaScript e, portanto, pode ser enriquecido com conteúdo dinâmico através da variável injetada <code>note</code> (nota sendo renomeada). Exemplos:"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"mon": "Seg",
|
"mon": "Seg",
|
||||||
@@ -877,7 +920,8 @@
|
|||||||
"relation_map_buttons": {
|
"relation_map_buttons": {
|
||||||
"zoom_in_title": "Aumentar",
|
"zoom_in_title": "Aumentar",
|
||||||
"zoom_out_title": "Reduzir",
|
"zoom_out_title": "Reduzir",
|
||||||
"create_child_note_title": "Criar nova nota filha e adicione neste mapa de relação"
|
"create_child_note_title": "Criar nova nota filha e adicione neste mapa de relação",
|
||||||
|
"reset_pan_zoom_title": "Redefinir pan & zoom para coordenadas e ampliação iniciais"
|
||||||
},
|
},
|
||||||
"zpetne_odkazy": {
|
"zpetne_odkazy": {
|
||||||
"backlink": "{{count}} Links Reversos",
|
"backlink": "{{count}} Links Reversos",
|
||||||
@@ -887,7 +931,8 @@
|
|||||||
"mobile_detail_menu": {
|
"mobile_detail_menu": {
|
||||||
"insert_child_note": "Inserir nota filha",
|
"insert_child_note": "Inserir nota filha",
|
||||||
"delete_this_note": "Excluir essa nota",
|
"delete_this_note": "Excluir essa nota",
|
||||||
"error_unrecognized_command": "Comando não reconhecido {{command}}"
|
"error_unrecognized_command": "Comando não reconhecido {{command}}",
|
||||||
|
"error_cannot_get_branch_id": "Não foi possível obter o branchId para o notePath '{{notePath}} '"
|
||||||
},
|
},
|
||||||
"note_icon": {
|
"note_icon": {
|
||||||
"change_note_icon": "Alterar ícone da nota",
|
"change_note_icon": "Alterar ícone da nota",
|
||||||
@@ -957,7 +1002,8 @@
|
|||||||
"note_size": "Tamanho da nota",
|
"note_size": "Tamanho da nota",
|
||||||
"calculate": "calcular",
|
"calculate": "calcular",
|
||||||
"title": "Informações da nota",
|
"title": "Informações da nota",
|
||||||
"subtree_size": "(tamanho da subárvore: {{size}} em {{count}} notas)"
|
"subtree_size": "(tamanho da subárvore: {{size}} em {{count}} notas)",
|
||||||
|
"note_size_info": "O tamanho da nota fornece uma estimativa aproximada dos requisitos de armazenamento para esta nota. Leva em conta o conteúdo e o conteúdo de suas revisões de nota."
|
||||||
},
|
},
|
||||||
"note_map": {
|
"note_map": {
|
||||||
"open_full": "Expandir completamente",
|
"open_full": "Expandir completamente",
|
||||||
@@ -972,7 +1018,8 @@
|
|||||||
"intro_placed": "Esta nova está localizada nos caminhos:",
|
"intro_placed": "Esta nova está localizada nos caminhos:",
|
||||||
"intro_not_placed": "Esta nota ainda não está em nenhuma árvore de notas.",
|
"intro_not_placed": "Esta nota ainda não está em nenhuma árvore de notas.",
|
||||||
"archived": "Arquivado",
|
"archived": "Arquivado",
|
||||||
"search": "Pesquisar"
|
"search": "Pesquisar",
|
||||||
|
"outside_hoisted": "Este caminho está fora de uma nota fixada e você teria que desafixar."
|
||||||
},
|
},
|
||||||
"note_properties": {
|
"note_properties": {
|
||||||
"this_note_was_originally_taken_from": "Esta nota foi originalmente obtida de:",
|
"this_note_was_originally_taken_from": "Esta nota foi originalmente obtida de:",
|
||||||
@@ -986,7 +1033,8 @@
|
|||||||
"unknown_attribute_type": "Tipo de atributo desconhecido '{{type}}'",
|
"unknown_attribute_type": "Tipo de atributo desconhecido '{{type}}'",
|
||||||
"add_new_attribute": "Adicionar novo atributo",
|
"add_new_attribute": "Adicionar novo atributo",
|
||||||
"remove_this_attribute": "Remover este atributo",
|
"remove_this_attribute": "Remover este atributo",
|
||||||
"remove_color": "Remover a etiqueta de cor"
|
"remove_color": "Remover a etiqueta de cor",
|
||||||
|
"url_placeholder": "http://website..."
|
||||||
},
|
},
|
||||||
"script_executor": {
|
"script_executor": {
|
||||||
"query": "Consulta",
|
"query": "Consulta",
|
||||||
@@ -1006,13 +1054,16 @@
|
|||||||
"limit_description": "Limitar número de resultados",
|
"limit_description": "Limitar número de resultados",
|
||||||
"debug": "depurar",
|
"debug": "depurar",
|
||||||
"action": "ação",
|
"action": "ação",
|
||||||
"search_button": "Pesquisar <kbd>enter</kbd>",
|
"search_button": "Pesquisar",
|
||||||
"search_execute": "Pesquisar & Executar ações",
|
"search_execute": "Pesquisar & Executar ações",
|
||||||
"save_to_note": "Salvar para nota",
|
"save_to_note": "Salvar para nota",
|
||||||
"search_parameters": "Parâmetros de Pesquisa",
|
"search_parameters": "Parâmetros de Pesquisa",
|
||||||
"unknown_search_option": "Opção de pesquisa desconhecida {{searchOptionName}}",
|
"unknown_search_option": "Opção de pesquisa desconhecida {{searchOptionName}}",
|
||||||
"actions_executed": "As ações foram executadas.",
|
"actions_executed": "As ações foram executadas.",
|
||||||
"search_note_saved": "Nota de pesquisa foi salva em {{- notePathTitle}}"
|
"search_note_saved": "Nota de pesquisa foi salva em {{- notePathTitle}}",
|
||||||
|
"fast_search_description": "A opção de pesquisa rápida desabilita a pesquisa de texto completo do conteúdo de nota, o que pode acelerar a pesquisa em grandes bancos de dados.",
|
||||||
|
"include_archived_notes_description": "As notas arquivadas são por padrão excluídas dos resultados da pesquisa, com esta opção elas serão incluídas.",
|
||||||
|
"debug_description": "A depuração irá imprimir informações adicionais no console para ajudar na depuração de consultas complexas"
|
||||||
},
|
},
|
||||||
"similar_notes": {
|
"similar_notes": {
|
||||||
"title": "Notas Similares",
|
"title": "Notas Similares",
|
||||||
@@ -1023,10 +1074,13 @@
|
|||||||
"failed_rendering": "A renderização da opção de busca falhou: {{dto}} com o erro: {{error}} {{stack}}"
|
"failed_rendering": "A renderização da opção de busca falhou: {{dto}} com o erro: {{error}} {{stack}}"
|
||||||
},
|
},
|
||||||
"debug": {
|
"debug": {
|
||||||
"debug": "Depurar"
|
"debug": "Depurar",
|
||||||
|
"debug_info": "A depuração irá imprimir informações adicionais no console para ajudar em depuração de consultas complexas.",
|
||||||
|
"access_info": "Para acessar as informações de depuração, execute a consulta e clique em \"Exibir log do servidor\" no canto superior esquerdo."
|
||||||
},
|
},
|
||||||
"fast_search": {
|
"fast_search": {
|
||||||
"fast_search": "Pesquisa rápida"
|
"fast_search": "Pesquisa rápida",
|
||||||
|
"description": "A opção de pesquisa rápida desabilita a pesquisa de texto completo do conteúdo de nota, o que pode acelerar a pesquisa em grandes bancos de dados."
|
||||||
},
|
},
|
||||||
"include_archived_notes": {
|
"include_archived_notes": {
|
||||||
"include_archived_notes": "Incluir notas arquivadas"
|
"include_archived_notes": "Incluir notas arquivadas"
|
||||||
@@ -1058,7 +1112,10 @@
|
|||||||
"title": "Buscar script:",
|
"title": "Buscar script:",
|
||||||
"placeholder": "buscar notas pelo nome",
|
"placeholder": "buscar notas pelo nome",
|
||||||
"example_title": "Veja este exemplo:",
|
"example_title": "Veja este exemplo:",
|
||||||
"example_code": "// 1. pré-filtro usando pesquisa padrão\nconst candidateNotes = api.searchForNotes(\"#journal\"); \n\n// 2. aplicando critérios de pesquisa customizados\nconst matchedNotes = candidateNotes\n .filter(note => note.title.match(/[0-9]{1,2}\\. ?[0-9]{1,2}\\. ?[0-9]{4}/));\n\nreturn matchedNotes;"
|
"example_code": "// 1. pré-filtro usando pesquisa padrão\nconst candidateNotes = api.searchForNotes(\"#journal\"); \n\n// 2. aplicando critérios de pesquisa customizados\nconst matchedNotes = candidateNotes\n .filter(note => note.title.match(/[0-9]{1,2}\\. ?[0-9]{1,2}\\. ?[0-9]{4}/));\n\nreturn matchedNotes;",
|
||||||
|
"description1": "O script de pesquisa permite definir os resultados da pesquisa executando um script. Isso proporciona flexibilidade máxima quando a busca padrão não é suficiente.",
|
||||||
|
"description2": "O script de pesquisa deve ser do tipo \"código\" e subtipo \"JavaScript no servidor\". O script precisa retornar um array de noteIds ou de notas.",
|
||||||
|
"note": "Note que o script de pesquisa e a pesquisa de texto não podem ser combinados entre si."
|
||||||
},
|
},
|
||||||
"search_string": {
|
"search_string": {
|
||||||
"title_column": "Buscar texto:",
|
"title_column": "Buscar texto:",
|
||||||
@@ -1072,7 +1129,8 @@
|
|||||||
"label_year_comparison": "comparação numérica (também >, >=, <).",
|
"label_year_comparison": "comparação numérica (também >, >=, <).",
|
||||||
"label_date_created": "notas criadas no último mês",
|
"label_date_created": "notas criadas no último mês",
|
||||||
"error": "Erro na busca: {{error}}",
|
"error": "Erro na busca: {{error}}",
|
||||||
"search_prefix": "Busca:"
|
"search_prefix": "Busca:",
|
||||||
|
"placeholder": "palavras-chave fulltext, #tag = valor..."
|
||||||
},
|
},
|
||||||
"attachment_list": {
|
"attachment_list": {
|
||||||
"open_help_page": "Abrir página de ajuda nos anexos",
|
"open_help_page": "Abrir página de ajuda nos anexos",
|
||||||
@@ -1134,17 +1192,26 @@
|
|||||||
"check_button": "Verificar integridade do banco de dados",
|
"check_button": "Verificar integridade do banco de dados",
|
||||||
"checking_integrity": "Verificando integridade do banco de dados…",
|
"checking_integrity": "Verificando integridade do banco de dados…",
|
||||||
"integrity_check_succeeded": "Verificação de integridade bem sucedida - nenhum problema encontrado.",
|
"integrity_check_succeeded": "Verificação de integridade bem sucedida - nenhum problema encontrado.",
|
||||||
"integrity_check_failed": "Verificação de integridade falhou: {{results}}"
|
"integrity_check_failed": "Verificação de integridade falhou: {{results}}",
|
||||||
|
"title": "Verificação de Integridade do Banco de Dados",
|
||||||
|
"description": "Isso verificará se o banco de dados não está corrompido no nível SQLite. Pode levar algum tempo, dependendo do tamanho do banco de dados."
|
||||||
},
|
},
|
||||||
"sync": {
|
"sync": {
|
||||||
"title": "Sincronizar",
|
"title": "Sincronizar",
|
||||||
"force_full_sync_button": "Forçar sincronização completa",
|
"force_full_sync_button": "Forçar sincronização completa",
|
||||||
"full_sync_triggered": "Sincronização completa iniciada",
|
"full_sync_triggered": "Sincronização completa iniciada",
|
||||||
"finished-successfully": "Sincronização finalizada com sucesso.",
|
"finished-successfully": "Sincronização finalizada com sucesso.",
|
||||||
"failed": "Sincronização falhou: {{message}}"
|
"failed": "Sincronização falhou: {{message}}",
|
||||||
|
"fill_entity_changes_button": "Preencher registros de alterações da entidade",
|
||||||
|
"filling_entity_changes": "Preenchendo linhas de alterações da entidade...",
|
||||||
|
"sync_rows_filled_successfully": "Linhas de sincronização preenchidas com sucesso"
|
||||||
},
|
},
|
||||||
"vacuum_database": {
|
"vacuum_database": {
|
||||||
"description": "Isso irá reconstruir o banco de dados, o que normalmente irá resultar em uma redução do arquivo do banco de dados. Nenhum dado será alterado."
|
"description": "Isso irá reconstruir o banco de dados, o que normalmente irá resultar em uma redução do arquivo do banco de dados. Nenhum dado será alterado.",
|
||||||
|
"title": "Executar Vacuum no Banco de Dados",
|
||||||
|
"button_text": "Executar Vacuum",
|
||||||
|
"vacuuming_database": "Executando Vacuum...",
|
||||||
|
"database_vacuumed": "Vacuum executado no banco de dados"
|
||||||
},
|
},
|
||||||
"fonts": {
|
"fonts": {
|
||||||
"theme_defined": "Tema definido",
|
"theme_defined": "Tema definido",
|
||||||
@@ -1166,7 +1233,8 @@
|
|||||||
"serif": "Serifa",
|
"serif": "Serifa",
|
||||||
"sans-serif": "Sem Serifa",
|
"sans-serif": "Sem Serifa",
|
||||||
"monospace": "Monospace",
|
"monospace": "Monospace",
|
||||||
"system-default": "Padrão do Sistema"
|
"system-default": "Padrão do Sistema",
|
||||||
|
"note_tree_and_detail_font_sizing": "Note que o tamanho da fonte da árvore e dos detalhes é relativo à configuração principal do tamanho de fonte."
|
||||||
},
|
},
|
||||||
"max_content_width": {
|
"max_content_width": {
|
||||||
"title": "Largura do Conteúdo",
|
"title": "Largura do Conteúdo",
|
||||||
@@ -1174,7 +1242,8 @@
|
|||||||
"max_width_unit": "pixels",
|
"max_width_unit": "pixels",
|
||||||
"apply_changes_description": "Para aplicar as alterações de largura do conteúdo, clique em",
|
"apply_changes_description": "Para aplicar as alterações de largura do conteúdo, clique em",
|
||||||
"reload_button": "recarregar frontend",
|
"reload_button": "recarregar frontend",
|
||||||
"reload_description": "alterações de opções de aparência"
|
"reload_description": "alterações de opções de aparência",
|
||||||
|
"default_description": "Por padrão, o Trilium limita a largura máxima do conteúdo para melhorar a legibilidade em janelas maximizadas em telas wide."
|
||||||
},
|
},
|
||||||
"native_title_bar": {
|
"native_title_bar": {
|
||||||
"title": "Barra de Título Nativa (requer recarregar o app)",
|
"title": "Barra de Título Nativa (requer recarregar o app)",
|
||||||
@@ -1202,5 +1271,411 @@
|
|||||||
},
|
},
|
||||||
"copy_image_reference_button": {
|
"copy_image_reference_button": {
|
||||||
"button_title": "Copiar referência da imagem para a área de transferência, pode ser colado em uma nota de texto."
|
"button_title": "Copiar referência da imagem para a área de transferência, pode ser colado em uma nota de texto."
|
||||||
|
},
|
||||||
|
"onclick_button": {
|
||||||
|
"no_click_handler": "Componente de botão '{{componentId}}' não possui manipulador de clique definido"
|
||||||
|
},
|
||||||
|
"owned_attribute_list": {
|
||||||
|
"owned_attributes": "Atributos próprios"
|
||||||
|
},
|
||||||
|
"database_anonymization": {
|
||||||
|
"no_anonymized_database_yet": "Nenhuma base de dados anonimizada no momento."
|
||||||
|
},
|
||||||
|
"ribbon": {
|
||||||
|
"widgets": "Widgets de fita",
|
||||||
|
"promoted_attributes_message": "A aba de Atributos Promovidos irá abrir automaticamente se existirem atributos promovidos na nota",
|
||||||
|
"edited_notes_message": "A aba de Notas Editadas será aberta automaticamente nas notas do dia"
|
||||||
|
},
|
||||||
|
"ui-performance": {
|
||||||
|
"title": "Desempenho",
|
||||||
|
"enable-motion": "Habilitar transições e animações",
|
||||||
|
"enable-shadows": "Habilitar sombras",
|
||||||
|
"enable-backdrop-effects": "Habilitar efeitos de fundo para menus, popups e painéis"
|
||||||
|
},
|
||||||
|
"zoom_factor": {
|
||||||
|
"title": "Fator do Zoom (apenas versão de área de trabalho)",
|
||||||
|
"description": "O zoom também pode ser controlado com atalhos CTRL+- e CTRL+=."
|
||||||
|
},
|
||||||
|
"code_auto_read_only_size": {
|
||||||
|
"title": "Tamanho para Somente Leitura Automático",
|
||||||
|
"description": "O tamanho para nota somente leitura automático é o tamanho após o qual as notas serão exibidas em um modo somente leitura (por razões de desempenho).",
|
||||||
|
"label": "Tamanho para somente leitura automático (notas de código)",
|
||||||
|
"unit": "caracteres"
|
||||||
|
},
|
||||||
|
"code-editor-options": {
|
||||||
|
"title": "Editor"
|
||||||
|
},
|
||||||
|
"code_mime_types": {
|
||||||
|
"title": "Tipos MIME disponíveis no dropdown"
|
||||||
|
},
|
||||||
|
"vim_key_bindings": {
|
||||||
|
"use_vim_keybindings_in_code_notes": "Atribuições de teclas do Vim",
|
||||||
|
"enable_vim_keybindings": "Habilite as atribuições de teclas do Vim em notas de código (sem modo ex)"
|
||||||
|
},
|
||||||
|
"wrap_lines": {
|
||||||
|
"wrap_lines_in_code_notes": "Quebrar linhas em notas de código",
|
||||||
|
"enable_line_wrap": "Habilitar Quebra de Linha (pode ser necessário recarregar o frontend para entrar em vigor)"
|
||||||
|
},
|
||||||
|
"images": {
|
||||||
|
"images_section_title": "Imagens",
|
||||||
|
"download_images_automatically": "Baixar imagens automaticamente para uso offline.",
|
||||||
|
"download_images_description": "HTML colado pode conter referências a imagens on-line, Trilium vai buscar estas referências e baixar as imagens para que eles estejam disponíveis off-line.",
|
||||||
|
"enable_image_compression": "Habilitar compressão de imagem",
|
||||||
|
"max_image_dimensions": "Largura/altura máxima de uma imagem (a imagem será redimensionada se exceder este valor).",
|
||||||
|
"max_image_dimensions_unit": "pixels",
|
||||||
|
"jpeg_quality_description": "Qualidade JPEG (10 - pior qualidade, 100 - melhor qualidade, 50 - 85 é recomendado)"
|
||||||
|
},
|
||||||
|
"attachment_erasure_timeout": {
|
||||||
|
"attachment_erasure_timeout": "Tempo Limite para Exclusão de um Anexo",
|
||||||
|
"attachment_auto_deletion_description": "Os anexos são automaticamente excluídos (e apagados) se não forem mais referenciados por sua nota após um tempo definido.",
|
||||||
|
"erase_attachments_after": "Apagar anexos não utilizados após:",
|
||||||
|
"manual_erasing_description": "Você também pode ativar a exclusão manualmente (sem considerar o timeout definido acima):",
|
||||||
|
"erase_unused_attachments_now": "Apagar anexos não utilizados agora",
|
||||||
|
"unused_attachments_erased": "Os anexos não utilizados foram apagados."
|
||||||
|
},
|
||||||
|
"network_connections": {
|
||||||
|
"network_connections_title": "Conexões de Rede",
|
||||||
|
"check_for_updates": "Verificar atualizações automaticamente"
|
||||||
|
},
|
||||||
|
"note_erasure_timeout": {
|
||||||
|
"note_erasure_timeout_title": "Tempo Limite para Exclusão de uma Nota",
|
||||||
|
"note_erasure_description": "Notas excluídas (e atributos, revisões...) inicialmente são apenas marcadas como excluídas e é possível recuperá-las a partir do diálogo de Notas Recentes. Depois de um tempo, as notas excluídas são \"apagadas\", o que significa que seu conteúdo não é mais recuperável. Esta configuração permite configurar o período entre excluir e apagar a nota.",
|
||||||
|
"erase_notes_after": "Apagar notas após:",
|
||||||
|
"manual_erasing_description": "Você também pode ativar a exclusão manualmente (sem considerar o timeout definido acima):",
|
||||||
|
"erase_deleted_notes_now": "Apague as notas excluídas agora",
|
||||||
|
"deleted_notes_erased": "As notas excluídas foram removidas permanentemente."
|
||||||
|
},
|
||||||
|
"revisions_snapshot_interval": {
|
||||||
|
"note_revisions_snapshot_interval_title": "Intervalo de Captura de Versão da Nota",
|
||||||
|
"note_revisions_snapshot_description": "O intervalo de captura de versão da nota é o tempo após o qual uma nova revisão será criada para a nota. Veja <doc>wiki</doc> para mais informações.",
|
||||||
|
"snapshot_time_interval_label": "Intervalo de Captura de Versão da Nota:"
|
||||||
|
},
|
||||||
|
"revisions_snapshot_limit": {
|
||||||
|
"note_revisions_snapshot_limit_title": "Limite de Capturas das Versões da Nota",
|
||||||
|
"note_revisions_snapshot_limit_description": "O limite de número de captura de versões das notas refere-se ao número máximo de revisões que podem ser salvas para cada nota. Onde -1 significa nenhum limite, 0 significa excluir todas as revisões. Você pode definir as revisões máximas para uma única nota através da etiqueta #versioningLimit.",
|
||||||
|
"snapshot_number_limit_label": "Quantidade limite de capturas de versão:",
|
||||||
|
"snapshot_number_limit_unit": "capturas",
|
||||||
|
"erase_excess_revision_snapshots": "Apagar capturas de versão excedentes agora",
|
||||||
|
"erase_excess_revision_snapshots_prompt": "As capturas de versão excedentes foram apagadas."
|
||||||
|
},
|
||||||
|
"search_engine": {
|
||||||
|
"title": "Motor de Pesquisa",
|
||||||
|
"custom_search_engine_info": "O motor de busca personalizado requer que sejam definidos um nome e uma URL. Se um destes não estiver definido, o DuckDuckGo será usado como o motor de busca padrão.",
|
||||||
|
"predefined_templates_label": "Modelos de motor de pesquisa predefinidos",
|
||||||
|
"bing": "Bing",
|
||||||
|
"baidu": "Baidu",
|
||||||
|
"duckduckgo": "DuckDuckGo",
|
||||||
|
"custom_name_label": "Nome do motor de pesquisa personalizado",
|
||||||
|
"google": "Google",
|
||||||
|
"custom_name_placeholder": "Nome personalizado do motor de pesquisa",
|
||||||
|
"custom_url_label": "A URL do motor de pesquisa personalizado deve incluir {keyword} como um substituto para o termo pesquisado.",
|
||||||
|
"custom_url_placeholder": "URL personalizada do motor de pesquisa",
|
||||||
|
"save_button": "Salvar"
|
||||||
|
},
|
||||||
|
"tray": {
|
||||||
|
"title": "Bandeja do Sistema",
|
||||||
|
"enable_tray": "Habilitar bandeja (O Trilium precisar ser reiniciado para esta mudança entrar em vigor)"
|
||||||
|
},
|
||||||
|
"heading_style": {
|
||||||
|
"title": "Estilo de Título",
|
||||||
|
"plain": "Plano",
|
||||||
|
"markdown": "Estilo Markdown",
|
||||||
|
"underline": "Sublinhado"
|
||||||
|
},
|
||||||
|
"highlights_list": {
|
||||||
|
"title": "Lista de Destaques",
|
||||||
|
"description": "Você pode personalizar a lista de destaques exibida no painel da direita:",
|
||||||
|
"bold": "Texto em negrito",
|
||||||
|
"italic": "Texto em itálico",
|
||||||
|
"underline": "Texto sublinhado",
|
||||||
|
"color": "Texto colorido",
|
||||||
|
"bg_color": "Texto com cor de fundo",
|
||||||
|
"visibility_title": "Visibilidade da Lista de Destaques",
|
||||||
|
"visibility_description": "Você pode esconder o componente de destaques por nota adicionado a etiqueta #hideHighlightWidget.",
|
||||||
|
"shortcut_info": "Você pode configurar um atalhe de teclado para alternar rapidamente o painel da direita (incluindo os Destaques) em Opções -> Atalhos (nome 'toggleRightPane')."
|
||||||
|
},
|
||||||
|
"table_of_contents": {
|
||||||
|
"title": "Tabela de Conteúdos",
|
||||||
|
"description": "A tabela de conteúdos irá aparecer em notas de texto que possuam mais que um número definido de títulos. Você pode personalizar este número:",
|
||||||
|
"unit": "títulos",
|
||||||
|
"disable_info": "Você também pode usar essa opção para desabilitar a Tabela de Conteúdos configurando um número muito alto.",
|
||||||
|
"shortcut_info": "Você pode configurar um atalhe de teclado para alternar rapidamente o painel da direita (incluindo a Tabela de Conteúdos) em Opções -> Atalhos (nome 'toggleRightPane')."
|
||||||
|
},
|
||||||
|
"text_auto_read_only_size": {
|
||||||
|
"title": "Tamanho para Somente Leitura Automático",
|
||||||
|
"description": "O tamanho para nota somente leitura automático é o tamanho a partir do qual as notas serão exibidas em modo somente leitura (por razões de desempenho)."
|
||||||
|
},
|
||||||
|
"custom_date_time_format": {
|
||||||
|
"title": "Formato Personalizado de Data/Hora",
|
||||||
|
"description": "Personaliza o formato de data e hora inseridos via <shortcut /> ou barra de ferramentas. Veja a <doc>documentação do Day.js</doc> para os tokens de formatos disponíveis.",
|
||||||
|
"format_string": "Formato:",
|
||||||
|
"formatted_time": "Data/hora formatada:"
|
||||||
|
},
|
||||||
|
"i18n": {
|
||||||
|
"title": "Localização",
|
||||||
|
"language": "Idioma",
|
||||||
|
"first-day-of-the-week": "Primeiro dia da semana",
|
||||||
|
"sunday": "Domingo",
|
||||||
|
"monday": "Segunda-feira",
|
||||||
|
"first-week-of-the-year": "Primeiro dia do ano",
|
||||||
|
"first-week-contains-first-day": "Primeira semana que contenha o primeiro dia do ano",
|
||||||
|
"first-week-contains-first-thursday": "Primeira semana que contenha a primeira quinta-feira do ano",
|
||||||
|
"first-week-has-minimum-days": "Primeira semana contendo um mínimo de dias",
|
||||||
|
"min-days-in-first-week": "Mínimo de dias da primeira semana",
|
||||||
|
"first-week-info": "Primeira semana que contenha a primeira Quinta-feira do ano é baseado na <a href=\"https://en.wikipedia.org/wiki/ISO_week_date#First_week\">ISO 8601</a>.",
|
||||||
|
"first-week-warning": "Alterar as opções de primeira semana pode causar duplicidade nas Notas Semanais existentes e estas Notas não serão atualizadas de acordo.",
|
||||||
|
"formatting-locale": "Formato de data e número"
|
||||||
|
},
|
||||||
|
"backup": {
|
||||||
|
"automatic_backup": "Backup automático",
|
||||||
|
"automatic_backup_description": "O Trilium pode fazer o backup do banco de dados automaticamente:",
|
||||||
|
"enable_daily_backup": "Habilitar backup diário",
|
||||||
|
"enable_weekly_backup": "Habilitar backup semanal",
|
||||||
|
"enable_monthly_backup": "Habilitar backup mensal",
|
||||||
|
"backup_recommendation": "É recomendado manter o backup habilitado, mas isso pode fazer com que a inicialização da aplicação seja lenta com grandes bancos de dados e/ou dispositivos de armazenamento lentos.",
|
||||||
|
"backup_now": "Realizar backup agora",
|
||||||
|
"backup_database_now": "Realizar backup do banco de dados agora",
|
||||||
|
"existing_backups": "Backups existentes",
|
||||||
|
"date-and-time": "Data & hora",
|
||||||
|
"path": "Caminho"
|
||||||
|
},
|
||||||
|
"note_types": {
|
||||||
|
"relation-map": "Map de Relação",
|
||||||
|
"note-map": "Map de Notas",
|
||||||
|
"render-note": "Nota de Renderização",
|
||||||
|
"book": "Coleção",
|
||||||
|
"mermaid-diagram": "Diagrama Mermaid",
|
||||||
|
"canvas": "Quadros",
|
||||||
|
"web-view": "Visualização Web",
|
||||||
|
"mind-map": "Mapa Mental",
|
||||||
|
"file": "Arquivo",
|
||||||
|
"image": "Imagem",
|
||||||
|
"launcher": "Lançador",
|
||||||
|
"doc": "Documento",
|
||||||
|
"widget": "Widget",
|
||||||
|
"confirm-change": "Não é recomentado alterar o tipo da nota quando o conteúdo da nota não está vazio. Quer continuar assim mesmo?",
|
||||||
|
"geo-map": "Geo Map",
|
||||||
|
"beta-feature": "Beta",
|
||||||
|
"ai-chat": "Chat IA",
|
||||||
|
"task-list": "Lista de Tarefas",
|
||||||
|
"new-feature": "Novo",
|
||||||
|
"collections": "Coleções"
|
||||||
|
},
|
||||||
|
"protect_note": {
|
||||||
|
"toggle-on": "Proteger a nota",
|
||||||
|
"toggle-off": "Desproteger a nota",
|
||||||
|
"toggle-on-hint": "A nota não está protegida, clique para protegê-la",
|
||||||
|
"toggle-off-hint": "A nota está protegida, clique para desprotegê-la"
|
||||||
|
},
|
||||||
|
"shared_switch": {
|
||||||
|
"shared": "Compartilhado",
|
||||||
|
"toggle-on-title": "Compartilhar a nota",
|
||||||
|
"toggle-off-title": "Parar de compartilhar a nota",
|
||||||
|
"shared-branch": "Esta nota só existe como uma nota compartilhada, então parar de compartilhar irá excluí-la. Deseja continuar e excluir esta nota?",
|
||||||
|
"inherited": "Não foi possível deixar de compartilhar a nota porque ela é compartilhada através da herança de uma nota superior."
|
||||||
|
},
|
||||||
|
"template_switch": {
|
||||||
|
"template": "Modelo",
|
||||||
|
"toggle-on-hint": "Transformar a nota em um modelo",
|
||||||
|
"toggle-off-hint": "Deixar de usar a nota como um modelo"
|
||||||
|
},
|
||||||
|
"open-help-page": "Abrir página de ajuda",
|
||||||
|
"find": {
|
||||||
|
"case_sensitive": "Diferencia maiúsculas de minúsculas",
|
||||||
|
"match_words": "Corresponder palavras",
|
||||||
|
"find_placeholder": "Buscar no texto...",
|
||||||
|
"replace_placeholder": "Substituir por...",
|
||||||
|
"replace": "Substituir",
|
||||||
|
"replace_all": "Substituir tudo"
|
||||||
|
},
|
||||||
|
"highlights_list_2": {
|
||||||
|
"title": "Lista de Destaques",
|
||||||
|
"options": "Opções"
|
||||||
|
},
|
||||||
|
"quick-search": {
|
||||||
|
"placeholder": "Busca rápida",
|
||||||
|
"searching": "Buscando...",
|
||||||
|
"no-results": "Nenhum resultado encontrado",
|
||||||
|
"more-results": "... e mais {{number}} resultados.",
|
||||||
|
"show-in-full-search": "Exibir na busca completa"
|
||||||
|
},
|
||||||
|
"note_tree": {
|
||||||
|
"collapse-title": "Recolher árvore de notas",
|
||||||
|
"scroll-active-title": "Ir até a nota ativa",
|
||||||
|
"tree-settings-title": "Configurações da árvore",
|
||||||
|
"hide-archived-notes": "Ocultar notas arquivadas",
|
||||||
|
"automatically-collapse-notes": "Recolher notas automaticamente",
|
||||||
|
"automatically-collapse-notes-title": "As notas serão recolhidas após um tempo de inatividade para simplificar a árvore.",
|
||||||
|
"save-changes": "Salvar e aplicar alterações",
|
||||||
|
"auto-collapsing-notes-after-inactivity": "Recolhendo notas automaticamente após inatividade...",
|
||||||
|
"saved-search-note-refreshed": "A nota de pesquisa salva foi atualizada.",
|
||||||
|
"hoist-this-note-workspace": "Fixar esta nota (workspace)",
|
||||||
|
"refresh-saved-search-results": "Atualizar resultados de pesquisa salvos",
|
||||||
|
"create-child-note": "Criar nota filha",
|
||||||
|
"unhoist": "Desafixar"
|
||||||
|
},
|
||||||
|
"title_bar_buttons": {
|
||||||
|
"window-on-top": "Manter Janela no Topo"
|
||||||
|
},
|
||||||
|
"note_detail": {
|
||||||
|
"could_not_find_typewidget": "Não foi possível encontrar typeWidget para o tipo '{{type}}'"
|
||||||
|
},
|
||||||
|
"note_title": {
|
||||||
|
"placeholder": "digite o título da nota aqui..."
|
||||||
|
},
|
||||||
|
"search_result": {
|
||||||
|
"no_notes_found": "Nenhuma nota encontrada para os parâmetros de busca digitados.",
|
||||||
|
"search_not_executed": "A busca ainda não foi executada. Clique no botão \"Buscar\" acima para ver os resultados."
|
||||||
|
},
|
||||||
|
"spacer": {
|
||||||
|
"configure_launchbar": "Configurar Barra de Lançamento"
|
||||||
|
},
|
||||||
|
"sql_result": {
|
||||||
|
"no_rows": "Nenhum linha foi retornada para esta consulta"
|
||||||
|
},
|
||||||
|
"sql_table_schemas": {
|
||||||
|
"tables": "Tabelas"
|
||||||
|
},
|
||||||
|
"tab_row": {
|
||||||
|
"close_tab": "Fechar aba",
|
||||||
|
"add_new_tab": "Adicionar nova aba",
|
||||||
|
"close": "Fechar",
|
||||||
|
"close_other_tabs": "Fechar as outras abas",
|
||||||
|
"close_right_tabs": "Fechar as abas à direita",
|
||||||
|
"close_all_tabs": "Fechar todas as abas",
|
||||||
|
"reopen_last_tab": "Reabrir a última aba fechada",
|
||||||
|
"move_tab_to_new_window": "Mover esta aba para uma nova janela",
|
||||||
|
"copy_tab_to_new_window": "Copiar esta aba para uma nova janela",
|
||||||
|
"new_tab": "Nova aba"
|
||||||
|
},
|
||||||
|
"toc": {
|
||||||
|
"table_of_contents": "Tabela de Conteúdos",
|
||||||
|
"options": "Opções"
|
||||||
|
},
|
||||||
|
"watched_file_update_status": {
|
||||||
|
"file_last_modified": "O arquivo <code class=\"file-path\"> foi modificado pela última vez em <span class=\"file-last-modified\">.",
|
||||||
|
"upload_modified_file": "Enviar arquivo modificado",
|
||||||
|
"ignore_this_change": "Ignorar esta alteração"
|
||||||
|
},
|
||||||
|
"app_context": {
|
||||||
|
"please_wait_for_save": "Por favor aguarde alguns segundos para finalizar a gravação, e então tente novamente."
|
||||||
|
},
|
||||||
|
"note_create": {
|
||||||
|
"duplicated": "A nota \"{{title}}\" foi duplicada."
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"copied-to-clipboard": "Uma referência para esta imagem foi copiada para a área de transferência. Isso pode ser colado em qualquer nota de texto.",
|
||||||
|
"cannot-copy": "Não foi possível copiar a referência da imagem para a área de transferência."
|
||||||
|
},
|
||||||
|
"clipboard": {
|
||||||
|
"cut": "Nota(s) recortadas(s) para a área de transferência.",
|
||||||
|
"copied": "Not(s) copiada(s) para a área de transferência.",
|
||||||
|
"copy_failed": "Não foi possível copiar para a área de transferência por problemas de permissão.",
|
||||||
|
"copy_success": "Copiado para a área de transferência."
|
||||||
|
},
|
||||||
|
"entrypoints": {
|
||||||
|
"note-revision-created": "A revisão da nota foi criada.",
|
||||||
|
"note-executed": "Nota executada.",
|
||||||
|
"sql-error": "Ocorreu um erro durante a execução da consulta SQL: {{message}}"
|
||||||
|
},
|
||||||
|
"branches": {
|
||||||
|
"cannot-move-notes-here": "Não é possível mover notas para cá.",
|
||||||
|
"delete-status": "Remover estado.",
|
||||||
|
"delete-notes-in-progress": "Exclusão de notas em andamento: {{count}}",
|
||||||
|
"delete-finished-successfully": "Exclusão concluída com sucesso.",
|
||||||
|
"undeleting-notes-in-progress": "Restauração de notas em andamento: {{count}}",
|
||||||
|
"undeleting-notes-finished-successfully": "Restauração de notas concluída com sucesso."
|
||||||
|
},
|
||||||
|
"frontend_script_api": {
|
||||||
|
"async_warning": "Você está passando uma função assíncrona para `api.runOnBackend()`, o que provavelmente não irá funcionar como esperado.\\nTorne a função síncrona (removendo a palavra-chave `async`), ou use `api.runAsyncOnBackendWithManualTransactionHandling()`.",
|
||||||
|
"sync_warning": "Você está passando uma função síncrona para `api.runAsyncOnBackendWithManualTransactionHandling()`,\\nquando deveria usar `api.runOnBackend()` no lugar."
|
||||||
|
},
|
||||||
|
"ws": {
|
||||||
|
"sync-check-failed": "A verificação de sincronização falhou!",
|
||||||
|
"consistency-checks-failed": "A verificação de consistência falhou! Veja os logs para detalhes.",
|
||||||
|
"encountered-error": "Encontrado o erro \"{{message}}\", verifique o console."
|
||||||
|
},
|
||||||
|
"hoisted_note": {
|
||||||
|
"confirm_unhoisting": "A nota solicitada '{{requestedNote}}' está fora da árvore da nota fixada '{{hoistedNote}}' e você precisa desafixar para acessar a nota. Quer prosseguir e desafixar?"
|
||||||
|
},
|
||||||
|
"launcher_context_menu": {
|
||||||
|
"reset_launcher_confirm": "Você deseja realmente reiniciar \"{{title}}\"? Todos os dados / configurações desta nota (e suas filhas) serão perdidos o lançador irá retornar para sua localização original.",
|
||||||
|
"add-note-launcher": "Adicionar um lançador de nota",
|
||||||
|
"add-script-launcher": "Adicionar um lançador de script",
|
||||||
|
"add-custom-widget": "Adicionar um componente personalizado",
|
||||||
|
"add-spacer": "Adicionar um espaçador",
|
||||||
|
"delete": "Excluir <kbd data-command=\"deleteNotes\"></kbd>",
|
||||||
|
"reset": "Reiniciar",
|
||||||
|
"move-to-visible-launchers": "Mover para lançadores visíveis",
|
||||||
|
"move-to-available-launchers": "Mover para lançadores disponíveis",
|
||||||
|
"duplicate-launcher": "Duplicar o lançador <kbd data-command=\"duplicateSubtree\">"
|
||||||
|
},
|
||||||
|
"editable-text": {
|
||||||
|
"auto-detect-language": "Detectado automaticamente"
|
||||||
|
},
|
||||||
|
"highlighting": {
|
||||||
|
"title": "Blocos de Código",
|
||||||
|
"description": "Controla o destaque de sintaxe para blocos de código dentro de notas de texto, notas de código não serão afetadas.",
|
||||||
|
"color-scheme": "Esquema de Cor"
|
||||||
|
},
|
||||||
|
"classic_editor_toolbar": {
|
||||||
|
"title": "Formatação"
|
||||||
|
},
|
||||||
|
"editor": {
|
||||||
|
"title": "Editor"
|
||||||
|
},
|
||||||
|
"editing": {
|
||||||
|
"editor_type": {
|
||||||
|
"label": "Barra de formatação",
|
||||||
|
"floating": {
|
||||||
|
"title": "Flutuando",
|
||||||
|
"description": "ferramentas de edição aparecem perto do cursor;"
|
||||||
|
},
|
||||||
|
"fixed": {
|
||||||
|
"title": "Fixado",
|
||||||
|
"description": "ferramentas de edição aparecem na aba de faixa \"Formatação\"."
|
||||||
|
},
|
||||||
|
"multiline-toolbar": "Exibir a barra de ferramentas em múltiplas linhas se não couber."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"electron_context_menu": {
|
||||||
|
"add-term-to-dictionary": "Adicionar \"{{term}}\" ao dicionário",
|
||||||
|
"cut": "Cortar",
|
||||||
|
"copy": "Copiar",
|
||||||
|
"copy-link": "Copiar link",
|
||||||
|
"paste": "Colar",
|
||||||
|
"paste-as-plain-text": "Colar como texto sem formatação",
|
||||||
|
"search_online": "Buscar por \"{{term}}\" usando {{searchEngine}}"
|
||||||
|
},
|
||||||
|
"image_context_menu": {
|
||||||
|
"copy_reference_to_clipboard": "Copiar referência para a área de transferência",
|
||||||
|
"copy_image_to_clipboard": "Copiar imagem para a área de transferência"
|
||||||
|
},
|
||||||
|
"link_context_menu": {
|
||||||
|
"open_note_in_new_tab": "Abrir nota em nova aba",
|
||||||
|
"open_note_in_new_split": "Abrir nota em nova divisão",
|
||||||
|
"open_note_in_new_window": "Abrir nota em nova janela",
|
||||||
|
"open_note_in_popup": "Edição rápida"
|
||||||
|
},
|
||||||
|
"electron_integration": {
|
||||||
|
"desktop-application": "Aplicação Desktop",
|
||||||
|
"native-title-bar": "Barra de título nativa",
|
||||||
|
"native-title-bar-description": "Para Windows e macOS, manter a barra de título nativa desabilitada faz a aplicação parecer mais compacta. No Linux, manter a barra de título nativa habilitada faz a aplicação se integrar melhor com o restante do sistema.",
|
||||||
|
"background-effects": "Habilitar efeitos de fundo (apenas Windows 11)",
|
||||||
|
"background-effects-description": "O efeito Mica adicionar um fundo borrado e estilizado às janelas da aplicação, criando profundidade e um visual moderno.",
|
||||||
|
"restart-app-button": "Reiniciar a aplicação para ver as alterações",
|
||||||
|
"zoom-factor": "Fator de Zoom"
|
||||||
|
},
|
||||||
|
"note_autocomplete": {
|
||||||
|
"search-for": "Buscar por \"{{term}}\"",
|
||||||
|
"create-note": "Criar conectar nota filha \"{{term}}\"",
|
||||||
|
"insert-external-link": "Inserir link externo para \"{{term}}\"",
|
||||||
|
"clear-text-field": "Limpar campo de texto",
|
||||||
|
"show-recent-notes": "Exibir notas recentes",
|
||||||
|
"full-text-search": "Busca por texto completo"
|
||||||
|
},
|
||||||
|
"note_tooltip": {
|
||||||
|
"note-has-been-deleted": "A nota foi excluída.",
|
||||||
|
"quick-edit": "Edição rápida."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1106,7 +1106,7 @@
|
|||||||
"limit_description": "Limitează numărul de rezultate",
|
"limit_description": "Limitează numărul de rezultate",
|
||||||
"order_by": "ordonează după",
|
"order_by": "ordonează după",
|
||||||
"save_to_note": "Salvează în notiță",
|
"save_to_note": "Salvează în notiță",
|
||||||
"search_button": "Căutare <kbd>Enter</kbd>",
|
"search_button": "Căutare",
|
||||||
"search_execute": "Caută și execută acțiunile",
|
"search_execute": "Caută și execută acțiunile",
|
||||||
"search_note_saved": "Notița de căutare a fost salvată în {{- notePathTitle}}",
|
"search_note_saved": "Notița de căutare a fost salvată în {{- notePathTitle}}",
|
||||||
"search_parameters": "Parametrii de căutare",
|
"search_parameters": "Parametrii de căutare",
|
||||||
|
|||||||
@@ -1060,7 +1060,7 @@
|
|||||||
"fast_search": "быстрый поиск",
|
"fast_search": "быстрый поиск",
|
||||||
"include_archived": "включать архивированные",
|
"include_archived": "включать архивированные",
|
||||||
"order_by": "сортировать по",
|
"order_by": "сортировать по",
|
||||||
"search_button": "Поиск <kbd>enter</kbd>",
|
"search_button": "Поиск",
|
||||||
"search_parameters": "Параметры поиска",
|
"search_parameters": "Параметры поиска",
|
||||||
"ancestor": "предок",
|
"ancestor": "предок",
|
||||||
"action": "действие",
|
"action": "действие",
|
||||||
|
|||||||
@@ -845,7 +845,7 @@
|
|||||||
"debug": "除錯",
|
"debug": "除錯",
|
||||||
"debug_description": "除錯將顯示額外的除錯資訊至控制台,以幫助除錯複雜查詢",
|
"debug_description": "除錯將顯示額外的除錯資訊至控制台,以幫助除錯複雜查詢",
|
||||||
"action": "操作",
|
"action": "操作",
|
||||||
"search_button": "搜尋 <kbd>Enter</kbd>",
|
"search_button": "搜尋",
|
||||||
"search_execute": "搜尋並執行操作",
|
"search_execute": "搜尋並執行操作",
|
||||||
"save_to_note": "儲存至筆記",
|
"save_to_note": "儲存至筆記",
|
||||||
"search_parameters": "搜尋參數",
|
"search_parameters": "搜尋參數",
|
||||||
|
|||||||
@@ -960,7 +960,7 @@
|
|||||||
"debug": "debug",
|
"debug": "debug",
|
||||||
"debug_description": "Debug виведе додаткову інформацію для налагодження в консоль, щоб допомогти у налагодженні складних запитів",
|
"debug_description": "Debug виведе додаткову інформацію для налагодження в консоль, щоб допомогти у налагодженні складних запитів",
|
||||||
"action": "дія",
|
"action": "дія",
|
||||||
"search_button": "Пошук <kbd>enter</kbd>",
|
"search_button": "Пошук",
|
||||||
"search_execute": "Пошук & Виконання дій",
|
"search_execute": "Пошук & Виконання дій",
|
||||||
"save_to_note": "Зберегти до нотатки",
|
"save_to_note": "Зберегти до нотатки",
|
||||||
"search_parameters": "Параметри пошуку",
|
"search_parameters": "Параметри пошуку",
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ type DateTimeStyle = "full" | "long" | "medium" | "short" | "none" | undefined;
|
|||||||
/**
|
/**
|
||||||
* Formats the given date and time to a string based on the current locale.
|
* Formats the given date and time to a string based on the current locale.
|
||||||
*/
|
*/
|
||||||
export function formatDateTime(date: string | Date | number, dateStyle: DateTimeStyle = "medium", timeStyle: DateTimeStyle = "medium") {
|
export function formatDateTime(date: string | Date | number | null | undefined, dateStyle: DateTimeStyle = "medium", timeStyle: DateTimeStyle = "medium") {
|
||||||
|
if (!date) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
const locale = navigator.language;
|
const locale = navigator.language;
|
||||||
|
|
||||||
let parsedDate;
|
let parsedDate;
|
||||||
|
|||||||
@@ -1,504 +0,0 @@
|
|||||||
import { t } from "../../services/i18n.js";
|
|
||||||
import NoteContextAwareWidget from "../note_context_aware_widget.js";
|
|
||||||
import noteAutocompleteService, { type Suggestion } from "../../services/note_autocomplete.js";
|
|
||||||
import server from "../../services/server.js";
|
|
||||||
import contextMenuService from "../../menus/context_menu.js";
|
|
||||||
import attributeParser, { type Attribute } from "../../services/attribute_parser.js";
|
|
||||||
import { AttributeEditor, type EditorConfig, type ModelElement, type MentionFeed, type ModelNode, type ModelPosition } from "@triliumnext/ckeditor5";
|
|
||||||
import froca from "../../services/froca.js";
|
|
||||||
import attributeRenderer from "../../services/attribute_renderer.js";
|
|
||||||
import noteCreateService from "../../services/note_create.js";
|
|
||||||
import attributeService from "../../services/attributes.js";
|
|
||||||
import linkService from "../../services/link.js";
|
|
||||||
import type AttributeDetailWidget from "./attribute_detail.js";
|
|
||||||
import type { CommandData, EventData, EventListener, FilteredCommandNames } from "../../components/app_context.js";
|
|
||||||
import type { default as FAttribute, AttributeType } from "../../entities/fattribute.js";
|
|
||||||
import type FNote from "../../entities/fnote.js";
|
|
||||||
import { escapeQuotes } from "../../services/utils.js";
|
|
||||||
|
|
||||||
const HELP_TEXT = `
|
|
||||||
<p>${t("attribute_editor.help_text_body1")}</p>
|
|
||||||
|
|
||||||
<p>${t("attribute_editor.help_text_body2")}</p>
|
|
||||||
|
|
||||||
<p>${t("attribute_editor.help_text_body3")}</p>`;
|
|
||||||
|
|
||||||
const TPL = /*html*/`
|
|
||||||
<div style="position: relative; padding-top: 10px; padding-bottom: 10px">
|
|
||||||
<style>
|
|
||||||
.attribute-list-editor {
|
|
||||||
border: 0 !important;
|
|
||||||
outline: 0 !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
padding: 0 0 0 5px !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
max-height: 100px;
|
|
||||||
overflow: auto;
|
|
||||||
transition: opacity .1s linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attribute-list-editor.ck-content .mention {
|
|
||||||
color: var(--muted-text-color) !important;
|
|
||||||
background: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.save-attributes-button {
|
|
||||||
color: var(--muted-text-color);
|
|
||||||
position: absolute;
|
|
||||||
bottom: 14px;
|
|
||||||
right: 25px;
|
|
||||||
cursor: pointer;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
font-size: 130%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-new-attribute-button {
|
|
||||||
color: var(--muted-text-color);
|
|
||||||
position: absolute;
|
|
||||||
bottom: 13px;
|
|
||||||
right: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
font-size: 130%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-new-attribute-button:hover, .save-attributes-button:hover {
|
|
||||||
border: 1px solid var(--button-border-color);
|
|
||||||
border-radius: var(--button-border-radius);
|
|
||||||
background: var(--button-background-color);
|
|
||||||
color: var(--button-text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.attribute-errors {
|
|
||||||
color: red;
|
|
||||||
padding: 5px 50px 0px 5px; /* large right padding to avoid buttons */
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div class="attribute-list-editor" tabindex="200"></div>
|
|
||||||
|
|
||||||
<div class="bx bx-save save-attributes-button tn-tool-button" title="${escapeQuotes(t("attribute_editor.save_attributes"))}"></div>
|
|
||||||
<div class="bx bx-plus add-new-attribute-button tn-tool-button" title="${escapeQuotes(t("attribute_editor.add_a_new_attribute"))}"></div>
|
|
||||||
|
|
||||||
<div class="attribute-errors" style="display: none;"></div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const mentionSetup: MentionFeed[] = [
|
|
||||||
{
|
|
||||||
marker: "@",
|
|
||||||
feed: (queryText) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText),
|
|
||||||
itemRenderer: (_item) => {
|
|
||||||
const item = _item as Suggestion;
|
|
||||||
const itemElement = document.createElement("button");
|
|
||||||
|
|
||||||
itemElement.innerHTML = `${item.highlightedNotePathTitle} `;
|
|
||||||
|
|
||||||
return itemElement;
|
|
||||||
},
|
|
||||||
minimumCharacters: 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
marker: "#",
|
|
||||||
feed: async (queryText) => {
|
|
||||||
const names = await server.get<string[]>(`attribute-names/?type=label&query=${encodeURIComponent(queryText)}`);
|
|
||||||
|
|
||||||
return names.map((name) => {
|
|
||||||
return {
|
|
||||||
id: `#${name}`,
|
|
||||||
name: name
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
minimumCharacters: 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
marker: "~",
|
|
||||||
feed: async (queryText) => {
|
|
||||||
const names = await server.get<string[]>(`attribute-names/?type=relation&query=${encodeURIComponent(queryText)}`);
|
|
||||||
|
|
||||||
return names.map((name) => {
|
|
||||||
return {
|
|
||||||
id: `~${name}`,
|
|
||||||
name: name
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
minimumCharacters: 0
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const editorConfig: EditorConfig = {
|
|
||||||
toolbar: {
|
|
||||||
items: []
|
|
||||||
},
|
|
||||||
placeholder: t("attribute_editor.placeholder"),
|
|
||||||
mention: {
|
|
||||||
feeds: mentionSetup
|
|
||||||
},
|
|
||||||
licenseKey: "GPL"
|
|
||||||
};
|
|
||||||
|
|
||||||
type AttributeCommandNames = FilteredCommandNames<CommandData>;
|
|
||||||
|
|
||||||
export default class AttributeEditorWidget extends NoteContextAwareWidget implements EventListener<"entitiesReloaded">, EventListener<"addNewLabel">, EventListener<"addNewRelation"> {
|
|
||||||
private attributeDetailWidget: AttributeDetailWidget;
|
|
||||||
private $editor!: JQuery<HTMLElement>;
|
|
||||||
private $addNewAttributeButton!: JQuery<HTMLElement>;
|
|
||||||
private $saveAttributesButton!: JQuery<HTMLElement>;
|
|
||||||
private $errors!: JQuery<HTMLElement>;
|
|
||||||
|
|
||||||
private textEditor!: AttributeEditor;
|
|
||||||
private lastUpdatedNoteId!: string | undefined;
|
|
||||||
private lastSavedContent!: string;
|
|
||||||
|
|
||||||
constructor(attributeDetailWidget: AttributeDetailWidget) {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.attributeDetailWidget = attributeDetailWidget;
|
|
||||||
}
|
|
||||||
|
|
||||||
doRender() {
|
|
||||||
this.$widget = $(TPL);
|
|
||||||
this.$editor = this.$widget.find(".attribute-list-editor");
|
|
||||||
|
|
||||||
this.initialized = this.initEditor();
|
|
||||||
|
|
||||||
this.$editor.on("keydown", async (e) => {
|
|
||||||
if (e.which === 13) {
|
|
||||||
// allow autocomplete to fill the result textarea
|
|
||||||
setTimeout(() => this.save(), 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.attributeDetailWidget.hide();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.$editor.on("blur", () => setTimeout(() => this.save(), 100)); // Timeout to fix https://github.com/zadam/trilium/issues/4160
|
|
||||||
|
|
||||||
this.$addNewAttributeButton = this.$widget.find(".add-new-attribute-button");
|
|
||||||
this.$addNewAttributeButton.on("click", (e) => this.addNewAttribute(e));
|
|
||||||
|
|
||||||
this.$saveAttributesButton = this.$widget.find(".save-attributes-button");
|
|
||||||
this.$saveAttributesButton.on("click", () => this.save());
|
|
||||||
|
|
||||||
this.$errors = this.$widget.find(".attribute-errors");
|
|
||||||
}
|
|
||||||
|
|
||||||
addNewAttribute(e: JQuery.ClickEvent) {
|
|
||||||
contextMenuService.show<AttributeCommandNames>({
|
|
||||||
x: e.pageX,
|
|
||||||
y: e.pageY,
|
|
||||||
orientation: "left",
|
|
||||||
items: [
|
|
||||||
{ 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)
|
|
||||||
});
|
|
||||||
// Prevent automatic hiding of the context menu due to the button being clicked.
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
|
|
||||||
// triggered from keyboard shortcut
|
|
||||||
async addNewLabelEvent({ ntxId }: EventData<"addNewLabel">) {
|
|
||||||
if (this.isNoteContext(ntxId)) {
|
|
||||||
await this.refresh();
|
|
||||||
|
|
||||||
this.handleAddNewAttributeCommand("addNewLabel");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// triggered from keyboard shortcut
|
|
||||||
async addNewRelationEvent({ ntxId }: EventData<"addNewRelation">) {
|
|
||||||
if (this.isNoteContext(ntxId)) {
|
|
||||||
await this.refresh();
|
|
||||||
|
|
||||||
this.handleAddNewAttributeCommand("addNewRelation");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleAddNewAttributeCommand(command: AttributeCommandNames | undefined) {
|
|
||||||
// TODO: Not sure what the relation between FAttribute[] and Attribute[] is.
|
|
||||||
const attrs = this.parseAttributes() as FAttribute[];
|
|
||||||
|
|
||||||
if (!attrs) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let type: AttributeType;
|
|
||||||
let name;
|
|
||||||
let value;
|
|
||||||
|
|
||||||
if (command === "addNewLabel") {
|
|
||||||
type = "label";
|
|
||||||
name = "myLabel";
|
|
||||||
value = "";
|
|
||||||
} else if (command === "addNewRelation") {
|
|
||||||
type = "relation";
|
|
||||||
name = "myRelation";
|
|
||||||
value = "";
|
|
||||||
} else if (command === "addNewLabelDefinition") {
|
|
||||||
type = "label";
|
|
||||||
name = "label:myLabel";
|
|
||||||
value = "promoted,single,text";
|
|
||||||
} else if (command === "addNewRelationDefinition") {
|
|
||||||
type = "label";
|
|
||||||
name = "relation:myRelation";
|
|
||||||
value = "promoted,single";
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Incomplete type
|
|
||||||
//@ts-ignore
|
|
||||||
attrs.push({
|
|
||||||
type,
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
isInheritable: false
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.renderOwnedAttributes(attrs, false);
|
|
||||||
|
|
||||||
this.$editor.scrollTop(this.$editor[0].scrollHeight);
|
|
||||||
|
|
||||||
const rect = this.$editor[0].getBoundingClientRect();
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
// showing a little bit later because there's a conflict with outside click closing the attr detail
|
|
||||||
this.attributeDetailWidget.showAttributeDetail({
|
|
||||||
allAttributes: attrs,
|
|
||||||
attribute: attrs[attrs.length - 1],
|
|
||||||
isOwned: true,
|
|
||||||
x: (rect.left + rect.right) / 2,
|
|
||||||
y: rect.bottom,
|
|
||||||
focus: "name"
|
|
||||||
});
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
async save() {
|
|
||||||
if (this.lastUpdatedNoteId !== this.noteId) {
|
|
||||||
// https://github.com/zadam/trilium/issues/3090
|
|
||||||
console.warn("Ignoring blur event because a different note is loaded.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const attributes = this.parseAttributes();
|
|
||||||
|
|
||||||
if (attributes) {
|
|
||||||
await server.put(`notes/${this.noteId}/attributes`, attributes, this.componentId);
|
|
||||||
|
|
||||||
this.$saveAttributesButton.fadeOut();
|
|
||||||
|
|
||||||
// blink the attribute text to give a visual hint that save has been executed
|
|
||||||
this.$editor.css("opacity", 0);
|
|
||||||
|
|
||||||
// revert back
|
|
||||||
setTimeout(() => this.$editor.css("opacity", 1), 100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parseAttributes() {
|
|
||||||
try {
|
|
||||||
return attributeParser.lexAndParse(this.getPreprocessedData());
|
|
||||||
} catch (e: any) {
|
|
||||||
this.$errors.text(e.message).slideDown();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getPreprocessedData() {
|
|
||||||
const str = this.textEditor
|
|
||||||
.getData()
|
|
||||||
.replace(/<a[^>]+href="(#[A-Za-z0-9_/]*)"[^>]*>[^<]*<\/a>/g, "$1")
|
|
||||||
.replace(/ /g, " "); // otherwise .text() below outputs non-breaking space in unicode
|
|
||||||
|
|
||||||
return $("<div>").html(str).text();
|
|
||||||
}
|
|
||||||
|
|
||||||
async initEditor() {
|
|
||||||
this.$widget.show();
|
|
||||||
|
|
||||||
this.$editor.on("click", (e) => this.handleEditorClick(e));
|
|
||||||
|
|
||||||
this.textEditor = await AttributeEditor.create(this.$editor[0], editorConfig);
|
|
||||||
this.textEditor.model.document.on("change:data", () => this.dataChanged());
|
|
||||||
this.textEditor.editing.view.document.on(
|
|
||||||
"enter",
|
|
||||||
(event, data) => {
|
|
||||||
// disable entering new line - see https://github.com/ckeditor/ckeditor5/issues/9422
|
|
||||||
data.preventDefault();
|
|
||||||
event.stop();
|
|
||||||
},
|
|
||||||
{ priority: "high" }
|
|
||||||
);
|
|
||||||
|
|
||||||
// disable spellcheck for attribute editor
|
|
||||||
const documentRoot = this.textEditor.editing.view.document.getRoot();
|
|
||||||
if (documentRoot) {
|
|
||||||
this.textEditor.editing.view.change((writer) => writer.setAttribute("spellcheck", "false", documentRoot));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dataChanged() {
|
|
||||||
this.lastUpdatedNoteId = this.noteId;
|
|
||||||
|
|
||||||
if (this.lastSavedContent === this.textEditor.getData()) {
|
|
||||||
this.$saveAttributesButton.fadeOut();
|
|
||||||
} else {
|
|
||||||
this.$saveAttributesButton.fadeIn();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.$errors.is(":visible")) {
|
|
||||||
// using .hide() instead of .slideUp() since this will also hide the error after confirming
|
|
||||||
// mention for relation name which suits up. When using.slideUp() error will appear and the slideUp which is weird
|
|
||||||
this.$errors.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleEditorClick(e: JQuery.ClickEvent) {
|
|
||||||
const pos = this.textEditor.model.document.selection.getFirstPosition();
|
|
||||||
|
|
||||||
if (pos && pos.textNode && pos.textNode.data) {
|
|
||||||
const clickIndex = this.getClickIndex(pos);
|
|
||||||
|
|
||||||
let parsedAttrs;
|
|
||||||
|
|
||||||
try {
|
|
||||||
parsedAttrs = attributeParser.lexAndParse(this.getPreprocessedData(), true);
|
|
||||||
} catch (e) {
|
|
||||||
// the input is incorrect because the user messed up with it and now needs to fix it manually
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let matchedAttr: Attribute | null = null;
|
|
||||||
|
|
||||||
for (const attr of parsedAttrs) {
|
|
||||||
if (attr.startIndex && clickIndex > attr.startIndex && attr.endIndex && clickIndex <= attr.endIndex) {
|
|
||||||
matchedAttr = attr;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
if (matchedAttr) {
|
|
||||||
this.$editor.tooltip("hide");
|
|
||||||
|
|
||||||
this.attributeDetailWidget.showAttributeDetail({
|
|
||||||
allAttributes: parsedAttrs,
|
|
||||||
attribute: matchedAttr,
|
|
||||||
isOwned: true,
|
|
||||||
x: e.pageX,
|
|
||||||
y: e.pageY
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.showHelpTooltip();
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
} else {
|
|
||||||
this.showHelpTooltip();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showHelpTooltip() {
|
|
||||||
this.attributeDetailWidget.hide();
|
|
||||||
|
|
||||||
this.$editor.tooltip({
|
|
||||||
trigger: "focus",
|
|
||||||
html: true,
|
|
||||||
title: HELP_TEXT,
|
|
||||||
placement: "bottom",
|
|
||||||
offset: "0,30"
|
|
||||||
});
|
|
||||||
|
|
||||||
this.$editor.tooltip("show");
|
|
||||||
}
|
|
||||||
|
|
||||||
getClickIndex(pos: ModelPosition) {
|
|
||||||
let clickIndex = pos.offset - (pos.textNode?.startOffset ?? 0);
|
|
||||||
|
|
||||||
let curNode: ModelNode | Text | ModelElement | null = pos.textNode;
|
|
||||||
|
|
||||||
while (curNode?.previousSibling) {
|
|
||||||
curNode = curNode.previousSibling;
|
|
||||||
|
|
||||||
if ((curNode as ModelElement).name === "reference") {
|
|
||||||
clickIndex += (curNode.getAttribute("href") as string).length + 1;
|
|
||||||
} else if ("data" in curNode) {
|
|
||||||
clickIndex += (curNode.data as string).length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return clickIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadReferenceLinkTitle($el: JQuery<HTMLElement>, href: string) {
|
|
||||||
const { noteId } = linkService.parseNavigationStateFromUrl(href);
|
|
||||||
const note = noteId ? await froca.getNote(noteId, true) : null;
|
|
||||||
const title = note ? note.title : "[missing]";
|
|
||||||
|
|
||||||
$el.text(title);
|
|
||||||
}
|
|
||||||
|
|
||||||
async refreshWithNote(note: FNote) {
|
|
||||||
await this.renderOwnedAttributes(note.getOwnedAttributes(), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
async renderOwnedAttributes(ownedAttributes: FAttribute[], saved: boolean) {
|
|
||||||
// attrs are not resorted if position changes after the initial load
|
|
||||||
ownedAttributes.sort((a, b) => a.position - b.position);
|
|
||||||
|
|
||||||
let htmlAttrs = (await attributeRenderer.renderAttributes(ownedAttributes, true)).html();
|
|
||||||
|
|
||||||
if (htmlAttrs.length > 0) {
|
|
||||||
htmlAttrs += " ";
|
|
||||||
}
|
|
||||||
|
|
||||||
this.textEditor.setData(htmlAttrs);
|
|
||||||
|
|
||||||
if (saved) {
|
|
||||||
this.lastSavedContent = this.textEditor.getData();
|
|
||||||
|
|
||||||
this.$saveAttributesButton.fadeOut(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async createNoteForReferenceLink(title: string) {
|
|
||||||
let result;
|
|
||||||
if (this.notePath) {
|
|
||||||
result = await noteCreateService.createNoteWithTypePrompt(this.notePath, {
|
|
||||||
activate: false,
|
|
||||||
title: title
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return result?.note?.getBestNotePathString();
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateAttributeList(attributes: FAttribute[]) {
|
|
||||||
await this.renderOwnedAttributes(attributes, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
focus() {
|
|
||||||
this.$editor.trigger("focus");
|
|
||||||
|
|
||||||
this.textEditor.model.change((writer) => {
|
|
||||||
const documentRoot = this.textEditor.editing.model.document.getRoot();
|
|
||||||
if (!documentRoot) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const positionAt = writer.createPositionAt(documentRoot, "end");
|
|
||||||
writer.setSelection(positionAt);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
|
||||||
if (loadResults.getAttributeRows(this.componentId).find((attr) => attributeService.isAffecting(attr, this.note))) {
|
|
||||||
this.refresh();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
|
import { isValidElement, VNode } from "preact";
|
||||||
import Component, { TypedComponent } from "../components/component.js";
|
import Component, { TypedComponent } from "../components/component.js";
|
||||||
import froca from "../services/froca.js";
|
import froca from "../services/froca.js";
|
||||||
import { t } from "../services/i18n.js";
|
import { t } from "../services/i18n.js";
|
||||||
import toastService from "../services/toast.js";
|
import toastService from "../services/toast.js";
|
||||||
|
import { renderReactWidget } from "./react/react_utils.jsx";
|
||||||
|
import { EventNames, EventData } from "../components/app_context.js";
|
||||||
|
import { Handler } from "leaflet";
|
||||||
|
|
||||||
export class TypedBasicWidget<T extends TypedComponent<any>> extends TypedComponent<T> {
|
export class TypedBasicWidget<T extends TypedComponent<any>> extends TypedComponent<T> {
|
||||||
protected attrs: Record<string, string>;
|
protected attrs: Record<string, string>;
|
||||||
@@ -22,11 +26,14 @@ export class TypedBasicWidget<T extends TypedComponent<any>> extends TypedCompon
|
|||||||
this.childPositionCounter = 10;
|
this.childPositionCounter = 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
child(...components: T[]) {
|
child(..._components: (T | VNode)[]) {
|
||||||
if (!components) {
|
if (!_components) {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert any React components to legacy wrapped components.
|
||||||
|
const components = wrapReactWidgets(_components);
|
||||||
|
|
||||||
super.child(...components);
|
super.child(...components);
|
||||||
|
|
||||||
for (const component of components) {
|
for (const component of components) {
|
||||||
@@ -258,3 +265,30 @@ export class TypedBasicWidget<T extends TypedComponent<any>> extends TypedCompon
|
|||||||
* For information on using widgets, see the tutorial {@tutorial widget_basics}.
|
* For information on using widgets, see the tutorial {@tutorial widget_basics}.
|
||||||
*/
|
*/
|
||||||
export default class BasicWidget extends TypedBasicWidget<Component> {}
|
export default class BasicWidget extends TypedBasicWidget<Component> {}
|
||||||
|
|
||||||
|
export function wrapReactWidgets<T extends TypedComponent<any>>(components: (T | VNode)[]) {
|
||||||
|
const wrappedResult: T[] = [];
|
||||||
|
for (const component of components) {
|
||||||
|
if (isValidElement(component)) {
|
||||||
|
wrappedResult.push(new ReactWrappedWidget(component) as unknown as T);
|
||||||
|
} else {
|
||||||
|
wrappedResult.push(component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return wrappedResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ReactWrappedWidget extends BasicWidget {
|
||||||
|
|
||||||
|
private el: VNode;
|
||||||
|
|
||||||
|
constructor(el: VNode) {
|
||||||
|
super();
|
||||||
|
this.el = el;
|
||||||
|
}
|
||||||
|
|
||||||
|
doRender() {
|
||||||
|
this.$widget = renderReactWidget(this, this.el);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
import SwitchWidget from "./switch.js";
|
|
||||||
import server from "../services/server.js";
|
|
||||||
import toastService from "../services/toast.js";
|
|
||||||
import { t } from "../services/i18n.js";
|
|
||||||
import type FNote from "../entities/fnote.js";
|
|
||||||
import type { EventData } from "../components/app_context.js";
|
|
||||||
|
|
||||||
// TODO: Deduplicate
|
|
||||||
type Response = {
|
|
||||||
success: true;
|
|
||||||
} | {
|
|
||||||
success: false;
|
|
||||||
message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class BookmarkSwitchWidget extends SwitchWidget {
|
|
||||||
isEnabled() {
|
|
||||||
return (
|
|
||||||
super.isEnabled() &&
|
|
||||||
// it's not possible to bookmark root because that would clone it under bookmarks and thus create a cycle
|
|
||||||
!["root", "_hidden"].includes(this.noteId ?? "")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
doRender() {
|
|
||||||
super.doRender();
|
|
||||||
|
|
||||||
this.switchOnName = t("bookmark_switch.bookmark");
|
|
||||||
this.switchOnTooltip = t("bookmark_switch.bookmark_this_note");
|
|
||||||
|
|
||||||
this.switchOffName = t("bookmark_switch.bookmark");
|
|
||||||
this.switchOffTooltip = t("bookmark_switch.remove_bookmark");
|
|
||||||
}
|
|
||||||
|
|
||||||
async toggle(state: boolean | null | undefined) {
|
|
||||||
const resp = await server.put<Response>(`notes/${this.noteId}/toggle-in-parent/_lbBookmarks/${!!state}`);
|
|
||||||
|
|
||||||
if (!resp.success && "message" in resp) {
|
|
||||||
toastService.showError(resp.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async refreshWithNote(note: FNote) {
|
|
||||||
const isBookmarked = !!note.getParentBranches().find((b) => b.parentNoteId === "_lbBookmarks");
|
|
||||||
|
|
||||||
this.isToggled = isBookmarked;
|
|
||||||
}
|
|
||||||
|
|
||||||
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
|
||||||
if (loadResults.getBranchRows().find((b) => b.noteId === this.noteId)) {
|
|
||||||
this.refresh();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ComponentChildren } from "preact";
|
import { ComponentChildren } from "preact";
|
||||||
import { memo } from "preact/compat";
|
import { memo } from "preact/compat";
|
||||||
import AbstractBulkAction from "./abstract_bulk_action";
|
import AbstractBulkAction from "./abstract_bulk_action";
|
||||||
|
import HelpRemoveButtons from "../react/HelpRemoveButtons";
|
||||||
|
|
||||||
interface BulkActionProps {
|
interface BulkActionProps {
|
||||||
label: string | ComponentChildren;
|
label: string | ComponentChildren;
|
||||||
@@ -24,19 +25,11 @@ const BulkAction = memo(({ label, children, helpText, bulkAction }: BulkActionPr
|
|||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="button-column">
|
<HelpRemoveButtons
|
||||||
{helpText && <div className="dropdown help-dropdown">
|
help={helpText}
|
||||||
<span className="bx bx-help-circle icon-action" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
|
removeText="Delete"
|
||||||
<div className="dropdown-menu dropdown-menu-right p-4">
|
onRemove={() => bulkAction?.deleteAction()}
|
||||||
{helpText}
|
|
||||||
</div>
|
|
||||||
</div>}
|
|
||||||
|
|
||||||
<span
|
|
||||||
className="bx bx-x icon-action action-conf-del"
|
|
||||||
onClick={() => bulkAction?.deleteAction()}
|
|
||||||
/>
|
/>
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { t } from "../../../services/i18n.js";
|
import { t } from "../../../services/i18n.js";
|
||||||
import AbstractBulkAction, { ActionDefinition } from "../abstract_bulk_action.js";
|
import AbstractBulkAction from "../abstract_bulk_action.js";
|
||||||
import BulkAction, { BulkActionText } from "../BulkAction.jsx";
|
import BulkAction, { BulkActionText } from "../BulkAction.jsx";
|
||||||
import NoteAutocomplete from "../../react/NoteAutocomplete.jsx";
|
import NoteAutocomplete from "../../react/NoteAutocomplete.jsx";
|
||||||
import { useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
import { useSpacedUpdate } from "../../react/hooks.jsx";
|
import { useSpacedUpdate } from "../../react/hooks.jsx";
|
||||||
|
|
||||||
function MoveNoteBulkActionComponent({ bulkAction, actionDef }: { bulkAction: AbstractBulkAction, actionDef: ActionDefinition }) {
|
function MoveNoteBulkActionComponent({ bulkAction }: { bulkAction: AbstractBulkAction }) {
|
||||||
const [ targetParentNoteId, setTargetParentNoteId ] = useState<string>();
|
const [ targetParentNoteId, setTargetParentNoteId ] = useState<string>();
|
||||||
const spacedUpdate = useSpacedUpdate(() => {
|
const spacedUpdate = useSpacedUpdate(() => {
|
||||||
return bulkAction.saveAction({ targetParentNoteId: targetParentNoteId })
|
return bulkAction.saveAction({ targetParentNoteId: targetParentNoteId })
|
||||||
@@ -45,6 +45,6 @@ export default class MoveNoteBulkAction extends AbstractBulkAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
doRender() {
|
doRender() {
|
||||||
return <MoveNoteBulkActionComponent bulkAction={this} actionDef={this.actionDef} />
|
return <MoveNoteBulkActionComponent bulkAction={this} />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import SpacedUpdate from "../../../services/spaced_update.js";
|
|
||||||
import AbstractBulkAction, { ActionDefinition } from "../abstract_bulk_action.js";
|
import AbstractBulkAction, { ActionDefinition } from "../abstract_bulk_action.js";
|
||||||
import { t } from "../../../services/i18n.js";
|
import { t } from "../../../services/i18n.js";
|
||||||
import BulkAction from "../BulkAction.jsx";
|
import BulkAction from "../BulkAction.jsx";
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import SpacedUpdate from "../../../services/spaced_update.js";
|
|
||||||
import AbstractBulkAction, { ActionDefinition } from "../abstract_bulk_action.js";
|
import AbstractBulkAction, { ActionDefinition } from "../abstract_bulk_action.js";
|
||||||
import noteAutocompleteService from "../../../services/note_autocomplete.js";
|
|
||||||
import { t } from "../../../services/i18n.js";
|
import { t } from "../../../services/i18n.js";
|
||||||
import BulkAction, { BulkActionText } from "../BulkAction.jsx";
|
import BulkAction, { BulkActionText } from "../BulkAction.jsx";
|
||||||
import NoteAutocomplete from "../../react/NoteAutocomplete.jsx";
|
import NoteAutocomplete from "../../react/NoteAutocomplete.jsx";
|
||||||
|
|||||||
@@ -1,252 +0,0 @@
|
|||||||
import NoteContextAwareWidget from "../note_context_aware_widget.js";
|
|
||||||
import utils from "../../services/utils.js";
|
|
||||||
import branchService from "../../services/branches.js";
|
|
||||||
import dialogService from "../../services/dialog.js";
|
|
||||||
import server from "../../services/server.js";
|
|
||||||
import toastService from "../../services/toast.js";
|
|
||||||
import ws from "../../services/ws.js";
|
|
||||||
import appContext, { type EventData } from "../../components/app_context.js";
|
|
||||||
import { t } from "../../services/i18n.js";
|
|
||||||
import type FNote from "../../entities/fnote.js";
|
|
||||||
import type { FAttachmentRow } from "../../entities/fattachment.js";
|
|
||||||
|
|
||||||
// TODO: Deduplicate with server
|
|
||||||
interface ConvertToAttachmentResponse {
|
|
||||||
attachment: FAttachmentRow;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TPL = /*html*/`
|
|
||||||
<div class="dropdown note-actions">
|
|
||||||
<style>
|
|
||||||
.note-actions {
|
|
||||||
width: 35px;
|
|
||||||
height: 35px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-actions .dropdown-menu {
|
|
||||||
min-width: 15em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-actions .dropdown-item .bx {
|
|
||||||
position: relative;
|
|
||||||
top: 3px;
|
|
||||||
font-size: 120%;
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-actions .dropdown-item[disabled], .note-actions .dropdown-item[disabled]:hover {
|
|
||||||
color: var(--muted-text-color) !important;
|
|
||||||
background-color: transparent !important;
|
|
||||||
pointer-events: none; /* makes it unclickable */
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<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">
|
|
||||||
<li data-trigger-command="convertNoteIntoAttachment" class="dropdown-item">
|
|
||||||
<span class="bx bx-paperclip"></span> ${t("note_actions.convert_into_attachment")}
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li data-trigger-command="renderActiveNote" class="dropdown-item render-note-button">
|
|
||||||
<span class="bx bx-extension"></span> ${t("note_actions.re_render_note")}<kbd data-command="renderActiveNote"></kbd>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li data-trigger-command="findInText" class="dropdown-item find-in-text-button">
|
|
||||||
<span class='bx bx-search'></span> ${t("note_actions.search_in_note")}<kbd data-command="findInText"></kbd>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li data-trigger-command="printActiveNote" class="dropdown-item print-active-note-button">
|
|
||||||
<span class="bx bx-printer"></span> ${t("note_actions.print_note")}<kbd data-command="printActiveNote"></kbd>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li data-trigger-command="exportAsPdf" class="dropdown-item export-as-pdf-button">
|
|
||||||
<span class="bx bxs-file-pdf"></span> ${t("note_actions.print_pdf")}<kbd data-command="exportAsPdf"></kbd>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<div class="dropdown-divider"></div>
|
|
||||||
|
|
||||||
|
|
||||||
<li class="dropdown-item import-files-button"><span class="bx bx-import"></span> ${t("note_actions.import_files")}</li>
|
|
||||||
|
|
||||||
<li class="dropdown-item export-note-button"><span class="bx bx-export"></span> ${t("note_actions.export_note")}</li>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="dropdown-divider"></div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<li data-trigger-command="openNoteExternally" class="dropdown-item open-note-externally-button" title="${t("note_actions.open_note_externally_title")}">
|
|
||||||
<span class="bx bx-file-find"></span> ${t("note_actions.open_note_externally")}<kbd data-command="openNoteExternally"></kbd>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li data-trigger-command="openNoteCustom" class="dropdown-item open-note-custom-button">
|
|
||||||
<span class="bx bx-customize"></span> ${t("note_actions.open_note_custom")}<kbd data-command="openNoteCustom"></kbd>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li data-trigger-command="showNoteSource" class="dropdown-item show-source-button">
|
|
||||||
<span class="bx bx-code"></span> ${t("note_actions.note_source")}<kbd data-command="showNoteSource"></kbd>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="dropdown-divider"></div>
|
|
||||||
|
|
||||||
|
|
||||||
<li data-trigger-command="forceSaveRevision" class="dropdown-item save-revision-button">
|
|
||||||
<span class="bx bx-save"></span> ${t("note_actions.save_revision")}<kbd data-command="forceSaveRevision"></kbd>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dropdown-item delete-note-button"><span class="bx bx-trash destructive-action-icon"></span> ${t("note_actions.delete_note")}</li>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="dropdown-divider"></div>
|
|
||||||
|
|
||||||
|
|
||||||
<li data-trigger-command="showAttachments" class="dropdown-item show-attachments-button">
|
|
||||||
<span class="bx bx-paperclip"></span> ${t("note_actions.note_attachments")}<kbd data-command="showAttachments"></kbd>
|
|
||||||
</li>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
export default class NoteActionsWidget extends NoteContextAwareWidget {
|
|
||||||
|
|
||||||
private $convertNoteIntoAttachmentButton!: JQuery<HTMLElement>;
|
|
||||||
private $findInTextButton!: JQuery<HTMLElement>;
|
|
||||||
private $printActiveNoteButton!: JQuery<HTMLElement>;
|
|
||||||
private $exportAsPdfButton!: JQuery<HTMLElement>;
|
|
||||||
private $showSourceButton!: JQuery<HTMLElement>;
|
|
||||||
private $showAttachmentsButton!: JQuery<HTMLElement>;
|
|
||||||
private $renderNoteButton!: JQuery<HTMLElement>;
|
|
||||||
private $saveRevisionButton!: JQuery<HTMLElement>;
|
|
||||||
private $exportNoteButton!: JQuery<HTMLElement>;
|
|
||||||
private $importNoteButton!: JQuery<HTMLElement>;
|
|
||||||
private $openNoteExternallyButton!: JQuery<HTMLElement>;
|
|
||||||
private $openNoteCustomButton!: JQuery<HTMLElement>;
|
|
||||||
private $deleteNoteButton!: JQuery<HTMLElement>;
|
|
||||||
|
|
||||||
isEnabled() {
|
|
||||||
return this.note?.type !== "launcher";
|
|
||||||
}
|
|
||||||
|
|
||||||
doRender() {
|
|
||||||
this.$widget = $(TPL);
|
|
||||||
this.$widget.on("show.bs.dropdown", () => {
|
|
||||||
if (this.note) {
|
|
||||||
this.refreshVisibility(this.note);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.$convertNoteIntoAttachmentButton = this.$widget.find("[data-trigger-command='convertNoteIntoAttachment']");
|
|
||||||
this.$findInTextButton = this.$widget.find(".find-in-text-button");
|
|
||||||
this.$printActiveNoteButton = this.$widget.find(".print-active-note-button");
|
|
||||||
this.$exportAsPdfButton = this.$widget.find(".export-as-pdf-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", () => {
|
|
||||||
if (this.$exportNoteButton.hasClass("disabled") || !this.noteContext?.notePath) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.triggerCommand("showExportDialog", {
|
|
||||||
notePath: this.noteContext.notePath,
|
|
||||||
defaultType: "single"
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.$importNoteButton = this.$widget.find(".import-files-button");
|
|
||||||
this.$importNoteButton.on("click", () => {
|
|
||||||
if (this.noteId) {
|
|
||||||
this.triggerCommand("showImportDialog", { noteId: this.noteId });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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");
|
|
||||||
|
|
||||||
this.$deleteNoteButton = this.$widget.find(".delete-note-button");
|
|
||||||
this.$deleteNoteButton.on("click", () => {
|
|
||||||
if (!this.note || this.note.noteId === "root") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
branchService.deleteNotes([this.note.getParentBranches()[0].branchId], true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async refreshVisibility(note: FNote) {
|
|
||||||
const isInOptions = note.noteId.startsWith("_options");
|
|
||||||
|
|
||||||
this.$convertNoteIntoAttachmentButton.toggle(note.isEligibleForConversionToAttachment());
|
|
||||||
|
|
||||||
this.toggleDisabled(this.$findInTextButton, ["text", "code", "book", "mindMap", "doc"].includes(note.type));
|
|
||||||
|
|
||||||
this.toggleDisabled(this.$showAttachmentsButton, !isInOptions);
|
|
||||||
this.toggleDisabled(this.$showSourceButton, ["text", "code", "relationMap", "mermaid", "canvas", "mindMap"].includes(note.type));
|
|
||||||
|
|
||||||
const canPrint = ["text", "code"].includes(note.type);
|
|
||||||
this.toggleDisabled(this.$printActiveNoteButton, canPrint);
|
|
||||||
this.toggleDisabled(this.$exportAsPdfButton, canPrint);
|
|
||||||
this.$exportAsPdfButton.toggleClass("hidden-ext", !utils.isElectron());
|
|
||||||
|
|
||||||
this.$renderNoteButton.toggle(note.type === "render");
|
|
||||||
|
|
||||||
this.toggleDisabled(this.$openNoteExternallyButton, utils.isElectron() && !["search", "book"].includes(note.type));
|
|
||||||
this.toggleDisabled(
|
|
||||||
this.$openNoteCustomButton,
|
|
||||||
utils.isElectron() &&
|
|
||||||
!utils.isMac() && // no implementation for Mac yet
|
|
||||||
!["search", "book"].includes(note.type)
|
|
||||||
);
|
|
||||||
|
|
||||||
// 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) && !isInOptions);
|
|
||||||
|
|
||||||
this.toggleDisabled(this.$importNoteButton, !["search"].includes(note.type) && !isInOptions);
|
|
||||||
this.toggleDisabled(this.$deleteNoteButton, !isInOptions);
|
|
||||||
this.toggleDisabled(this.$saveRevisionButton, !isInOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
async convertNoteIntoAttachmentCommand() {
|
|
||||||
if (!this.note || !(await dialogService.confirm(t("note_actions.convert_into_attachment_prompt", { title: this.note.title })))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { attachment: newAttachment } = await server.post<ConvertToAttachmentResponse>(`notes/${this.noteId}/convert-to-attachment`);
|
|
||||||
|
|
||||||
if (!newAttachment) {
|
|
||||||
toastService.showMessage(t("note_actions.convert_into_attachment_failed", { title: this.note.title }));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
toastService.showMessage(t("note_actions.convert_into_attachment_successful", { title: newAttachment.title }));
|
|
||||||
await ws.waitForMaxKnownEntityChangeId();
|
|
||||||
await appContext.tabManager.getActiveContext()?.setNote(newAttachment.ownerId, {
|
|
||||||
viewScope: {
|
|
||||||
viewMode: "attachments",
|
|
||||||
attachmentId: newAttachment.attachmentId
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleDisabled($el: JQuery<HTMLElement>, enable: boolean) {
|
|
||||||
if (enable) {
|
|
||||||
$el.removeAttr("disabled");
|
|
||||||
} else {
|
|
||||||
$el.attr("disabled", "disabled");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
|
||||||
if (loadResults.isNoteReloaded(this.noteId)) {
|
|
||||||
this.refresh();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { t } from "../../services/i18n.js";
|
|
||||||
import CommandButtonWidget from "./command_button.js";
|
|
||||||
|
|
||||||
export default class RevisionsButton extends CommandButtonWidget {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.icon("bx-history").title(t("revisions_button.note_revisions")).command("showRevisions").titlePlacement("bottom").class("icon-action");
|
|
||||||
}
|
|
||||||
|
|
||||||
isEnabled() {
|
|
||||||
return super.isEnabled() && !["launcher", "doc"].includes(this.note?.type ?? "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,388 +0,0 @@
|
|||||||
import NoteContextAwareWidget from "../note_context_aware_widget.js";
|
|
||||||
import keyboardActionsService from "../../services/keyboard_actions.js";
|
|
||||||
import attributeService from "../../services/attributes.js";
|
|
||||||
import type CommandButtonWidget from "../buttons/command_button.js";
|
|
||||||
import type FNote from "../../entities/fnote.js";
|
|
||||||
import type { NoteType } from "../../entities/fnote.js";
|
|
||||||
import type { EventData, EventNames } from "../../components/app_context.js";
|
|
||||||
import type NoteActionsWidget from "../buttons/note_actions.js";
|
|
||||||
|
|
||||||
const TPL = /*html*/`
|
|
||||||
<div class="ribbon-container">
|
|
||||||
<style>
|
|
||||||
.ribbon-container {
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ribbon-top-row {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ribbon-tab-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: center;
|
|
||||||
margin-left: 10px;
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-flow: row wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ribbon-tab-title {
|
|
||||||
color: var(--muted-text-color);
|
|
||||||
border-bottom: 1px solid var(--main-border-color);
|
|
||||||
min-width: 24px;
|
|
||||||
flex-basis: 24px;
|
|
||||||
max-width: max-content;
|
|
||||||
flex-grow: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ribbon-tab-title .bx {
|
|
||||||
font-size: 150%;
|
|
||||||
position: relative;
|
|
||||||
top: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ribbon-tab-title.active {
|
|
||||||
color: var(--main-text-color);
|
|
||||||
border-bottom: 3px solid var(--main-text-color);
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ribbon-tab-title:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ribbon-tab-title:hover {
|
|
||||||
color: var(--main-text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ribbon-tab-title:first-of-type {
|
|
||||||
padding-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ribbon-tab-spacer {
|
|
||||||
flex-basis: 0;
|
|
||||||
min-width: 0;
|
|
||||||
max-width: 35px;
|
|
||||||
flex-grow: 1;
|
|
||||||
border-bottom: 1px solid var(--main-border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ribbon-tab-spacer:last-of-type {
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-basis: 0;
|
|
||||||
min-width: 0;
|
|
||||||
max-width: 10000px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ribbon-button-container {
|
|
||||||
display: flex;
|
|
||||||
border-bottom: 1px solid var(--main-border-color);
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ribbon-button-container > * {
|
|
||||||
position: relative;
|
|
||||||
top: -3px;
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ribbon-body {
|
|
||||||
display: none;
|
|
||||||
border-bottom: 1px solid var(--main-border-color);
|
|
||||||
margin-left: 10px;
|
|
||||||
margin-right: 5px; /* needs to have this value so that the bottom border is the same width as the top one */
|
|
||||||
}
|
|
||||||
|
|
||||||
.ribbon-body.active {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ribbon-tab-title-label {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ribbon-tab-title.active .ribbon-tab-title-label {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div class="ribbon-top-row">
|
|
||||||
<div class="ribbon-tab-container"></div>
|
|
||||||
<div class="ribbon-button-container"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="ribbon-body-container"></div>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
type ButtonWidget = (CommandButtonWidget | NoteActionsWidget);
|
|
||||||
|
|
||||||
export default class RibbonContainer extends NoteContextAwareWidget {
|
|
||||||
|
|
||||||
private lastActiveComponentId?: string | null;
|
|
||||||
private lastNoteType?: NoteType;
|
|
||||||
|
|
||||||
private ribbonWidgets: NoteContextAwareWidget[];
|
|
||||||
private buttonWidgets: ButtonWidget[];
|
|
||||||
private $tabContainer!: JQuery<HTMLElement>;
|
|
||||||
private $buttonContainer!: JQuery<HTMLElement>;
|
|
||||||
private $bodyContainer!: JQuery<HTMLElement>;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.contentSized();
|
|
||||||
this.ribbonWidgets = [];
|
|
||||||
this.buttonWidgets = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
isEnabled() {
|
|
||||||
return super.isEnabled() && this.noteContext?.viewScope?.viewMode === "default";
|
|
||||||
}
|
|
||||||
|
|
||||||
ribbon(widget: NoteContextAwareWidget) {
|
|
||||||
// TODO: Base class
|
|
||||||
super.child(widget);
|
|
||||||
|
|
||||||
this.ribbonWidgets.push(widget);
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
button(widget: ButtonWidget) {
|
|
||||||
super.child(widget);
|
|
||||||
|
|
||||||
this.buttonWidgets.push(widget);
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
doRender() {
|
|
||||||
this.$widget = $(TPL);
|
|
||||||
|
|
||||||
this.$tabContainer = this.$widget.find(".ribbon-tab-container");
|
|
||||||
this.$buttonContainer = this.$widget.find(".ribbon-button-container");
|
|
||||||
this.$bodyContainer = this.$widget.find(".ribbon-body-container");
|
|
||||||
|
|
||||||
for (const ribbonWidget of this.ribbonWidgets) {
|
|
||||||
this.$bodyContainer.append($('<div class="ribbon-body">').attr("data-ribbon-component-id", ribbonWidget.componentId).append(ribbonWidget.render()));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const buttonWidget of this.buttonWidgets) {
|
|
||||||
this.$buttonContainer.append(buttonWidget.render());
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$tabContainer.on("click", ".ribbon-tab-title", (e) => {
|
|
||||||
const $ribbonTitle = $(e.target).closest(".ribbon-tab-title");
|
|
||||||
|
|
||||||
this.toggleRibbonTab($ribbonTitle);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleRibbonTab($ribbonTitle: JQuery<HTMLElement>, refreshActiveTab = true) {
|
|
||||||
const activate = !$ribbonTitle.hasClass("active");
|
|
||||||
|
|
||||||
this.$tabContainer.find(".ribbon-tab-title").removeClass("active");
|
|
||||||
this.$bodyContainer.find(".ribbon-body").removeClass("active");
|
|
||||||
|
|
||||||
if (activate) {
|
|
||||||
const ribbonComponendId = $ribbonTitle.attr("data-ribbon-component-id");
|
|
||||||
|
|
||||||
const wasAlreadyActive = this.lastActiveComponentId === ribbonComponendId;
|
|
||||||
|
|
||||||
this.lastActiveComponentId = ribbonComponendId;
|
|
||||||
|
|
||||||
this.$tabContainer.find(`.ribbon-tab-title[data-ribbon-component-id="${ribbonComponendId}"]`).addClass("active");
|
|
||||||
this.$bodyContainer.find(`.ribbon-body[data-ribbon-component-id="${ribbonComponendId}"]`).addClass("active");
|
|
||||||
|
|
||||||
const activeChild = this.getActiveRibbonWidget();
|
|
||||||
|
|
||||||
if (activeChild && (refreshActiveTab || !wasAlreadyActive) && this.noteContext && this.notePath) {
|
|
||||||
const handleEventPromise = activeChild.handleEvent("noteSwitched", { noteContext: this.noteContext, notePath: this.notePath });
|
|
||||||
|
|
||||||
if (refreshActiveTab) {
|
|
||||||
if (handleEventPromise) {
|
|
||||||
handleEventPromise.then(() => (activeChild as any).focus?.()); // TODO: Base class
|
|
||||||
} else {
|
|
||||||
// TODO: Base class
|
|
||||||
(activeChild as any).focus?.();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.lastActiveComponentId = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async noteSwitched() {
|
|
||||||
this.lastActiveComponentId = null;
|
|
||||||
|
|
||||||
await super.noteSwitched();
|
|
||||||
}
|
|
||||||
|
|
||||||
async refreshWithNote(note: FNote, noExplicitActivation = false) {
|
|
||||||
this.lastNoteType = note.type;
|
|
||||||
|
|
||||||
let $ribbonTabToActivate, $lastActiveRibbon;
|
|
||||||
|
|
||||||
this.$tabContainer.empty();
|
|
||||||
|
|
||||||
for (const ribbonWidget of this.ribbonWidgets) {
|
|
||||||
// TODO: Base class for ribbon widget
|
|
||||||
const ret = await (ribbonWidget as any).getTitle(note);
|
|
||||||
|
|
||||||
if (!ret.show) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const $ribbonTitle = $('<div class="ribbon-tab-title">')
|
|
||||||
.attr("data-ribbon-component-id", ribbonWidget.componentId)
|
|
||||||
.attr("data-ribbon-component-name", (ribbonWidget as any).name as string) // TODO: base class for ribbon widgets
|
|
||||||
.append(
|
|
||||||
$('<span class="ribbon-tab-title-icon">')
|
|
||||||
.addClass(ret.icon)
|
|
||||||
.attr("title", ret.title)
|
|
||||||
.attr("data-toggle-command", (ribbonWidget as any).toggleCommand)
|
|
||||||
) // TODO: base class
|
|
||||||
.append(" ")
|
|
||||||
.append($('<span class="ribbon-tab-title-label">').text(ret.title));
|
|
||||||
|
|
||||||
this.$tabContainer.append($ribbonTitle);
|
|
||||||
this.$tabContainer.append('<div class="ribbon-tab-spacer">');
|
|
||||||
|
|
||||||
if (ret.activate && !this.lastActiveComponentId && !$ribbonTabToActivate && !noExplicitActivation) {
|
|
||||||
$ribbonTabToActivate = $ribbonTitle;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.lastActiveComponentId === ribbonWidget.componentId) {
|
|
||||||
$lastActiveRibbon = $ribbonTitle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
keyboardActionsService.getActions().then((actions) => {
|
|
||||||
this.$tabContainer.find(".ribbon-tab-title-icon").tooltip({
|
|
||||||
title: () => {
|
|
||||||
const toggleCommandName = $(this).attr("data-toggle-command");
|
|
||||||
const action = actions.find((act) => act.actionName === toggleCommandName);
|
|
||||||
const title = $(this).attr("data-title");
|
|
||||||
|
|
||||||
if (action?.effectiveShortcuts && action.effectiveShortcuts.length > 0) {
|
|
||||||
return `${title} (${action.effectiveShortcuts.join(", ")})`;
|
|
||||||
} else {
|
|
||||||
return title ?? "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!$ribbonTabToActivate) {
|
|
||||||
$ribbonTabToActivate = $lastActiveRibbon;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($ribbonTabToActivate) {
|
|
||||||
this.toggleRibbonTab($ribbonTabToActivate, false);
|
|
||||||
} else {
|
|
||||||
this.$bodyContainer.find(".ribbon-body").removeClass("active");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isRibbonTabActive(name: string) {
|
|
||||||
const $ribbonComponent = this.$widget.find(`.ribbon-tab-title[data-ribbon-component-name='${name}']`);
|
|
||||||
|
|
||||||
return $ribbonComponent.hasClass("active");
|
|
||||||
}
|
|
||||||
|
|
||||||
ensureOwnedAttributesAreOpen(ntxId: string | null | undefined) {
|
|
||||||
if (ntxId && this.isNoteContext(ntxId) && !this.isRibbonTabActive("ownedAttributes")) {
|
|
||||||
this.toggleRibbonTabWithName("ownedAttributes", ntxId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addNewLabelEvent({ ntxId }: EventData<"addNewLabel">) {
|
|
||||||
this.ensureOwnedAttributesAreOpen(ntxId);
|
|
||||||
}
|
|
||||||
|
|
||||||
addNewRelationEvent({ ntxId }: EventData<"addNewRelation">) {
|
|
||||||
this.ensureOwnedAttributesAreOpen(ntxId);
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleRibbonTabWithName(name: string, ntxId?: string) {
|
|
||||||
if (!this.isNoteContext(ntxId)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const $ribbonComponent = this.$widget.find(`.ribbon-tab-title[data-ribbon-component-name='${name}']`);
|
|
||||||
|
|
||||||
if ($ribbonComponent) {
|
|
||||||
this.toggleRibbonTab($ribbonComponent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleEvent<T extends EventNames>(name: T, data: EventData<T>) {
|
|
||||||
const PREFIX = "toggleRibbonTab";
|
|
||||||
|
|
||||||
if (name.startsWith(PREFIX)) {
|
|
||||||
let componentName = name.substr(PREFIX.length);
|
|
||||||
componentName = componentName[0].toLowerCase() + componentName.substr(1);
|
|
||||||
|
|
||||||
this.toggleRibbonTabWithName(componentName, (data as any).ntxId);
|
|
||||||
} else {
|
|
||||||
return super.handleEvent(name, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleEventInChildren<T extends EventNames>(name: T, data: EventData<T>) {
|
|
||||||
if (["activeContextChanged", "setNoteContext"].includes(name)) {
|
|
||||||
// won't trigger .refresh();
|
|
||||||
await super.handleEventInChildren("setNoteContext", data as EventData<"activeContextChanged" | "setNoteContext">);
|
|
||||||
} else if (this.isEnabled() || name === "initialRenderComplete") {
|
|
||||||
const activeRibbonWidget = this.getActiveRibbonWidget();
|
|
||||||
|
|
||||||
// forward events only to active ribbon tab, inactive ones don't need to be updated
|
|
||||||
if (activeRibbonWidget) {
|
|
||||||
await activeRibbonWidget.handleEvent(name, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const buttonWidget of this.buttonWidgets) {
|
|
||||||
await buttonWidget.handleEvent(name, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
|
||||||
if (!this.note) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.noteId && loadResults.isNoteReloaded(this.noteId) && this.lastNoteType !== this.note.type) {
|
|
||||||
// note type influences the list of available ribbon tabs the most
|
|
||||||
// check for the type is so that we don't update on each title rename
|
|
||||||
this.lastNoteType = this.note.type;
|
|
||||||
|
|
||||||
this.refresh();
|
|
||||||
} else if (loadResults.getAttributeRows(this.componentId).find((attr) => attributeService.isAffecting(attr, this.note))) {
|
|
||||||
this.refreshWithNote(this.note, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async noteTypeMimeChangedEvent() {
|
|
||||||
// We are ignoring the event which triggers a refresh since it is usually already done by a different
|
|
||||||
// event and causing a race condition in which the items appear twice.
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Executed as soon as the user presses the "Edit" floating button in a read-only text note.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* We need to refresh the ribbon for cases such as the classic editor which relies on the read-only state.
|
|
||||||
*/
|
|
||||||
readOnlyTemporarilyDisabledEvent() {
|
|
||||||
this.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
getActiveRibbonWidget() {
|
|
||||||
return this.ribbonWidgets.find((ch) => ch.componentId === this.lastActiveComponentId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import ReactBasicWidget from "../react/ReactBasicWidget.js";
|
|
||||||
import Modal from "../react/Modal.js";
|
import Modal from "../react/Modal.js";
|
||||||
import { t } from "../../services/i18n.js";
|
import { t } from "../../services/i18n.js";
|
||||||
import { formatDateTime } from "../../utils/formatters.js";
|
import { formatDateTime } from "../../utils/formatters.js";
|
||||||
@@ -8,11 +7,11 @@ import openService from "../../services/open.js";
|
|||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
import type { CSSProperties } from "preact/compat";
|
import type { CSSProperties } from "preact/compat";
|
||||||
import type { AppInfo } from "@triliumnext/commons";
|
import type { AppInfo } from "@triliumnext/commons";
|
||||||
import useTriliumEvent from "../react/hooks.jsx";
|
import { useTriliumEvent } from "../react/hooks.jsx";
|
||||||
|
|
||||||
function AboutDialogComponent() {
|
export default function AboutDialog() {
|
||||||
let [appInfo, setAppInfo] = useState<AppInfo | null>(null);
|
const [appInfo, setAppInfo] = useState<AppInfo | null>(null);
|
||||||
let [shown, setShown] = useState(false);
|
const [shown, setShown] = useState(false);
|
||||||
const forceWordBreak: CSSProperties = { wordBreak: "break-all" };
|
const forceWordBreak: CSSProperties = { wordBreak: "break-all" };
|
||||||
|
|
||||||
useTriliumEvent("openAboutDialog", () => setShown(true));
|
useTriliumEvent("openAboutDialog", () => setShown(true));
|
||||||
@@ -82,11 +81,3 @@ function DirectoryLink({ directory, style }: { directory: string, style?: CSSPro
|
|||||||
return <span style={style}>{directory}</span>;
|
return <span style={style}>{directory}</span>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class AboutDialog extends ReactBasicWidget {
|
|
||||||
|
|
||||||
get component() {
|
|
||||||
return <AboutDialogComponent />;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { t } from "../../services/i18n";
|
import { t } from "../../services/i18n";
|
||||||
import Modal from "../react/Modal";
|
import Modal from "../react/Modal";
|
||||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
|
||||||
import Button from "../react/Button";
|
import Button from "../react/Button";
|
||||||
import FormRadioGroup from "../react/FormRadioGroup";
|
import FormRadioGroup from "../react/FormRadioGroup";
|
||||||
import NoteAutocomplete from "../react/NoteAutocomplete";
|
import NoteAutocomplete from "../react/NoteAutocomplete";
|
||||||
@@ -11,11 +10,11 @@ import { default as TextTypeWidget } from "../type_widgets/editable_text.js";
|
|||||||
import { logError } from "../../services/ws";
|
import { logError } from "../../services/ws";
|
||||||
import FormGroup from "../react/FormGroup.js";
|
import FormGroup from "../react/FormGroup.js";
|
||||||
import { refToJQuerySelector } from "../react/react_utils";
|
import { refToJQuerySelector } from "../react/react_utils";
|
||||||
import useTriliumEvent from "../react/hooks";
|
import { useTriliumEvent } from "../react/hooks";
|
||||||
|
|
||||||
type LinkType = "reference-link" | "external-link" | "hyper-link";
|
type LinkType = "reference-link" | "external-link" | "hyper-link";
|
||||||
|
|
||||||
function AddLinkDialogComponent() {
|
export default function AddLinkDialog() {
|
||||||
const [ textTypeWidget, setTextTypeWidget ] = useState<TextTypeWidget>();
|
const [ textTypeWidget, setTextTypeWidget ] = useState<TextTypeWidget>();
|
||||||
const initialText = useRef<string>();
|
const initialText = useRef<string>();
|
||||||
const [ linkTitle, setLinkTitle ] = useState("");
|
const [ linkTitle, setLinkTitle ] = useState("");
|
||||||
@@ -160,11 +159,3 @@ function AddLinkDialogComponent() {
|
|||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class AddLinkDialog extends ReactBasicWidget {
|
|
||||||
|
|
||||||
get component() {
|
|
||||||
return <AddLinkDialogComponent />;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,15 +4,14 @@ import { t } from "../../services/i18n.js";
|
|||||||
import server from "../../services/server.js";
|
import server from "../../services/server.js";
|
||||||
import toast from "../../services/toast.js";
|
import toast from "../../services/toast.js";
|
||||||
import Modal from "../react/Modal.jsx";
|
import Modal from "../react/Modal.jsx";
|
||||||
import ReactBasicWidget from "../react/ReactBasicWidget.js";
|
|
||||||
import froca from "../../services/froca.js";
|
import froca from "../../services/froca.js";
|
||||||
import tree from "../../services/tree.js";
|
import tree from "../../services/tree.js";
|
||||||
import Button from "../react/Button.jsx";
|
import Button from "../react/Button.jsx";
|
||||||
import FormGroup from "../react/FormGroup.js";
|
import FormGroup from "../react/FormGroup.js";
|
||||||
import useTriliumEvent from "../react/hooks.jsx";
|
import { useTriliumEvent } from "../react/hooks.jsx";
|
||||||
import FBranch from "../../entities/fbranch.js";
|
import FBranch from "../../entities/fbranch.js";
|
||||||
|
|
||||||
function BranchPrefixDialogComponent() {
|
export default function BranchPrefixDialog() {
|
||||||
const [ shown, setShown ] = useState(false);
|
const [ shown, setShown ] = useState(false);
|
||||||
const [ branch, setBranch ] = useState<FBranch>();
|
const [ branch, setBranch ] = useState<FBranch>();
|
||||||
const [ prefix, setPrefix ] = useState(branch?.prefix ?? "");
|
const [ prefix, setPrefix ] = useState(branch?.prefix ?? "");
|
||||||
@@ -75,14 +74,6 @@ function BranchPrefixDialogComponent() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class BranchPrefixDialog extends ReactBasicWidget {
|
|
||||||
|
|
||||||
get component() {
|
|
||||||
return <BranchPrefixDialogComponent />;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
async function savePrefix(branchId: string, prefix: string) {
|
async function savePrefix(branchId: string, prefix: string) {
|
||||||
await server.put(`branches/${branchId}/set-prefix`, { prefix: prefix });
|
await server.put(`branches/${branchId}/set-prefix`, { prefix: prefix });
|
||||||
toast.showMessage(t("branch_prefix.branch_prefix_saved"));
|
toast.showMessage(t("branch_prefix.branch_prefix_saved"));
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useEffect, useState, useCallback } from "preact/hooks";
|
import { useEffect, useState, useCallback } from "preact/hooks";
|
||||||
import { t } from "../../services/i18n";
|
import { t } from "../../services/i18n";
|
||||||
import Modal from "../react/Modal";
|
import Modal from "../react/Modal";
|
||||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
|
||||||
import "./bulk_actions.css";
|
import "./bulk_actions.css";
|
||||||
import { BulkActionAffectedNotes } from "@triliumnext/commons";
|
import { BulkActionAffectedNotes } from "@triliumnext/commons";
|
||||||
import server from "../../services/server";
|
import server from "../../services/server";
|
||||||
@@ -12,9 +11,9 @@ import toast from "../../services/toast";
|
|||||||
import RenameNoteBulkAction from "../bulk_actions/note/rename_note";
|
import RenameNoteBulkAction from "../bulk_actions/note/rename_note";
|
||||||
import FNote from "../../entities/fnote";
|
import FNote from "../../entities/fnote";
|
||||||
import froca from "../../services/froca";
|
import froca from "../../services/froca";
|
||||||
import useTriliumEvent from "../react/hooks";
|
import { useTriliumEvent } from "../react/hooks";
|
||||||
|
|
||||||
function BulkActionComponent() {
|
export default function BulkActionsDialog() {
|
||||||
const [ selectedOrActiveNoteIds, setSelectedOrActiveNoteIds ] = useState<string[]>();
|
const [ selectedOrActiveNoteIds, setSelectedOrActiveNoteIds ] = useState<string[]>();
|
||||||
const [ bulkActionNote, setBulkActionNote ] = useState<FNote | null>();
|
const [ bulkActionNote, setBulkActionNote ] = useState<FNote | null>();
|
||||||
const [ includeDescendants, setIncludeDescendants ] = useState(false);
|
const [ includeDescendants, setIncludeDescendants ] = useState(false);
|
||||||
@@ -51,7 +50,7 @@ function BulkActionComponent() {
|
|||||||
row.type === "label" && row.name === "action" && row.noteId === "_bulkAction")) {
|
row.type === "label" && row.name === "action" && row.noteId === "_bulkAction")) {
|
||||||
refreshExistingActions();
|
refreshExistingActions();
|
||||||
}
|
}
|
||||||
}, shown);
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -117,11 +116,3 @@ function ExistingActionsList({ existingActions }: { existingActions?: RenameNote
|
|||||||
</table>
|
</table>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class BulkActionsDialog extends ReactBasicWidget {
|
|
||||||
|
|
||||||
get component() {
|
|
||||||
return <BulkActionComponent />
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,11 @@
|
|||||||
import { useState } from "preact/hooks";
|
import { useMemo, useState } from "preact/hooks";
|
||||||
import Button from "../react/Button";
|
import Button from "../react/Button";
|
||||||
import Modal from "../react/Modal";
|
import Modal from "../react/Modal";
|
||||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
import { dismissCallToAction, getCallToActions } from "./call_to_action_definitions";
|
||||||
import { CallToAction, dismissCallToAction, getCallToActions } from "./call_to_action_definitions";
|
|
||||||
import { t } from "../../services/i18n";
|
import { t } from "../../services/i18n";
|
||||||
|
|
||||||
function CallToActionDialogComponent({ activeCallToActions }: { activeCallToActions: CallToAction[] }) {
|
export default function CallToActionDialog() {
|
||||||
if (!activeCallToActions.length) {
|
const activeCallToActions = useMemo(() => getCallToActions(), []);
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [ activeIndex, setActiveIndex ] = useState(0);
|
const [ activeIndex, setActiveIndex ] = useState(0);
|
||||||
const [ shown, setShown ] = useState(true);
|
const [ shown, setShown ] = useState(true);
|
||||||
const activeItem = activeCallToActions[activeIndex];
|
const activeItem = activeCallToActions[activeIndex];
|
||||||
@@ -22,7 +18,7 @@ function CallToActionDialogComponent({ activeCallToActions }: { activeCallToActi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (activeCallToActions.length &&
|
||||||
<Modal
|
<Modal
|
||||||
className="call-to-action"
|
className="call-to-action"
|
||||||
size="md"
|
size="md"
|
||||||
@@ -48,11 +44,3 @@ function CallToActionDialogComponent({ activeCallToActions }: { activeCallToActi
|
|||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CallToActionDialog extends ReactBasicWidget {
|
|
||||||
|
|
||||||
get component() {
|
|
||||||
return <CallToActionDialogComponent activeCallToActions={getCallToActions()} />
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@ import { useRef, useState } from "preact/hooks";
|
|||||||
import appContext from "../../components/app_context";
|
import appContext from "../../components/app_context";
|
||||||
import { t } from "../../services/i18n";
|
import { t } from "../../services/i18n";
|
||||||
import Modal from "../react/Modal";
|
import Modal from "../react/Modal";
|
||||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
|
||||||
import NoteAutocomplete from "../react/NoteAutocomplete";
|
import NoteAutocomplete from "../react/NoteAutocomplete";
|
||||||
import froca from "../../services/froca";
|
import froca from "../../services/froca";
|
||||||
import FormGroup from "../react/FormGroup";
|
import FormGroup from "../react/FormGroup";
|
||||||
@@ -14,9 +13,9 @@ import tree from "../../services/tree";
|
|||||||
import branches from "../../services/branches";
|
import branches from "../../services/branches";
|
||||||
import toast from "../../services/toast";
|
import toast from "../../services/toast";
|
||||||
import NoteList from "../react/NoteList";
|
import NoteList from "../react/NoteList";
|
||||||
import useTriliumEvent from "../react/hooks";
|
import { useTriliumEvent } from "../react/hooks";
|
||||||
|
|
||||||
function CloneToDialogComponent() {
|
export default function CloneToDialog() {
|
||||||
const [ clonedNoteIds, setClonedNoteIds ] = useState<string[]>();
|
const [ clonedNoteIds, setClonedNoteIds ] = useState<string[]>();
|
||||||
const [ prefix, setPrefix ] = useState("");
|
const [ prefix, setPrefix ] = useState("");
|
||||||
const [ suggestion, setSuggestion ] = useState<Suggestion | null>(null);
|
const [ suggestion, setSuggestion ] = useState<Suggestion | null>(null);
|
||||||
@@ -83,14 +82,6 @@ function CloneToDialogComponent() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class CloneToDialog extends ReactBasicWidget {
|
|
||||||
|
|
||||||
get component() {
|
|
||||||
return <CloneToDialogComponent />;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
async function cloneNotesTo(notePath: string, clonedNoteIds: string[], prefix?: string) {
|
async function cloneNotesTo(notePath: string, clonedNoteIds: string[], prefix?: string) {
|
||||||
const { noteId, parentNoteId } = tree.getNoteIdAndParentIdFromUrl(notePath);
|
const { noteId, parentNoteId } = tree.getNoteIdAndParentIdFromUrl(notePath);
|
||||||
if (!noteId || !parentNoteId) {
|
if (!noteId || !parentNoteId) {
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
|
||||||
import Modal from "../react/Modal";
|
import Modal from "../react/Modal";
|
||||||
import Button from "../react/Button";
|
import Button from "../react/Button";
|
||||||
import { t } from "../../services/i18n";
|
import { t } from "../../services/i18n";
|
||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
import FormCheckbox from "../react/FormCheckbox";
|
import FormCheckbox from "../react/FormCheckbox";
|
||||||
import useTriliumEvent from "../react/hooks";
|
import { useTriliumEvent } from "../react/hooks";
|
||||||
|
|
||||||
interface ConfirmDialogProps {
|
interface ConfirmDialogProps {
|
||||||
title?: string;
|
title?: string;
|
||||||
@@ -13,7 +12,7 @@ interface ConfirmDialogProps {
|
|||||||
isConfirmDeleteNoteBox?: boolean;
|
isConfirmDeleteNoteBox?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ConfirmDialogComponent() {
|
export default function ConfirmDialog() {
|
||||||
const [ opts, setOpts ] = useState<ConfirmDialogProps>();
|
const [ opts, setOpts ] = useState<ConfirmDialogProps>();
|
||||||
const [ isDeleteNoteChecked, setIsDeleteNoteChecked ] = useState(false);
|
const [ isDeleteNoteChecked, setIsDeleteNoteChecked ] = useState(false);
|
||||||
const [ shown, setShown ] = useState(false);
|
const [ shown, setShown ] = useState(false);
|
||||||
@@ -92,11 +91,3 @@ export interface ConfirmWithTitleOptions {
|
|||||||
title: string;
|
title: string;
|
||||||
callback: ConfirmDialogCallback;
|
callback: ConfirmDialogCallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class ConfirmDialog extends ReactBasicWidget {
|
|
||||||
|
|
||||||
get component() {
|
|
||||||
return <ConfirmDialogComponent />;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@ import { useRef, useState, useEffect } from "preact/hooks";
|
|||||||
import { t } from "../../services/i18n.js";
|
import { t } from "../../services/i18n.js";
|
||||||
import FormCheckbox from "../react/FormCheckbox.js";
|
import FormCheckbox from "../react/FormCheckbox.js";
|
||||||
import Modal from "../react/Modal.js";
|
import Modal from "../react/Modal.js";
|
||||||
import ReactBasicWidget from "../react/ReactBasicWidget.js";
|
|
||||||
import type { DeleteNotesPreview } from "@triliumnext/commons";
|
import type { DeleteNotesPreview } from "@triliumnext/commons";
|
||||||
import server from "../../services/server.js";
|
import server from "../../services/server.js";
|
||||||
import froca from "../../services/froca.js";
|
import froca from "../../services/froca.js";
|
||||||
@@ -10,7 +9,7 @@ import FNote from "../../entities/fnote.js";
|
|||||||
import link from "../../services/link.js";
|
import link from "../../services/link.js";
|
||||||
import Button from "../react/Button.jsx";
|
import Button from "../react/Button.jsx";
|
||||||
import Alert from "../react/Alert.jsx";
|
import Alert from "../react/Alert.jsx";
|
||||||
import useTriliumEvent from "../react/hooks.jsx";
|
import { useTriliumEvent } from "../react/hooks.jsx";
|
||||||
|
|
||||||
export interface ResolveOptions {
|
export interface ResolveOptions {
|
||||||
proceed: boolean;
|
proceed: boolean;
|
||||||
@@ -30,7 +29,7 @@ interface BrokenRelationData {
|
|||||||
source: string;
|
source: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DeleteNotesDialogComponent() {
|
export default function DeleteNotesDialog() {
|
||||||
const [ opts, setOpts ] = useState<ShowDeleteNotesDialogOpts>({});
|
const [ opts, setOpts ] = useState<ShowDeleteNotesDialogOpts>({});
|
||||||
const [ deleteAllClones, setDeleteAllClones ] = useState(false);
|
const [ deleteAllClones, setDeleteAllClones ] = useState(false);
|
||||||
const [ eraseNotes, setEraseNotes ] = useState(!!opts.forceDeleteAllClones);
|
const [ eraseNotes, setEraseNotes ] = useState(!!opts.forceDeleteAllClones);
|
||||||
@@ -140,7 +139,7 @@ function BrokenRelations({ brokenRelations }: { brokenRelations: DeleteNotesPrev
|
|||||||
const noteIds = brokenRelations
|
const noteIds = brokenRelations
|
||||||
.map(relation => relation.noteId)
|
.map(relation => relation.noteId)
|
||||||
.filter(noteId => noteId) as string[];
|
.filter(noteId => noteId) as string[];
|
||||||
froca.getNotes(noteIds).then(async (notes) => {
|
froca.getNotes(noteIds).then(async () => {
|
||||||
const notesWithBrokenRelations: BrokenRelationData[] = [];
|
const notesWithBrokenRelations: BrokenRelationData[] = [];
|
||||||
for (const attr of brokenRelations) {
|
for (const attr of brokenRelations) {
|
||||||
notesWithBrokenRelations.push({
|
notesWithBrokenRelations.push({
|
||||||
@@ -171,11 +170,3 @@ function BrokenRelations({ brokenRelations }: { brokenRelations: DeleteNotesPrev
|
|||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class DeleteNotesDialog extends ReactBasicWidget {
|
|
||||||
|
|
||||||
get component() {
|
|
||||||
return <DeleteNotesDialogComponent />;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -4,14 +4,13 @@ import tree from "../../services/tree";
|
|||||||
import Button from "../react/Button";
|
import Button from "../react/Button";
|
||||||
import FormRadioGroup from "../react/FormRadioGroup";
|
import FormRadioGroup from "../react/FormRadioGroup";
|
||||||
import Modal from "../react/Modal";
|
import Modal from "../react/Modal";
|
||||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
|
||||||
import "./export.css";
|
import "./export.css";
|
||||||
import ws from "../../services/ws";
|
import ws from "../../services/ws";
|
||||||
import toastService, { ToastOptions } from "../../services/toast";
|
import toastService, { ToastOptions } from "../../services/toast";
|
||||||
import utils from "../../services/utils";
|
import utils from "../../services/utils";
|
||||||
import open from "../../services/open";
|
import open from "../../services/open";
|
||||||
import froca from "../../services/froca";
|
import froca from "../../services/froca";
|
||||||
import useTriliumEvent from "../react/hooks";
|
import { useTriliumEvent } from "../react/hooks";
|
||||||
|
|
||||||
interface ExportDialogProps {
|
interface ExportDialogProps {
|
||||||
branchId?: string | null;
|
branchId?: string | null;
|
||||||
@@ -19,7 +18,7 @@ interface ExportDialogProps {
|
|||||||
defaultType?: "subtree" | "single";
|
defaultType?: "subtree" | "single";
|
||||||
}
|
}
|
||||||
|
|
||||||
function ExportDialogComponent() {
|
export default function ExportDialog() {
|
||||||
const [ opts, setOpts ] = useState<ExportDialogProps>();
|
const [ opts, setOpts ] = useState<ExportDialogProps>();
|
||||||
const [ exportType, setExportType ] = useState<string>(opts?.defaultType ?? "subtree");
|
const [ exportType, setExportType ] = useState<string>(opts?.defaultType ?? "subtree");
|
||||||
const [ subtreeFormat, setSubtreeFormat ] = useState("html");
|
const [ subtreeFormat, setSubtreeFormat ] = useState("html");
|
||||||
@@ -125,14 +124,6 @@ function ExportDialogComponent() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class ExportDialog extends ReactBasicWidget {
|
|
||||||
|
|
||||||
get component() {
|
|
||||||
return <ExportDialogComponent />
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function exportBranch(branchId: string, type: string, format: string, version: string) {
|
function exportBranch(branchId: string, type: string, format: string, version: string) {
|
||||||
const taskId = utils.randomString(10);
|
const taskId = utils.randomString(10);
|
||||||
const url = open.getUrlForDownload(`api/branches/${branchId}/export/${type}/${format}/${version}/${taskId}`);
|
const url = open.getUrlForDownload(`api/branches/${branchId}/export/${type}/${format}/${version}/${taskId}`);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import ReactBasicWidget from "../react/ReactBasicWidget.js";
|
|
||||||
import Modal from "../react/Modal.jsx";
|
import Modal from "../react/Modal.jsx";
|
||||||
import { t } from "../../services/i18n.js";
|
import { t } from "../../services/i18n.js";
|
||||||
import { ComponentChildren } from "preact";
|
import { ComponentChildren } from "preact";
|
||||||
@@ -6,9 +5,9 @@ import { CommandNames } from "../../components/app_context.js";
|
|||||||
import RawHtml from "../react/RawHtml.jsx";
|
import RawHtml from "../react/RawHtml.jsx";
|
||||||
import { useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
import keyboard_actions from "../../services/keyboard_actions.js";
|
import keyboard_actions from "../../services/keyboard_actions.js";
|
||||||
import useTriliumEvent from "../react/hooks.jsx";
|
import { useTriliumEvent } from "../react/hooks.jsx";
|
||||||
|
|
||||||
function HelpDialogComponent() {
|
export default function HelpDialog() {
|
||||||
const [ shown, setShown ] = useState(false);
|
const [ shown, setShown ] = useState(false);
|
||||||
useTriliumEvent("showCheatsheet", () => setShown(true));
|
useTriliumEvent("showCheatsheet", () => setShown(true));
|
||||||
|
|
||||||
@@ -161,11 +160,3 @@ function Card({ title, children }: { title: string, children: ComponentChildren
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class HelpDialog extends ReactBasicWidget {
|
|
||||||
|
|
||||||
get component() {
|
|
||||||
return <HelpDialogComponent />;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,11 +7,10 @@ import FormFileUpload from "../react/FormFileUpload";
|
|||||||
import FormGroup, { FormMultiGroup } from "../react/FormGroup";
|
import FormGroup, { FormMultiGroup } from "../react/FormGroup";
|
||||||
import Modal from "../react/Modal";
|
import Modal from "../react/Modal";
|
||||||
import RawHtml from "../react/RawHtml";
|
import RawHtml from "../react/RawHtml";
|
||||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
|
||||||
import importService, { UploadFilesOptions } from "../../services/import";
|
import importService, { UploadFilesOptions } from "../../services/import";
|
||||||
import useTriliumEvent from "../react/hooks";
|
import { useTriliumEvent } from "../react/hooks";
|
||||||
|
|
||||||
function ImportDialogComponent() {
|
export default function ImportDialog() {
|
||||||
const [ parentNoteId, setParentNoteId ] = useState<string>();
|
const [ parentNoteId, setParentNoteId ] = useState<string>();
|
||||||
const [ noteTitle, setNoteTitle ] = useState<string>();
|
const [ noteTitle, setNoteTitle ] = useState<string>();
|
||||||
const [ files, setFiles ] = useState<FileList | null>(null);
|
const [ files, setFiles ] = useState<FileList | null>(null);
|
||||||
@@ -89,14 +88,6 @@ function ImportDialogComponent() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class ImportDialog extends ReactBasicWidget {
|
|
||||||
|
|
||||||
get component() {
|
|
||||||
return <ImportDialogComponent />
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function boolToString(value: boolean) {
|
function boolToString(value: boolean) {
|
||||||
return value ? "true" : "false";
|
return value ? "true" : "false";
|
||||||
}
|
}
|
||||||
@@ -4,15 +4,14 @@ import FormGroup from "../react/FormGroup";
|
|||||||
import FormRadioGroup from "../react/FormRadioGroup";
|
import FormRadioGroup from "../react/FormRadioGroup";
|
||||||
import Modal from "../react/Modal";
|
import Modal from "../react/Modal";
|
||||||
import NoteAutocomplete from "../react/NoteAutocomplete";
|
import NoteAutocomplete from "../react/NoteAutocomplete";
|
||||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
|
||||||
import Button from "../react/Button";
|
import Button from "../react/Button";
|
||||||
import { Suggestion, triggerRecentNotes } from "../../services/note_autocomplete";
|
import { Suggestion, triggerRecentNotes } from "../../services/note_autocomplete";
|
||||||
import tree from "../../services/tree";
|
import tree from "../../services/tree";
|
||||||
import froca from "../../services/froca";
|
import froca from "../../services/froca";
|
||||||
import EditableTextTypeWidget from "../type_widgets/editable_text";
|
import EditableTextTypeWidget from "../type_widgets/editable_text";
|
||||||
import useTriliumEvent from "../react/hooks";
|
import { useTriliumEvent } from "../react/hooks";
|
||||||
|
|
||||||
function IncludeNoteDialogComponent() {
|
export default function IncludeNoteDialog() {
|
||||||
const [textTypeWidget, setTextTypeWidget] = useState<EditableTextTypeWidget>();
|
const [textTypeWidget, setTextTypeWidget] = useState<EditableTextTypeWidget>();
|
||||||
const [suggestion, setSuggestion] = useState<Suggestion | null>(null);
|
const [suggestion, setSuggestion] = useState<Suggestion | null>(null);
|
||||||
const [boxSize, setBoxSize] = useState("medium");
|
const [boxSize, setBoxSize] = useState("medium");
|
||||||
@@ -70,14 +69,6 @@ function IncludeNoteDialogComponent() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class IncludeNoteDialog extends ReactBasicWidget {
|
|
||||||
|
|
||||||
get component() {
|
|
||||||
return <IncludeNoteDialogComponent />;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
async function includeNote(notePath: string, textTypeWidget: EditableTextTypeWidget) {
|
async function includeNote(notePath: string, textTypeWidget: EditableTextTypeWidget) {
|
||||||
const noteId = tree.getNoteIdFromUrl(notePath);
|
const noteId = tree.getNoteIdFromUrl(notePath);
|
||||||
if (!noteId) {
|
if (!noteId) {
|
||||||
|
|||||||
@@ -3,11 +3,10 @@ import { t } from "../../services/i18n.js";
|
|||||||
import utils from "../../services/utils.js";
|
import utils from "../../services/utils.js";
|
||||||
import Button from "../react/Button.js";
|
import Button from "../react/Button.js";
|
||||||
import Modal from "../react/Modal.js";
|
import Modal from "../react/Modal.js";
|
||||||
import ReactBasicWidget from "../react/ReactBasicWidget.js";
|
|
||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
import useTriliumEvent from "../react/hooks.jsx";
|
import { useTriliumEvent } from "../react/hooks.jsx";
|
||||||
|
|
||||||
function IncorrectCpuArchDialogComponent() {
|
export default function IncorrectCpuArchDialogComponent() {
|
||||||
const [ shown, setShown ] = useState(false);
|
const [ shown, setShown ] = useState(false);
|
||||||
const downloadButtonRef = useRef<HTMLButtonElement>(null);
|
const downloadButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
useTriliumEvent("showCpuArchWarning", () => setShown(true));
|
useTriliumEvent("showCpuArchWarning", () => setShown(true));
|
||||||
@@ -44,11 +43,3 @@ function IncorrectCpuArchDialogComponent() {
|
|||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class IncorrectCpuArchDialog extends ReactBasicWidget {
|
|
||||||
|
|
||||||
get component() {
|
|
||||||
return <IncorrectCpuArchDialogComponent />
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { EventData } from "../../components/app_context";
|
import { EventData } from "../../components/app_context";
|
||||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
|
||||||
import Modal from "../react/Modal";
|
import Modal from "../react/Modal";
|
||||||
import { t } from "../../services/i18n";
|
import { t } from "../../services/i18n";
|
||||||
import Button from "../react/Button";
|
import Button from "../react/Button";
|
||||||
import { useRef, useState } from "preact/hooks";
|
import { useRef, useState } from "preact/hooks";
|
||||||
import { RawHtmlBlock } from "../react/RawHtml";
|
import { RawHtmlBlock } from "../react/RawHtml";
|
||||||
import useTriliumEvent from "../react/hooks";
|
import { useTriliumEvent } from "../react/hooks";
|
||||||
|
|
||||||
function ShowInfoDialogComponent() {
|
export default function InfoDialog() {
|
||||||
const [ opts, setOpts ] = useState<EventData<"showInfoDialog">>();
|
const [ opts, setOpts ] = useState<EventData<"showInfoDialog">>();
|
||||||
const [ shown, setShown ] = useState(false);
|
const [ shown, setShown ] = useState(false);
|
||||||
const okButtonRef = useRef<HTMLButtonElement>(null);
|
const okButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
@@ -37,11 +36,3 @@ function ShowInfoDialogComponent() {
|
|||||||
<RawHtmlBlock className="info-dialog-content" html={opts?.message ?? ""} />
|
<RawHtmlBlock className="info-dialog-content" html={opts?.message ?? ""} />
|
||||||
</Modal>);
|
</Modal>);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class InfoDialog extends ReactBasicWidget {
|
|
||||||
|
|
||||||
get component() {
|
|
||||||
return <ShowInfoDialogComponent />;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
|
||||||
import Modal from "../react/Modal";
|
import Modal from "../react/Modal";
|
||||||
import Button from "../react/Button";
|
import Button from "../react/Button";
|
||||||
import NoteAutocomplete from "../react/NoteAutocomplete";
|
import NoteAutocomplete from "../react/NoteAutocomplete";
|
||||||
@@ -8,14 +7,14 @@ import note_autocomplete, { Suggestion } from "../../services/note_autocomplete"
|
|||||||
import appContext from "../../components/app_context";
|
import appContext from "../../components/app_context";
|
||||||
import commandRegistry from "../../services/command_registry";
|
import commandRegistry from "../../services/command_registry";
|
||||||
import { refToJQuerySelector } from "../react/react_utils";
|
import { refToJQuerySelector } from "../react/react_utils";
|
||||||
import useTriliumEvent from "../react/hooks";
|
import { useTriliumEvent } from "../react/hooks";
|
||||||
import shortcutService from "../../services/shortcuts";
|
import shortcutService from "../../services/shortcuts";
|
||||||
|
|
||||||
const KEEP_LAST_SEARCH_FOR_X_SECONDS = 120;
|
const KEEP_LAST_SEARCH_FOR_X_SECONDS = 120;
|
||||||
|
|
||||||
type Mode = "last-search" | "recent-notes" | "commands";
|
type Mode = "last-search" | "recent-notes" | "commands";
|
||||||
|
|
||||||
function JumpToNoteDialogComponent() {
|
export default function JumpToNoteDialogComponent() {
|
||||||
const [ mode, setMode ] = useState<Mode>();
|
const [ mode, setMode ] = useState<Mode>();
|
||||||
const [ lastOpenedTs, setLastOpenedTs ] = useState<number>(0);
|
const [ lastOpenedTs, setLastOpenedTs ] = useState<number>(0);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -27,12 +26,12 @@ function JumpToNoteDialogComponent() {
|
|||||||
|
|
||||||
async function openDialog(commandMode: boolean) {
|
async function openDialog(commandMode: boolean) {
|
||||||
let newMode: Mode;
|
let newMode: Mode;
|
||||||
let initialText: string = "";
|
let initialText = "";
|
||||||
|
|
||||||
if (commandMode) {
|
if (commandMode) {
|
||||||
newMode = "commands";
|
newMode = "commands";
|
||||||
initialText = ">";
|
initialText = ">";
|
||||||
} else if (Date.now() - lastOpenedTs <= KEEP_LAST_SEARCH_FOR_X_SECONDS * 1000 && actualText) {
|
} else if (Date.now() - lastOpenedTs <= KEEP_LAST_SEARCH_FOR_X_SECONDS * 1000 && actualText.current) {
|
||||||
// if you open the Jump To dialog soon after using it previously, it can often mean that you
|
// if you open the Jump To dialog soon after using it previously, it can often mean that you
|
||||||
// actually want to search for the same thing (e.g., you opened the wrong note at first try)
|
// actually want to search for the same thing (e.g., you opened the wrong note at first try)
|
||||||
// so we'll keep the content.
|
// so we'll keep the content.
|
||||||
@@ -142,11 +141,3 @@ function JumpToNoteDialogComponent() {
|
|||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class JumpToNoteDialog extends ReactBasicWidget {
|
|
||||||
|
|
||||||
get component() {
|
|
||||||
return <JumpToNoteDialogComponent />;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -5,18 +5,17 @@ import server from "../../services/server";
|
|||||||
import toast from "../../services/toast";
|
import toast from "../../services/toast";
|
||||||
import utils from "../../services/utils";
|
import utils from "../../services/utils";
|
||||||
import Modal from "../react/Modal";
|
import Modal from "../react/Modal";
|
||||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
|
||||||
import Button from "../react/Button";
|
import Button from "../react/Button";
|
||||||
import useTriliumEvent from "../react/hooks";
|
import { useTriliumEvent } from "../react/hooks";
|
||||||
|
|
||||||
interface RenderMarkdownResponse {
|
interface RenderMarkdownResponse {
|
||||||
htmlContent: string;
|
htmlContent: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function MarkdownImportDialogComponent() {
|
export default function MarkdownImportDialog() {
|
||||||
const markdownImportTextArea = useRef<HTMLTextAreaElement>(null);
|
const markdownImportTextArea = useRef<HTMLTextAreaElement>(null);
|
||||||
let [ text, setText ] = useState("");
|
const [ text, setText ] = useState("");
|
||||||
let [ shown, setShown ] = useState(false);
|
const [ shown, setShown ] = useState(false);
|
||||||
|
|
||||||
const triggerImport = useCallback(() => {
|
const triggerImport = useCallback(() => {
|
||||||
if (appContext.tabManager.getActiveContextNoteType() !== "text") {
|
if (appContext.tabManager.getActiveContextNoteType() !== "text") {
|
||||||
@@ -64,14 +63,6 @@ function MarkdownImportDialogComponent() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class MarkdownImportDialog extends ReactBasicWidget {
|
|
||||||
|
|
||||||
get component() {
|
|
||||||
return <MarkdownImportDialogComponent />;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
async function convertMarkdownToHtml(markdownContent: string) {
|
async function convertMarkdownToHtml(markdownContent: string) {
|
||||||
const { htmlContent } = await server.post<RenderMarkdownResponse>("other/render-markdown", { markdownContent });
|
const { htmlContent } = await server.post<RenderMarkdownResponse>("other/render-markdown", { markdownContent });
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
|
||||||
import Modal from "../react/Modal";
|
import Modal from "../react/Modal";
|
||||||
import { t } from "../../services/i18n";
|
import { t } from "../../services/i18n";
|
||||||
import NoteList from "../react/NoteList";
|
import NoteList from "../react/NoteList";
|
||||||
@@ -11,9 +10,9 @@ import tree from "../../services/tree";
|
|||||||
import froca from "../../services/froca";
|
import froca from "../../services/froca";
|
||||||
import branches from "../../services/branches";
|
import branches from "../../services/branches";
|
||||||
import toast from "../../services/toast";
|
import toast from "../../services/toast";
|
||||||
import useTriliumEvent from "../react/hooks";
|
import { useTriliumEvent } from "../react/hooks";
|
||||||
|
|
||||||
function MoveToDialogComponent() {
|
export default function MoveToDialog() {
|
||||||
const [ movedBranchIds, setMovedBranchIds ] = useState<string[]>();
|
const [ movedBranchIds, setMovedBranchIds ] = useState<string[]>();
|
||||||
const [ suggestion, setSuggestion ] = useState<Suggestion | null>(null);
|
const [ suggestion, setSuggestion ] = useState<Suggestion | null>(null);
|
||||||
const [ shown, setShown ] = useState(false);
|
const [ shown, setShown ] = useState(false);
|
||||||
@@ -67,14 +66,6 @@ function MoveToDialogComponent() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class MoveToDialog extends ReactBasicWidget {
|
|
||||||
|
|
||||||
get component() {
|
|
||||||
return <MoveToDialogComponent />;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
async function moveNotesTo(movedBranchIds: string[] | undefined, parentBranchId: string) {
|
async function moveNotesTo(movedBranchIds: string[] | undefined, parentBranchId: string) {
|
||||||
if (movedBranchIds) {
|
if (movedBranchIds) {
|
||||||
await branches.moveToParentNote(movedBranchIds, parentBranchId);
|
await branches.moveToParentNote(movedBranchIds, parentBranchId);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
|
||||||
import Modal from "../react/Modal";
|
import Modal from "../react/Modal";
|
||||||
import { t } from "../../services/i18n";
|
import { t } from "../../services/i18n";
|
||||||
import FormGroup from "../react/FormGroup";
|
import FormGroup from "../react/FormGroup";
|
||||||
@@ -10,7 +9,7 @@ import { MenuCommandItem, MenuItem } from "../../menus/context_menu";
|
|||||||
import { TreeCommandNames } from "../../menus/tree_context_menu";
|
import { TreeCommandNames } from "../../menus/tree_context_menu";
|
||||||
import { Suggestion } from "../../services/note_autocomplete";
|
import { Suggestion } from "../../services/note_autocomplete";
|
||||||
import Badge from "../react/Badge";
|
import Badge from "../react/Badge";
|
||||||
import useTriliumEvent from "../react/hooks";
|
import { useTriliumEvent } from "../react/hooks";
|
||||||
|
|
||||||
export interface ChooseNoteTypeResponse {
|
export interface ChooseNoteTypeResponse {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@@ -26,7 +25,7 @@ const SEPARATOR_TITLE_REPLACEMENTS = [
|
|||||||
t("note_type_chooser.templates")
|
t("note_type_chooser.templates")
|
||||||
];
|
];
|
||||||
|
|
||||||
function NoteTypeChooserDialogComponent() {
|
export default function NoteTypeChooserDialogComponent() {
|
||||||
const [ callback, setCallback ] = useState<ChooseNoteTypeCallback>();
|
const [ callback, setCallback ] = useState<ChooseNoteTypeCallback>();
|
||||||
const [ shown, setShown ] = useState(false);
|
const [ shown, setShown ] = useState(false);
|
||||||
const [ parentNote, setParentNote ] = useState<Suggestion | null>();
|
const [ parentNote, setParentNote ] = useState<Suggestion | null>();
|
||||||
@@ -37,12 +36,11 @@ function NoteTypeChooserDialogComponent() {
|
|||||||
setShown(true);
|
setShown(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!noteTypes.length) {
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
note_types.getNoteTypeItems().then(noteTypes => {
|
note_types.getNoteTypeItems().then(noteTypes => {
|
||||||
let index = -1;
|
let index = -1;
|
||||||
|
|
||||||
setNoteTypes((noteTypes ?? []).map((item, _index) => {
|
setNoteTypes((noteTypes ?? []).map((item) => {
|
||||||
if (item.title === "----") {
|
if (item.title === "----") {
|
||||||
index++;
|
index++;
|
||||||
return {
|
return {
|
||||||
@@ -54,8 +52,7 @@ function NoteTypeChooserDialogComponent() {
|
|||||||
return item;
|
return item;
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
});
|
}, []);
|
||||||
}
|
|
||||||
|
|
||||||
function onNoteTypeSelected(value: string) {
|
function onNoteTypeSelected(value: string) {
|
||||||
const [ noteType, templateNoteId ] = value.split(",");
|
const [ noteType, templateNoteId ] = value.split(",");
|
||||||
@@ -120,11 +117,3 @@ function NoteTypeChooserDialogComponent() {
|
|||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class NoteTypeChooserDialog extends ReactBasicWidget {
|
|
||||||
|
|
||||||
get component() {
|
|
||||||
return <NoteTypeChooserDialogComponent />
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
|
||||||
import Modal from "../react/Modal";
|
import Modal from "../react/Modal";
|
||||||
import { t } from "../../services/i18n";
|
import { t } from "../../services/i18n";
|
||||||
import Button from "../react/Button";
|
import Button from "../react/Button";
|
||||||
import appContext from "../../components/app_context";
|
import appContext from "../../components/app_context";
|
||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
import useTriliumEvent from "../react/hooks";
|
import { useTriliumEvent } from "../react/hooks";
|
||||||
|
|
||||||
function PasswordNotSetDialogComponent() {
|
export default function PasswordNotSetDialog() {
|
||||||
const [ shown, setShown ] = useState(false);
|
const [ shown, setShown ] = useState(false);
|
||||||
useTriliumEvent("showPasswordNotSet", () => setShown(true));
|
useTriliumEvent("showPasswordNotSet", () => setShown(true));
|
||||||
|
|
||||||
@@ -27,10 +26,3 @@ function PasswordNotSetDialogComponent() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class PasswordNotSetDialog extends ReactBasicWidget {
|
|
||||||
|
|
||||||
get component() {
|
|
||||||
return <PasswordNotSetDialogComponent />;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,12 +2,10 @@ import { useRef, useState } from "preact/hooks";
|
|||||||
import { t } from "../../services/i18n";
|
import { t } from "../../services/i18n";
|
||||||
import Button from "../react/Button";
|
import Button from "../react/Button";
|
||||||
import Modal from "../react/Modal";
|
import Modal from "../react/Modal";
|
||||||
import { Modal as BootstrapModal } from "bootstrap";
|
|
||||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
|
||||||
import FormTextBox from "../react/FormTextBox";
|
import FormTextBox from "../react/FormTextBox";
|
||||||
import FormGroup from "../react/FormGroup";
|
import FormGroup from "../react/FormGroup";
|
||||||
import { refToJQuerySelector } from "../react/react_utils";
|
import { refToJQuerySelector } from "../react/react_utils";
|
||||||
import useTriliumEvent from "../react/hooks";
|
import { useTriliumEvent } from "../react/hooks";
|
||||||
|
|
||||||
// JQuery here is maintained for compatibility with existing code.
|
// JQuery here is maintained for compatibility with existing code.
|
||||||
interface ShownCallbackData {
|
interface ShownCallbackData {
|
||||||
@@ -28,7 +26,7 @@ export interface PromptDialogOptions {
|
|||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function PromptDialogComponent() {
|
export default function PromptDialog() {
|
||||||
const modalRef = useRef<HTMLDivElement>(null);
|
const modalRef = useRef<HTMLDivElement>(null);
|
||||||
const formRef = useRef<HTMLFormElement>(null);
|
const formRef = useRef<HTMLFormElement>(null);
|
||||||
const labelRef = useRef<HTMLLabelElement>(null);
|
const labelRef = useRef<HTMLLabelElement>(null);
|
||||||
@@ -84,11 +82,3 @@ function PromptDialogComponent() {
|
|||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class PromptDialog extends ReactBasicWidget {
|
|
||||||
|
|
||||||
get component() {
|
|
||||||
return <PromptDialogComponent />;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,11 +3,10 @@ import { t } from "../../services/i18n";
|
|||||||
import Button from "../react/Button";
|
import Button from "../react/Button";
|
||||||
import FormTextBox from "../react/FormTextBox";
|
import FormTextBox from "../react/FormTextBox";
|
||||||
import Modal from "../react/Modal";
|
import Modal from "../react/Modal";
|
||||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
|
||||||
import protected_session from "../../services/protected_session";
|
import protected_session from "../../services/protected_session";
|
||||||
import useTriliumEvent from "../react/hooks";
|
import { useTriliumEvent } from "../react/hooks";
|
||||||
|
|
||||||
function ProtectedSessionPasswordDialogComponent() {
|
export default function ProtectedSessionPasswordDialog() {
|
||||||
const [ shown, setShown ] = useState(false);
|
const [ shown, setShown ] = useState(false);
|
||||||
const [ password, setPassword ] = useState("");
|
const [ password, setPassword ] = useState("");
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
@@ -38,11 +37,3 @@ function ProtectedSessionPasswordDialogComponent() {
|
|||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class ProtectedSessionPasswordDialog extends ReactBasicWidget {
|
|
||||||
|
|
||||||
get component() {
|
|
||||||
return <ProtectedSessionPasswordDialogComponent />;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,6 @@ import server from "../../services/server";
|
|||||||
import toast from "../../services/toast";
|
import toast from "../../services/toast";
|
||||||
import Button from "../react/Button";
|
import Button from "../react/Button";
|
||||||
import Modal from "../react/Modal";
|
import Modal from "../react/Modal";
|
||||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
|
||||||
import hoisted_note from "../../services/hoisted_note";
|
import hoisted_note from "../../services/hoisted_note";
|
||||||
import type { RecentChangeRow } from "@triliumnext/commons";
|
import type { RecentChangeRow } from "@triliumnext/commons";
|
||||||
import froca from "../../services/froca";
|
import froca from "../../services/froca";
|
||||||
@@ -14,26 +13,20 @@ import { formatDateTime } from "../../utils/formatters";
|
|||||||
import link from "../../services/link";
|
import link from "../../services/link";
|
||||||
import RawHtml from "../react/RawHtml";
|
import RawHtml from "../react/RawHtml";
|
||||||
import ws from "../../services/ws";
|
import ws from "../../services/ws";
|
||||||
import useTriliumEvent from "../react/hooks";
|
import { useTriliumEvent } from "../react/hooks";
|
||||||
|
|
||||||
function RecentChangesDialogComponent() {
|
export default function RecentChangesDialog() {
|
||||||
const [ ancestorNoteId, setAncestorNoteId ] = useState<string>();
|
const [ ancestorNoteId, setAncestorNoteId ] = useState<string>();
|
||||||
const [ groupedByDate, setGroupedByDate ] = useState<Map<String, RecentChangeRow[]>>();
|
const [ groupedByDate, setGroupedByDate ] = useState<Map<string, RecentChangeRow[]>>();
|
||||||
const [ needsRefresh, setNeedsRefresh ] = useState(false);
|
const [ refreshCounter, setRefreshCounter ] = useState(0);
|
||||||
const [ shown, setShown ] = useState(false);
|
const [ shown, setShown ] = useState(false);
|
||||||
|
|
||||||
useTriliumEvent("showRecentChanges", ({ ancestorNoteId }) => {
|
useTriliumEvent("showRecentChanges", ({ ancestorNoteId }) => {
|
||||||
setNeedsRefresh(true);
|
|
||||||
setAncestorNoteId(ancestorNoteId ?? hoisted_note.getHoistedNoteId());
|
setAncestorNoteId(ancestorNoteId ?? hoisted_note.getHoistedNoteId());
|
||||||
setShown(true);
|
setShown(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!groupedByDate || needsRefresh) {
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (needsRefresh) {
|
|
||||||
setNeedsRefresh(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
server.get<RecentChangeRow[]>(`recent-changes/${ancestorNoteId}`)
|
server.get<RecentChangeRow[]>(`recent-changes/${ancestorNoteId}`)
|
||||||
.then(async (recentChanges) => {
|
.then(async (recentChanges) => {
|
||||||
// preload all notes into cache
|
// preload all notes into cache
|
||||||
@@ -45,8 +38,7 @@ function RecentChangesDialogComponent() {
|
|||||||
const groupedByDate = groupByDate(recentChanges);
|
const groupedByDate = groupByDate(recentChanges);
|
||||||
setGroupedByDate(groupedByDate);
|
setGroupedByDate(groupedByDate);
|
||||||
});
|
});
|
||||||
})
|
}, [ shown, refreshCounter ])
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -61,7 +53,7 @@ function RecentChangesDialogComponent() {
|
|||||||
style={{ padding: "0 10px" }}
|
style={{ padding: "0 10px" }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
server.post("notes/erase-deleted-notes-now").then(() => {
|
server.post("notes/erase-deleted-notes-now").then(() => {
|
||||||
setNeedsRefresh(true);
|
setRefreshCounter(refreshCounter + 1);
|
||||||
toast.showMessage(t("recent_changes.deleted_notes_message"));
|
toast.showMessage(t("recent_changes.deleted_notes_message"));
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -79,7 +71,7 @@ function RecentChangesDialogComponent() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function RecentChangesTimeline({ groupedByDate, setShown }: { groupedByDate: Map<String, RecentChangeRow[]>, setShown: Dispatch<StateUpdater<boolean>> }) {
|
function RecentChangesTimeline({ groupedByDate, setShown }: { groupedByDate: Map<string, RecentChangeRow[]>, setShown: Dispatch<StateUpdater<boolean>> }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{ Array.from(groupedByDate.entries()).map(([dateDay, dayChanges]) => {
|
{ Array.from(groupedByDate.entries()).map(([dateDay, dayChanges]) => {
|
||||||
@@ -114,10 +106,6 @@ function RecentChangesTimeline({ groupedByDate, setShown }: { groupedByDate: Map
|
|||||||
}
|
}
|
||||||
|
|
||||||
function NoteLink({ notePath, title }: { notePath: string, title: string }) {
|
function NoteLink({ notePath, title }: { notePath: string, title: string }) {
|
||||||
if (!notePath || !title) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [ noteLink, setNoteLink ] = useState<JQuery<HTMLElement> | null>(null);
|
const [ noteLink, setNoteLink ] = useState<JQuery<HTMLElement> | null>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
link.createLink(notePath, {
|
link.createLink(notePath, {
|
||||||
@@ -156,25 +144,19 @@ function DeletedNoteLink({ change, setShown }: { change: RecentChangeRow, setSho
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class RecentChangesDialog extends ReactBasicWidget {
|
|
||||||
|
|
||||||
get component() {
|
|
||||||
return <RecentChangesDialogComponent />
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function groupByDate(rows: RecentChangeRow[]) {
|
function groupByDate(rows: RecentChangeRow[]) {
|
||||||
const groupedByDate = new Map<String, RecentChangeRow[]>();
|
const groupedByDate = new Map<string, RecentChangeRow[]>();
|
||||||
|
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const dateDay = row.date.substr(0, 10);
|
const dateDay = row.date.substr(0, 10);
|
||||||
|
|
||||||
if (!groupedByDate.has(dateDay)) {
|
let dateDayArray = groupedByDate.get(dateDay);
|
||||||
groupedByDate.set(dateDay, []);
|
if (!dateDayArray) {
|
||||||
|
dateDayArray = [];
|
||||||
|
groupedByDate.set(dateDay, dateDayArray);
|
||||||
}
|
}
|
||||||
|
|
||||||
groupedByDate.get(dateDay)!.push(row);
|
dateDayArray.push(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
return groupedByDate;
|
return groupedByDate;
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import server from "../../services/server";
|
|||||||
import toast from "../../services/toast";
|
import toast from "../../services/toast";
|
||||||
import Button from "../react/Button";
|
import Button from "../react/Button";
|
||||||
import Modal from "../react/Modal";
|
import Modal from "../react/Modal";
|
||||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
|
||||||
import FormList, { FormListItem } from "../react/FormList";
|
import FormList, { FormListItem } from "../react/FormList";
|
||||||
import utils from "../../services/utils";
|
import utils from "../../services/utils";
|
||||||
import { Dispatch, StateUpdater, useEffect, useRef, useState } from "preact/hooks";
|
import { Dispatch, StateUpdater, useEffect, useRef, useState } from "preact/hooks";
|
||||||
@@ -18,9 +17,9 @@ import type { CSSProperties } from "preact/compat";
|
|||||||
import open from "../../services/open";
|
import open from "../../services/open";
|
||||||
import ActionButton from "../react/ActionButton";
|
import ActionButton from "../react/ActionButton";
|
||||||
import options from "../../services/options";
|
import options from "../../services/options";
|
||||||
import useTriliumEvent from "../react/hooks";
|
import { useTriliumEvent } from "../react/hooks";
|
||||||
|
|
||||||
function RevisionsDialogComponent() {
|
export default function RevisionsDialog() {
|
||||||
const [ note, setNote ] = useState<FNote>();
|
const [ note, setNote ] = useState<FNote>();
|
||||||
const [ revisions, setRevisions ] = useState<RevisionItem[]>();
|
const [ revisions, setRevisions ] = useState<RevisionItem[]>();
|
||||||
const [ currentRevision, setCurrentRevision ] = useState<RevisionItem>();
|
const [ currentRevision, setCurrentRevision ] = useState<RevisionItem>();
|
||||||
@@ -72,6 +71,8 @@ function RevisionsDialogComponent() {
|
|||||||
onHidden={() => {
|
onHidden={() => {
|
||||||
setShown(false);
|
setShown(false);
|
||||||
setNote(undefined);
|
setNote(undefined);
|
||||||
|
setCurrentRevision(undefined);
|
||||||
|
setRevisions(undefined);
|
||||||
}}
|
}}
|
||||||
show={shown}
|
show={shown}
|
||||||
>
|
>
|
||||||
@@ -202,17 +203,9 @@ function RevisionContent({ revisionItem, fullRevision }: { revisionItem?: Revisi
|
|||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
switch (revisionItem.type) {
|
switch (revisionItem.type) {
|
||||||
case "text": {
|
case "text":
|
||||||
const contentRef = useRef<HTMLDivElement>(null);
|
return <RevisionContentText content={content} />
|
||||||
useEffect(() => {
|
|
||||||
if (contentRef.current?.querySelector("span.math-tex")) {
|
|
||||||
renderMathInElement(contentRef.current, { trust: true });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return <div ref={contentRef} className="ck-content" dangerouslySetInnerHTML={{ __html: content as string }}></div>
|
|
||||||
}
|
|
||||||
case "code":
|
case "code":
|
||||||
return <pre style={CODE_STYLE}>{content}</pre>;
|
return <pre style={CODE_STYLE}>{content}</pre>;
|
||||||
case "image":
|
case "image":
|
||||||
@@ -264,6 +257,16 @@ function RevisionContent({ revisionItem, fullRevision }: { revisionItem?: Revisi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function RevisionContentText({ content }: { content: string | Buffer<ArrayBufferLike> | undefined }) {
|
||||||
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
if (contentRef.current?.querySelector("span.math-tex")) {
|
||||||
|
renderMathInElement(contentRef.current, { trust: true });
|
||||||
|
}
|
||||||
|
}, [content]);
|
||||||
|
return <div ref={contentRef} className="ck-content" dangerouslySetInnerHTML={{ __html: content as string }}></div>
|
||||||
|
}
|
||||||
|
|
||||||
function RevisionFooter({ note }: { note?: FNote }) {
|
function RevisionFooter({ note }: { note?: FNote }) {
|
||||||
if (!note) {
|
if (!note) {
|
||||||
return <></>;
|
return <></>;
|
||||||
@@ -291,14 +294,6 @@ function RevisionFooter({ note }: { note?: FNote }) {
|
|||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class RevisionsDialog extends ReactBasicWidget {
|
|
||||||
|
|
||||||
get component() {
|
|
||||||
return <RevisionsDialogComponent />
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getNote(noteId?: string | null) {
|
async function getNote(noteId?: string | null) {
|
||||||
if (noteId) {
|
if (noteId) {
|
||||||
return await froca.getNote(noteId);
|
return await froca.getNote(noteId);
|
||||||
|
|||||||
@@ -5,12 +5,11 @@ import FormCheckbox from "../react/FormCheckbox";
|
|||||||
import FormRadioGroup from "../react/FormRadioGroup";
|
import FormRadioGroup from "../react/FormRadioGroup";
|
||||||
import FormTextBox from "../react/FormTextBox";
|
import FormTextBox from "../react/FormTextBox";
|
||||||
import Modal from "../react/Modal";
|
import Modal from "../react/Modal";
|
||||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
|
||||||
import server from "../../services/server";
|
import server from "../../services/server";
|
||||||
import FormGroup from "../react/FormGroup";
|
import FormGroup from "../react/FormGroup";
|
||||||
import useTriliumEvent from "../react/hooks";
|
import { useTriliumEvent } from "../react/hooks";
|
||||||
|
|
||||||
function SortChildNotesDialogComponent() {
|
export default function SortChildNotesDialog() {
|
||||||
const [ parentNoteId, setParentNoteId ] = useState<string>();
|
const [ parentNoteId, setParentNoteId ] = useState<string>();
|
||||||
const [ sortBy, setSortBy ] = useState("title");
|
const [ sortBy, setSortBy ] = useState("title");
|
||||||
const [ sortDirection, setSortDirection ] = useState("asc");
|
const [ sortDirection, setSortDirection ] = useState("asc");
|
||||||
@@ -89,11 +88,3 @@ function SortChildNotesDialogComponent() {
|
|||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class SortChildNotesDialog extends ReactBasicWidget {
|
|
||||||
|
|
||||||
get component() {
|
|
||||||
return <SortChildNotesDialogComponent />;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -5,13 +5,12 @@ import FormCheckbox from "../react/FormCheckbox";
|
|||||||
import FormFileUpload from "../react/FormFileUpload";
|
import FormFileUpload from "../react/FormFileUpload";
|
||||||
import FormGroup from "../react/FormGroup";
|
import FormGroup from "../react/FormGroup";
|
||||||
import Modal from "../react/Modal";
|
import Modal from "../react/Modal";
|
||||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
|
||||||
import options from "../../services/options";
|
import options from "../../services/options";
|
||||||
import importService from "../../services/import.js";
|
import importService from "../../services/import.js";
|
||||||
import tree from "../../services/tree";
|
import tree from "../../services/tree";
|
||||||
import useTriliumEvent from "../react/hooks";
|
import { useTriliumEvent } from "../react/hooks";
|
||||||
|
|
||||||
function UploadAttachmentsDialogComponent() {
|
export default function UploadAttachmentsDialog() {
|
||||||
const [ parentNoteId, setParentNoteId ] = useState<string>();
|
const [ parentNoteId, setParentNoteId ] = useState<string>();
|
||||||
const [ files, setFiles ] = useState<FileList | null>(null);
|
const [ files, setFiles ] = useState<FileList | null>(null);
|
||||||
const [ shrinkImages, setShrinkImages ] = useState(options.is("compressImages"));
|
const [ shrinkImages, setShrinkImages ] = useState(options.is("compressImages"));
|
||||||
@@ -24,12 +23,12 @@ function UploadAttachmentsDialogComponent() {
|
|||||||
setShown(true);
|
setShown(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (parentNoteId) {
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!parentNoteId) return;
|
||||||
|
|
||||||
tree.getNoteTitle(parentNoteId).then((noteTitle) =>
|
tree.getNoteTitle(parentNoteId).then((noteTitle) =>
|
||||||
setDescription(t("upload_attachments.files_will_be_uploaded", { noteTitle })));
|
setDescription(t("upload_attachments.files_will_be_uploaded", { noteTitle })));
|
||||||
}, [parentNoteId]);
|
}, [parentNoteId]);
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -64,11 +63,3 @@ function UploadAttachmentsDialogComponent() {
|
|||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class UploadAttachmentsDialog extends ReactBasicWidget {
|
|
||||||
|
|
||||||
get component() {
|
|
||||||
return <UploadAttachmentsDialogComponent />;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,120 +0,0 @@
|
|||||||
import attributeService from "../services/attributes.js";
|
|
||||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
|
||||||
import { t } from "../services/i18n.js";
|
|
||||||
import type FNote from "../entities/fnote.js";
|
|
||||||
import type { EventData } from "../components/app_context.js";
|
|
||||||
import { Dropdown } from "bootstrap";
|
|
||||||
|
|
||||||
type Editability = "auto" | "readOnly" | "autoReadOnlyDisabled";
|
|
||||||
|
|
||||||
const TPL = /*html*/`
|
|
||||||
<div class="dropdown editability-select-widget">
|
|
||||||
<style>
|
|
||||||
.editability-dropdown {
|
|
||||||
width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editability-dropdown .dropdown-item {
|
|
||||||
display: flex !importamt;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editability-dropdown .dropdown-item > div {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editability-dropdown .description {
|
|
||||||
font-size: small;
|
|
||||||
color: var(--muted-text-color);
|
|
||||||
white-space: normal;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<button type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm select-button dropdown-toggle editability-button">
|
|
||||||
<span class="editability-active-desc">${t("editability_select.auto")}</span>
|
|
||||||
<span class="caret"></span>
|
|
||||||
</button>
|
|
||||||
<div class="editability-dropdown dropdown-menu dropdown-menu-right tn-dropdown-list">
|
|
||||||
<a class="dropdown-item" href="#" data-editability="auto">
|
|
||||||
<span class="check">✓</span>
|
|
||||||
<div>
|
|
||||||
${t("editability_select.auto")}
|
|
||||||
<div class="description">${t("editability_select.note_is_editable")}</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a class="dropdown-item" href="#" data-editability="readOnly">
|
|
||||||
<span class="check">✓</span>
|
|
||||||
<div>
|
|
||||||
${t("editability_select.read_only")}
|
|
||||||
<div class="description">${t("editability_select.note_is_read_only")}</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a class="dropdown-item" href="#" data-editability="autoReadOnlyDisabled">
|
|
||||||
<span class="check">✓</span>
|
|
||||||
<div>
|
|
||||||
${t("editability_select.always_editable")}
|
|
||||||
<div class="description">${t("editability_select.note_is_always_editable")}</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default class EditabilitySelectWidget extends NoteContextAwareWidget {
|
|
||||||
|
|
||||||
private dropdown!: Dropdown;
|
|
||||||
private $editabilityActiveDesc!: JQuery<HTMLElement>;
|
|
||||||
|
|
||||||
doRender() {
|
|
||||||
this.$widget = $(TPL);
|
|
||||||
|
|
||||||
this.dropdown = Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']")[0]);
|
|
||||||
|
|
||||||
this.$editabilityActiveDesc = this.$widget.find(".editability-active-desc");
|
|
||||||
|
|
||||||
this.$widget.on("click", ".dropdown-item", async (e) => {
|
|
||||||
this.dropdown.toggle();
|
|
||||||
|
|
||||||
const editability = $(e.target).closest("[data-editability]").attr("data-editability");
|
|
||||||
|
|
||||||
if (!this.note || !this.noteId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const ownedAttr of this.note.getOwnedLabels()) {
|
|
||||||
if (["readOnly", "autoReadOnlyDisabled"].includes(ownedAttr.name)) {
|
|
||||||
await attributeService.removeAttributeById(this.noteId, ownedAttr.attributeId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (editability && editability !== "auto") {
|
|
||||||
await attributeService.addLabel(this.noteId, editability);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async refreshWithNote(note: FNote) {
|
|
||||||
let editability: Editability = "auto";
|
|
||||||
|
|
||||||
if (this.note?.isLabelTruthy("readOnly")) {
|
|
||||||
editability = "readOnly";
|
|
||||||
} else if (this.note?.isLabelTruthy("autoReadOnlyDisabled")) {
|
|
||||||
editability = "autoReadOnlyDisabled";
|
|
||||||
}
|
|
||||||
|
|
||||||
const labels = {
|
|
||||||
auto: t("editability_select.auto"),
|
|
||||||
readOnly: t("editability_select.read_only"),
|
|
||||||
autoReadOnlyDisabled: t("editability_select.always_editable")
|
|
||||||
};
|
|
||||||
|
|
||||||
this.$widget.find(".dropdown-item").removeClass("selected");
|
|
||||||
this.$widget.find(`.dropdown-item[data-editability='${editability}']`).addClass("selected");
|
|
||||||
|
|
||||||
this.$editabilityActiveDesc.text(labels[editability]);
|
|
||||||
}
|
|
||||||
|
|
||||||
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
|
||||||
if (loadResults.getAttributeRows().find((attr) => attr.noteId === this.noteId)) {
|
|
||||||
this.refresh();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
// taken from the HTML source of https://boxicons.com/
|
// taken from the HTML source of https://boxicons.com/
|
||||||
|
|
||||||
interface Category {
|
export interface Category {
|
||||||
name: string;
|
name: string;
|
||||||
id: number;
|
id: number;
|
||||||
}
|
}
|
||||||
|
|||||||
59
apps/client/src/widgets/note_icon.css
Normal file
59
apps/client/src/widgets/note_icon.css
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
.note-icon-widget {
|
||||||
|
padding-top: 3px;
|
||||||
|
padding-left: 7px;
|
||||||
|
margin-right: 0;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-icon-widget button.note-icon {
|
||||||
|
font-size: 180%;
|
||||||
|
background-color: transparent;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 6px;
|
||||||
|
color: var(--main-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-icon-widget button.note-icon:hover {
|
||||||
|
border: 1px solid var(--main-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-icon-widget .dropdown-menu {
|
||||||
|
border-radius: 10px;
|
||||||
|
border-width: 2px;
|
||||||
|
box-shadow: 10px 10px 93px -25px black;
|
||||||
|
padding: 10px 15px 10px 15px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-icon-widget .filter-row {
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
padding-right: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-icon-widget .filter-row span {
|
||||||
|
display: block;
|
||||||
|
padding-left: 15px;
|
||||||
|
padding-right: 15px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-icon-widget .icon-list {
|
||||||
|
height: 500px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-icon-widget .icon-list span {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
font-size: 180%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-icon-widget .icon-list span:hover {
|
||||||
|
border: 1px solid var(--main-border-color);
|
||||||
|
}
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
import { t } from "../services/i18n.js";
|
|
||||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
|
||||||
import attributeService from "../services/attributes.js";
|
|
||||||
import server from "../services/server.js";
|
|
||||||
import type FNote from "../entities/fnote.js";
|
|
||||||
import type { EventData } from "../components/app_context.js";
|
|
||||||
import type { Icon } from "./icon_list.js";
|
|
||||||
import { Dropdown } from "bootstrap";
|
|
||||||
|
|
||||||
const TPL = /*html*/`
|
|
||||||
<div class="note-icon-widget dropdown">
|
|
||||||
<style>
|
|
||||||
.note-icon-widget {
|
|
||||||
padding-top: 3px;
|
|
||||||
padding-left: 7px;
|
|
||||||
margin-right: 0;
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-icon-widget button.note-icon {
|
|
||||||
font-size: 180%;
|
|
||||||
background-color: transparent;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 6px;
|
|
||||||
color: var(--main-text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-icon-widget button.note-icon:hover {
|
|
||||||
border: 1px solid var(--main-border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-icon-widget .dropdown-menu {
|
|
||||||
border-radius: 10px;
|
|
||||||
border-width: 2px;
|
|
||||||
box-shadow: 10px 10px 93px -25px black;
|
|
||||||
padding: 10px 15px 10px 15px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-icon-widget .filter-row {
|
|
||||||
padding-top: 10px;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
padding-right: 20px;
|
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-icon-widget .filter-row span {
|
|
||||||
display: block;
|
|
||||||
padding-left: 15px;
|
|
||||||
padding-right: 15px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-icon-widget .icon-list {
|
|
||||||
height: 500px;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-icon-widget .icon-list span {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
font-size: 180%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-icon-widget .icon-list span:hover {
|
|
||||||
border: 1px solid var(--main-border-color);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<button class="btn dropdown-toggle note-icon" type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="${t("note_icon.change_note_icon")}"></button>
|
|
||||||
<div class="dropdown-menu" aria-labelledby="note-path-list-button" style="width: 610px;">
|
|
||||||
<div class="filter-row">
|
|
||||||
<span>${t("note_icon.category")}</span> <select name="icon-category" class="form-select"></select>
|
|
||||||
|
|
||||||
<span>${t("note_icon.search")}</span> <input type="text" name="icon-search" class="form-control" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="icon-list"></div>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
interface IconToCountCache {
|
|
||||||
iconClassToCountMap: Record<string, number>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class NoteIconWidget extends NoteContextAwareWidget {
|
|
||||||
|
|
||||||
private dropdown!: bootstrap.Dropdown;
|
|
||||||
private $icon!: JQuery<HTMLElement>;
|
|
||||||
private $iconList!: JQuery<HTMLElement>;
|
|
||||||
private $iconCategory!: JQuery<HTMLElement>;
|
|
||||||
private $iconSearch!: JQuery<HTMLElement>;
|
|
||||||
private iconToCountCache!: Promise<IconToCountCache | null> | null;
|
|
||||||
|
|
||||||
doRender() {
|
|
||||||
this.$widget = $(TPL);
|
|
||||||
this.dropdown = Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']")[0]);
|
|
||||||
|
|
||||||
this.$icon = this.$widget.find("button.note-icon");
|
|
||||||
this.$iconList = this.$widget.find(".icon-list");
|
|
||||||
this.$iconList.on("click", "span", async (e) => {
|
|
||||||
const clazz = $(e.target).attr("class");
|
|
||||||
|
|
||||||
if (this.noteId && this.note) {
|
|
||||||
await attributeService.setLabel(this.noteId, this.note.hasOwnedLabel("workspace") ? "workspaceIconClass" : "iconClass", clazz);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.$iconCategory = this.$widget.find("select[name='icon-category']");
|
|
||||||
this.$iconCategory.on("change", () => this.renderDropdown());
|
|
||||||
this.$iconCategory.on("click", (e) => e.stopPropagation());
|
|
||||||
|
|
||||||
this.$iconSearch = this.$widget.find("input[name='icon-search']");
|
|
||||||
this.$iconSearch.on("input", () => this.renderDropdown());
|
|
||||||
|
|
||||||
this.$widget.on("show.bs.dropdown", async () => {
|
|
||||||
const { categories } = (await import("./icon_list.js")).default;
|
|
||||||
|
|
||||||
this.$iconCategory.empty();
|
|
||||||
|
|
||||||
for (const category of categories) {
|
|
||||||
this.$iconCategory.append($("<option>").text(category.name).attr("value", category.id));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$iconSearch.val("");
|
|
||||||
|
|
||||||
this.renderDropdown();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async refreshWithNote(note: FNote) {
|
|
||||||
this.$icon.removeClass().addClass(`${note.getIcon()} note-icon`);
|
|
||||||
this.$icon.prop("disabled", !!(this.noteContext?.viewScope?.viewMode !== "default"));
|
|
||||||
this.dropdown.hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
|
||||||
if (this.noteId && loadResults.isNoteReloaded(this.noteId)) {
|
|
||||||
this.refresh();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const attr of loadResults.getAttributeRows()) {
|
|
||||||
if (attr.type === "label" && ["iconClass", "workspaceIconClass"].includes(attr.name ?? "") && attributeService.isAffecting(attr, this.note)) {
|
|
||||||
this.refresh();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async renderDropdown() {
|
|
||||||
const iconToCount = await this.getIconToCountMap();
|
|
||||||
const { icons } = (await import("./icon_list.js")).default;
|
|
||||||
|
|
||||||
this.$iconList.empty();
|
|
||||||
|
|
||||||
if (this.getIconLabels().length > 0) {
|
|
||||||
this.$iconList.append(
|
|
||||||
$(`<div style="text-align: center">`).append(
|
|
||||||
$(`<button class="btn btn-sm">${t("note_icon.reset-default")}</button>`).on("click", () =>
|
|
||||||
this.getIconLabels().forEach((label) => {
|
|
||||||
if (this.noteId) {
|
|
||||||
attributeService.removeAttributeById(this.noteId, label.attributeId);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const categoryId = parseInt(String(this.$iconCategory.find("option:selected")?.val()));
|
|
||||||
const search = String(this.$iconSearch.val())?.trim()?.toLowerCase();
|
|
||||||
|
|
||||||
const filteredIcons = icons.filter((icon) => {
|
|
||||||
if (categoryId && icon.category_id !== categoryId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (search) {
|
|
||||||
if (!icon.name.includes(search) && !icon.term?.find((t) => t.includes(search))) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (iconToCount) {
|
|
||||||
filteredIcons.sort((a, b) => {
|
|
||||||
const countA = iconToCount[a.className ?? ""] || 0;
|
|
||||||
const countB = iconToCount[b.className ?? ""] || 0;
|
|
||||||
|
|
||||||
return countB - countA;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const icon of filteredIcons) {
|
|
||||||
this.$iconList.append(this.renderIcon(icon));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$iconSearch.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
async getIconToCountMap() {
|
|
||||||
if (!this.iconToCountCache) {
|
|
||||||
this.iconToCountCache = server.get<IconToCountCache>("other/icon-usage");
|
|
||||||
setTimeout(() => (this.iconToCountCache = null), 20000); // invalidate cache after 20 seconds
|
|
||||||
}
|
|
||||||
|
|
||||||
return (await this.iconToCountCache)?.iconClassToCountMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderIcon(icon: Icon) {
|
|
||||||
return $("<span>")
|
|
||||||
.addClass("bx " + icon.className)
|
|
||||||
.attr("title", icon.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
getIconLabels() {
|
|
||||||
if (!this.note) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return this.note.getOwnedLabels().filter((label) => ["workspaceIconClass", "iconClass"].includes(label.name));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
184
apps/client/src/widgets/note_icon.tsx
Normal file
184
apps/client/src/widgets/note_icon.tsx
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
import Dropdown from "./react/Dropdown";
|
||||||
|
import "./note_icon.css";
|
||||||
|
import { t } from "i18next";
|
||||||
|
import { useNoteContext, useNoteLabel } from "./react/hooks";
|
||||||
|
import { useEffect, useRef, useState } from "preact/hooks";
|
||||||
|
import server from "../services/server";
|
||||||
|
import type { Category, Icon } from "./icon_list";
|
||||||
|
import FormTextBox from "./react/FormTextBox";
|
||||||
|
import FormSelect from "./react/FormSelect";
|
||||||
|
import FNote from "../entities/fnote";
|
||||||
|
import attributes from "../services/attributes";
|
||||||
|
import Button from "./react/Button";
|
||||||
|
|
||||||
|
interface IconToCountCache {
|
||||||
|
iconClassToCountMap: Record<string, number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IconData {
|
||||||
|
iconToCount: Record<string, number>;
|
||||||
|
categories: Category[];
|
||||||
|
icons: Icon[];
|
||||||
|
}
|
||||||
|
|
||||||
|
let fullIconData: {
|
||||||
|
categories: Category[];
|
||||||
|
icons: Icon[];
|
||||||
|
};
|
||||||
|
let iconToCountCache!: Promise<IconToCountCache> | null;
|
||||||
|
|
||||||
|
export default function NoteIcon() {
|
||||||
|
const { note, viewScope } = useNoteContext();
|
||||||
|
const [ icon, setIcon ] = useState<string | null | undefined>();
|
||||||
|
const [ iconClass ] = useNoteLabel(note, "iconClass");
|
||||||
|
const [ workspaceIconClass ] = useNoteLabel(note, "workspaceIconClass");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIcon(note?.getIcon());
|
||||||
|
}, [ note, iconClass, workspaceIconClass ]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
className="note-icon-widget"
|
||||||
|
title={t("note_icon.change_note_icon")}
|
||||||
|
dropdownContainerStyle={{ width: "610px" }}
|
||||||
|
buttonClassName={`note-icon ${icon ?? "bx bx-empty"}`}
|
||||||
|
hideToggleArrow
|
||||||
|
disabled={viewScope?.viewMode !== "default"}
|
||||||
|
>
|
||||||
|
{ note && <NoteIconList note={note} /> }
|
||||||
|
</Dropdown>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NoteIconList({ note }: { note: FNote }) {
|
||||||
|
const searchBoxRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [ search, setSearch ] = useState<string>();
|
||||||
|
const [ categoryId, setCategoryId ] = useState<string>("0");
|
||||||
|
const [ iconData, setIconData ] = useState<IconData>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function loadIcons() {
|
||||||
|
if (!fullIconData) {
|
||||||
|
fullIconData = (await import("./icon_list.js")).default;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by text and/or category.
|
||||||
|
let icons: Icon[] = fullIconData.icons;
|
||||||
|
const processedSearch = search?.trim()?.toLowerCase();
|
||||||
|
if (processedSearch || categoryId) {
|
||||||
|
icons = icons.filter((icon) => {
|
||||||
|
if (categoryId !== "0" && String(icon.category_id) !== categoryId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (processedSearch) {
|
||||||
|
if (!icon.name.includes(processedSearch) &&
|
||||||
|
!icon.term?.find((t) => t.includes(processedSearch))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by count.
|
||||||
|
const iconToCount = await getIconToCountMap();
|
||||||
|
if (iconToCount) {
|
||||||
|
icons.sort((a, b) => {
|
||||||
|
const countA = iconToCount[a.className ?? ""] || 0;
|
||||||
|
const countB = iconToCount[b.className ?? ""] || 0;
|
||||||
|
|
||||||
|
return countB - countA;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setIconData({
|
||||||
|
iconToCount,
|
||||||
|
icons,
|
||||||
|
categories: fullIconData.categories
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
loadIcons();
|
||||||
|
}, [ search, categoryId ]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div class="filter-row">
|
||||||
|
<span>{t("note_icon.category")}</span>
|
||||||
|
<FormSelect
|
||||||
|
name="icon-category"
|
||||||
|
values={fullIconData?.categories ?? []}
|
||||||
|
currentValue={categoryId} onChange={setCategoryId}
|
||||||
|
keyProperty="id" titleProperty="name"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span>{t("note_icon.search")}</span>
|
||||||
|
<FormTextBox
|
||||||
|
inputRef={searchBoxRef}
|
||||||
|
type="text"
|
||||||
|
name="icon-search"
|
||||||
|
currentValue={search} onChange={setSearch}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="icon-list"
|
||||||
|
onClick={(e) => {
|
||||||
|
const clickedTarget = e.target as HTMLElement;
|
||||||
|
|
||||||
|
if (!clickedTarget.classList.contains("bx")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const iconClass = Array.from(clickedTarget.classList.values()).join(" ");
|
||||||
|
if (note) {
|
||||||
|
const attributeToSet = note.hasOwnedLabel("workspace") ? "workspaceIconClass" : "iconClass";
|
||||||
|
attributes.setLabel(note.noteId, attributeToSet, iconClass);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getIconLabels(note).length > 0 && (
|
||||||
|
<div style={{ textAlign: "center" }}>
|
||||||
|
<Button
|
||||||
|
text={t("note_icon.reset-default")}
|
||||||
|
onClick={() => {
|
||||||
|
if (!note) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const label of getIconLabels(note)) {
|
||||||
|
attributes.removeAttributeById(note.noteId, label.attributeId);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(iconData?.icons ?? []).map(({className, name}) => (
|
||||||
|
<span class={`bx ${className}`} title={name} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getIconToCountMap() {
|
||||||
|
if (!iconToCountCache) {
|
||||||
|
iconToCountCache = server.get<IconToCountCache>("other/icon-usage");
|
||||||
|
setTimeout(() => (iconToCountCache = null), 20000); // invalidate cache after 20 seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
return (await iconToCountCache).iconClassToCountMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIconLabels(note: FNote) {
|
||||||
|
if (!note) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return note.getOwnedLabels()
|
||||||
|
.filter((label) => ["workspaceIconClass", "iconClass"]
|
||||||
|
.includes(label.name));
|
||||||
|
}
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
import { Dropdown } from "bootstrap";
|
|
||||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
|
||||||
import { getAvailableLocales, getLocaleById, t } from "../services/i18n.js";
|
|
||||||
import type { EventData } from "../components/app_context.js";
|
|
||||||
import type FNote from "../entities/fnote.js";
|
|
||||||
import attributes from "../services/attributes.js";
|
|
||||||
import type { Locale } from "@triliumnext/commons";
|
|
||||||
import options from "../services/options.js";
|
|
||||||
import appContext from "../components/app_context.js";
|
|
||||||
|
|
||||||
const TPL = /*html*/`\
|
|
||||||
<div class="dropdown note-language-widget">
|
|
||||||
<button type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm dropdown-toggle select-button note-language-button">
|
|
||||||
<span class="note-language-desc"></span>
|
|
||||||
<span class="caret"></span>
|
|
||||||
</button>
|
|
||||||
<div class="note-language-dropdown dropdown-menu dropdown-menu-left tn-dropdown-list"></div>
|
|
||||||
<button class="language-help-button icon-action bx bx-help-circle" type="button" data-in-app-help="B0lcI9xz1r8K" title="${t("open-help-page")}"></button>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.note-language-widget {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.language-help-button {
|
|
||||||
margin-left: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-language-dropdown [dir=rtl] {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-item.rtl > .check {
|
|
||||||
order: 1;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const DEFAULT_LOCALE: Locale = {
|
|
||||||
id: "",
|
|
||||||
name: t("note_language.not_set")
|
|
||||||
};
|
|
||||||
|
|
||||||
export default class NoteLanguageWidget extends NoteContextAwareWidget {
|
|
||||||
|
|
||||||
private dropdown!: Dropdown;
|
|
||||||
private $noteLanguageDropdown!: JQuery<HTMLElement>;
|
|
||||||
private $noteLanguageDesc!: JQuery<HTMLElement>;
|
|
||||||
private locales: (Locale | "---")[];
|
|
||||||
private currentLanguageId?: string;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.locales = NoteLanguageWidget.#buildLocales();
|
|
||||||
}
|
|
||||||
|
|
||||||
doRender() {
|
|
||||||
this.$widget = $(TPL);
|
|
||||||
this.dropdown = Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']")[0]);
|
|
||||||
this.$widget.on("show.bs.dropdown", () => this.renderDropdown());
|
|
||||||
|
|
||||||
this.$noteLanguageDropdown = this.$widget.find(".note-language-dropdown")
|
|
||||||
this.$noteLanguageDesc = this.$widget.find(".note-language-desc");
|
|
||||||
}
|
|
||||||
|
|
||||||
renderDropdown() {
|
|
||||||
this.$noteLanguageDropdown.empty();
|
|
||||||
|
|
||||||
if (!this.note) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const locale of this.locales) {
|
|
||||||
if (typeof locale === "object") {
|
|
||||||
const $title = $("<span>").text(locale.name);
|
|
||||||
|
|
||||||
const $link = $('<a class="dropdown-item">')
|
|
||||||
.attr("data-language", locale.id)
|
|
||||||
.append('<span class="check">✓</span> ')
|
|
||||||
.append($title)
|
|
||||||
.on("click", () => {
|
|
||||||
const languageId = $link.attr("data-language") ?? "";
|
|
||||||
this.save(languageId);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (locale.rtl) {
|
|
||||||
$link.attr("dir", "rtl");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (locale.id === this.currentLanguageId) {
|
|
||||||
$link.addClass("selected");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$noteLanguageDropdown.append($link);
|
|
||||||
} else {
|
|
||||||
this.$noteLanguageDropdown.append('<div class="dropdown-divider"></div>');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const $configureLink = $('<a class="dropdown-item">')
|
|
||||||
.append(`<span>${t("note_language.configure-languages")}</span>`)
|
|
||||||
.on("click", () => appContext.tabManager.openContextWithNote("_optionsLocalization", { activate: true }));
|
|
||||||
this.$noteLanguageDropdown.append($configureLink);
|
|
||||||
}
|
|
||||||
|
|
||||||
async save(languageId: string) {
|
|
||||||
if (!this.note) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
attributes.setAttribute(this.note, "label", "language", languageId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async refreshWithNote(note: FNote) {
|
|
||||||
const currentLanguageId = note.getLabelValue("language") ?? "";
|
|
||||||
const language = getLocaleById(currentLanguageId) ?? DEFAULT_LOCALE;
|
|
||||||
this.currentLanguageId = currentLanguageId;
|
|
||||||
this.$noteLanguageDesc.text(language.name);
|
|
||||||
this.dropdown.hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
|
||||||
if (loadResults.isOptionReloaded("languages")) {
|
|
||||||
this.locales = NoteLanguageWidget.#buildLocales();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loadResults.getAttributeRows().find((a) => a.noteId === this.noteId && a.name === "language")) {
|
|
||||||
this.refresh();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static #buildLocales() {
|
|
||||||
const enabledLanguages = JSON.parse(options.get("languages") ?? "[]") as string[];
|
|
||||||
const filteredLanguages = getAvailableLocales().filter((l) => typeof l !== "object" || enabledLanguages.includes(l.id));
|
|
||||||
const leftToRightLanguages = filteredLanguages.filter((l) => !l.rtl);
|
|
||||||
const rightToLeftLanguages = filteredLanguages.filter((l) => l.rtl);
|
|
||||||
|
|
||||||
let locales: ("---" | Locale)[] = [
|
|
||||||
DEFAULT_LOCALE
|
|
||||||
];
|
|
||||||
|
|
||||||
if (leftToRightLanguages.length > 0) {
|
|
||||||
locales = [
|
|
||||||
...locales,
|
|
||||||
"---",
|
|
||||||
...leftToRightLanguages
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rightToLeftLanguages.length > 0) {
|
|
||||||
locales = [
|
|
||||||
...locales,
|
|
||||||
"---",
|
|
||||||
...rightToLeftLanguages
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// This will separate the list of languages from the "Configure languages" button.
|
|
||||||
// If there is at least one language.
|
|
||||||
locales.push("---");
|
|
||||||
return locales;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
25
apps/client/src/widgets/note_title.css
Normal file
25
apps/client/src/widgets/note_title.css
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
.note-title-widget {
|
||||||
|
flex-grow: 1000;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-title-widget input.note-title {
|
||||||
|
font-size: 110%;
|
||||||
|
border: 0;
|
||||||
|
margin: 2px 0px;
|
||||||
|
min-width: 5em;
|
||||||
|
width: 100%;
|
||||||
|
padding: 1px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-title-widget input.note-title.protected {
|
||||||
|
text-shadow: 4px 4px 4px var(--muted-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile .note-title-widget input.note-title {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.desktop .note-title-widget input.note-title {
|
||||||
|
font-size: 180%;
|
||||||
|
}
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
import { t } from "../services/i18n.js";
|
|
||||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
|
||||||
import protectedSessionHolder from "../services/protected_session_holder.js";
|
|
||||||
import server from "../services/server.js";
|
|
||||||
import SpacedUpdate from "../services/spaced_update.js";
|
|
||||||
import appContext, { type EventData } from "../components/app_context.js";
|
|
||||||
import branchService from "../services/branches.js";
|
|
||||||
import shortcutService from "../services/shortcuts.js";
|
|
||||||
import utils from "../services/utils.js";
|
|
||||||
import type FNote from "../entities/fnote.js";
|
|
||||||
|
|
||||||
const TPL = /*html*/`
|
|
||||||
<div class="note-title-widget">
|
|
||||||
<style>
|
|
||||||
.note-title-widget {
|
|
||||||
flex-grow: 1000;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-title-widget input.note-title {
|
|
||||||
font-size: 110%;
|
|
||||||
border: 0;
|
|
||||||
margin: 2px 0px;
|
|
||||||
min-width: 5em;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.mobile .note-title-widget input.note-title {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.desktop .note-title-widget input.note-title {
|
|
||||||
font-size: 180%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-title-widget input.note-title.protected {
|
|
||||||
text-shadow: 4px 4px 4px var(--muted-text-color);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<input autocomplete="off" value="" placeholder="${t("note_title.placeholder")}" class="note-title" tabindex="100">
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
export default class NoteTitleWidget extends NoteContextAwareWidget {
|
|
||||||
|
|
||||||
private $noteTitle!: JQuery<HTMLElement>;
|
|
||||||
private deleteNoteOnEscape: boolean;
|
|
||||||
private spacedUpdate: SpacedUpdate;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.spacedUpdate = new SpacedUpdate(async () => {
|
|
||||||
const title = this.$noteTitle.val();
|
|
||||||
|
|
||||||
if (this.note) {
|
|
||||||
protectedSessionHolder.touchProtectedSessionIfNecessary(this.note);
|
|
||||||
}
|
|
||||||
|
|
||||||
await server.put(`notes/${this.noteId}/title`, { title }, this.componentId);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.deleteNoteOnEscape = false;
|
|
||||||
|
|
||||||
appContext.addBeforeUnloadListener(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
doRender() {
|
|
||||||
this.$widget = $(TPL);
|
|
||||||
this.$noteTitle = this.$widget.find(".note-title");
|
|
||||||
|
|
||||||
this.$noteTitle.on("input", () => this.spacedUpdate.scheduleUpdate());
|
|
||||||
|
|
||||||
this.$noteTitle.on("blur", () => {
|
|
||||||
this.spacedUpdate.updateNowIfNecessary();
|
|
||||||
|
|
||||||
this.deleteNoteOnEscape = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
shortcutService.bindElShortcut(this.$noteTitle, "esc", () => {
|
|
||||||
if (this.deleteNoteOnEscape && this.noteContext?.isActive() && this.noteContext?.note) {
|
|
||||||
branchService.deleteNotes(Object.values(this.noteContext.note.parentToBranch));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
shortcutService.bindElShortcut(this.$noteTitle, "return", () => {
|
|
||||||
this.triggerCommand("focusOnDetail", { ntxId: this.noteContext?.ntxId });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async refreshWithNote(note: FNote) {
|
|
||||||
const isReadOnly =
|
|
||||||
(note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable())
|
|
||||||
|| utils.isLaunchBarConfig(note.noteId)
|
|
||||||
|| this.noteContext?.viewScope?.viewMode !== "default";
|
|
||||||
|
|
||||||
this.$noteTitle.val(isReadOnly ? (await this.noteContext?.getNavigationTitle()) || "" : note.title);
|
|
||||||
this.$noteTitle.prop("readonly", isReadOnly);
|
|
||||||
|
|
||||||
this.setProtectedStatus(note);
|
|
||||||
}
|
|
||||||
|
|
||||||
setProtectedStatus(note: FNote) {
|
|
||||||
this.$noteTitle.toggleClass("protected", !!note.isProtected);
|
|
||||||
}
|
|
||||||
|
|
||||||
async beforeNoteSwitchEvent({ noteContext }: EventData<"beforeNoteSwitch">) {
|
|
||||||
if (this.isNoteContext(noteContext.ntxId)) {
|
|
||||||
await this.spacedUpdate.updateNowIfNecessary();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async beforeNoteContextRemoveEvent({ ntxIds }: EventData<"beforeNoteContextRemove">) {
|
|
||||||
if (this.isNoteContext(ntxIds)) {
|
|
||||||
await this.spacedUpdate.updateNowIfNecessary();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
focusOnTitleEvent() {
|
|
||||||
if (this.noteContext && this.noteContext.isActive()) {
|
|
||||||
this.$noteTitle.trigger("focus");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
focusAndSelectTitleEvent({ isNewNote } = { isNewNote: false }) {
|
|
||||||
if (this.noteContext && this.noteContext.isActive()) {
|
|
||||||
this.$noteTitle.trigger("focus").trigger("select");
|
|
||||||
|
|
||||||
this.deleteNoteOnEscape = isNewNote;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
|
||||||
if (loadResults.isNoteReloaded(this.noteId) && this.note) {
|
|
||||||
// not updating the title specifically since the synced title might be older than what the user is currently typing
|
|
||||||
this.setProtectedStatus(this.note);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loadResults.isNoteReloaded(this.noteId, this.componentId)) {
|
|
||||||
this.refresh();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeUnloadEvent() {
|
|
||||||
return this.spacedUpdate.isAllSavedAndTriggerUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
99
apps/client/src/widgets/note_title.tsx
Normal file
99
apps/client/src/widgets/note_title.tsx
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import { useEffect, useRef, useState } from "preact/hooks";
|
||||||
|
import { t } from "../services/i18n";
|
||||||
|
import FormTextBox from "./react/FormTextBox";
|
||||||
|
import { useNoteContext, useNoteProperty, useSpacedUpdate, useTriliumEvent, useTriliumEvents } from "./react/hooks";
|
||||||
|
import protected_session_holder from "../services/protected_session_holder";
|
||||||
|
import server from "../services/server";
|
||||||
|
import "./note_title.css";
|
||||||
|
import { isLaunchBarConfig } from "../services/utils";
|
||||||
|
import appContext from "../components/app_context";
|
||||||
|
import branches from "../services/branches";
|
||||||
|
|
||||||
|
export default function NoteTitleWidget() {
|
||||||
|
const { note, noteId, componentId, viewScope, noteContext, parentComponent } = useNoteContext();
|
||||||
|
const title = useNoteProperty(note, "title", componentId);
|
||||||
|
const isProtected = useNoteProperty(note, "isProtected");
|
||||||
|
const newTitle = useRef("");
|
||||||
|
|
||||||
|
const [ isReadOnly, setReadOnly ] = useState<boolean>(false);
|
||||||
|
const [ navigationTitle, setNavigationTitle ] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Manage read-only
|
||||||
|
useEffect(() => {
|
||||||
|
const isReadOnly = note === null
|
||||||
|
|| note === undefined
|
||||||
|
|| (note.isProtected && !protected_session_holder.isProtectedSessionAvailable())
|
||||||
|
|| isLaunchBarConfig(note.noteId)
|
||||||
|
|| viewScope?.viewMode !== "default";
|
||||||
|
setReadOnly(isReadOnly);
|
||||||
|
}, [ note, note?.noteId, note?.isProtected, viewScope?.viewMode ]);
|
||||||
|
|
||||||
|
// Manage the title for read-only notes
|
||||||
|
useEffect(() => {
|
||||||
|
if (isReadOnly) {
|
||||||
|
noteContext?.getNavigationTitle().then(setNavigationTitle);
|
||||||
|
}
|
||||||
|
}, [isReadOnly]);
|
||||||
|
|
||||||
|
// Save changes to title.
|
||||||
|
const spacedUpdate = useSpacedUpdate(async () => {
|
||||||
|
if (!note) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
protected_session_holder.touchProtectedSessionIfNecessary(note);
|
||||||
|
await server.put<void>(`notes/${noteId}/title`, { title: newTitle.current }, componentId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent user from navigating away if the spaced update is not done.
|
||||||
|
useEffect(() => {
|
||||||
|
appContext.addBeforeUnloadListener(() => spacedUpdate.isAllSavedAndTriggerUpdate());
|
||||||
|
}, []);
|
||||||
|
useTriliumEvents([ "beforeNoteSwitch", "beforeNoteContextRemove" ], () => spacedUpdate.updateNowIfNecessary());
|
||||||
|
|
||||||
|
// Manage focus.
|
||||||
|
const textBoxRef = useRef<HTMLInputElement>(null);
|
||||||
|
const isNewNote = useRef<boolean>();
|
||||||
|
useTriliumEvents([ "focusOnTitle", "focusAndSelectTitle" ], (e, eventName) => {
|
||||||
|
if (noteContext?.isActive() && textBoxRef.current) {
|
||||||
|
textBoxRef.current.focus();
|
||||||
|
if (eventName === "focusAndSelectTitle") {
|
||||||
|
textBoxRef.current.select();
|
||||||
|
}
|
||||||
|
isNewNote.current = ("isNewNote" in e ? e.isNewNote : false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="note-title-widget">
|
||||||
|
{note && <FormTextBox
|
||||||
|
inputRef={textBoxRef}
|
||||||
|
autocomplete="off"
|
||||||
|
currentValue={(!isReadOnly ? title : navigationTitle) ?? ""}
|
||||||
|
placeholder={t("note_title.placeholder")}
|
||||||
|
className={`note-title ${isProtected ? "protected" : ""}`}
|
||||||
|
tabIndex={100}
|
||||||
|
readOnly={isReadOnly}
|
||||||
|
onChange={(newValue) => {
|
||||||
|
newTitle.current = newValue;
|
||||||
|
spacedUpdate.scheduleUpdate();
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
// Focus on the note content when pressing enter.
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
parentComponent.triggerCommand("focusOnDetail", { ntxId: noteContext?.ntxId });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === "Escape" && isNewNote.current && noteContext?.isActive() && note) {
|
||||||
|
branches.deleteNotes(Object.values(note.parentToBranch));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
spacedUpdate.updateNowIfNecessary();
|
||||||
|
isNewNote.current = false;
|
||||||
|
}}
|
||||||
|
/>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
import { Dropdown } from "bootstrap";
|
|
||||||
import { NOTE_TYPES } from "../services/note_types.js";
|
|
||||||
import { t } from "../services/i18n.js";
|
|
||||||
import dialogService from "../services/dialog.js";
|
|
||||||
import mimeTypesService from "../services/mime_types.js";
|
|
||||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
|
||||||
import server from "../services/server.js";
|
|
||||||
import type { EventData } from "../components/app_context.js";
|
|
||||||
import type { NoteType } from "../entities/fnote.js";
|
|
||||||
import type FNote from "../entities/fnote.js";
|
|
||||||
|
|
||||||
const NOT_SELECTABLE_NOTE_TYPES = NOTE_TYPES.filter((nt) => nt.reserved || nt.static).map((nt) => nt.type);
|
|
||||||
|
|
||||||
const TPL = /*html*/`
|
|
||||||
<div class="dropdown note-type-widget">
|
|
||||||
<style>
|
|
||||||
.note-type-dropdown {
|
|
||||||
max-height: 500px;
|
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<button type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm dropdown-toggle select-button note-type-button">
|
|
||||||
<span class="note-type-desc"></span>
|
|
||||||
<span class="caret"></span>
|
|
||||||
</button>
|
|
||||||
<div class="note-type-dropdown dropdown-menu dropdown-menu-left tn-dropdown-list"></div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default class NoteTypeWidget extends NoteContextAwareWidget {
|
|
||||||
|
|
||||||
private dropdown!: Dropdown;
|
|
||||||
private $noteTypeDropdown!: JQuery<HTMLElement>;
|
|
||||||
private $noteTypeButton!: JQuery<HTMLElement>;
|
|
||||||
private $noteTypeDesc!: JQuery<HTMLElement>;
|
|
||||||
|
|
||||||
doRender() {
|
|
||||||
this.$widget = $(TPL);
|
|
||||||
|
|
||||||
this.dropdown = Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']")[0]);
|
|
||||||
|
|
||||||
this.$widget.on("show.bs.dropdown", () => this.renderDropdown());
|
|
||||||
|
|
||||||
this.$noteTypeDropdown = this.$widget.find(".note-type-dropdown");
|
|
||||||
this.$noteTypeButton = this.$widget.find(".note-type-button");
|
|
||||||
this.$noteTypeDesc = this.$widget.find(".note-type-desc");
|
|
||||||
|
|
||||||
this.$widget.on("click", ".dropdown-item", () => this.dropdown.toggle());
|
|
||||||
}
|
|
||||||
|
|
||||||
async refreshWithNote(note: FNote) {
|
|
||||||
this.$noteTypeButton.prop("disabled", () => NOT_SELECTABLE_NOTE_TYPES.includes(note.type));
|
|
||||||
|
|
||||||
this.$noteTypeDesc.text(await this.findTypeTitle(note.type, note.mime));
|
|
||||||
|
|
||||||
this.dropdown.hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** the actual body is rendered lazily on note-type button click */
|
|
||||||
async renderDropdown() {
|
|
||||||
this.$noteTypeDropdown.empty();
|
|
||||||
|
|
||||||
if (!this.note) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const noteType of NOTE_TYPES.filter((nt) => !nt.reserved && !nt.static)) {
|
|
||||||
let $typeLink: JQuery<HTMLElement>;
|
|
||||||
|
|
||||||
const $title = $("<span>").text(noteType.title);
|
|
||||||
|
|
||||||
if (noteType.isNew) {
|
|
||||||
$title.append($(`<span class="badge new-note-type-badge">`).text(t("note_types.new-feature")));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (noteType.isBeta) {
|
|
||||||
$title.append($(`<span class="badge">`).text(t("note_types.beta-feature")));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (noteType.type !== "code") {
|
|
||||||
$typeLink = $('<a class="dropdown-item">')
|
|
||||||
.attr("data-note-type", noteType.type)
|
|
||||||
.append('<span class="check">✓</span> ')
|
|
||||||
.append($title)
|
|
||||||
.on("click", (e) => {
|
|
||||||
const type = $typeLink.attr("data-note-type");
|
|
||||||
const noteType = NOTE_TYPES.find((nt) => nt.type === type);
|
|
||||||
|
|
||||||
if (noteType) {
|
|
||||||
this.save(noteType.type, noteType.mime);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.$noteTypeDropdown.append('<div class="dropdown-divider"></div>');
|
|
||||||
$typeLink = $('<a class="dropdown-item disabled">').attr("data-note-type", noteType.type).append('<span class="check">✓</span> ').append($("<strong>").text(noteType.title));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.note.type === noteType.type) {
|
|
||||||
$typeLink.addClass("selected");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$noteTypeDropdown.append($typeLink);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const mimeType of mimeTypesService.getMimeTypes()) {
|
|
||||||
if (!mimeType.enabled) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const $mimeLink = $('<a class="dropdown-item">')
|
|
||||||
.attr("data-mime-type", mimeType.mime)
|
|
||||||
.append('<span class="check">✓</span> ')
|
|
||||||
.append($("<span>").text(mimeType.title))
|
|
||||||
.on("click", (e) => {
|
|
||||||
const $link = $(e.target).closest(".dropdown-item");
|
|
||||||
|
|
||||||
this.save("code", $link.attr("data-mime-type") ?? "");
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.note.type === "code" && this.note.mime === mimeType.mime) {
|
|
||||||
$mimeLink.addClass("selected");
|
|
||||||
|
|
||||||
this.$noteTypeDesc.text(mimeType.title);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$noteTypeDropdown.append($mimeLink);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async findTypeTitle(type: NoteType, mime: string) {
|
|
||||||
if (type === "code") {
|
|
||||||
const mimeTypes = mimeTypesService.getMimeTypes();
|
|
||||||
const found = mimeTypes.find((mt) => mt.mime === mime);
|
|
||||||
|
|
||||||
return found ? found.title : mime;
|
|
||||||
} else {
|
|
||||||
const noteType = NOTE_TYPES.find((nt) => nt.type === type);
|
|
||||||
|
|
||||||
return noteType ? noteType.title : type;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async save(type: NoteType, mime?: string) {
|
|
||||||
if (type === this.note?.type && mime === this.note?.mime) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type !== this.note?.type && !(await this.confirmChangeIfContent())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await server.put(`notes/${this.noteId}/type`, { type, mime });
|
|
||||||
}
|
|
||||||
|
|
||||||
async confirmChangeIfContent() {
|
|
||||||
if (!this.note) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const blob = await this.note.getBlob();
|
|
||||||
|
|
||||||
if (!blob?.content || !blob.content.trim().length) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await dialogService.confirm(t("note_types.confirm-change"));
|
|
||||||
}
|
|
||||||
|
|
||||||
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
|
||||||
if (loadResults.isNoteReloaded(this.noteId)) {
|
|
||||||
this.refresh();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
import { t } from "../../services/i18n.js";
|
import { t } from "../services/i18n.js";
|
||||||
import server from "../../services/server.js";
|
import server from "../services/server.js";
|
||||||
import ws from "../../services/ws.js";
|
import ws from "../services/ws.js";
|
||||||
import treeService from "../../services/tree.js";
|
import treeService from "../services/tree.js";
|
||||||
import noteAutocompleteService from "../../services/note_autocomplete.js";
|
import noteAutocompleteService from "../services/note_autocomplete.js";
|
||||||
import NoteContextAwareWidget from "../note_context_aware_widget.js";
|
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
||||||
import attributeService from "../../services/attributes.js";
|
import attributeService from "../services/attributes.js";
|
||||||
import options from "../../services/options.js";
|
import options from "../services/options.js";
|
||||||
import utils from "../../services/utils.js";
|
import utils from "../services/utils.js";
|
||||||
import type FNote from "../../entities/fnote.js";
|
import type FNote from "../entities/fnote.js";
|
||||||
import type { Attribute } from "../../services/attribute_parser.js";
|
import type { Attribute } from "../services/attribute_parser.js";
|
||||||
import type FAttribute from "../../entities/fattribute.js";
|
import type FAttribute from "../entities/fattribute.js";
|
||||||
import type { EventData } from "../../components/app_context.js";
|
import type { EventData } from "../components/app_context.js";
|
||||||
|
|
||||||
const TPL = /*html*/`
|
const TPL = /*html*/`
|
||||||
<div class="promoted-attributes-widget">
|
<div class="promoted-attributes-widget">
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import type { EventData } from "../components/app_context.js";
|
|
||||||
import type FNote from "../entities/fnote.js";
|
|
||||||
import { t } from "../services/i18n.js";
|
|
||||||
import protectedSessionService from "../services/protected_session.js";
|
|
||||||
import SwitchWidget from "./switch.js";
|
|
||||||
|
|
||||||
export default class ProtectedNoteSwitchWidget extends SwitchWidget {
|
|
||||||
doRender() {
|
|
||||||
super.doRender();
|
|
||||||
|
|
||||||
this.switchOnName = t("protect_note.toggle-on");
|
|
||||||
this.switchOnTooltip = t("protect_note.toggle-on-hint");
|
|
||||||
|
|
||||||
this.switchOffName = t("protect_note.toggle-off");
|
|
||||||
this.switchOffTooltip = t("protect_note.toggle-off-hint");
|
|
||||||
}
|
|
||||||
|
|
||||||
switchOn() {
|
|
||||||
if (this.noteId) {
|
|
||||||
protectedSessionService.protectNote(this.noteId, true, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switchOff() {
|
|
||||||
if (this.noteId) {
|
|
||||||
protectedSessionService.protectNote(this.noteId, false, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async refreshWithNote(note: FNote) {
|
|
||||||
this.isToggled = note.isProtected;
|
|
||||||
}
|
|
||||||
|
|
||||||
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
|
||||||
if (loadResults.isNoteReloaded(this.noteId)) {
|
|
||||||
this.refresh();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,19 @@
|
|||||||
|
import { CommandNames } from "../../components/app_context";
|
||||||
|
|
||||||
interface ActionButtonProps {
|
interface ActionButtonProps {
|
||||||
text: string;
|
text: string;
|
||||||
|
titlePosition?: "bottom"; // TODO: Use it
|
||||||
icon: string;
|
icon: string;
|
||||||
onClick?: () => void;
|
className?: string;
|
||||||
|
onClick?: (e: MouseEvent) => void;
|
||||||
|
triggerCommand?: CommandNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ActionButton({ text, icon, onClick }: ActionButtonProps) {
|
export default function ActionButton({ text, icon, className, onClick, triggerCommand }: ActionButtonProps) {
|
||||||
return <button
|
return <button
|
||||||
class={`icon-action ${icon}`}
|
class={`icon-action ${icon} ${className ?? ""}`}
|
||||||
title={text}
|
title={text}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
data-trigger-command={triggerCommand}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import type { RefObject } from "preact";
|
import type { RefObject } from "preact";
|
||||||
import type { CSSProperties } from "preact/compat";
|
import type { CSSProperties } from "preact/compat";
|
||||||
import { useRef, useMemo } from "preact/hooks";
|
import { useMemo } from "preact/hooks";
|
||||||
import { memo } from "preact/compat";
|
import { memo } from "preact/compat";
|
||||||
|
import { CommandNames } from "../../components/app_context";
|
||||||
|
|
||||||
interface ButtonProps {
|
export interface ButtonProps {
|
||||||
name?: string;
|
name?: string;
|
||||||
/** Reference to the button element. Mostly useful for requesting focus. */
|
/** Reference to the button element. Mostly useful for requesting focus. */
|
||||||
buttonRef?: RefObject<HTMLButtonElement>;
|
buttonRef?: RefObject<HTMLButtonElement>;
|
||||||
@@ -17,9 +18,11 @@ interface ButtonProps {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
size?: "normal" | "small" | "micro";
|
size?: "normal" | "small" | "micro";
|
||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
|
triggerCommand?: CommandNames;
|
||||||
|
title?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Button = memo(({ name, buttonRef: _buttonRef, className, text, onClick, keyboardShortcut, icon, primary, disabled, size, style }: ButtonProps) => {
|
const Button = memo(({ name, buttonRef, className, text, onClick, keyboardShortcut, icon, primary, disabled, size, style, triggerCommand, ...restProps }: ButtonProps) => {
|
||||||
// Memoize classes array to prevent recreation
|
// Memoize classes array to prevent recreation
|
||||||
const classes = useMemo(() => {
|
const classes = useMemo(() => {
|
||||||
const classList: string[] = ["btn"];
|
const classList: string[] = ["btn"];
|
||||||
@@ -39,8 +42,6 @@ const Button = memo(({ name, buttonRef: _buttonRef, className, text, onClick, ke
|
|||||||
return classList.join(" ");
|
return classList.join(" ");
|
||||||
}, [primary, className, size]);
|
}, [primary, className, size]);
|
||||||
|
|
||||||
const buttonRef = _buttonRef ?? useRef<HTMLButtonElement>(null);
|
|
||||||
|
|
||||||
// Memoize keyboard shortcut rendering
|
// Memoize keyboard shortcut rendering
|
||||||
const shortcutElements = useMemo(() => {
|
const shortcutElements = useMemo(() => {
|
||||||
if (!keyboardShortcut) return null;
|
if (!keyboardShortcut) return null;
|
||||||
@@ -57,11 +58,13 @@ const Button = memo(({ name, buttonRef: _buttonRef, className, text, onClick, ke
|
|||||||
<button
|
<button
|
||||||
name={name}
|
name={name}
|
||||||
className={classes}
|
className={classes}
|
||||||
type={onClick ? "button" : "submit"}
|
type={onClick || triggerCommand ? "button" : "submit"}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
style={style}
|
style={style}
|
||||||
|
data-trigger-command={triggerCommand}
|
||||||
|
{...restProps}
|
||||||
>
|
>
|
||||||
{icon && <span className={`bx ${icon}`}></span>}
|
{icon && <span className={`bx ${icon}`}></span>}
|
||||||
{text} {shortcutElements}
|
{text} {shortcutElements}
|
||||||
|
|||||||
106
apps/client/src/widgets/react/CKEditor.tsx
Normal file
106
apps/client/src/widgets/react/CKEditor.tsx
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import type { CKTextEditor, AttributeEditor, EditorConfig, ModelPosition } from "@triliumnext/ckeditor5";
|
||||||
|
import { useEffect, useImperativeHandle, useRef } from "preact/compat";
|
||||||
|
import { MutableRef } from "preact/hooks";
|
||||||
|
|
||||||
|
export interface CKEditorApi {
|
||||||
|
focus(): void;
|
||||||
|
/**
|
||||||
|
* Imperatively sets the text in the editor.
|
||||||
|
*
|
||||||
|
* Prefer setting `currentValue` prop where possible.
|
||||||
|
*
|
||||||
|
* @param text text to set in the editor
|
||||||
|
*/
|
||||||
|
setText(text: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CKEditorOpts {
|
||||||
|
apiRef: MutableRef<CKEditorApi | undefined>;
|
||||||
|
currentValue?: string;
|
||||||
|
className: string;
|
||||||
|
tabIndex?: number;
|
||||||
|
config: EditorConfig;
|
||||||
|
editor: typeof AttributeEditor;
|
||||||
|
disableNewlines?: boolean;
|
||||||
|
disableSpellcheck?: boolean;
|
||||||
|
onChange?: (newValue?: string) => void;
|
||||||
|
onClick?: (e: MouseEvent, pos?: ModelPosition | null) => void;
|
||||||
|
onKeyDown?: (e: KeyboardEvent) => void;
|
||||||
|
onBlur?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CKEditor({ apiRef, currentValue, editor, config, disableNewlines, disableSpellcheck, onChange, onClick, ...restProps }: CKEditorOpts) {
|
||||||
|
const editorContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const textEditorRef = useRef<CKTextEditor>(null);
|
||||||
|
useImperativeHandle(apiRef, () => {
|
||||||
|
return {
|
||||||
|
focus() {
|
||||||
|
editorContainerRef.current?.focus();
|
||||||
|
textEditorRef.current?.model.change((writer) => {
|
||||||
|
const documentRoot = textEditorRef.current?.editing.model.document.getRoot();
|
||||||
|
if (documentRoot) {
|
||||||
|
writer.setSelection(writer.createPositionAt(documentRoot, "end"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setText(text: string) {
|
||||||
|
textEditorRef.current?.setData(text);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [ editorContainerRef ]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!editorContainerRef.current) return;
|
||||||
|
|
||||||
|
editor.create(editorContainerRef.current, config).then((textEditor) => {
|
||||||
|
textEditorRef.current = textEditor;
|
||||||
|
|
||||||
|
if (disableNewlines) {
|
||||||
|
textEditor.editing.view.document.on(
|
||||||
|
"enter",
|
||||||
|
(event, data) => {
|
||||||
|
// disable entering new line - see https://github.com/ckeditor/ckeditor5/issues/9422
|
||||||
|
data.preventDefault();
|
||||||
|
event.stop();
|
||||||
|
},
|
||||||
|
{ priority: "high" }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (disableSpellcheck) {
|
||||||
|
const documentRoot = textEditor.editing.view.document.getRoot();
|
||||||
|
if (documentRoot) {
|
||||||
|
textEditor.editing.view.change((writer) => writer.setAttribute("spellcheck", "false", documentRoot));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onChange) {
|
||||||
|
textEditor.model.document.on("change:data", () => {
|
||||||
|
onChange(textEditor.getData())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentValue) {
|
||||||
|
textEditor.setData(currentValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!textEditorRef.current) return;
|
||||||
|
textEditorRef.current.setData(currentValue ?? "");
|
||||||
|
}, [ currentValue ]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={editorContainerRef}
|
||||||
|
onClick={(e) => {
|
||||||
|
if (onClick) {
|
||||||
|
const pos = textEditorRef.current?.model.document.selection.getFirstPosition();
|
||||||
|
onClick(e, pos);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,17 +1,31 @@
|
|||||||
import { Dropdown as BootstrapDropdown } from "bootstrap";
|
import { Dropdown as BootstrapDropdown } from "bootstrap";
|
||||||
import { ComponentChildren } from "preact";
|
import { ComponentChildren } from "preact";
|
||||||
import { useEffect, useRef } from "preact/hooks";
|
import { CSSProperties } from "preact/compat";
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
|
||||||
|
import { useUniqueName } from "./hooks";
|
||||||
|
|
||||||
interface DropdownProps {
|
export interface DropdownProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
buttonClassName?: string;
|
||||||
isStatic?: boolean;
|
isStatic?: boolean;
|
||||||
children: ComponentChildren;
|
children: ComponentChildren;
|
||||||
|
title?: string;
|
||||||
|
dropdownContainerStyle?: CSSProperties;
|
||||||
|
dropdownContainerClassName?: string;
|
||||||
|
hideToggleArrow?: boolean;
|
||||||
|
/** If set to true, then the dropdown button will be considered an icon action (without normal border and sized for icons only). */
|
||||||
|
iconAction?: boolean;
|
||||||
|
noSelectButtonStyle?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
text?: ComponentChildren;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Dropdown({ className, isStatic, children }: DropdownProps) {
|
export default function Dropdown({ className, buttonClassName, isStatic, children, title, text, dropdownContainerStyle, dropdownContainerClassName, hideToggleArrow, iconAction, disabled, noSelectButtonStyle }: DropdownProps) {
|
||||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||||
const triggerRef = useRef<HTMLButtonElement | null>(null);
|
const triggerRef = useRef<HTMLButtonElement | null>(null);
|
||||||
|
|
||||||
|
const [ shown, setShown ] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!triggerRef.current) return;
|
if (!triggerRef.current) return;
|
||||||
|
|
||||||
@@ -19,33 +33,54 @@ export default function Dropdown({ className, isStatic, children }: DropdownProp
|
|||||||
return () => dropdown.dispose();
|
return () => dropdown.dispose();
|
||||||
}, []); // Add dependency array
|
}, []); // Add dependency array
|
||||||
|
|
||||||
|
const onShown = useCallback(() => {
|
||||||
|
setShown(true);
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const onHidden = useCallback(() => {
|
||||||
|
setShown(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!dropdownRef.current) return;
|
if (!dropdownRef.current) return;
|
||||||
|
|
||||||
const handleHide = () => {
|
|
||||||
// Remove console.log from production code
|
|
||||||
};
|
|
||||||
|
|
||||||
const $dropdown = $(dropdownRef.current);
|
const $dropdown = $(dropdownRef.current);
|
||||||
$dropdown.on("hide.bs.dropdown", handleHide);
|
$dropdown.on("show.bs.dropdown", onShown);
|
||||||
|
$dropdown.on("hide.bs.dropdown", onHidden);
|
||||||
|
|
||||||
// Add proper cleanup
|
// Add proper cleanup
|
||||||
return () => {
|
return () => {
|
||||||
$dropdown.off("hide.bs.dropdown", handleHide);
|
$dropdown.off("show.bs.dropdown", onShown);
|
||||||
|
$dropdown.off("hide.bs.dropdown", onHidden);
|
||||||
};
|
};
|
||||||
}, []); // Add dependency array
|
}, []); // Add dependency array
|
||||||
|
|
||||||
|
const ariaId = useUniqueName("button");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={dropdownRef} class="dropdown" style={{ display: "flex" }}>
|
<div ref={dropdownRef} class={`dropdown ${className ?? ""}`} style={{ display: "flex" }}>
|
||||||
<button
|
<button
|
||||||
|
className={`${iconAction ? "icon-action" : "btn"} ${!noSelectButtonStyle ? "select-button" : ""} ${buttonClassName ?? ""} ${!hideToggleArrow ? "dropdown-toggle" : ""}`}
|
||||||
ref={triggerRef}
|
ref={triggerRef}
|
||||||
type="button"
|
type="button"
|
||||||
style={{ display: "none" }}
|
|
||||||
data-bs-toggle="dropdown"
|
data-bs-toggle="dropdown"
|
||||||
data-bs-display={ isStatic ? "static" : undefined } />
|
data-bs-display={ isStatic ? "static" : undefined }
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-expanded="false"
|
||||||
|
title={title}
|
||||||
|
id={ariaId}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
<span className="caret"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<div class={`dropdown-menu ${className ?? ""} ${isStatic ? "static" : undefined}`}>
|
<div
|
||||||
{children}
|
class={`dropdown-menu ${isStatic ? "static" : ""} ${dropdownContainerClassName ?? ""} tn-dropdown-list`}
|
||||||
|
style={dropdownContainerStyle}
|
||||||
|
aria-labelledby={ariaId}
|
||||||
|
>
|
||||||
|
{shown && children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { CSSProperties, memo } from "preact/compat";
|
|||||||
import { useUniqueName } from "./hooks";
|
import { useUniqueName } from "./hooks";
|
||||||
|
|
||||||
interface FormCheckboxProps {
|
interface FormCheckboxProps {
|
||||||
id?: string;
|
|
||||||
name?: string;
|
name?: string;
|
||||||
label: string | ComponentChildren;
|
label: string | ComponentChildren;
|
||||||
/**
|
/**
|
||||||
@@ -19,9 +18,9 @@ interface FormCheckboxProps {
|
|||||||
containerStyle?: CSSProperties;
|
containerStyle?: CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FormCheckbox = memo(({ name, id: _id, disabled, label, currentValue, onChange, hint, containerStyle }: FormCheckboxProps) => {
|
const FormCheckbox = memo(({ name, disabled, label, currentValue, onChange, hint, containerStyle }: FormCheckboxProps) => {
|
||||||
const id = _id ?? useUniqueName(name);
|
|
||||||
const labelRef = useRef<HTMLLabelElement>(null);
|
const labelRef = useRef<HTMLLabelElement>(null);
|
||||||
|
const id = useUniqueName(name);
|
||||||
|
|
||||||
// Fix: Move useEffect outside conditional
|
// Fix: Move useEffect outside conditional
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
30
apps/client/src/widgets/react/FormDropdownList.tsx
Normal file
30
apps/client/src/widgets/react/FormDropdownList.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import Dropdown, { DropdownProps } from "./Dropdown";
|
||||||
|
import { FormListItem } from "./FormList";
|
||||||
|
|
||||||
|
interface FormDropdownList<T> extends Omit<DropdownProps, "children"> {
|
||||||
|
values: T[];
|
||||||
|
keyProperty: keyof T;
|
||||||
|
titleProperty: keyof T;
|
||||||
|
descriptionProperty?: keyof T;
|
||||||
|
currentValue: string;
|
||||||
|
onChange(newValue: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FormDropdownList<T>({ values, keyProperty, titleProperty, descriptionProperty, currentValue, onChange, ...restProps }: FormDropdownList<T>) {
|
||||||
|
const currentValueData = values.find(value => value[keyProperty] === currentValue);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown text={currentValueData?.[titleProperty] ?? ""} {...restProps}>
|
||||||
|
{values.map(item => (
|
||||||
|
<FormListItem
|
||||||
|
onClick={() => onChange(item[keyProperty] as string)}
|
||||||
|
checked={currentValue === item[keyProperty]}
|
||||||
|
description={descriptionProperty && item[descriptionProperty] as string}
|
||||||
|
selected={currentValue === item[keyProperty]}
|
||||||
|
>
|
||||||
|
{item[titleProperty] as string}
|
||||||
|
</FormListItem>
|
||||||
|
))}
|
||||||
|
</Dropdown>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,13 +1,48 @@
|
|||||||
|
import { Ref } from "preact";
|
||||||
|
import Button, { ButtonProps } from "./Button";
|
||||||
|
import { useRef } from "preact/hooks";
|
||||||
|
|
||||||
interface FormFileUploadProps {
|
interface FormFileUploadProps {
|
||||||
|
name?: string;
|
||||||
onChange: (files: FileList | null) => void;
|
onChange: (files: FileList | null) => void;
|
||||||
multiple?: boolean;
|
multiple?: boolean;
|
||||||
|
hidden?: boolean;
|
||||||
|
inputRef?: Ref<HTMLInputElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FormFileUpload({ onChange, multiple }: FormFileUploadProps) {
|
export default function FormFileUpload({ inputRef, name, onChange, multiple, hidden }: FormFileUploadProps) {
|
||||||
return (
|
return (
|
||||||
<label class="tn-file-input tn-input-field">
|
<label class="tn-file-input tn-input-field" style={hidden ? { display: "none" } : undefined}>
|
||||||
<input type="file" class="form-control-file" multiple={multiple}
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
name={name}
|
||||||
|
type="file"
|
||||||
|
class="form-control-file"
|
||||||
|
multiple={multiple}
|
||||||
onChange={e => onChange((e.target as HTMLInputElement).files)} />
|
onChange={e => onChange((e.target as HTMLInputElement).files)} />
|
||||||
</label>
|
</label>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combination of a button with a hidden file upload field.
|
||||||
|
*
|
||||||
|
* @param param the change listener for the file upload and the properties for the button.
|
||||||
|
*/
|
||||||
|
export function FormFileUploadButton({ onChange, ...buttonProps }: Omit<ButtonProps, "onClick"> & Pick<FormFileUploadProps, "onChange">) {
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
{...buttonProps}
|
||||||
|
onClick={() => inputRef.current?.click()}
|
||||||
|
/>
|
||||||
|
<FormFileUpload
|
||||||
|
inputRef={inputRef}
|
||||||
|
hidden
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ interface FormGroupProps {
|
|||||||
label?: string;
|
label?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
children: VNode<any>;
|
children: VNode<any>;
|
||||||
description?: string | ComponentChildren;
|
description?: string | ComponentChildren;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
@@ -25,7 +26,7 @@ export default function FormGroup({ name, label, title, className, children, des
|
|||||||
|
|
||||||
{childWithId}
|
{childWithId}
|
||||||
|
|
||||||
{description && <small className="form-text">{description}</small>}
|
{description && <div><small className="form-text">{description}</small></div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
9
apps/client/src/widgets/react/FormList.css
Normal file
9
apps/client/src/widgets/react/FormList.css
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
.dropdown-item .description {
|
||||||
|
font-size: small;
|
||||||
|
color: var(--muted-text-color);
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item span.bx {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ import { Dropdown as BootstrapDropdown } from "bootstrap";
|
|||||||
import { ComponentChildren } from "preact";
|
import { ComponentChildren } from "preact";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
import { useEffect, useMemo, useRef, type CSSProperties } from "preact/compat";
|
import { useEffect, useMemo, useRef, type CSSProperties } from "preact/compat";
|
||||||
|
import "./FormList.css";
|
||||||
|
import { CommandNames } from "../../components/app_context";
|
||||||
|
|
||||||
interface FormListOpts {
|
interface FormListOpts {
|
||||||
children: ComponentChildren;
|
children: ComponentChildren;
|
||||||
@@ -33,6 +35,7 @@ export default function FormList({ children, onSelect, style, fullHeight }: Form
|
|||||||
const style: CSSProperties = {};
|
const style: CSSProperties = {};
|
||||||
if (fullHeight) {
|
if (fullHeight) {
|
||||||
style.height = "100%";
|
style.height = "100%";
|
||||||
|
style.overflow = "auto";
|
||||||
}
|
}
|
||||||
return style;
|
return style;
|
||||||
}, [ fullHeight ]);
|
}, [ fullHeight ]);
|
||||||
@@ -51,7 +54,8 @@ export default function FormList({ children, onSelect, style, fullHeight }: Form
|
|||||||
...builtinStyles,
|
...builtinStyles,
|
||||||
position: "relative",
|
position: "relative",
|
||||||
}} onClick={(e) => {
|
}} onClick={(e) => {
|
||||||
const value = (e.target as HTMLElement)?.dataset?.value;
|
const dropdownItem = (e.target as HTMLElement).closest(".dropdown-item") as HTMLElement | null;
|
||||||
|
const value = dropdownItem?.dataset?.value;
|
||||||
if (value && onSelect) {
|
if (value && onSelect) {
|
||||||
onSelect(value);
|
onSelect(value);
|
||||||
}
|
}
|
||||||
@@ -63,23 +67,50 @@ export default function FormList({ children, onSelect, style, fullHeight }: Form
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FormListBadge {
|
||||||
|
className?: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface FormListItemOpts {
|
interface FormListItemOpts {
|
||||||
children: ComponentChildren;
|
children: ComponentChildren;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
value?: string;
|
value?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
|
badges?: FormListBadge[];
|
||||||
|
disabled?: boolean;
|
||||||
|
checked?: boolean | null;
|
||||||
|
selected?: boolean;
|
||||||
|
onClick?: (e: MouseEvent) => void;
|
||||||
|
triggerCommand?: CommandNames;
|
||||||
|
description?: string;
|
||||||
|
className?: string;
|
||||||
|
rtl?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FormListItem({ children, icon, value, title, active }: FormListItemOpts) {
|
export function FormListItem({ children, icon, value, title, active, badges, disabled, checked, onClick, description, selected, rtl, triggerCommand }: FormListItemOpts) {
|
||||||
|
if (checked) {
|
||||||
|
icon = "bx bx-check";
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
class={`dropdown-item ${active ? "active" : ""}`}
|
class={`dropdown-item ${active ? "active" : ""} ${disabled ? "disabled" : ""} ${selected ? "selected" : ""}`}
|
||||||
data-value={value} title={title}
|
data-value={value} title={title}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
|
onClick={onClick}
|
||||||
|
data-trigger-command={triggerCommand}
|
||||||
|
dir={rtl ? "rtl" : undefined}
|
||||||
>
|
>
|
||||||
<Icon icon={icon} />
|
<Icon icon={icon} />
|
||||||
|
<div>
|
||||||
{children}
|
{children}
|
||||||
|
{badges && badges.map(({ className, text }) => (
|
||||||
|
<span className={`badge ${className ?? ""}`}>{text}</span>
|
||||||
|
))}
|
||||||
|
{description && <div className="description">{description}</div>}
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -95,3 +126,7 @@ export function FormListHeader({ text }: FormListHeaderOpts) {
|
|||||||
</li>
|
</li>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function FormDropdownDivider() {
|
||||||
|
return <div className="dropdown-divider" />;
|
||||||
|
}
|
||||||
@@ -20,16 +20,18 @@ interface ValueConfig<T, Q> {
|
|||||||
|
|
||||||
interface FormSelectProps<T, Q> extends ValueConfig<T, Q> {
|
interface FormSelectProps<T, Q> extends ValueConfig<T, Q> {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
name?: string;
|
||||||
onChange: OnChangeListener;
|
onChange: OnChangeListener;
|
||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Combobox component that takes in any object array as data. Each item of the array is rendered as an item, and the key and values are obtained by looking into the object by a specified key.
|
* Combobox component that takes in any object array as data. Each item of the array is rendered as an item, and the key and values are obtained by looking into the object by a specified key.
|
||||||
*/
|
*/
|
||||||
export default function FormSelect<T>({ id, onChange, style, ...restProps }: FormSelectProps<T, T>) {
|
export default function FormSelect<T>({ name, id, onChange, style, className, ...restProps }: FormSelectProps<T, T>) {
|
||||||
return (
|
return (
|
||||||
<FormSelectBody id={id} onChange={onChange} style={style}>
|
<FormSelectBody name={name} id={id} onChange={onChange} style={style} className={className}>
|
||||||
<FormSelectGroup {...restProps} />
|
<FormSelectGroup {...restProps} />
|
||||||
</FormSelectBody>
|
</FormSelectBody>
|
||||||
);
|
);
|
||||||
@@ -38,27 +40,35 @@ export default function FormSelect<T>({ id, onChange, style, ...restProps }: For
|
|||||||
/**
|
/**
|
||||||
* Similar to {@link FormSelect}, but the top-level elements are actually groups.
|
* Similar to {@link FormSelect}, but the top-level elements are actually groups.
|
||||||
*/
|
*/
|
||||||
export function FormSelectWithGroups<T>({ id, values, keyProperty, titleProperty, currentValue, onChange }: FormSelectProps<T, FormSelectGroup<T>>) {
|
export function FormSelectWithGroups<T>({ name, id, values, keyProperty, titleProperty, currentValue, onChange, ...restProps }: FormSelectProps<T, FormSelectGroup<T> | T>) {
|
||||||
return (
|
return (
|
||||||
<FormSelectBody id={id} onChange={onChange}>
|
<FormSelectBody name={name} id={id} onChange={onChange} {...restProps}>
|
||||||
{values.map(({ title, items }) => {
|
{values.map((item) => {
|
||||||
|
if (!item) return <></>;
|
||||||
|
if (typeof item === "object" && "items" in item) {
|
||||||
return (
|
return (
|
||||||
<optgroup label={title}>
|
<optgroup label={item.title}>
|
||||||
<FormSelectGroup values={items} keyProperty={keyProperty} titleProperty={titleProperty} currentValue={currentValue} />
|
<FormSelectGroup values={item.items} keyProperty={keyProperty} titleProperty={titleProperty} currentValue={currentValue} />
|
||||||
</optgroup>
|
</optgroup>
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<FormSelectGroup values={[ item ]} keyProperty={keyProperty} titleProperty={titleProperty} currentValue={currentValue} />
|
||||||
|
)
|
||||||
|
}
|
||||||
})}
|
})}
|
||||||
</FormSelectBody>
|
</FormSelectBody>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function FormSelectBody({ id, children, onChange, style }: { id?: string, children: ComponentChildren, onChange: OnChangeListener, style?: CSSProperties }) {
|
function FormSelectBody({ id, name, children, onChange, style, className }: { id?: string, name?: string, children: ComponentChildren, onChange: OnChangeListener, style?: CSSProperties, className?: string }) {
|
||||||
return (
|
return (
|
||||||
<select
|
<select
|
||||||
id={id}
|
id={id}
|
||||||
class="form-select"
|
name={name}
|
||||||
onChange={e => onChange((e.target as HTMLInputElement).value)}
|
onChange={e => onChange((e.target as HTMLInputElement).value)}
|
||||||
style={style}
|
style={style}
|
||||||
|
className={`form-select ${className ?? ""}`}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</select>
|
</select>
|
||||||
@@ -69,10 +79,10 @@ function FormSelectGroup<T>({ values, keyProperty, titleProperty, currentValue }
|
|||||||
return values.map(item => {
|
return values.map(item => {
|
||||||
return (
|
return (
|
||||||
<option
|
<option
|
||||||
value={item[keyProperty] as any}
|
value={item[keyProperty] as string | number}
|
||||||
selected={item[keyProperty] === currentValue}
|
selected={item[keyProperty] === currentValue}
|
||||||
>
|
>
|
||||||
{item[titleProperty ?? keyProperty] ?? item[keyProperty] as any}
|
{item[titleProperty ?? keyProperty] ?? item[keyProperty] as string | number}
|
||||||
</option>
|
</option>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,18 +1,26 @@
|
|||||||
interface FormTextAreaProps {
|
import { RefObject, TextareaHTMLAttributes } from "preact/compat";
|
||||||
|
|
||||||
|
interface FormTextAreaProps extends Omit<TextareaHTMLAttributes, "onBlur" | "onChange"> {
|
||||||
id?: string;
|
id?: string;
|
||||||
currentValue: string;
|
currentValue: string;
|
||||||
|
onChange?(newValue: string): void;
|
||||||
onBlur?(newValue: string): void;
|
onBlur?(newValue: string): void;
|
||||||
rows: number;
|
inputRef?: RefObject<HTMLTextAreaElement>
|
||||||
}
|
}
|
||||||
export default function FormTextArea({ id, onBlur, rows, currentValue }: FormTextAreaProps) {
|
export default function FormTextArea({ inputRef, id, onBlur, onChange, currentValue, className, ...restProps }: FormTextAreaProps) {
|
||||||
return (
|
return (
|
||||||
<textarea
|
<textarea
|
||||||
|
ref={inputRef}
|
||||||
id={id}
|
id={id}
|
||||||
rows={rows}
|
className={`form-control ${className ?? ""}`}
|
||||||
|
onChange={(e) => {
|
||||||
|
onChange?.(e.currentTarget.value);
|
||||||
|
}}
|
||||||
onBlur={(e) => {
|
onBlur={(e) => {
|
||||||
onBlur?.(e.currentTarget.value);
|
onBlur?.(e.currentTarget.value);
|
||||||
}}
|
}}
|
||||||
style={{ width: "100%" }}
|
style={{ width: "100%" }}
|
||||||
|
{...restProps}
|
||||||
>{currentValue}</textarea>
|
>{currentValue}</textarea>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { InputHTMLAttributes, RefObject } from "preact/compat";
|
import { useEffect, type InputHTMLAttributes, type RefObject } from "preact/compat";
|
||||||
|
|
||||||
interface FormTextBoxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "onChange" | "onBlur" | "value"> {
|
interface FormTextBoxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "onChange" | "onBlur" | "value"> {
|
||||||
id?: string;
|
id?: string;
|
||||||
@@ -8,7 +8,7 @@ interface FormTextBoxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "
|
|||||||
inputRef?: RefObject<HTMLInputElement>;
|
inputRef?: RefObject<HTMLInputElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FormTextBox({ inputRef, className, type, currentValue, onChange, onBlur,...rest}: FormTextBoxProps) {
|
export default function FormTextBox({ inputRef, className, type, currentValue, onChange, onBlur, autoFocus, ...rest}: FormTextBoxProps) {
|
||||||
if (type === "number" && currentValue) {
|
if (type === "number" && currentValue) {
|
||||||
const { min, max } = rest;
|
const { min, max } = rest;
|
||||||
const currentValueNum = parseInt(currentValue, 10);
|
const currentValueNum = parseInt(currentValue, 10);
|
||||||
@@ -19,6 +19,12 @@ export default function FormTextBox({ inputRef, className, type, currentValue, o
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (autoFocus) {
|
||||||
|
inputRef?.current?.focus();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
|
|||||||
98
apps/client/src/widgets/react/FormToggle.css
Normal file
98
apps/client/src/widgets/react/FormToggle.css
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
.switch-widget {
|
||||||
|
--switch-track-width: 50px;
|
||||||
|
--switch-track-height: 24px;
|
||||||
|
--switch-off-track-background: var(--more-accented-background-color);
|
||||||
|
--switch-on-track-background: var(--main-text-color);
|
||||||
|
|
||||||
|
--switch-thumb-width: 16px;
|
||||||
|
--switch-thumb-height: 16px;
|
||||||
|
--switch-off-thumb-background: var(--main-background-color);
|
||||||
|
--switch-on-thumb-background: var(--main-background-color);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The track of the toggle switch */
|
||||||
|
|
||||||
|
.switch-widget .switch-button {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
margin-left: 8px;
|
||||||
|
width: var(--switch-track-width);
|
||||||
|
height: var(--switch-track-height);
|
||||||
|
border-radius: 24px;
|
||||||
|
background-color: var(--switch-off-track-background);
|
||||||
|
transition: background 200ms ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-widget .switch-button.on {
|
||||||
|
background: var(--switch-on-track-background);
|
||||||
|
transition: background 100ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The thumb of the toggle switch */
|
||||||
|
|
||||||
|
.switch-widget .switch-button:after {
|
||||||
|
--y: calc((var(--switch-track-height) - var(--switch-thumb-height)) / 2);
|
||||||
|
--x: var(--y);
|
||||||
|
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: var(--switch-thumb-width);
|
||||||
|
height: var(--switch-thumb-height);
|
||||||
|
background-color: var(--switch-off-thumb-background);
|
||||||
|
border-radius: 50%;
|
||||||
|
transform: translate(var(--x), var(--y));
|
||||||
|
transition: transform 600ms cubic-bezier(0.22, 1, 0.36, 1),
|
||||||
|
background 200ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-widget .switch-button.on:after {
|
||||||
|
--x: calc(var(--switch-track-width) - var(--switch-thumb-width) - var(--y));
|
||||||
|
|
||||||
|
background: var(--switch-on-thumb-background);
|
||||||
|
transition: transform 200ms cubic-bezier(0.64, 0, 0.78, 0),
|
||||||
|
background 100ms ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.switch-widget .switch-button input[type="checkbox"] {
|
||||||
|
/* A hidden check box for accesibility purposes */
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disabled state */
|
||||||
|
.switch-widget .switch-button:not(.disabled) input[type="checkbox"],
|
||||||
|
.switch-widget .switch-button:not(.disabled) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-widget .switch-button:has(input[type="checkbox"]:focus-visible) {
|
||||||
|
outline: 2px solid var(--button-border-color);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-widget .switch-button.disabled {
|
||||||
|
opacity: 70%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-widget .switch-help-button {
|
||||||
|
border: 0;
|
||||||
|
margin-left: 4px;
|
||||||
|
background: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.1em;
|
||||||
|
color: var(--muted-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-widget .switch-help-button:hover {
|
||||||
|
color: var(--main-text-color);
|
||||||
|
}
|
||||||
41
apps/client/src/widgets/react/FormToggle.tsx
Normal file
41
apps/client/src/widgets/react/FormToggle.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import "./FormToggle.css";
|
||||||
|
import HelpButton from "./HelpButton";
|
||||||
|
|
||||||
|
interface FormToggleProps {
|
||||||
|
currentValue: boolean | null;
|
||||||
|
onChange(newValue: boolean): void;
|
||||||
|
switchOnName: string;
|
||||||
|
switchOnTooltip: string;
|
||||||
|
switchOffName: string;
|
||||||
|
switchOffTooltip: string;
|
||||||
|
helpPage?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FormToggle({ currentValue, helpPage, switchOnName, switchOnTooltip, switchOffName, switchOffTooltip, onChange, disabled }: FormToggleProps) {
|
||||||
|
return (
|
||||||
|
<div className="switch-widget">
|
||||||
|
<span className="switch-name">{ currentValue ? switchOffName : switchOnName }</span>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<div
|
||||||
|
className={`switch-button ${currentValue ? "on" : ""} ${disabled ? "disabled" : ""}`}
|
||||||
|
title={currentValue ? switchOffTooltip : switchOnTooltip }
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
className="switch-toggle"
|
||||||
|
type="checkbox"
|
||||||
|
checked={currentValue === true}
|
||||||
|
onInput={(e) => {
|
||||||
|
onChange(!currentValue);
|
||||||
|
e.preventDefault();
|
||||||
|
}}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{ helpPage && <HelpButton className="switch-help-button" helpPage={helpPage} />}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
21
apps/client/src/widgets/react/HelpButton.tsx
Normal file
21
apps/client/src/widgets/react/HelpButton.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { CSSProperties } from "preact/compat";
|
||||||
|
import { t } from "../../services/i18n";
|
||||||
|
import { openInAppHelpFromUrl } from "../../services/utils";
|
||||||
|
|
||||||
|
interface HelpButtonProps {
|
||||||
|
className?: string;
|
||||||
|
helpPage: string;
|
||||||
|
style?: CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HelpButton({ className, helpPage, style }: HelpButtonProps) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
class={`${className ?? ""} icon-action bx bx-help-circle`}
|
||||||
|
type="button"
|
||||||
|
onClick={() => openInAppHelpFromUrl(helpPage)}
|
||||||
|
title={t("open-help-page")}
|
||||||
|
style={style}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
30
apps/client/src/widgets/react/HelpRemoveButtons.tsx
Normal file
30
apps/client/src/widgets/react/HelpRemoveButtons.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import type { ComponentChildren } from "preact";
|
||||||
|
import ActionButton from "./ActionButton";
|
||||||
|
import Dropdown from "./Dropdown";
|
||||||
|
|
||||||
|
interface HelpRemoveButtonsProps {
|
||||||
|
help?: ComponentChildren;
|
||||||
|
removeText?: string;
|
||||||
|
onRemove?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HelpRemoveButtons({ help, removeText, onRemove }: HelpRemoveButtonsProps) {
|
||||||
|
return (
|
||||||
|
<td className="button-column">
|
||||||
|
{help && <>
|
||||||
|
<Dropdown
|
||||||
|
className="help-dropdown"
|
||||||
|
buttonClassName="bx bx-help-circle icon-action"
|
||||||
|
hideToggleArrow
|
||||||
|
>{help}</Dropdown>
|
||||||
|
{" "}
|
||||||
|
</>}
|
||||||
|
<ActionButton
|
||||||
|
icon="bx bx-x"
|
||||||
|
className="search-option-del"
|
||||||
|
text={removeText ?? ""}
|
||||||
|
onClick={onRemove}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ActionKeyboardShortcut, KeyboardActionNames } from "@triliumnext/commons";
|
import { ActionKeyboardShortcut, KeyboardActionNames } from "@triliumnext/commons";
|
||||||
import { useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
import keyboard_actions from "../../services/keyboard_actions";
|
import keyboard_actions from "../../services/keyboard_actions";
|
||||||
|
import { joinElements } from "./react_utils";
|
||||||
|
|
||||||
interface KeyboardShortcutProps {
|
interface KeyboardShortcutProps {
|
||||||
actionName: KeyboardActionNames;
|
actionName: KeyboardActionNames;
|
||||||
@@ -19,15 +20,15 @@ export default function KeyboardShortcut({ actionName }: KeyboardShortcutProps)
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{action.effectiveShortcuts?.map((shortcut, i) => {
|
{action.effectiveShortcuts?.map((shortcut) => {
|
||||||
const keys = shortcut.split("+");
|
const keys = shortcut.split("+");
|
||||||
return keys
|
return joinElements(keys
|
||||||
.map((key, i) => (
|
.map((key, i) => (
|
||||||
<>
|
<>
|
||||||
<kbd>{key}</kbd> {i + 1 < keys.length && "+ "}
|
<kbd>{key}</kbd> {i + 1 < keys.length && "+ "}
|
||||||
</>
|
</>
|
||||||
))
|
)))
|
||||||
}).reduce<any>((acc, item) => (acc.length ? [...acc, ", ", item] : [item]), [])}
|
})}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
3
apps/client/src/widgets/react/LoadingSpinner.tsx
Normal file
3
apps/client/src/widgets/react/LoadingSpinner.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default function LoadingSpinner() {
|
||||||
|
return <span className="bx bx-loader bx-spin" />
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import { useContext, useEffect, useRef, useMemo, useCallback } from "preact/hooks";
|
import { useEffect, useRef, useMemo } from "preact/hooks";
|
||||||
import { t } from "../../services/i18n";
|
import { t } from "../../services/i18n";
|
||||||
import { ComponentChildren } from "preact";
|
import { ComponentChildren } from "preact";
|
||||||
import type { CSSProperties, RefObject } from "preact/compat";
|
import type { CSSProperties, RefObject } from "preact/compat";
|
||||||
import { openDialog } from "../../services/dialog";
|
import { openDialog } from "../../services/dialog";
|
||||||
import { ParentComponent } from "./ReactBasicWidget";
|
|
||||||
import { Modal as BootstrapModal } from "bootstrap";
|
import { Modal as BootstrapModal } from "bootstrap";
|
||||||
import { memo } from "preact/compat";
|
import { memo } from "preact/compat";
|
||||||
|
import { useSyncedRef } from "./hooks";
|
||||||
|
|
||||||
interface ModalProps {
|
interface ModalProps {
|
||||||
className: string;
|
className: string;
|
||||||
@@ -64,14 +64,11 @@ interface ModalProps {
|
|||||||
stackable?: boolean;
|
stackable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Modal({ children, className, size, title, header, footer, footerStyle, footerAlignment, onShown, onSubmit, helpPageId, minWidth, maxWidth, zIndex, scrollable, onHidden: onHidden, modalRef: _modalRef, formRef: _formRef, bodyStyle, show, stackable }: ModalProps) {
|
export default function Modal({ children, className, size, title, header, footer, footerStyle, footerAlignment, onShown, onSubmit, helpPageId, minWidth, maxWidth, zIndex, scrollable, onHidden: onHidden, modalRef: externalModalRef, formRef, bodyStyle, show, stackable }: ModalProps) {
|
||||||
const modalRef = _modalRef ?? useRef<HTMLDivElement>(null);
|
const modalRef = useSyncedRef<HTMLDivElement>(externalModalRef);
|
||||||
const modalInstanceRef = useRef<BootstrapModal>();
|
const modalInstanceRef = useRef<BootstrapModal>();
|
||||||
const formRef = _formRef ?? useRef<HTMLFormElement>(null);
|
|
||||||
const parentWidget = useContext(ParentComponent);
|
|
||||||
const elementToFocus = useRef<Element | null>();
|
const elementToFocus = useRef<Element | null>();
|
||||||
|
|
||||||
if (onShown || onHidden) {
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const modalElement = modalRef.current;
|
const modalElement = modalRef.current;
|
||||||
if (!modalElement) {
|
if (!modalElement) {
|
||||||
@@ -92,22 +89,18 @@ export default function Modal({ children, className, size, title, header, footer
|
|||||||
}
|
}
|
||||||
modalElement.removeEventListener("hidden.bs.modal", onHidden);
|
modalElement.removeEventListener("hidden.bs.modal", onHidden);
|
||||||
};
|
};
|
||||||
}, [ ]);
|
}, [ onShown, onHidden ]);
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!parentWidget) {
|
if (show && modalRef.current) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (show) {
|
|
||||||
elementToFocus.current = document.activeElement;
|
elementToFocus.current = document.activeElement;
|
||||||
openDialog(parentWidget.$widget, !stackable).then(($widget) => {
|
openDialog($(modalRef.current), !stackable).then(($widget) => {
|
||||||
modalInstanceRef.current = BootstrapModal.getOrCreateInstance($widget[0]);
|
modalInstanceRef.current = BootstrapModal.getOrCreateInstance($widget[0]);
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
modalInstanceRef.current?.hide();
|
modalInstanceRef.current?.hide();
|
||||||
}
|
}
|
||||||
}, [ show ]);
|
}, [ show, modalRef.current ]);
|
||||||
|
|
||||||
// Memoize styles to prevent recreation on every render
|
// Memoize styles to prevent recreation on every render
|
||||||
const dialogStyle = useMemo<CSSProperties>(() => {
|
const dialogStyle = useMemo<CSSProperties>(() => {
|
||||||
@@ -147,10 +140,10 @@ export default function Modal({ children, className, size, title, header, footer
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{onSubmit ? (
|
{onSubmit ? (
|
||||||
<form ref={formRef} onSubmit={useCallback((e) => {
|
<form ref={formRef} onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onSubmit();
|
onSubmit();
|
||||||
}, [onSubmit])}>
|
}}>
|
||||||
<ModalInner footer={footer} bodyStyle={bodyStyle} footerStyle={footerStyle} footerAlignment={footerAlignment}>{children}</ModalInner>
|
<ModalInner footer={footer} bodyStyle={bodyStyle} footerStyle={footerStyle} footerAlignment={footerAlignment}>{children}</ModalInner>
|
||||||
</form>
|
</form>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useRef } from "preact/hooks";
|
|
||||||
import { t } from "../../services/i18n";
|
import { t } from "../../services/i18n";
|
||||||
import { useEffect } from "preact/hooks";
|
import { useEffect } from "preact/hooks";
|
||||||
import note_autocomplete, { Options, type Suggestion } from "../../services/note_autocomplete";
|
import note_autocomplete, { Options, type Suggestion } from "../../services/note_autocomplete";
|
||||||
import type { RefObject } from "preact";
|
import type { RefObject } from "preact";
|
||||||
import type { CSSProperties } from "preact/compat";
|
import type { CSSProperties } from "preact/compat";
|
||||||
|
import { useSyncedRef } from "./hooks";
|
||||||
|
|
||||||
interface NoteAutocompleteProps {
|
interface NoteAutocompleteProps {
|
||||||
id?: string;
|
id?: string;
|
||||||
@@ -19,8 +19,8 @@ interface NoteAutocompleteProps {
|
|||||||
noteId?: string;
|
noteId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function NoteAutocomplete({ id, inputRef: _ref, text, placeholder, onChange, onTextChange, container, containerStyle, opts, noteId, noteIdChanged }: NoteAutocompleteProps) {
|
export default function NoteAutocomplete({ id, inputRef: externalInputRef, text, placeholder, onChange, onTextChange, container, containerStyle, opts, noteId, noteIdChanged }: NoteAutocompleteProps) {
|
||||||
const ref = _ref ?? useRef<HTMLInputElement>(null);
|
const ref = useSyncedRef<HTMLInputElement>(externalInputRef);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ref.current) return;
|
if (!ref.current) return;
|
||||||
|
|||||||
37
apps/client/src/widgets/react/NoteLink.tsx
Normal file
37
apps/client/src/widgets/react/NoteLink.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
import link from "../../services/link";
|
||||||
|
import RawHtml from "./RawHtml";
|
||||||
|
|
||||||
|
interface NoteLinkOpts {
|
||||||
|
notePath: string | string[];
|
||||||
|
showNotePath?: boolean;
|
||||||
|
style?: Record<string, string | number>;
|
||||||
|
noPreview?: boolean;
|
||||||
|
noTnLink?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NoteLink({ notePath, showNotePath, style, noPreview, noTnLink }: NoteLinkOpts) {
|
||||||
|
const stringifiedNotePath = Array.isArray(notePath) ? notePath.join("/") : notePath;
|
||||||
|
const [ jqueryEl, setJqueryEl ] = useState<JQuery<HTMLElement>>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
link.createLink(stringifiedNotePath, { showNotePath })
|
||||||
|
.then(setJqueryEl);
|
||||||
|
}, [ stringifiedNotePath, showNotePath ]);
|
||||||
|
|
||||||
|
if (style) {
|
||||||
|
jqueryEl?.css(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
const $linkEl = jqueryEl?.find("a");
|
||||||
|
if (noPreview) {
|
||||||
|
$linkEl?.addClass("no-tooltip-preview");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!noTnLink) {
|
||||||
|
$linkEl?.addClass("tn-link");
|
||||||
|
}
|
||||||
|
|
||||||
|
return <RawHtml html={jqueryEl} />
|
||||||
|
|
||||||
|
}
|
||||||
@@ -4,8 +4,9 @@ type HTMLElementLike = string | HTMLElement | JQuery<HTMLElement>;
|
|||||||
|
|
||||||
interface RawHtmlProps {
|
interface RawHtmlProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
html: HTMLElementLike;
|
html?: HTMLElementLike;
|
||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
|
onClick?: (e: MouseEvent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RawHtml(props: RawHtmlProps) {
|
export default function RawHtml(props: RawHtmlProps) {
|
||||||
@@ -16,11 +17,12 @@ export function RawHtmlBlock(props: RawHtmlProps) {
|
|||||||
return <div {...getProps(props)} />
|
return <div {...getProps(props)} />
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProps({ className, html, style }: RawHtmlProps) {
|
function getProps({ className, html, style, onClick }: RawHtmlProps) {
|
||||||
return {
|
return {
|
||||||
className: className,
|
className: className,
|
||||||
dangerouslySetInnerHTML: getHtml(html),
|
dangerouslySetInnerHTML: getHtml(html ?? ""),
|
||||||
style
|
style,
|
||||||
|
onClick
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,96 +1,61 @@
|
|||||||
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
import { useCallback, useContext, useDebugValue, useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||||
import { EventData, EventNames } from "../../components/app_context";
|
import { EventData, EventNames } from "../../components/app_context";
|
||||||
import { ParentComponent } from "./ReactBasicWidget";
|
import { ParentComponent } from "./react_utils";
|
||||||
import SpacedUpdate from "../../services/spaced_update";
|
import SpacedUpdate from "../../services/spaced_update";
|
||||||
import { OptionNames } from "@triliumnext/commons";
|
import { OptionNames } from "@triliumnext/commons";
|
||||||
import options, { type OptionValue } from "../../services/options";
|
import options, { type OptionValue } from "../../services/options";
|
||||||
import utils, { reloadFrontendApp } from "../../services/utils";
|
import utils, { reloadFrontendApp } from "../../services/utils";
|
||||||
import Component from "../../components/component";
|
import NoteContext from "../../components/note_context";
|
||||||
import server from "../../services/server";
|
import BasicWidget, { ReactWrappedWidget } from "../basic_widget";
|
||||||
|
import FNote from "../../entities/fnote";
|
||||||
|
import attributes from "../../services/attributes";
|
||||||
|
import FBlob from "../../entities/fblob";
|
||||||
|
import NoteContextAwareWidget from "../note_context_aware_widget";
|
||||||
|
import { RefObject, VNode } from "preact";
|
||||||
|
import { Tooltip } from "bootstrap";
|
||||||
|
import { CSSProperties } from "preact/compat";
|
||||||
|
|
||||||
type TriliumEventHandler<T extends EventNames> = (data: EventData<T>) => void;
|
export function useTriliumEvent<T extends EventNames>(eventName: T, handler: (data: EventData<T>) => void) {
|
||||||
const registeredHandlers: Map<Component, Map<EventNames, TriliumEventHandler<any>[]>> = new Map();
|
const parentComponent = useContext(ParentComponent);
|
||||||
|
useLayoutEffect(() => {
|
||||||
/**
|
parentComponent?.registerHandler(eventName, handler);
|
||||||
* Allows a React component to react to Trilium events (e.g. `entitiesReloaded`). When the desired event is triggered, the handler is invoked with the event parameters.
|
return (() => parentComponent?.removeHandler(eventName, handler));
|
||||||
*
|
}, [ eventName, handler ]);
|
||||||
* Under the hood, it works by altering the parent (Trilium) component of the React element to introduce the corresponding event.
|
useDebugValue(eventName);
|
||||||
*
|
|
||||||
* @param eventName the name of the Trilium event to listen for.
|
|
||||||
* @param handler the handler to be invoked when the event is triggered.
|
|
||||||
* @param enabled determines whether the event should be listened to or not. Useful to conditionally limit the listener based on a state (e.g. a modal being displayed).
|
|
||||||
*/
|
|
||||||
export default function useTriliumEvent<T extends EventNames>(eventName: T, handler: TriliumEventHandler<T>, enabled = true) {
|
|
||||||
const parentWidget = useContext(ParentComponent);
|
|
||||||
if (!parentWidget) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlerName = `${eventName}Event`;
|
|
||||||
const customHandler = useMemo(() => {
|
|
||||||
return async (data: EventData<T>) => {
|
|
||||||
// Inform the attached event listeners.
|
|
||||||
const eventHandlers = registeredHandlers.get(parentWidget)?.get(eventName) ?? [];
|
|
||||||
for (const eventHandler of eventHandlers) {
|
|
||||||
eventHandler(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [ eventName, parentWidget ]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Attach to the list of handlers.
|
|
||||||
let handlersByWidget = registeredHandlers.get(parentWidget);
|
|
||||||
if (!handlersByWidget) {
|
|
||||||
handlersByWidget = new Map();
|
|
||||||
registeredHandlers.set(parentWidget, handlersByWidget);
|
|
||||||
}
|
|
||||||
|
|
||||||
let handlersByWidgetAndEventName = handlersByWidget.get(eventName);
|
|
||||||
if (!handlersByWidgetAndEventName) {
|
|
||||||
handlersByWidgetAndEventName = [];
|
|
||||||
handlersByWidget.set(eventName, handlersByWidgetAndEventName);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!handlersByWidgetAndEventName.includes(handler)) {
|
|
||||||
handlersByWidgetAndEventName.push(handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply the custom event handler.
|
|
||||||
if (parentWidget[handlerName] && parentWidget[handlerName] !== customHandler) {
|
|
||||||
console.warn(`Widget ${parentWidget.componentId} already had an event listener and it was replaced by the React one.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
parentWidget[handlerName] = customHandler;
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
const eventHandlers = registeredHandlers.get(parentWidget)?.get(eventName);
|
|
||||||
if (!eventHandlers || !eventHandlers.includes(handler)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the event handler from the array.
|
|
||||||
const newEventHandlers = eventHandlers.filter(e => e !== handler);
|
|
||||||
if (newEventHandlers.length) {
|
|
||||||
registeredHandlers.get(parentWidget)?.set(eventName, newEventHandlers);
|
|
||||||
} else {
|
|
||||||
registeredHandlers.get(parentWidget)?.delete(eventName);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!registeredHandlers.get(parentWidget)?.size) {
|
|
||||||
registeredHandlers.delete(parentWidget);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [ eventName, parentWidget, handler ]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSpacedUpdate(callback: () => Promise<void>, interval = 1000) {
|
export function useTriliumEvents<T extends EventNames>(eventNames: T[], handler: (data: EventData<T>, eventName: T) => void) {
|
||||||
|
const parentComponent = useContext(ParentComponent);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const handlers: ({ eventName: T, callback: (data: EventData<T>) => void })[] = [];
|
||||||
|
for (const eventName of eventNames) {
|
||||||
|
handlers.push({ eventName, callback: (data) => {
|
||||||
|
handler(data, eventName);
|
||||||
|
}})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const { eventName, callback } of handlers) {
|
||||||
|
parentComponent?.registerHandler(eventName, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (() => {
|
||||||
|
for (const { eventName, callback } of handlers) {
|
||||||
|
parentComponent?.removeHandler(eventName, callback);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [ eventNames, handler ]);
|
||||||
|
useDebugValue(() => eventNames.join(", "));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSpacedUpdate(callback: () => void | Promise<void>, interval = 1000) {
|
||||||
const callbackRef = useRef(callback);
|
const callbackRef = useRef(callback);
|
||||||
const spacedUpdateRef = useRef<SpacedUpdate>();
|
const spacedUpdateRef = useRef<SpacedUpdate>();
|
||||||
|
|
||||||
// Update callback ref when it changes
|
// Update callback ref when it changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
callbackRef.current = callback;
|
callbackRef.current = callback;
|
||||||
});
|
}, [callback]);
|
||||||
|
|
||||||
// Create SpacedUpdate instance only once
|
// Create SpacedUpdate instance only once
|
||||||
if (!spacedUpdateRef.current) {
|
if (!spacedUpdateRef.current) {
|
||||||
@@ -137,7 +102,9 @@ export function useTriliumOption(name: OptionNames, needsRefresh?: boolean): [st
|
|||||||
const newValue = options.get(name);
|
const newValue = options.get(name);
|
||||||
setValue(newValue);
|
setValue(newValue);
|
||||||
}
|
}
|
||||||
}, [ name ]));
|
}, [ name, setValue ]));
|
||||||
|
|
||||||
|
useDebugValue(name);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
value,
|
value,
|
||||||
@@ -154,6 +121,7 @@ export function useTriliumOption(name: OptionNames, needsRefresh?: boolean): [st
|
|||||||
*/
|
*/
|
||||||
export function useTriliumOptionBool(name: OptionNames, needsRefresh?: boolean): [boolean, (newValue: boolean) => Promise<void>] {
|
export function useTriliumOptionBool(name: OptionNames, needsRefresh?: boolean): [boolean, (newValue: boolean) => Promise<void>] {
|
||||||
const [ value, setValue ] = useTriliumOption(name, needsRefresh);
|
const [ value, setValue ] = useTriliumOption(name, needsRefresh);
|
||||||
|
useDebugValue(name);
|
||||||
return [
|
return [
|
||||||
(value === "true"),
|
(value === "true"),
|
||||||
(newValue) => setValue(newValue ? "true" : "false")
|
(newValue) => setValue(newValue ? "true" : "false")
|
||||||
@@ -169,6 +137,7 @@ export function useTriliumOptionBool(name: OptionNames, needsRefresh?: boolean):
|
|||||||
*/
|
*/
|
||||||
export function useTriliumOptionInt(name: OptionNames): [number, (newValue: number) => Promise<void>] {
|
export function useTriliumOptionInt(name: OptionNames): [number, (newValue: number) => Promise<void>] {
|
||||||
const [ value, setValue ] = useTriliumOption(name);
|
const [ value, setValue ] = useTriliumOption(name);
|
||||||
|
useDebugValue(name);
|
||||||
return [
|
return [
|
||||||
(parseInt(value, 10)),
|
(parseInt(value, 10)),
|
||||||
(newValue) => setValue(newValue)
|
(newValue) => setValue(newValue)
|
||||||
@@ -183,6 +152,7 @@ export function useTriliumOptionInt(name: OptionNames): [number, (newValue: numb
|
|||||||
*/
|
*/
|
||||||
export function useTriliumOptionJson<T>(name: OptionNames): [ T, (newValue: T) => Promise<void> ] {
|
export function useTriliumOptionJson<T>(name: OptionNames): [ T, (newValue: T) => Promise<void> ] {
|
||||||
const [ value, setValue ] = useTriliumOption(name);
|
const [ value, setValue ] = useTriliumOption(name);
|
||||||
|
useDebugValue(name);
|
||||||
return [
|
return [
|
||||||
(JSON.parse(value) as T),
|
(JSON.parse(value) as T),
|
||||||
(newValue => setValue(JSON.stringify(newValue)))
|
(newValue => setValue(JSON.stringify(newValue)))
|
||||||
@@ -201,6 +171,8 @@ export function useTriliumOptions<T extends OptionNames>(...names: T[]) {
|
|||||||
values[name] = options.get(name);
|
values[name] = options.get(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useDebugValue(() => names.join(", "));
|
||||||
|
|
||||||
return [
|
return [
|
||||||
values as Record<T, string>,
|
values as Record<T, string>,
|
||||||
options.saveMany
|
options.saveMany
|
||||||
@@ -219,3 +191,339 @@ export function useTriliumOptions<T extends OptionNames>(...names: T[]) {
|
|||||||
export function useUniqueName(prefix?: string) {
|
export function useUniqueName(prefix?: string) {
|
||||||
return useMemo(() => (prefix ? prefix + "-" : "") + utils.randomString(10), [ prefix ]);
|
return useMemo(() => (prefix ? prefix + "-" : "") + utils.randomString(10), [ prefix ]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useNoteContext() {
|
||||||
|
const [ noteContext, setNoteContext ] = useState<NoteContext>();
|
||||||
|
const [ notePath, setNotePath ] = useState<string | null | undefined>();
|
||||||
|
const [ note, setNote ] = useState<FNote | null | undefined>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setNote(noteContext?.note);
|
||||||
|
}, [ notePath ]);
|
||||||
|
|
||||||
|
useTriliumEvents([ "setNoteContext", "activeContextChanged", "noteSwitchedAndActivated", "noteSwitched" ], ({ noteContext }) => {
|
||||||
|
setNoteContext(noteContext);
|
||||||
|
setNotePath(noteContext.notePath);
|
||||||
|
});
|
||||||
|
useTriliumEvent("frocaReloaded", () => {
|
||||||
|
setNote(noteContext?.note);
|
||||||
|
});
|
||||||
|
|
||||||
|
const parentComponent = useContext(ParentComponent) as ReactWrappedWidget;
|
||||||
|
useDebugValue(() => `notePath=${notePath}, ntxId=${noteContext?.ntxId}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
note: note,
|
||||||
|
noteId: noteContext?.note?.noteId,
|
||||||
|
notePath: noteContext?.notePath,
|
||||||
|
hoistedNoteId: noteContext?.hoistedNoteId,
|
||||||
|
ntxId: noteContext?.ntxId,
|
||||||
|
viewScope: noteContext?.viewScope,
|
||||||
|
componentId: parentComponent.componentId,
|
||||||
|
noteContext,
|
||||||
|
parentComponent
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows a React component to listen to obtain a property of a {@link FNote} while also automatically watching for changes, either via the user changing to a different note or the property being changed externally.
|
||||||
|
*
|
||||||
|
* @param note the {@link FNote} whose property to obtain.
|
||||||
|
* @param property a property of a {@link FNote} to obtain the value from (e.g. `title`, `isProtected`).
|
||||||
|
* @param componentId optionally, constricts the refresh of the value if an update occurs externally via the component ID of a legacy widget. This can be used to avoid external data replacing fresher, user-inputted data.
|
||||||
|
* @returns the value of the requested property.
|
||||||
|
*/
|
||||||
|
export function useNoteProperty<T extends keyof FNote>(note: FNote | null | undefined, property: T, componentId?: string) {
|
||||||
|
const [, setValue ] = useState<FNote[T] | undefined>(note?.[property]);
|
||||||
|
const refreshValue = () => setValue(note?.[property]);
|
||||||
|
|
||||||
|
// Watch for note changes.
|
||||||
|
useEffect(() => refreshValue(), [ note, note?.[property] ]);
|
||||||
|
|
||||||
|
// Watch for external changes.
|
||||||
|
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||||
|
if (loadResults.isNoteReloaded(note?.noteId, componentId)) {
|
||||||
|
refreshValue();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useDebugValue(property);
|
||||||
|
return note?.[property];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useNoteRelation(note: FNote | undefined | null, relationName: string): [string | null | undefined, (newValue: string) => void] {
|
||||||
|
const [ relationValue, setRelationValue ] = useState<string | null | undefined>(note?.getRelationValue(relationName));
|
||||||
|
|
||||||
|
useEffect(() => setRelationValue(note?.getRelationValue(relationName) ?? null), [ note ]);
|
||||||
|
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||||
|
for (const attr of loadResults.getAttributeRows()) {
|
||||||
|
if (attr.type === "relation" && attr.name === relationName && attributes.isAffecting(attr, note)) {
|
||||||
|
setRelationValue(attr.value ?? null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const setter = useCallback((value: string | undefined) => {
|
||||||
|
if (note) {
|
||||||
|
attributes.setAttribute(note, "relation", relationName, value)
|
||||||
|
}
|
||||||
|
}, [note]);
|
||||||
|
|
||||||
|
useDebugValue(relationName);
|
||||||
|
|
||||||
|
return [
|
||||||
|
relationValue,
|
||||||
|
setter
|
||||||
|
] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows a React component to read or write a note's label while also reacting to changes in value.
|
||||||
|
*
|
||||||
|
* @param note the note whose label to read/write.
|
||||||
|
* @param labelName the name of the label to read/write.
|
||||||
|
* @returns an array where the first element is the getter and the second element is the setter. The setter has a special behaviour for convenience: if the value is undefined, the label is created without a value (e.g. a tag), if the value is null then the label is removed.
|
||||||
|
*/
|
||||||
|
export function useNoteLabel(note: FNote | undefined | null, labelName: string): [string | null | undefined, (newValue: string | null | undefined) => void] {
|
||||||
|
const [ labelValue, setLabelValue ] = useState<string | null | undefined>(note?.getLabelValue(labelName));
|
||||||
|
|
||||||
|
useEffect(() => setLabelValue(note?.getLabelValue(labelName) ?? null), [ note ]);
|
||||||
|
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||||
|
for (const attr of loadResults.getAttributeRows()) {
|
||||||
|
if (attr.type === "label" && attr.name === labelName && attributes.isAffecting(attr, note)) {
|
||||||
|
setLabelValue(attr.value ?? null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const setter = useCallback((value: string | null | undefined) => {
|
||||||
|
if (note) {
|
||||||
|
if (value || value === undefined) {
|
||||||
|
attributes.setLabel(note.noteId, labelName, value)
|
||||||
|
} else if (value === null) {
|
||||||
|
attributes.removeOwnedLabelByName(note, labelName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [note]);
|
||||||
|
|
||||||
|
useDebugValue(labelName);
|
||||||
|
|
||||||
|
return [
|
||||||
|
labelValue,
|
||||||
|
setter
|
||||||
|
] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useNoteLabelBoolean(note: FNote | undefined | null, labelName: string): [ boolean, (newValue: boolean) => void] {
|
||||||
|
const [ labelValue, setLabelValue ] = useState<boolean>(!!note?.hasLabel(labelName));
|
||||||
|
|
||||||
|
useEffect(() => setLabelValue(!!note?.hasLabel(labelName)), [ note ]);
|
||||||
|
|
||||||
|
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||||
|
for (const attr of loadResults.getAttributeRows()) {
|
||||||
|
if (attr.type === "label" && attr.name === labelName && attributes.isAffecting(attr, note)) {
|
||||||
|
setLabelValue(!attr.isDeleted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const setter = useCallback((value: boolean) => {
|
||||||
|
if (note) {
|
||||||
|
if (value) {
|
||||||
|
attributes.setLabel(note.noteId, labelName, "");
|
||||||
|
} else {
|
||||||
|
attributes.removeOwnedLabelByName(note, labelName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [note]);
|
||||||
|
|
||||||
|
useDebugValue(labelName);
|
||||||
|
|
||||||
|
return [ labelValue, setter ] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useNoteBlob(note: FNote | null | undefined): [ FBlob | null | undefined ] {
|
||||||
|
const [ blob, setBlob ] = useState<FBlob | null>();
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
note?.getBlob().then(setBlob);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(refresh, [ note?.noteId ]);
|
||||||
|
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||||
|
if (note && loadResults.hasRevisionForNote(note.noteId)) {
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useDebugValue(note?.noteId);
|
||||||
|
|
||||||
|
return [ blob ] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useLegacyWidget<T extends BasicWidget>(widgetFactory: () => T, { noteContext, containerClassName, containerStyle }: {
|
||||||
|
noteContext?: NoteContext;
|
||||||
|
containerClassName?: string;
|
||||||
|
containerStyle?: CSSProperties;
|
||||||
|
} = {}): [VNode, T] {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const parentComponent = useContext(ParentComponent);
|
||||||
|
|
||||||
|
// Render the widget once.
|
||||||
|
const [ widget, renderedWidget ] = useMemo(() => {
|
||||||
|
const widget = widgetFactory();
|
||||||
|
|
||||||
|
if (parentComponent) {
|
||||||
|
parentComponent.child(widget);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (noteContext && widget instanceof NoteContextAwareWidget) {
|
||||||
|
widget.setNoteContextEvent({ noteContext });
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderedWidget = widget.render();
|
||||||
|
return [ widget, renderedWidget ];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Attach the widget to the parent.
|
||||||
|
useEffect(() => {
|
||||||
|
if (ref.current) {
|
||||||
|
ref.current.innerHTML = "";
|
||||||
|
renderedWidget.appendTo(ref.current);
|
||||||
|
}
|
||||||
|
}, [ renderedWidget ]);
|
||||||
|
|
||||||
|
// Inject the note context.
|
||||||
|
useEffect(() => {
|
||||||
|
if (noteContext && widget instanceof NoteContextAwareWidget) {
|
||||||
|
widget.activeContextChangedEvent({ noteContext });
|
||||||
|
}
|
||||||
|
}, [ noteContext ]);
|
||||||
|
|
||||||
|
useDebugValue(widget);
|
||||||
|
|
||||||
|
return [ <div className={containerClassName} style={containerStyle} ref={ref} />, widget ]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches a {@link ResizeObserver} to the given ref and reads the bounding client rect whenever it changes.
|
||||||
|
*
|
||||||
|
* @param ref a ref to a {@link HTMLElement} to determine the size and observe the changes in size.
|
||||||
|
* @returns the size of the element, reacting to changes.
|
||||||
|
*/
|
||||||
|
export function useElementSize(ref: RefObject<HTMLElement>) {
|
||||||
|
const [ size, setSize ] = useState<DOMRect | undefined>(ref.current?.getBoundingClientRect());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ref.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onResize() {
|
||||||
|
setSize(ref.current?.getBoundingClientRect());
|
||||||
|
}
|
||||||
|
|
||||||
|
const element = ref.current;
|
||||||
|
const resizeObserver = new ResizeObserver(onResize);
|
||||||
|
resizeObserver.observe(element);
|
||||||
|
return () => {
|
||||||
|
resizeObserver.unobserve(element);
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
}
|
||||||
|
}, [ ref ]);
|
||||||
|
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains the inner width and height of the window, as well as reacts to changes in size.
|
||||||
|
*
|
||||||
|
* @returns the width and height of the window.
|
||||||
|
*/
|
||||||
|
export function useWindowSize() {
|
||||||
|
const [ size, setSize ] = useState<{ windowWidth: number, windowHeight: number }>({
|
||||||
|
windowWidth: window.innerWidth,
|
||||||
|
windowHeight: window.innerHeight
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function onResize() {
|
||||||
|
setSize({
|
||||||
|
windowWidth: window.innerWidth,
|
||||||
|
windowHeight: window.innerHeight
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("resize", onResize);
|
||||||
|
return () => window.removeEventListener("resize", onResize);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTooltip(elRef: RefObject<HTMLElement>, config: Partial<Tooltip.Options>) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!elRef?.current) return;
|
||||||
|
|
||||||
|
const $el = $(elRef.current);
|
||||||
|
$el.tooltip("dispose");
|
||||||
|
$el.tooltip(config);
|
||||||
|
}, [ elRef, config ]);
|
||||||
|
|
||||||
|
const showTooltip = useCallback(() => {
|
||||||
|
if (!elRef?.current) return;
|
||||||
|
|
||||||
|
const $el = $(elRef.current);
|
||||||
|
$el.tooltip("show");
|
||||||
|
}, [ elRef, config ]);
|
||||||
|
|
||||||
|
const hideTooltip = useCallback(() => {
|
||||||
|
if (!elRef?.current) return;
|
||||||
|
|
||||||
|
const $el = $(elRef.current);
|
||||||
|
$el.tooltip("hide");
|
||||||
|
}, [ elRef ]);
|
||||||
|
|
||||||
|
useDebugValue(config.title);
|
||||||
|
|
||||||
|
return { showTooltip, hideTooltip };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Similar to {@link useTooltip}, but doesn't expose methods to imperatively hide or show the tooltip.
|
||||||
|
*
|
||||||
|
* @param elRef the element to bind the tooltip to.
|
||||||
|
* @param config optionally, the tooltip configuration.
|
||||||
|
*/
|
||||||
|
export function useStaticTooltip(elRef: RefObject<HTMLElement>, config?: Partial<Tooltip.Options>) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!elRef?.current) return;
|
||||||
|
|
||||||
|
const $el = $(elRef.current);
|
||||||
|
$el.tooltip(config);
|
||||||
|
return () => {
|
||||||
|
$el.tooltip("dispose");
|
||||||
|
}
|
||||||
|
}, [ elRef, config ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||||
|
export function useLegacyImperativeHandlers(handlers: Record<string, Function>) {
|
||||||
|
const parentComponent = useContext(ParentComponent);
|
||||||
|
useEffect(() => {
|
||||||
|
Object.assign(parentComponent as never, handlers);
|
||||||
|
}, [ handlers ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSyncedRef<T>(externalRef?: RefObject<T>, initialValue: T | null = null): RefObject<T> {
|
||||||
|
const ref = useRef<T>(initialValue);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (externalRef) {
|
||||||
|
externalRef.current = ref.current;
|
||||||
|
}
|
||||||
|
}, [ ref, externalRef ]);
|
||||||
|
|
||||||
|
return ref;
|
||||||
|
}
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import type { RefObject } from "preact";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Takes in a React ref and returns a corresponding JQuery selector.
|
|
||||||
*
|
|
||||||
* @param ref the React ref from which to obtain the jQuery selector.
|
|
||||||
* @returns the corresponding jQuery selector.
|
|
||||||
*/
|
|
||||||
export function refToJQuerySelector<T extends HTMLElement>(ref: RefObject<T> | null): JQuery<T> {
|
|
||||||
if (ref?.current) {
|
|
||||||
return $(ref.current);
|
|
||||||
} else {
|
|
||||||
return $();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user