mirror of
https://github.com/zadam/trilium.git
synced 2025-11-04 20:36:13 +01:00
Compare commits
2 Commits
feat/quick
...
feat/add-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5332f015ef | ||
|
|
a3d77421fd |
@@ -1,6 +1,6 @@
|
||||
root = true
|
||||
|
||||
[*.{js,ts,.tsx}]
|
||||
[*.{js,ts}]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
|
||||
5
.vscode/snippets.code-snippets
vendored
5
.vscode/snippets.code-snippets
vendored
@@ -20,10 +20,5 @@
|
||||
"scope": "typescript",
|
||||
"prefix": "jqf",
|
||||
"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",
|
||||
"@stylistic/eslint-plugin": "5.2.3",
|
||||
"@types/express": "5.0.3",
|
||||
"@types/node": "22.18.0",
|
||||
"@types/node": "22.17.2",
|
||||
"@types/yargs": "17.0.33",
|
||||
"@vitest/coverage-v8": "3.2.4",
|
||||
"eslint": "9.34.0",
|
||||
@@ -49,7 +49,7 @@
|
||||
"rcedit": "4.0.1",
|
||||
"rimraf": "6.0.1",
|
||||
"tslib": "2.8.1",
|
||||
"typedoc": "0.28.11",
|
||||
"typedoc": "0.28.10",
|
||||
"typedoc-plugin-missing-exports": "4.1.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@triliumnext/client",
|
||||
"version": "0.98.1",
|
||||
"version": "0.98.0",
|
||||
"description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0-only",
|
||||
@@ -28,7 +28,7 @@
|
||||
"@triliumnext/highlightjs": "workspace:*",
|
||||
"@triliumnext/share-theme": "workspace:*",
|
||||
"autocomplete.js": "0.38.1",
|
||||
"bootstrap": "5.3.8",
|
||||
"bootstrap": "5.3.7",
|
||||
"boxicons": "2.1.4",
|
||||
"dayjs": "1.11.13",
|
||||
"dayjs-plugin-utc": "0.1.2",
|
||||
@@ -36,7 +36,7 @@
|
||||
"draggabilly": "3.0.0",
|
||||
"force-graph": "1.50.1",
|
||||
"globals": "16.3.0",
|
||||
"i18next": "25.4.2",
|
||||
"i18next": "25.4.1",
|
||||
"i18next-http-backend": "3.0.2",
|
||||
"jquery": "3.7.1",
|
||||
"jquery.fancytree": "2.38.5",
|
||||
@@ -62,7 +62,7 @@
|
||||
"@ckeditor/ckeditor5-inspector": "5.0.0",
|
||||
"@preact/preset-vite": "2.10.2",
|
||||
"@types/bootstrap": "5.2.10",
|
||||
"@types/jquery": "3.5.33",
|
||||
"@types/jquery": "3.5.32",
|
||||
"@types/leaflet": "1.9.20",
|
||||
"@types/leaflet-gpx": "1.3.7",
|
||||
"@types/mark.js": "8.11.12",
|
||||
|
||||
@@ -31,13 +31,16 @@ import { StartupChecks } from "./startup_checks.js";
|
||||
import type { CreateNoteOpts } from "../services/note_create.js";
|
||||
import { ColumnComponent } from "tabulator-tables";
|
||||
import { ChooseNoteTypeCallback } from "../widgets/dialogs/note_type_chooser.jsx";
|
||||
import type RootContainer from "../widgets/containers/root_container.js";
|
||||
|
||||
interface Layout {
|
||||
getRootWidget: (appContext: AppContext) => RootContainer;
|
||||
getRootWidget: (appContext: AppContext) => RootWidget;
|
||||
}
|
||||
|
||||
export interface BeforeUploadListener extends Component {
|
||||
interface RootWidget extends Component {
|
||||
render: () => JQuery<HTMLElement>;
|
||||
}
|
||||
|
||||
interface BeforeUploadListener extends Component {
|
||||
beforeUnloadEvent(): boolean;
|
||||
}
|
||||
|
||||
@@ -82,6 +85,7 @@ export type CommandMappings = {
|
||||
focusTree: CommandData;
|
||||
focusOnTitle: CommandData;
|
||||
focusOnDetail: CommandData;
|
||||
focusOnSearchDefinition: Required<CommandData>;
|
||||
searchNotes: CommandData & {
|
||||
searchString?: string;
|
||||
ancestorNoteId?: string | null;
|
||||
@@ -319,7 +323,6 @@ export type CommandMappings = {
|
||||
printActiveNote: CommandData;
|
||||
exportAsPdf: CommandData;
|
||||
openNoteExternally: CommandData;
|
||||
openNoteCustom: CommandData;
|
||||
renderActiveNote: CommandData;
|
||||
unhoist: CommandData;
|
||||
reloadFrontendApp: CommandData;
|
||||
@@ -523,7 +526,7 @@ export type FilteredCommandNames<T extends CommandData> = keyof Pick<CommandMapp
|
||||
export class AppContext extends Component {
|
||||
isMainWindow: boolean;
|
||||
components: Component[];
|
||||
beforeUnloadListeners: (WeakRef<BeforeUploadListener> | (() => boolean))[];
|
||||
beforeUnloadListeners: WeakRef<BeforeUploadListener>[];
|
||||
tabManager!: TabManager;
|
||||
layout?: Layout;
|
||||
noteTreeWidget?: NoteTreeWidget;
|
||||
@@ -616,7 +619,7 @@ export class AppContext extends Component {
|
||||
component.triggerCommand(commandName, { $el: $(this) });
|
||||
});
|
||||
|
||||
this.child(rootWidget as Component);
|
||||
this.child(rootWidget);
|
||||
|
||||
this.triggerEvent("initialRenderComplete", {});
|
||||
}
|
||||
@@ -646,17 +649,13 @@ export class AppContext extends Component {
|
||||
return $(el).closest(".component").prop("component");
|
||||
}
|
||||
|
||||
addBeforeUnloadListener(obj: BeforeUploadListener | (() => boolean)) {
|
||||
addBeforeUnloadListener(obj: BeforeUploadListener) {
|
||||
if (typeof WeakRef !== "function") {
|
||||
// older browsers don't support WeakRef
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof obj === "object") {
|
||||
this.beforeUnloadListeners.push(new WeakRef<BeforeUploadListener>(obj));
|
||||
} else {
|
||||
this.beforeUnloadListeners.push(obj);
|
||||
}
|
||||
this.beforeUnloadListeners.push(new WeakRef<BeforeUploadListener>(obj));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -666,29 +665,25 @@ const appContext = new AppContext(window.glob.isMainWindow);
|
||||
$(window).on("beforeunload", () => {
|
||||
let allSaved = true;
|
||||
|
||||
appContext.beforeUnloadListeners = appContext.beforeUnloadListeners.filter((wr) => typeof wr === "function" || !!wr.deref());
|
||||
appContext.beforeUnloadListeners = appContext.beforeUnloadListeners.filter((wr) => !!wr.deref());
|
||||
|
||||
for (const listener of appContext.beforeUnloadListeners) {
|
||||
if (typeof listener === "object") {
|
||||
const component = listener.deref();
|
||||
for (const weakRef of appContext.beforeUnloadListeners) {
|
||||
const component = weakRef.deref();
|
||||
|
||||
if (!component) {
|
||||
continue;
|
||||
}
|
||||
if (!component) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!component.beforeUnloadEvent()) {
|
||||
console.log(`Component ${component.componentId} is not finished saving its state.`);
|
||||
allSaved = false;
|
||||
}
|
||||
} else {
|
||||
if (!listener()) {
|
||||
allSaved = false;
|
||||
}
|
||||
if (!component.beforeUnloadEvent()) {
|
||||
console.log(`Component ${component.componentId} is not finished saving its state.`);
|
||||
|
||||
toast.showMessage(t("app_context.please_wait_for_save"), 10000);
|
||||
|
||||
allSaved = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!allSaved) {
|
||||
toast.showMessage(t("app_context.please_wait_for_save"), 10000);
|
||||
return "some string";
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import utils from "../services/utils.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.
|
||||
*
|
||||
@@ -21,7 +19,6 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
|
||||
initialized: Promise<void> | null;
|
||||
parent?: TypedComponent<any>;
|
||||
_position!: number;
|
||||
private listeners: Record<string, EventHandler[]> | null = {};
|
||||
|
||||
constructor() {
|
||||
this.componentId = `${this.sanitizedClassName}-${utils.randomString(8)}`;
|
||||
@@ -79,14 +76,6 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
|
||||
handleEventInChildren<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown[] | unknown> | null {
|
||||
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) {
|
||||
const ret = child.handleEvent(name, data) as Promise<void>;
|
||||
|
||||
@@ -131,35 +120,6 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
|
||||
|
||||
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> {}
|
||||
|
||||
@@ -43,6 +43,8 @@ export default class RootCommandExecutor extends Component {
|
||||
const noteContext = await appContext.tabManager.openTabWithNoteWithHoisting(searchNote.noteId, {
|
||||
activate: true
|
||||
});
|
||||
|
||||
appContext.triggerCommand("focusOnSearchDefinition", { ntxId: noteContext.ntxId });
|
||||
}
|
||||
|
||||
async searchInSubtreeCommand({ notePath }: CommandListenerData<"searchInSubtree">) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import electronContextMenu from "./menus/electron_context_menu.js";
|
||||
import glob from "./services/glob.js";
|
||||
import { t } from "./services/i18n.js";
|
||||
import options from "./services/options.js";
|
||||
import server from "./services/server.js";
|
||||
import type ElectronRemote from "@electron/remote";
|
||||
import type Electron from "electron";
|
||||
import "./stylesheets/bootstrap.scss";
|
||||
|
||||
@@ -1020,14 +1020,6 @@ class FNote {
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -4,13 +4,21 @@ import TabRowWidget from "../widgets/tab_row.js";
|
||||
import TitleBarButtonsWidget from "../widgets/title_bar_buttons.js";
|
||||
import LeftPaneContainer from "../widgets/containers/left_pane_container.js";
|
||||
import NoteTreeWidget from "../widgets/note_tree.js";
|
||||
import NoteTitleWidget from "../widgets/note_title.jsx";
|
||||
import NoteTitleWidget from "../widgets/note_title.js";
|
||||
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 PromotedAttributesWidget from "../widgets/promoted_attributes.js";
|
||||
import RibbonContainer from "../widgets/containers/ribbon_container.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 SearchDefinitionWidget from "../widgets/ribbon_widgets/search_definition.js";
|
||||
import SqlResultWidget from "../widgets/sql_result.js";
|
||||
import SqlTableSchemasWidget from "../widgets/sql_table_schemas.js";
|
||||
import NoteIconWidget from "../widgets/note_icon.jsx";
|
||||
import FilePropertiesWidget from "../widgets/ribbon_widgets/file_properties.js";
|
||||
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 ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
||||
import RootContainer from "../widgets/containers/root_container.js";
|
||||
@@ -21,8 +29,15 @@ import SplitNoteContainer from "../widgets/containers/split_note_container.js";
|
||||
import LeftPaneToggleWidget from "../widgets/buttons/left_pane_toggle.js";
|
||||
import CreatePaneButton from "../widgets/buttons/create_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 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 ShowHighlightsListWidgetButton from "../widgets/buttons/show_highlights_list_widget_button.js";
|
||||
import NoteWrapperWidget from "../widgets/note_wrapper.js";
|
||||
@@ -36,13 +51,16 @@ import FloatingButtons from "../widgets/floating_buttons/floating_buttons.js";
|
||||
import RelationMapButtons from "../widgets/floating_buttons/relation_map_buttons.js";
|
||||
import SvgExportButton from "../widgets/floating_buttons/svg_export_button.js";
|
||||
import LauncherContainer from "../widgets/containers/launcher_container.js";
|
||||
import RevisionsButton from "../widgets/buttons/revisions_button.js";
|
||||
import CodeButtonsWidget from "../widgets/floating_buttons/code_buttons.js";
|
||||
import ApiLogWidget from "../widgets/api_log.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 UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
|
||||
import CopyImageReferenceButton from "../widgets/floating_buttons/copy_image_reference_button.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 utils from "../services/utils.js";
|
||||
import GeoMapButtons from "../widgets/floating_buttons/geo_map_button.js";
|
||||
@@ -55,7 +73,6 @@ import ToggleReadOnlyButton from "../widgets/floating_buttons/toggle_read_only_b
|
||||
import PngExportButton from "../widgets/floating_buttons/png_export_button.js";
|
||||
import RefreshButton from "../widgets/floating_buttons/refresh_button.js";
|
||||
import { applyModals } from "./layout_commons.js";
|
||||
import Ribbon from "../widgets/ribbon/Ribbon.jsx";
|
||||
|
||||
export default class DesktopLayout {
|
||||
|
||||
@@ -134,15 +151,37 @@ export default class DesktopLayout {
|
||||
.css("min-height", "50px")
|
||||
.css("align-items", "center")
|
||||
.cssBlock(".title-row > * { margin: 5px; }")
|
||||
.child(<NoteIconWidget />)
|
||||
.child(<NoteTitleWidget />)
|
||||
.child(new NoteIconWidget())
|
||||
.child(new NoteTitleWidget())
|
||||
.child(new SpacerWidget(0, 1))
|
||||
.child(new MovePaneButton(true))
|
||||
.child(new MovePaneButton(false))
|
||||
.child(new ClosePaneButton())
|
||||
.child(new CreatePaneButton())
|
||||
)
|
||||
.child(<Ribbon />)
|
||||
.child(
|
||||
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 WatchedFileUpdateStatusWidget())
|
||||
.child(
|
||||
@@ -196,8 +235,8 @@ export default class DesktopLayout {
|
||||
.child(new CloseZenButton())
|
||||
|
||||
// Desktop-specific dialogs.
|
||||
.child(<PasswordNoteSetDialog />)
|
||||
.child(<UploadAttachmentsDialog />);
|
||||
.child(new PasswordNoteSetDialog())
|
||||
.child(new UploadAttachmentsDialog());
|
||||
|
||||
applyModals(rootContainer);
|
||||
return rootContainer;
|
||||
@@ -24,48 +24,48 @@ import InfoDialog from "../widgets/dialogs/info.js";
|
||||
import IncorrectCpuArchDialog from "../widgets/dialogs/incorrect_cpu_arch.js";
|
||||
import PopupEditorDialog from "../widgets/dialogs/popup_editor.js";
|
||||
import FlexContainer from "../widgets/containers/flex_container.js";
|
||||
import NoteIconWidget from "../widgets/note_icon";
|
||||
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
|
||||
import NoteIconWidget from "../widgets/note_icon.js";
|
||||
import NoteTitleWidget from "../widgets/note_title.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 NoteListWidget from "../widgets/note_list.js";
|
||||
import CallToActionDialog from "../widgets/dialogs/call_to_action.jsx";
|
||||
import NoteTitleWidget from "../widgets/note_title.jsx";
|
||||
import { PopupEditorFormattingToolbar } from "../widgets/ribbon/FormattingToolbar.js";
|
||||
import { CallToActionDialog } from "../widgets/dialogs/call_to_action.jsx";
|
||||
|
||||
export function applyModals(rootContainer: RootContainer) {
|
||||
rootContainer
|
||||
.child(<BulkActionsDialog />)
|
||||
.child(<AboutDialog />)
|
||||
.child(<HelpDialog />)
|
||||
.child(<RecentChangesDialog />)
|
||||
.child(<BranchPrefixDialog />)
|
||||
.child(<SortChildNotesDialog />)
|
||||
.child(<IncludeNoteDialog />)
|
||||
.child(<NoteTypeChooserDialog />)
|
||||
.child(<JumpToNoteDialog />)
|
||||
.child(<AddLinkDialog />)
|
||||
.child(<CloneToDialog />)
|
||||
.child(<MoveToDialog />)
|
||||
.child(<ImportDialog />)
|
||||
.child(<ExportDialog />)
|
||||
.child(<MarkdownImportDialog />)
|
||||
.child(<ProtectedSessionPasswordDialog />)
|
||||
.child(<RevisionsDialog />)
|
||||
.child(<DeleteNotesDialog />)
|
||||
.child(<InfoDialog />)
|
||||
.child(<ConfirmDialog />)
|
||||
.child(<PromptDialog />)
|
||||
.child(<IncorrectCpuArchDialog />)
|
||||
.child(new BulkActionsDialog())
|
||||
.child(new AboutDialog())
|
||||
.child(new HelpDialog())
|
||||
.child(new RecentChangesDialog())
|
||||
.child(new BranchPrefixDialog())
|
||||
.child(new SortChildNotesDialog())
|
||||
.child(new IncludeNoteDialog())
|
||||
.child(new NoteTypeChooserDialog())
|
||||
.child(new JumpToNoteDialog())
|
||||
.child(new AddLinkDialog())
|
||||
.child(new CloneToDialog())
|
||||
.child(new MoveToDialog())
|
||||
.child(new ImportDialog())
|
||||
.child(new ExportDialog())
|
||||
.child(new MarkdownImportDialog())
|
||||
.child(new ProtectedSessionPasswordDialog())
|
||||
.child(new RevisionsDialog())
|
||||
.child(new DeleteNotesDialog())
|
||||
.child(new InfoDialog())
|
||||
.child(new ConfirmDialog())
|
||||
.child(new PromptDialog())
|
||||
.child(new IncorrectCpuArchDialog())
|
||||
.child(new PopupEditorDialog()
|
||||
.child(new FlexContainer("row")
|
||||
.class("title-row")
|
||||
.css("align-items", "center")
|
||||
.cssBlock(".title-row > * { margin: 5px; }")
|
||||
.child(<NoteIconWidget />)
|
||||
.child(<NoteTitleWidget />))
|
||||
.child(<PopupEditorFormattingToolbar />)
|
||||
.child(new NoteIconWidget())
|
||||
.child(new NoteTitleWidget()))
|
||||
.child(new ClassicEditorToolbar())
|
||||
.child(new PromotedAttributesWidget())
|
||||
.child(new NoteDetailWidget())
|
||||
.child(new NoteListWidget(true)))
|
||||
.child(<CallToActionDialog />);
|
||||
.child(new CallToActionDialog());
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import ToggleSidebarButtonWidget from "../widgets/mobile_widgets/toggle_sidebar_
|
||||
import MobileDetailMenuWidget from "../widgets/mobile_widgets/mobile_detail_menu.js";
|
||||
import ScreenContainer from "../widgets/mobile_widgets/screen_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 EditButton from "../widgets/floating_buttons/edit_button.js";
|
||||
import RelationMapButtons from "../widgets/floating_buttons/relation_map_buttons.js";
|
||||
@@ -18,18 +19,14 @@ import GlobalMenuWidget from "../widgets/buttons/global_menu.js";
|
||||
import LauncherContainer from "../widgets/containers/launcher_container.js";
|
||||
import RootContainer from "../widgets/containers/root_container.js";
|
||||
import SharedInfoWidget from "../widgets/shared_info.js";
|
||||
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
|
||||
import PromotedAttributesWidget from "../widgets/ribbon_widgets/promoted_attributes.js";
|
||||
import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js";
|
||||
import type AppContext from "../components/app_context.js";
|
||||
import TabRowWidget from "../widgets/tab_row.js";
|
||||
import RefreshButton from "../widgets/floating_buttons/refresh_button.js";
|
||||
import MobileEditorToolbar from "../widgets/type_widgets/ckeditor/mobile_editor_toolbar.js";
|
||||
import MobileEditorToolbar from "../widgets/ribbon_widgets/mobile_editor_toolbar.js";
|
||||
import { applyModals } from "./layout_commons.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 = `
|
||||
<style>
|
||||
@@ -147,7 +144,7 @@ export default class MobileLayout {
|
||||
.css("font-size", "larger")
|
||||
.css("align-items", "center")
|
||||
.child(new ToggleSidebarButtonWidget().contentSized())
|
||||
.child(<NoteTitleWidget />)
|
||||
.child(new NoteTitleWidget().contentSized().css("position", "relative").css("padding-left", "0.5em"))
|
||||
.child(new MobileDetailMenuWidget(true).contentSized())
|
||||
)
|
||||
.child(new SharedInfoWidget())
|
||||
@@ -167,7 +164,7 @@ export default class MobileLayout {
|
||||
.contentSized()
|
||||
.child(new NoteDetailWidget())
|
||||
.child(new NoteListWidget(false))
|
||||
.child(<FilePropertiesWrapper />)
|
||||
.child(new FilePropertiesWidget().css("font-size", "smaller"))
|
||||
)
|
||||
.child(new MobileEditorToolbar())
|
||||
)
|
||||
@@ -184,13 +181,3 @@ export default class MobileLayout {
|
||||
return rootContainer;
|
||||
}
|
||||
}
|
||||
|
||||
function FilePropertiesWrapper() {
|
||||
const { note } = useNoteContext();
|
||||
|
||||
return (
|
||||
<div>
|
||||
{note?.type === "file" && <FilePropertiesTab note={note} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import server from "./server.js";
|
||||
import froca from "./froca.js";
|
||||
import type FNote from "../entities/fnote.js";
|
||||
import type { AttributeRow } from "./load_results.js";
|
||||
import { AttributeType } from "@triliumnext/commons";
|
||||
|
||||
async function addLabel(noteId: string, name: string, value: string = "", isInheritable = false) {
|
||||
await server.put(`notes/${noteId}/attribute`, {
|
||||
@@ -26,14 +25,6 @@ async function removeAttributeById(noteId: string, attributeId: string) {
|
||||
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.
|
||||
* it will not remove inherited attributes.
|
||||
@@ -61,7 +52,7 @@ function removeOwnedLabelByName(note: FNote, labelName: string) {
|
||||
* @param value the value of the attribute to set.
|
||||
*/
|
||||
export async function setAttribute(note: FNote, type: "label" | "relation", name: string, value: string | null | undefined) {
|
||||
if (value !== null && value !== undefined) {
|
||||
if (value) {
|
||||
// Create or update the attribute.
|
||||
await server.put(`notes/${note.noteId}/set-attribute`, { type, name, value });
|
||||
} else {
|
||||
|
||||
@@ -18,7 +18,7 @@ import type FNote from "../entities/fnote.js";
|
||||
import toast from "./toast.js";
|
||||
import { BulkAction } from "@triliumnext/commons";
|
||||
|
||||
export const ACTION_GROUPS = [
|
||||
const ACTION_GROUPS = [
|
||||
{
|
||||
title: t("bulk_actions.labels"),
|
||||
actions: [AddLabelBulkAction, UpdateLabelValueBulkAction, RenameLabelBulkAction, DeleteLabelBulkAction]
|
||||
|
||||
223
apps/client/src/services/ckeditor_plugin_config.ts
Normal file
223
apps/client/src/services/ckeditor_plugin_config.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* @module CKEditor Plugin Configuration Service
|
||||
*
|
||||
* This service manages the dynamic configuration of CKEditor plugins based on user preferences.
|
||||
* It handles plugin enablement, dependency resolution, and toolbar configuration.
|
||||
*/
|
||||
|
||||
import server from "./server.js";
|
||||
import type {
|
||||
PluginConfiguration,
|
||||
PluginMetadata,
|
||||
PluginRegistry,
|
||||
PluginValidationResult
|
||||
} from "@triliumnext/commons";
|
||||
|
||||
/**
|
||||
* Cache for plugin registry and user configuration
|
||||
*/
|
||||
let pluginRegistryCache: PluginRegistry | null = null;
|
||||
let userConfigCache: PluginConfiguration[] | null = null;
|
||||
let cacheTimestamp = 0;
|
||||
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
/**
|
||||
* Get the plugin registry from server
|
||||
*/
|
||||
export async function getPluginRegistry(): Promise<PluginRegistry> {
|
||||
const now = Date.now();
|
||||
if (pluginRegistryCache && (now - cacheTimestamp) < CACHE_DURATION) {
|
||||
return pluginRegistryCache;
|
||||
}
|
||||
|
||||
try {
|
||||
pluginRegistryCache = await server.get<PluginRegistry>('ckeditor-plugins/registry');
|
||||
cacheTimestamp = now;
|
||||
return pluginRegistryCache;
|
||||
} catch (error) {
|
||||
console.error('Failed to load CKEditor plugin registry:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's plugin configuration from server
|
||||
*/
|
||||
export async function getUserPluginConfig(): Promise<PluginConfiguration[]> {
|
||||
const now = Date.now();
|
||||
if (userConfigCache && (now - cacheTimestamp) < CACHE_DURATION) {
|
||||
return userConfigCache;
|
||||
}
|
||||
|
||||
try {
|
||||
userConfigCache = await server.get<PluginConfiguration[]>('ckeditor-plugins/config');
|
||||
cacheTimestamp = now;
|
||||
return userConfigCache;
|
||||
} catch (error) {
|
||||
console.error('Failed to load user plugin configuration:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the cache (call when configuration is updated)
|
||||
*/
|
||||
export function clearCache(): void {
|
||||
pluginRegistryCache = null;
|
||||
userConfigCache = null;
|
||||
cacheTimestamp = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the enabled plugins for the current user
|
||||
*/
|
||||
export async function getEnabledPlugins(): Promise<Set<string>> {
|
||||
const userConfig = await getUserPluginConfig();
|
||||
const enabledPlugins = new Set<string>();
|
||||
|
||||
// Add all enabled user plugins
|
||||
userConfig.forEach(config => {
|
||||
if (config.enabled) {
|
||||
enabledPlugins.add(config.id);
|
||||
}
|
||||
});
|
||||
|
||||
// Always include core plugins
|
||||
const registry = await getPluginRegistry();
|
||||
Object.values(registry.plugins).forEach(plugin => {
|
||||
if (plugin.isCore) {
|
||||
enabledPlugins.add(plugin.id);
|
||||
}
|
||||
});
|
||||
|
||||
return enabledPlugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get disabled plugin names for CKEditor config
|
||||
*/
|
||||
export async function getDisabledPlugins(): Promise<string[]> {
|
||||
try {
|
||||
const registry = await getPluginRegistry();
|
||||
const enabledPlugins = await getEnabledPlugins();
|
||||
const disabledPlugins: string[] = [];
|
||||
|
||||
// Find plugins that are disabled
|
||||
Object.values(registry.plugins).forEach(plugin => {
|
||||
if (!plugin.isCore && !enabledPlugins.has(plugin.id)) {
|
||||
// Map plugin ID to actual CKEditor plugin names if needed
|
||||
const pluginNames = getPluginNames(plugin.id);
|
||||
disabledPlugins.push(...pluginNames);
|
||||
}
|
||||
});
|
||||
|
||||
return disabledPlugins;
|
||||
} catch (error) {
|
||||
console.warn("Failed to get disabled plugins, returning empty list:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map plugin ID to actual CKEditor plugin names
|
||||
* Some plugins might have multiple names or different names than their ID
|
||||
*/
|
||||
function getPluginNames(pluginId: string): string[] {
|
||||
const nameMap: Record<string, string[]> = {
|
||||
"emoji": ["EmojiMention", "EmojiPicker"],
|
||||
"math": ["Math", "AutoformatMath"],
|
||||
"image": ["Image", "ImageCaption", "ImageInline", "ImageResize", "ImageStyle", "ImageToolbar", "ImageUpload"],
|
||||
"table": ["Table", "TableToolbar", "TableProperties", "TableCellProperties", "TableSelection", "TableCaption", "TableColumnResize"],
|
||||
"font": ["Font", "FontColor", "FontBackgroundColor"],
|
||||
"list": ["List", "ListProperties"],
|
||||
"specialcharacters": ["SpecialCharacters", "SpecialCharactersEssentials"],
|
||||
"findandreplace": ["FindAndReplace"],
|
||||
"horizontalline": ["HorizontalLine"],
|
||||
"pagebreak": ["PageBreak"],
|
||||
"removeformat": ["RemoveFormat"],
|
||||
"alignment": ["Alignment"],
|
||||
"indent": ["Indent", "IndentBlock"],
|
||||
"codeblock": ["CodeBlock"],
|
||||
"blockquote": ["BlockQuote"],
|
||||
"todolist": ["TodoList"],
|
||||
"heading": ["Heading", "HeadingButtonsUI"],
|
||||
"paragraph": ["ParagraphButtonUI"],
|
||||
// Add more mappings as needed
|
||||
};
|
||||
|
||||
return nameMap[pluginId] || [pluginId.charAt(0).toUpperCase() + pluginId.slice(1)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the current plugin configuration
|
||||
*/
|
||||
export async function validatePluginConfiguration(): Promise<PluginValidationResult> {
|
||||
try {
|
||||
const userConfig = await getUserPluginConfig();
|
||||
return await server.post<PluginValidationResult>('ckeditor-plugins/validate', {
|
||||
plugins: userConfig
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to validate plugin configuration:', error);
|
||||
return {
|
||||
valid: false,
|
||||
errors: [{
|
||||
type: "missing_dependency",
|
||||
pluginId: "unknown",
|
||||
message: `Validation failed: ${error}`
|
||||
}],
|
||||
warnings: [],
|
||||
resolvedPlugins: []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get toolbar items that should be hidden based on disabled plugins
|
||||
*/
|
||||
export async function getHiddenToolbarItems(): Promise<string[]> {
|
||||
const registry = await getPluginRegistry();
|
||||
const enabledPlugins = await getEnabledPlugins();
|
||||
const hiddenItems: string[] = [];
|
||||
|
||||
Object.values(registry.plugins).forEach(plugin => {
|
||||
if (!enabledPlugins.has(plugin.id) && plugin.toolbarItems) {
|
||||
hiddenItems.push(...plugin.toolbarItems);
|
||||
}
|
||||
});
|
||||
|
||||
return hiddenItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user plugin configuration
|
||||
*/
|
||||
export async function updatePluginConfiguration(plugins: PluginConfiguration[]): Promise<void> {
|
||||
try {
|
||||
const response = await server.put('ckeditor-plugins/config', {
|
||||
plugins,
|
||||
validate: true
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
throw new Error(response.errors?.join(", ") || "Update failed");
|
||||
}
|
||||
|
||||
// Clear cache so next requests fetch fresh data
|
||||
clearCache();
|
||||
} catch (error) {
|
||||
console.error('Failed to update plugin configuration:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
getPluginRegistry,
|
||||
getUserPluginConfig,
|
||||
getEnabledPlugins,
|
||||
getDisabledPlugins,
|
||||
getHiddenToolbarItems,
|
||||
validatePluginConfiguration,
|
||||
updatePluginConfiguration,
|
||||
clearCache
|
||||
};
|
||||
@@ -36,8 +36,6 @@ export interface Suggestion {
|
||||
commandId?: string;
|
||||
commandDescription?: string;
|
||||
commandShortcut?: string;
|
||||
attributeSnippet?: string;
|
||||
highlightedAttributeSnippet?: string;
|
||||
}
|
||||
|
||||
export interface Options {
|
||||
@@ -325,33 +323,7 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
// Add special class for search-notes action
|
||||
const actionClass = suggestion.action === "search-notes" ? "search-notes-action" : "";
|
||||
|
||||
// Choose appropriate icon based on action
|
||||
let iconClass = suggestion.icon ?? "bx bx-note";
|
||||
if (suggestion.action === "search-notes") {
|
||||
iconClass = "bx bx-search";
|
||||
} else if (suggestion.action === "create-note") {
|
||||
iconClass = "bx bx-plus";
|
||||
} else if (suggestion.action === "external-link") {
|
||||
iconClass = "bx bx-link-external";
|
||||
}
|
||||
|
||||
// Simplified HTML structure without nested divs
|
||||
let html = `<div class="note-suggestion ${actionClass}">`;
|
||||
html += `<span class="icon ${iconClass}"></span>`;
|
||||
html += `<span class="text">`;
|
||||
html += `<span class="search-result-title">${suggestion.highlightedNotePathTitle}</span>`;
|
||||
|
||||
// Add attribute snippet inline if available
|
||||
if (suggestion.highlightedAttributeSnippet) {
|
||||
html += `<span class="search-result-attributes">${suggestion.highlightedAttributeSnippet}</span>`;
|
||||
}
|
||||
|
||||
html += `</span>`;
|
||||
html += `</div>`;
|
||||
return html;
|
||||
return `<span class="${suggestion.icon ?? "bx bx-note"}"></span> ${suggestion.highlightedNotePathTitle}`;
|
||||
}
|
||||
},
|
||||
// we can't cache identical searches because notes can be created / renamed, new recent notes can be added
|
||||
|
||||
@@ -35,7 +35,7 @@ function download(url: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export function downloadFileNote(noteId: string) {
|
||||
function downloadFileNote(noteId: string) {
|
||||
const url = `${getFileUrl("notes", noteId)}?${Date.now()}`; // don't use cache
|
||||
|
||||
download(url);
|
||||
@@ -163,7 +163,7 @@ async function openExternally(type: string, entityId: string, mime: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export const openNoteExternally = async (noteId: string, mime: string) => await openExternally("notes", noteId, mime);
|
||||
const openNoteExternally = async (noteId: string, mime: string) => await openExternally("notes", noteId, mime);
|
||||
const openAttachmentExternally = async (attachmentId: string, mime: string) => await openExternally("attachments", attachmentId, mime);
|
||||
|
||||
function getHost() {
|
||||
|
||||
@@ -148,7 +148,7 @@ export function isElectron() {
|
||||
return !!(window && window.process && window.process.type);
|
||||
}
|
||||
|
||||
export function isMac() {
|
||||
function isMac() {
|
||||
return navigator.platform.indexOf("Mac") > -1;
|
||||
}
|
||||
|
||||
@@ -185,11 +185,7 @@ export function escapeQuotes(value: string) {
|
||||
return value.replaceAll('"', """);
|
||||
}
|
||||
|
||||
export function formatSize(size: number | null | undefined) {
|
||||
if (size === null || size === undefined) {
|
||||
return "";
|
||||
}
|
||||
|
||||
function formatSize(size: number) {
|
||||
size = Math.max(Math.round(size / 1024), 1);
|
||||
|
||||
if (size < 1024) {
|
||||
@@ -296,7 +292,7 @@ function isHtmlEmpty(html: string) {
|
||||
);
|
||||
}
|
||||
|
||||
export async function clearBrowserCache() {
|
||||
async function clearBrowserCache() {
|
||||
if (isElectron()) {
|
||||
const win = dynamicRequire("@electron/remote").getCurrentWindow();
|
||||
await win.webContents.session.clearCache();
|
||||
@@ -744,7 +740,7 @@ function isUpdateAvailable(latestVersion: string | null | undefined, currentVers
|
||||
return compareVersions(latestVersion, currentVersion) > 0;
|
||||
}
|
||||
|
||||
export function isLaunchBarConfig(noteId: string) {
|
||||
function isLaunchBarConfig(noteId: string) {
|
||||
return ["_lbRoot", "_lbAvailableLaunchers", "_lbVisibleLaunchers", "_lbMobileRoot", "_lbMobileAvailableLaunchers", "_lbMobileVisibleLaunchers"].includes(noteId);
|
||||
}
|
||||
|
||||
@@ -792,38 +788,6 @@ export function arrayEqual<T>(a: T[], b: T[]) {
|
||||
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 {
|
||||
reloadFrontendApp,
|
||||
restartDesktopApp,
|
||||
|
||||
@@ -862,34 +862,10 @@ table.promoted-attributes-in-tooltip th {
|
||||
|
||||
.aa-dropdown-menu .aa-suggestion {
|
||||
cursor: pointer;
|
||||
padding: 6px 16px;
|
||||
padding: 5px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.aa-dropdown-menu .aa-suggestion .icon {
|
||||
display: inline-block;
|
||||
line-height: inherit;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.aa-dropdown-menu .aa-suggestion .text {
|
||||
display: inline-block;
|
||||
width: calc(100% - 20px);
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.aa-dropdown-menu .aa-suggestion .search-result-title {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.aa-dropdown-menu .aa-suggestion .search-result-attributes {
|
||||
display: block;
|
||||
font-size: 0.8em;
|
||||
color: var(--muted-text-color);
|
||||
opacity: 0.6;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.aa-dropdown-menu .aa-suggestion p {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
@@ -1819,42 +1795,20 @@ textarea {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.jump-to-note-dialog .modal-dialog {
|
||||
max-width: 900px;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.jump-to-note-dialog .modal-header {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.jump-to-note-dialog .modal-body {
|
||||
padding: 0;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.jump-to-note-results .aa-dropdown-menu {
|
||||
max-height: calc(80vh - 200px);
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.jump-to-note-results {
|
||||
width: 100%;
|
||||
max-height: 40vh;
|
||||
}
|
||||
|
||||
.jump-to-note-results .aa-suggestions {
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.jump-to-note-results .aa-dropdown-menu .aa-suggestion:hover,
|
||||
.jump-to-note-results .aa-dropdown-menu .aa-cursor {
|
||||
background-color: var(--hover-item-background-color, #f8f9fa);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* Command palette styling */
|
||||
@@ -1872,24 +1826,8 @@ textarea {
|
||||
|
||||
.jump-to-note-dialog .aa-cursor .command-suggestion,
|
||||
.jump-to-note-dialog .aa-suggestion:hover .command-suggestion {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.jump-to-note-dialog .show-in-full-search,
|
||||
.jump-to-note-results .show-in-full-search {
|
||||
border-top: 1px solid var(--main-border-color);
|
||||
padding-top: 12px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.jump-to-note-results .aa-suggestion .search-notes-action {
|
||||
border-top: 1px solid var(--main-border-color);
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.jump-to-note-results .aa-suggestion:has(.search-notes-action)::after {
|
||||
display: none;
|
||||
border-left-color: var(--link-color);
|
||||
background-color: var(--hover-background-color);
|
||||
}
|
||||
|
||||
.jump-to-note-dialog .command-icon {
|
||||
@@ -2346,8 +2284,7 @@ footer.webview-footer button {
|
||||
|
||||
/* Search result highlighting */
|
||||
.search-result-title b,
|
||||
.search-result-content b,
|
||||
.search-result-attributes b {
|
||||
.search-result-content b {
|
||||
font-weight: 900;
|
||||
color: var(--admonition-warning-accent-color);
|
||||
}
|
||||
|
||||
@@ -536,9 +536,10 @@ body.mobile .dropdown-menu .dropdown-item.submenu-open .dropdown-toggle::after {
|
||||
}
|
||||
|
||||
/* List item */
|
||||
.jump-to-note-dialog .aa-suggestion,
|
||||
.note-detail-empty .aa-suggestion {
|
||||
.jump-to-note-dialog .aa-suggestions div,
|
||||
.note-detail-empty .aa-suggestions div {
|
||||
border-radius: 6px;
|
||||
padding: 6px 12px;
|
||||
color: var(--menu-text-color);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
@@ -848,7 +848,7 @@
|
||||
"debug": "调试",
|
||||
"debug_description": "调试将打印额外的调试信息到控制台,以帮助调试复杂查询",
|
||||
"action": "操作",
|
||||
"search_button": "搜索",
|
||||
"search_button": "搜索 <kbd>回车</kbd>",
|
||||
"search_execute": "搜索并执行操作",
|
||||
"save_to_note": "保存到笔记",
|
||||
"search_parameters": "搜索参数",
|
||||
|
||||
@@ -202,12 +202,11 @@
|
||||
"okButton": "OK"
|
||||
},
|
||||
"jump_to_note": {
|
||||
"search_button": "Suche im Volltext",
|
||||
"search_placeholder": "Suche nach Notiz anhand ihres Titels oder gib > ein für Kommandos..."
|
||||
"search_button": "Suche im Volltext"
|
||||
},
|
||||
"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_success": "Markdown-Inhalt wurde in das Dokument importiert."
|
||||
},
|
||||
@@ -218,26 +217,21 @@
|
||||
"search_placeholder": "Suche nach einer Notiz anhand ihres Namens",
|
||||
"move_button": "Zur ausgewählten Notiz wechseln",
|
||||
"error_no_path": "Kein Weg, auf den man sich bewegen kann.",
|
||||
"move_success_message": "Ausgewählte Notizen wurden verschoben in "
|
||||
"move_success_message": "Ausgewählte Notizen wurden verschoben"
|
||||
},
|
||||
"note_type_chooser": {
|
||||
"modal_title": "Wähle den Notiztyp aus",
|
||||
"modal_body": "Wähle den Notiztyp / die Vorlage der neuen Notiz:",
|
||||
"templates": "Vorlagen",
|
||||
"change_path_prompt": "Ändern wo die neue Notiz erzeugt wird:",
|
||||
"search_placeholder": "Durchsuche Pfad nach Namen (Standard falls leer)",
|
||||
"builtin_templates": "Eingebaute Vorlage"
|
||||
"templates": "Vorlagen"
|
||||
},
|
||||
"password_not_set": {
|
||||
"title": "Das Passwort ist nicht festgelegt",
|
||||
"body1": "Geschützte Notizen werden mit einem Benutzerpasswort verschlüsselt, es wurde jedoch noch kein Passwort festgelegt.",
|
||||
"body2": "Um Notizen zu schützen, klicke den unteren Button um den Optionsdialog zu öffnen und dein Passwort festzulegen.",
|
||||
"go_to_password_options": "Gehe zu Passwortoptionen"
|
||||
"body1": "Geschützte Notizen werden mit einem Benutzerpasswort verschlüsselt, es wurde jedoch noch kein Passwort festgelegt."
|
||||
},
|
||||
"prompt": {
|
||||
"title": "Eingabeaufforderung",
|
||||
"title": "Prompt",
|
||||
"ok": "OK",
|
||||
"defaultTitle": "Eingabeaufforderung"
|
||||
"defaultTitle": "Prompt"
|
||||
},
|
||||
"protected_session_password": {
|
||||
"modal_title": "Geschützte Sitzung",
|
||||
@@ -271,12 +265,10 @@
|
||||
"maximum_revisions": "Maximale Revisionen für aktuelle Notiz: {{number}}.",
|
||||
"settings": "Einstellungen für Notizrevisionen",
|
||||
"download_button": "Herunterladen",
|
||||
"mime": "MIME: ",
|
||||
"mime": "MIME:",
|
||||
"file_size": "Dateigröße:",
|
||||
"preview": "Vorschau:",
|
||||
"preview_not_available": "Für diesen Notiztyp ist keine Vorschau verfügbar.",
|
||||
"restore_button": "Wiederherstellen",
|
||||
"delete_button": "Löschen"
|
||||
"preview_not_available": "Für diesen Notiztyp ist keine Vorschau verfügbar."
|
||||
},
|
||||
"sort_child_notes": {
|
||||
"sort_children_by": "Unternotizen sortieren nach...",
|
||||
@@ -356,7 +348,7 @@
|
||||
"sorted": "Hält untergeordnete Notizen alphabetisch nach Titel sortiert",
|
||||
"sort_direction": "ASC (Standard) oder DESC",
|
||||
"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",
|
||||
"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",
|
||||
@@ -379,10 +371,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.",
|
||||
"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",
|
||||
"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_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, unter 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_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",
|
||||
@@ -398,7 +390,7 @@
|
||||
"color": "Definiert die Farbe der Notiz im Notizbaum, in Links usw. Verwende einen beliebigen gültigen CSS-Farbwert wie „rot“ oder #a13d5f",
|
||||
"keyboard_shortcut": "Definiert eine Tastenkombination, die sofort zu dieser Notiz springt. Beispiel: „Strg+Alt+E“. Erfordert ein Neuladen des Frontends, damit die Änderung wirksam wird.",
|
||||
"keep_current_hoisting": "Das Öffnen dieses Links ändert das Hochziehen nicht, selbst wenn die Notiz im aktuell hochgezogenen Unterbaum nicht angezeigt werden kann.",
|
||||
"execute_button": "Titel der Schaltfläche, welche die aktuelle Codenotiz ausführt",
|
||||
"execute_button": "Titel der Schaltfläche, die die aktuelle Codenotiz ausführt",
|
||||
"execute_description": "Längere Beschreibung der aktuellen Codenotiz, die zusammen mit der Schaltfläche „Ausführen“ angezeigt wird",
|
||||
"exclude_from_note_map": "Notizen mit dieser Bezeichnung werden in der Notizenkarte ausgeblendet",
|
||||
"new_notes_on_top": "Neue Notizen werden oben in der übergeordneten Notiz erstellt, nicht unten.",
|
||||
@@ -413,10 +405,10 @@
|
||||
"run_on_branch_change": "wird ausgeführt, wenn ein Zweig aktualisiert wird.",
|
||||
"run_on_branch_deletion": "wird ausgeführt, wenn ein Zweig gelöscht wird. Der Zweig ist eine Verknüpfung zwischen der übergeordneten Notiz und der untergeordneten Notiz und wird z. B. gelöscht. beim Verschieben der Notiz (alter Zweig/Link wird gelöscht).",
|
||||
"run_on_attribute_creation": "wird ausgeführt, wenn für die Notiz ein neues Attribut erstellt wird, das diese Beziehung definiert",
|
||||
"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.",
|
||||
"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",
|
||||
"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“.",
|
||||
@@ -426,8 +418,7 @@
|
||||
"other_notes_with_name": "Other notes with {{attributeType}} name \"{{attributeName}}\"",
|
||||
"and_more": "... und {{count}} mehr.",
|
||||
"print_landscape": "Beim Export als PDF, wird die Seitenausrichtung Querformat anstatt Hochformat verwendet.",
|
||||
"print_page_size": "Beim Export als PDF, wird die Größe der Seite angepasst. Unterstützte Größen: <code>A0</code>, <code>A1</code>, <code>A2</code>, <code>A3</code>, <code>A4</code>, <code>A5</code>, <code>A6</code>, <code>Legal</code>, <code>Letter</code>, <code>Tabloid</code>, <code>Ledger</code>.",
|
||||
"color_type": "Farbe"
|
||||
"print_page_size": "Beim Export als PDF, wird die Größe der Seite angepasst. Unterstützte Größen: <code>A0</code>, <code>A1</code>, <code>A2</code>, <code>A3</code>, <code>A4</code>, <code>A5</code>, <code>A6</code>, <code>Legal</code>, <code>Letter</code>, <code>Tabloid</code>, <code>Ledger</code>."
|
||||
},
|
||||
"attribute_editor": {
|
||||
"help_text_body1": "Um ein Label hinzuzufügen, gebe einfach z.B. ein. <code>#rock</code> oder wenn du auch einen Wert hinzufügen möchten, dann z.B. <code>#year = 2024</code>",
|
||||
@@ -499,9 +490,9 @@
|
||||
"to": "nach",
|
||||
"target_parent_note": "Ziel-Übergeordnetenotiz",
|
||||
"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)",
|
||||
"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 (d. h. dies würde einen Baumzyklus erzeugen)."
|
||||
},
|
||||
"rename_note": {
|
||||
"rename_note": "Notiz umbenennen",
|
||||
@@ -509,10 +500,10 @@
|
||||
"new_note_title": "neuer Notiztitel",
|
||||
"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:",
|
||||
"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_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 hrefu003d'https://zadam.github.io/trilium/backend_api/Note.html'>Notiz</a> und seinen <a hrefu003d'https://day.js.org/ docs/en/display/format'>dateCreatedObj / utcDateCreatedObj-Eigenschaften</a> für Details."
|
||||
},
|
||||
"add_relation": {
|
||||
"add_relation": "Beziehung hinzufügen",
|
||||
@@ -586,8 +577,7 @@
|
||||
"september": "September",
|
||||
"october": "Oktober",
|
||||
"november": "November",
|
||||
"december": "Dezember",
|
||||
"cannot_find_week_note": "Wochennotiz kann nicht gefunden werden"
|
||||
"december": "Dezember"
|
||||
},
|
||||
"close_pane_button": {
|
||||
"close_this_pane": "Schließe diesen Bereich"
|
||||
@@ -709,14 +699,14 @@
|
||||
"zoom_out_title": "Herauszoomen"
|
||||
},
|
||||
"zpetne_odkazy": {
|
||||
"backlink": "{{count}} Rückverlinkung",
|
||||
"backlinks": "{{count}} Rückverlinkungen",
|
||||
"backlink": "{{count}} Backlink",
|
||||
"backlinks": "{{count}} Backlinks",
|
||||
"relation": "Beziehung"
|
||||
},
|
||||
"mobile_detail_menu": {
|
||||
"insert_child_note": "Untergeordnete Notiz einfügen",
|
||||
"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}}"
|
||||
},
|
||||
"note_icon": {
|
||||
@@ -728,8 +718,7 @@
|
||||
"basic_properties": {
|
||||
"note_type": "Notiztyp",
|
||||
"editable": "Bearbeitbar",
|
||||
"basic_properties": "Grundlegende Eigenschaften",
|
||||
"language": "Sprache"
|
||||
"basic_properties": "Grundlegende Eigenschaften"
|
||||
},
|
||||
"book_properties": {
|
||||
"view_type": "Ansichtstyp",
|
||||
@@ -740,11 +729,7 @@
|
||||
"collapse": "Einklappen",
|
||||
"expand": "Ausklappen",
|
||||
"invalid_view_type": "Ungültiger Ansichtstyp „{{type}}“",
|
||||
"calendar": "Kalender",
|
||||
"book_properties": "Sammlungseigenschaften",
|
||||
"table": "Tabelle",
|
||||
"geo-map": "Weltkarte",
|
||||
"board": "Tafel"
|
||||
"calendar": "Kalender"
|
||||
},
|
||||
"edited_notes": {
|
||||
"no_edited_notes_found": "An diesem Tag wurden noch keine Notizen bearbeitet...",
|
||||
@@ -820,9 +805,7 @@
|
||||
"unknown_label_type": "Unbekannter Labeltyp „{{type}}“",
|
||||
"unknown_attribute_type": "Unbekannter Attributtyp „{{type}}“",
|
||||
"add_new_attribute": "Neues Attribut hinzufügen",
|
||||
"remove_this_attribute": "Entferne dieses Attribut",
|
||||
"unset-field-placeholder": "nicht gesetzt",
|
||||
"remove_color": "Entferne Farblabel"
|
||||
"remove_this_attribute": "Entferne dieses Attribut"
|
||||
},
|
||||
"script_executor": {
|
||||
"query": "Abfrage",
|
||||
@@ -845,7 +828,7 @@
|
||||
"debug": "debuggen",
|
||||
"debug_description": "Debug gibt zusätzliche Debuginformationen in die Konsole aus, um das Debuggen komplexer Abfragen zu erleichtern",
|
||||
"action": "Aktion",
|
||||
"search_button": "Suchen",
|
||||
"search_button": "Suchen <kbd>Eingabetaste</kbd>",
|
||||
"search_execute": "Aktionen suchen und ausführen",
|
||||
"save_to_note": "Als Notiz speichern",
|
||||
"search_parameters": "Suchparameter",
|
||||
@@ -884,7 +867,7 @@
|
||||
"include_archived_notes": "Füge archivierte Notizen hinzu"
|
||||
},
|
||||
"limit": {
|
||||
"limit": "Limitierung",
|
||||
"limit": "Limit",
|
||||
"take_first_x_results": "Nehmen Sie nur die ersten X angegebenen Ergebnisse."
|
||||
},
|
||||
"order_by": {
|
||||
@@ -934,7 +917,7 @@
|
||||
"attachment_detail": {
|
||||
"open_help_page": "Hilfeseite zu Anhängen öffnen",
|
||||
"owning_note": "Eigentümernotiz: ",
|
||||
"you_can_also_open": ", Du kannst auch das öffnen ",
|
||||
"you_can_also_open": ", Du kannst auch das öffnen",
|
||||
"list_of_all_attachments": "Liste aller Anhänge",
|
||||
"attachment_deleted": "Dieser Anhang wurde gelöscht."
|
||||
},
|
||||
@@ -959,8 +942,7 @@
|
||||
"enter_workspace": "Betrete den Arbeitsbereich {{title}}"
|
||||
},
|
||||
"file": {
|
||||
"file_preview_not_available": "Für dieses Dateiformat ist keine Dateivorschau verfügbar.",
|
||||
"too_big": "Die Vorschau zeigt aus Effizienzgründen nur die ersten {{maxNumChars}} Zeichen der Datei an. Lade die Datei herunter und öffne sie extern um den gesamten Inhalt zu sehen."
|
||||
"file_preview_not_available": "Für dieses Dateiformat ist keine Dateivorschau verfügbar."
|
||||
},
|
||||
"protected_session": {
|
||||
"enter_password_instruction": "Um die geschützte Notiz anzuzeigen, musst du dein Passwort eingeben:",
|
||||
@@ -999,7 +981,7 @@
|
||||
"web_view": {
|
||||
"web_view": "Webansicht",
|
||||
"embed_websites": "Notiz vom Typ Web View ermöglicht das Einbetten von Websites in Trilium.",
|
||||
"create_label": "Um zu beginnen, erstelle bitte ein Label mit einer URL-Adresse, die eingebettet werden soll, z. B. #webViewSrc=\"https://www.google.com\""
|
||||
"create_label": "To start, please create a label with a URL address you want to embed, e.g. #webViewSrc=\"https://www.google.com\""
|
||||
},
|
||||
"backend_log": {
|
||||
"refresh": "Aktualisieren"
|
||||
@@ -1025,7 +1007,7 @@
|
||||
"error_creating_anonymized_database": "Die anonymisierte Datenbank konnte nicht erstellt werden. Überprüfe die Backend-Protokolle auf Details",
|
||||
"successfully_created_fully_anonymized_database": "Vollständig anonymisierte Datenbank in {{anonymizedFilePath}} erstellt",
|
||||
"successfully_created_lightly_anonymized_database": "Leicht anonymisierte Datenbank in {{anonymizedFilePath}} erstellt",
|
||||
"no_anonymized_database_yet": "Noch keine anonymisierte Datenbank."
|
||||
"no_anonymized_database_yet": "Noch keine anonymisierte Datenbank"
|
||||
},
|
||||
"database_integrity_check": {
|
||||
"title": "Datenbankintegritätsprüfung",
|
||||
@@ -1046,7 +1028,7 @@
|
||||
"failed": "Synchronisierung fehlgeschlagen: {{message}}"
|
||||
},
|
||||
"vacuum_database": {
|
||||
"title": "Datenbank aufräumen",
|
||||
"title": "Vakuumdatenbank",
|
||||
"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",
|
||||
"vacuuming_database": "Datenbank wird geleert...",
|
||||
@@ -1081,8 +1063,7 @@
|
||||
"max_width_label": "Maximale Inhaltsbreite in Pixel",
|
||||
"apply_changes_description": "Um Änderungen an der Inhaltsbreite anzuwenden, klicke auf",
|
||||
"reload_button": "Frontend neu laden",
|
||||
"reload_description": "Änderungen an den Darstellungsoptionen",
|
||||
"max_width_unit": "Pixel"
|
||||
"reload_description": "Änderungen an den Darstellungsoptionen"
|
||||
},
|
||||
"native_title_bar": {
|
||||
"title": "Native Titelleiste (App-Neustart erforderlich)",
|
||||
@@ -1105,10 +1086,7 @@
|
||||
"layout-vertical-title": "Vertikal",
|
||||
"layout-horizontal-title": "Horizontal",
|
||||
"layout-vertical-description": "Startleiste ist auf der linken Seite (standard)",
|
||||
"layout-horizontal-description": "Startleiste ist unter der Tableiste. Die Tableiste wird dadurch auf die ganze Breite erweitert.",
|
||||
"auto_theme": "Alt (Folge dem Farbschema des Systems)",
|
||||
"light_theme": "Alt (Hell)",
|
||||
"dark_theme": "Alt (Dunkel)"
|
||||
"layout-horizontal-description": "Startleiste ist unter der Tableiste. Die Tableiste wird dadurch auf die ganze Breite erweitert."
|
||||
},
|
||||
"zoom_factor": {
|
||||
"title": "Zoomfaktor (nur Desktop-Build)",
|
||||
@@ -1117,8 +1095,7 @@
|
||||
"code_auto_read_only_size": {
|
||||
"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).",
|
||||
"label": "Automatische schreibgeschützte Größe (Codenotizen)",
|
||||
"unit": "Zeichen"
|
||||
"label": "Automatische schreibgeschützte Größe (Codenotizen)"
|
||||
},
|
||||
"code_mime_types": {
|
||||
"title": "Verfügbare MIME-Typen im Dropdown-Menü"
|
||||
@@ -1137,13 +1114,12 @@
|
||||
"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",
|
||||
"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)",
|
||||
"max_image_dimensions_unit": "Pixel"
|
||||
"jpeg_quality_description": "JPEG-Qualität (10 – schlechteste Qualität, 100 – beste Qualität, 50 – 85 wird empfohlen)"
|
||||
},
|
||||
"attachment_erasure_timeout": {
|
||||
"attachment_erasure_timeout": "Zeitüberschreitung beim Löschen von Anhängen",
|
||||
"attachment_auto_deletion_description": "Anhänge werden automatisch gelöscht (und gelöscht), wenn sie nach einer definierten Zeitspanne nicht mehr in ihrer Notiz referenziert werden.",
|
||||
"erase_attachments_after": "Nicht verwendete Anhänge löschen nach:",
|
||||
"erase_attachments_after": "Erase unused attachments after:",
|
||||
"manual_erasing_description": "Du kannst das Löschen auch manuell auslösen (ohne Berücksichtigung des oben definierten Timeouts):",
|
||||
"erase_unused_attachments_now": "Lösche jetzt nicht verwendete Anhangnotizen",
|
||||
"unused_attachments_erased": "Nicht verwendete Anhänge wurden gelöscht."
|
||||
@@ -1154,7 +1130,7 @@
|
||||
},
|
||||
"note_erasure_timeout": {
|
||||
"note_erasure_timeout_title": "Beachte das Zeitlimit für die Löschung",
|
||||
"note_erasure_description": "Gelöschte Notizen (und Attribute, Notizrevisionen...) werden zunächst nur als gelöscht markiert und können über den Dialog „Zuletzt verwendete Notizen” wiederhergestellt werden. Nach einer bestimmten Zeit werden gelöschte Notizen „gelöscht”, was bedeutet, dass ihr Inhalt nicht mehr wiederhergestellt werden kann. Mit dieser Einstellung können Sie die Zeitspanne zwischen dem Löschen und dem endgültigen Löschen der Notiz festlegen.",
|
||||
"note_erasure_description": "Deleted notes (and attributes, revisions...) are at first only marked as deleted and it is possible to recover them from Recent Notes dialog. After a period of time, deleted notes are \"erased\" which means their content is not recoverable anymore. This setting allows you to configure the length of the period between deleting and erasing the note.",
|
||||
"erase_notes_after": "Notizen löschen nach:",
|
||||
"manual_erasing_description": "Du kannst das Löschen auch manuell auslösen (ohne Berücksichtigung des oben definierten Timeouts):",
|
||||
"erase_deleted_notes_now": "Jetzt gelöschte Notizen löschen",
|
||||
@@ -1170,8 +1146,7 @@
|
||||
"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:",
|
||||
"erase_excess_revision_snapshots": "Überschüssige Revision-Snapshots jetzt löschen",
|
||||
"erase_excess_revision_snapshots_prompt": "Überschüssige Revision-Snapshots wurden gelöscht.",
|
||||
"snapshot_number_limit_unit": "Momentaufnahmen"
|
||||
"erase_excess_revision_snapshots_prompt": "Überschüssige Revision-Snapshots wurden gelöscht."
|
||||
},
|
||||
"search_engine": {
|
||||
"title": "Suchmaschine",
|
||||
@@ -1213,29 +1188,19 @@
|
||||
"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:",
|
||||
"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“).",
|
||||
"unit": "Überschriften"
|
||||
"shortcut_info": "Du kannst eine Tastenkombination zum schnellen Umschalten des rechten Bereichs (einschließlich Inhaltsverzeichnis) unter Optionen -> Tastenkombinationen konfigurieren (Name „toggleRightPane“)."
|
||||
},
|
||||
"text_auto_read_only_size": {
|
||||
"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).",
|
||||
"label": "Automatische schreibgeschützte Größe (Textnotizen)",
|
||||
"unit": "Zeichen"
|
||||
"label": "Automatische schreibgeschützte Größe (Textnotizen)"
|
||||
},
|
||||
"i18n": {
|
||||
"title": "Lokalisierung",
|
||||
"language": "Sprache",
|
||||
"first-day-of-the-week": "Erster Tag der Woche",
|
||||
"sunday": "Sonntag",
|
||||
"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"
|
||||
"monday": "Montag"
|
||||
},
|
||||
"backup": {
|
||||
"automatic_backup": "Automatische Sicherung",
|
||||
@@ -1396,7 +1361,7 @@
|
||||
"duplicate": "Duplizieren",
|
||||
"export": "Exportieren",
|
||||
"import-into-note": "In Notiz importieren",
|
||||
"apply-bulk-actions": "Massenaktionen anwenden",
|
||||
"apply-bulk-actions": "Massenaktionen ausführen",
|
||||
"converted-to-attachments": "{{count}} Notizen wurden als Anhang konvertiert.",
|
||||
"convert-to-attachment-confirm": "Bist du sicher, dass du die ausgewählten Notizen in Anhänge ihrer übergeordneten Notizen umwandeln möchtest?"
|
||||
},
|
||||
@@ -1412,7 +1377,7 @@
|
||||
"relation-map": "Beziehungskarte",
|
||||
"note-map": "Notizkarte",
|
||||
"render-note": "Render Notiz",
|
||||
"mermaid-diagram": "Mermaid Diagramm",
|
||||
"mermaid-diagram": "Mermaid Diagram",
|
||||
"canvas": "Canvas",
|
||||
"web-view": "Webansicht",
|
||||
"mind-map": "Mind Map",
|
||||
@@ -1422,7 +1387,7 @@
|
||||
"doc": "Dokument",
|
||||
"widget": "Widget",
|
||||
"confirm-change": "Es is nicht empfehlenswert den Notiz-Typ zu ändern, wenn der Inhalt der Notiz nicht leer ist. Möchtest du dennoch fortfahren?",
|
||||
"geo-map": "Geo-Karte",
|
||||
"geo-map": "Geo Map",
|
||||
"beta-feature": "Beta"
|
||||
},
|
||||
"protect_note": {
|
||||
@@ -1596,11 +1561,11 @@
|
||||
"label": "Format Toolbar",
|
||||
"floating": {
|
||||
"title": "Schwebend",
|
||||
"description": "Werkzeuge erscheinen in Cursornähe;"
|
||||
"description": "Werkzeuge erscheinen in Cursornähe"
|
||||
},
|
||||
"fixed": {
|
||||
"title": "Fixiert",
|
||||
"description": "Werkzeuge erscheinen im \"Format\" Tab."
|
||||
"description": "Werkzeuge erscheinen im \"Format\" Tab"
|
||||
},
|
||||
"multiline-toolbar": "Toolbar wenn nötig in mehreren Zeilen darstellen."
|
||||
}
|
||||
@@ -1666,188 +1631,5 @@
|
||||
},
|
||||
"modal": {
|
||||
"close": "Schließen"
|
||||
},
|
||||
"ai_llm": {
|
||||
"n_notes_queued": "{{ count }} Notiz zur Indizierung vorgemerkt",
|
||||
"n_notes_queued_plural": "{{ count }} Notizen zur Indizierung vorgemerkt",
|
||||
"notes_indexed": "{{ count }} Notiz indiziert",
|
||||
"notes_indexed_plural": "{{ count }} Notizen indiziert",
|
||||
"not_started": "Nicht gestartet",
|
||||
"title": "KI Einstellungen",
|
||||
"processed_notes": "Verarbeitete Notizen",
|
||||
"total_notes": "Gesamt Notizen",
|
||||
"progress": "Fortschritt",
|
||||
"queued_notes": "Eingereihte Notizen",
|
||||
"failed_notes": "Fehlgeschlagenen Notizen",
|
||||
"last_processed": "Zuletzt verarbeitet",
|
||||
"refresh_stats": "Statistiken neu laden",
|
||||
"enable_ai_features": "Aktiviere KI/LLM Funktionen",
|
||||
"enable_ai_description": "Aktiviere KI-Funktionen wie Notizzusammenfassungen, Inhaltserzeugung und andere LLM-Funktionen",
|
||||
"openai_tab": "OpenAI",
|
||||
"anthropic_tab": "Anthropic",
|
||||
"voyage_tab": "Voyage AI",
|
||||
"ollama_tab": "Ollama",
|
||||
"enable_ai": "Aktiviere KI/LLM Funktionen",
|
||||
"enable_ai_desc": "Aktiviere KI-Funktionen wie Notizzusammenfassungen, Inhaltserzeugung und andere LLM-Funktionen",
|
||||
"provider_configuration": "KI-Anbieterkonfiguration",
|
||||
"provider_precedence": "Anbieter Priorität",
|
||||
"provider_precedence_description": "Komma-getrennte Liste von Anbieter in der Reihenfolge ihrer Priorität (z.B. 'openai, anthropic,ollama')",
|
||||
"temperature": "Temperatur",
|
||||
"temperature_description": "Regelt die Zufälligkeit in Antworten (0 = deterministisch, 2 = maximale Zufälligkeit)",
|
||||
"system_prompt": "Systemaufforderung",
|
||||
"system_prompt_description": "Standard Systemaufforderung für alle KI-Interaktionen",
|
||||
"openai_configuration": "OpenAI Konfiguration",
|
||||
"openai_settings": "OpenAI Einstellungen",
|
||||
"api_key": "API Schlüssel",
|
||||
"url": "Basis-URL",
|
||||
"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": {
|
||||
"button_exit": "Verlasse Zen Modus"
|
||||
},
|
||||
"ui-performance": {
|
||||
"title": "Leistung",
|
||||
"enable-motion": "Aktiviere Übergänge und Animationen",
|
||||
"enable-shadows": "Aktiviere Schatten",
|
||||
"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,8 +732,7 @@
|
||||
"note_type": "Note type",
|
||||
"editable": "Editable",
|
||||
"basic_properties": "Basic Properties",
|
||||
"language": "Language",
|
||||
"configure_code_notes": "Configure code notes..."
|
||||
"language": "Language"
|
||||
},
|
||||
"book_properties": {
|
||||
"view_type": "View type",
|
||||
@@ -849,7 +848,7 @@
|
||||
"debug": "debug",
|
||||
"debug_description": "Debug will print extra debugging information into the console to aid in debugging complex queries",
|
||||
"action": "action",
|
||||
"search_button": "Search",
|
||||
"search_button": "Search <kbd>enter</kbd>",
|
||||
"search_execute": "Search & Execute actions",
|
||||
"save_to_note": "Save to note",
|
||||
"search_parameters": "Search Parameters",
|
||||
@@ -1815,6 +1814,43 @@
|
||||
"multiline-toolbar": "Display the toolbar on multiple lines if it doesn't fit."
|
||||
}
|
||||
},
|
||||
"ckeditor_plugins": {
|
||||
"title": "Editor Plugins",
|
||||
"description": "Configure which CKEditor plugins are enabled. Changes take effect when the editor is reloaded.",
|
||||
"loading": "Loading plugin configuration...",
|
||||
"load_failed": "Failed to load plugin configuration.",
|
||||
"load_error": "Error loading plugins",
|
||||
"retry": "Retry",
|
||||
"category_formatting": "Text Formatting",
|
||||
"category_structure": "Document Structure",
|
||||
"category_media": "Media & Files",
|
||||
"category_tables": "Tables",
|
||||
"category_advanced": "Advanced Features",
|
||||
"category_trilium": "Trilium Features",
|
||||
"category_external": "External Plugins",
|
||||
"stats_enabled": "Enabled",
|
||||
"stats_total": "Total",
|
||||
"stats_core": "Core",
|
||||
"stats_premium": "Premium",
|
||||
"no_license": "no license",
|
||||
"premium": "Premium",
|
||||
"premium_required": "Requires premium CKEditor license",
|
||||
"has_dependencies": "Dependencies",
|
||||
"depends_on": "Depends on",
|
||||
"toolbar_items": "Toolbar items",
|
||||
"validate": "Validate",
|
||||
"validation_error": "Validation failed",
|
||||
"validation_errors": "Configuration Errors:",
|
||||
"validation_warnings": "Configuration Warnings:",
|
||||
"save": "Save Changes",
|
||||
"save_success": "Plugin configuration saved successfully",
|
||||
"save_error": "Failed to save configuration",
|
||||
"reload_editor_notice": "Please reload any open text notes to apply changes",
|
||||
"reset_defaults": "Reset to Defaults",
|
||||
"reset_confirm": "Are you sure you want to reset all plugin settings to their default values?",
|
||||
"reset_success": "Plugin configuration reset to defaults",
|
||||
"reset_error": "Failed to reset configuration"
|
||||
},
|
||||
"electron_context_menu": {
|
||||
"add-term-to-dictionary": "Add \"{{term}}\" to dictionary",
|
||||
"cut": "Cut",
|
||||
|
||||
@@ -848,7 +848,7 @@
|
||||
"debug": "depurar",
|
||||
"debug_description": "La depuración imprimirá información de depuración adicional en la consola para ayudar a depurar consultas complejas",
|
||||
"action": "acción",
|
||||
"search_button": "Buscar",
|
||||
"search_button": "Buscar <kbd>Enter</kbd>",
|
||||
"search_execute": "Buscar y ejecutar acciones",
|
||||
"save_to_note": "Guardar en nota",
|
||||
"search_parameters": "Parámetros de búsqueda",
|
||||
|
||||
@@ -848,7 +848,7 @@
|
||||
"debug": "debug",
|
||||
"debug_description": "Debug imprimera des informations supplémentaires dans la console pour faciliter le débogage des requêtes complexes",
|
||||
"action": "action",
|
||||
"search_button": "Recherche",
|
||||
"search_button": "Recherche <kbd>Entrée</kbd>",
|
||||
"search_execute": "Rechercher et exécuter des actions",
|
||||
"save_to_note": "Enregistrer dans la note",
|
||||
"search_parameters": "Paramètres de recherche",
|
||||
|
||||
@@ -185,7 +185,7 @@
|
||||
"debug": "デバッグ",
|
||||
"debug_description": "デバッグは複雑なクエリのデバッグを支援するために、追加のデバッグ情報をコンソールに出力します",
|
||||
"action": "アクション",
|
||||
"search_button": "検索",
|
||||
"search_button": "検索 <kbd>Enter</kbd>",
|
||||
"search_execute": "検索とアクションの実行",
|
||||
"save_to_note": "ノートに保存",
|
||||
"search_parameters": "検索パラメータ",
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"about": {
|
||||
"title": "Over Trilium Notes",
|
||||
"homepage": "Homepagina:",
|
||||
"app_version": "App versie:",
|
||||
"db_version": "DB Versie:",
|
||||
"sync_version": "Sync Versie:"
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,7 @@
|
||||
"code_block": {
|
||||
"theme_none": "Sem destaque de sintaxe",
|
||||
"theme_group_light": "Temas claros",
|
||||
"theme_group_dark": "Temas escuros",
|
||||
"word_wrapping": "Quebra automática de linhas",
|
||||
"copy_title": "Copiar para a área de transferência"
|
||||
"theme_group_dark": "Temas escuros"
|
||||
},
|
||||
"about": {
|
||||
"title": "Sobre o Trilium Notes",
|
||||
@@ -194,34 +192,7 @@
|
||||
"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_ollama": "Nenhum modelo Ollama encontrado. Por favor, verifique se o Ollama está em execução.",
|
||||
"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"
|
||||
"error_fetching": "Erro ao obter modelos: {{error}}"
|
||||
},
|
||||
"confirm": {
|
||||
"confirmation": "Confirmação",
|
||||
@@ -573,17 +544,7 @@
|
||||
"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_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",
|
||||
"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."
|
||||
"widget_relation": "o destino desta relação será executado e renderizado como um widget na barra lateral"
|
||||
},
|
||||
"attachments_actions": {
|
||||
"delete_attachment": "Excluir anexo",
|
||||
@@ -771,10 +732,7 @@
|
||||
"move_note": "Mover nota",
|
||||
"to": "para",
|
||||
"target_parent_note": "nota pai destino",
|
||||
"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)"
|
||||
"on_all_matched_notes": "Em todas as notas correspondentes"
|
||||
},
|
||||
"rename_note": {
|
||||
"rename_note": "Renomear nota",
|
||||
@@ -784,8 +742,7 @@
|
||||
"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_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.",
|
||||
"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:"
|
||||
"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."
|
||||
},
|
||||
"calendar": {
|
||||
"mon": "Seg",
|
||||
@@ -920,8 +877,7 @@
|
||||
"relation_map_buttons": {
|
||||
"zoom_in_title": "Aumentar",
|
||||
"zoom_out_title": "Reduzir",
|
||||
"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"
|
||||
"create_child_note_title": "Criar nova nota filha e adicione neste mapa de relação"
|
||||
},
|
||||
"zpetne_odkazy": {
|
||||
"backlink": "{{count}} Links Reversos",
|
||||
@@ -931,8 +887,7 @@
|
||||
"mobile_detail_menu": {
|
||||
"insert_child_note": "Inserir nota filha",
|
||||
"delete_this_note": "Excluir essa nota",
|
||||
"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}} '"
|
||||
"error_unrecognized_command": "Comando não reconhecido {{command}}"
|
||||
},
|
||||
"note_icon": {
|
||||
"change_note_icon": "Alterar ícone da nota",
|
||||
@@ -1002,8 +957,7 @@
|
||||
"note_size": "Tamanho da nota",
|
||||
"calculate": "calcular",
|
||||
"title": "Informações da nota",
|
||||
"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."
|
||||
"subtree_size": "(tamanho da subárvore: {{size}} em {{count}} notas)"
|
||||
},
|
||||
"note_map": {
|
||||
"open_full": "Expandir completamente",
|
||||
@@ -1018,8 +972,7 @@
|
||||
"intro_placed": "Esta nova está localizada nos caminhos:",
|
||||
"intro_not_placed": "Esta nota ainda não está em nenhuma árvore de notas.",
|
||||
"archived": "Arquivado",
|
||||
"search": "Pesquisar",
|
||||
"outside_hoisted": "Este caminho está fora de uma nota fixada e você teria que desafixar."
|
||||
"search": "Pesquisar"
|
||||
},
|
||||
"note_properties": {
|
||||
"this_note_was_originally_taken_from": "Esta nota foi originalmente obtida de:",
|
||||
@@ -1033,8 +986,7 @@
|
||||
"unknown_attribute_type": "Tipo de atributo desconhecido '{{type}}'",
|
||||
"add_new_attribute": "Adicionar novo atributo",
|
||||
"remove_this_attribute": "Remover este atributo",
|
||||
"remove_color": "Remover a etiqueta de cor",
|
||||
"url_placeholder": "http://website..."
|
||||
"remove_color": "Remover a etiqueta de cor"
|
||||
},
|
||||
"script_executor": {
|
||||
"query": "Consulta",
|
||||
@@ -1054,16 +1006,13 @@
|
||||
"limit_description": "Limitar número de resultados",
|
||||
"debug": "depurar",
|
||||
"action": "ação",
|
||||
"search_button": "Pesquisar",
|
||||
"search_button": "Pesquisar <kbd>enter</kbd>",
|
||||
"search_execute": "Pesquisar & Executar ações",
|
||||
"save_to_note": "Salvar para nota",
|
||||
"search_parameters": "Parâmetros de Pesquisa",
|
||||
"unknown_search_option": "Opção de pesquisa desconhecida {{searchOptionName}}",
|
||||
"actions_executed": "As ações foram executadas.",
|
||||
"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"
|
||||
"search_note_saved": "Nota de pesquisa foi salva em {{- notePathTitle}}"
|
||||
},
|
||||
"similar_notes": {
|
||||
"title": "Notas Similares",
|
||||
@@ -1074,13 +1023,10 @@
|
||||
"failed_rendering": "A renderização da opção de busca falhou: {{dto}} com o erro: {{error}} {{stack}}"
|
||||
},
|
||||
"debug": {
|
||||
"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."
|
||||
"debug": "Depurar"
|
||||
},
|
||||
"fast_search": {
|
||||
"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."
|
||||
"fast_search": "Pesquisa rápida"
|
||||
},
|
||||
"include_archived_notes": {
|
||||
"include_archived_notes": "Incluir notas arquivadas"
|
||||
@@ -1112,10 +1058,7 @@
|
||||
"title": "Buscar script:",
|
||||
"placeholder": "buscar notas pelo nome",
|
||||
"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;",
|
||||
"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."
|
||||
"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;"
|
||||
},
|
||||
"search_string": {
|
||||
"title_column": "Buscar texto:",
|
||||
@@ -1129,8 +1072,7 @@
|
||||
"label_year_comparison": "comparação numérica (também >, >=, <).",
|
||||
"label_date_created": "notas criadas no último mês",
|
||||
"error": "Erro na busca: {{error}}",
|
||||
"search_prefix": "Busca:",
|
||||
"placeholder": "palavras-chave fulltext, #tag = valor..."
|
||||
"search_prefix": "Busca:"
|
||||
},
|
||||
"attachment_list": {
|
||||
"open_help_page": "Abrir página de ajuda nos anexos",
|
||||
@@ -1192,26 +1134,17 @@
|
||||
"check_button": "Verificar 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_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."
|
||||
"integrity_check_failed": "Verificação de integridade falhou: {{results}}"
|
||||
},
|
||||
"sync": {
|
||||
"title": "Sincronizar",
|
||||
"force_full_sync_button": "Forçar sincronização completa",
|
||||
"full_sync_triggered": "Sincronização completa iniciada",
|
||||
"finished-successfully": "Sincronização finalizada com sucesso.",
|
||||
"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"
|
||||
"failed": "Sincronização falhou: {{message}}"
|
||||
},
|
||||
"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.",
|
||||
"title": "Executar Vacuum no Banco de Dados",
|
||||
"button_text": "Executar Vacuum",
|
||||
"vacuuming_database": "Executando Vacuum...",
|
||||
"database_vacuumed": "Vacuum executado no banco de dados"
|
||||
"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."
|
||||
},
|
||||
"fonts": {
|
||||
"theme_defined": "Tema definido",
|
||||
@@ -1233,8 +1166,7 @@
|
||||
"serif": "Serifa",
|
||||
"sans-serif": "Sem Serifa",
|
||||
"monospace": "Monospace",
|
||||
"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."
|
||||
"system-default": "Padrão do Sistema"
|
||||
},
|
||||
"max_content_width": {
|
||||
"title": "Largura do Conteúdo",
|
||||
@@ -1242,8 +1174,7 @@
|
||||
"max_width_unit": "pixels",
|
||||
"apply_changes_description": "Para aplicar as alterações de largura do conteúdo, clique em",
|
||||
"reload_button": "recarregar frontend",
|
||||
"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."
|
||||
"reload_description": "alterações de opções de aparência"
|
||||
},
|
||||
"native_title_bar": {
|
||||
"title": "Barra de Título Nativa (requer recarregar o app)",
|
||||
@@ -1271,411 +1202,5 @@
|
||||
},
|
||||
"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."
|
||||
},
|
||||
"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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"add_relation": {
|
||||
"add_relation": "Adaugă relație",
|
||||
"allowed_characters": "Sunt permise doar caractere alfanumerice, underline și două puncte.",
|
||||
"create_relation_on_all_matched_notes": "Creează relația pentru toate notițele găsite.",
|
||||
"create_relation_on_all_matched_notes": "Crează relația pentru toate notițele găsite",
|
||||
"relation_name": "denumirea relației",
|
||||
"target_note": "notița destinație",
|
||||
"to": "către"
|
||||
@@ -76,9 +76,9 @@
|
||||
"attachment_erasure_timeout": {
|
||||
"attachment_auto_deletion_description": "Atașamentele se șterg automat (permanent) dacă nu sunt referențiate de către notița lor părinte după un timp prestabilit de timp.",
|
||||
"attachment_erasure_timeout": "Perioadă de ștergere a atașamentelor",
|
||||
"erase_attachments_after": "Șterge atașamentele neutilizate după:",
|
||||
"erase_attachments_after": "Erase unused attachments after:",
|
||||
"erase_unused_attachments_now": "Elimină atașamentele șterse acum",
|
||||
"manual_erasing_description": "Puteți șterge atașamentele nefolosite manual (fără a lua în considerare timpul de mai sus):",
|
||||
"manual_erasing_description": "Șterge acum toate atașamentele nefolosite din notițe",
|
||||
"unused_attachments_erased": "Atașamentele nefolosite au fost șterse."
|
||||
},
|
||||
"attachment_list": {
|
||||
@@ -141,7 +141,7 @@
|
||||
"hide_promoted_attributes": "Ascunde lista atributelor promovate pentru această notiță",
|
||||
"hide_relations": "lista denumirilor relațiilor ce trebuie ascunse, delimitate prin virgulă. Toate celelalte vor fi afișate.",
|
||||
"icon_class": "valoarea acestei etichete este adăugată ca o clasă CSS la iconița notiței din ierarhia notițelor, fapt ce poate ajuta la identificarea vizuală mai rapidă a notițelor. Un exemplu ar fi „bx bx-home” pentru iconițe preluate din boxicons. Poate fi folosită în notițe de tip șablon.",
|
||||
"inbox": "locația implicită în care vor apărea noile notițe atunci când se crează o noitiță utilizând butonul „Crează notiță” din bara laterală, notițele vor fi create în interiorul notiței marcată cu eticheta <code>#inbox</code>.",
|
||||
"inbox": "locația implicită în care vor apărea noile notițe atunci când se crează o noitiță utilizând butonul „Crează notiță” din bara laterală, notițele vor fi create în interiorul notiței cu această etichetă.",
|
||||
"inherit": "atributele acestei notițe vor fi moștenite chiar dacă nu există o relație părinte-copil între notițe. A se vedea relația de tip șablon pentru un concept similar. De asemenea, a se vedea moștenirea atributelor în documentație.",
|
||||
"inheritable": "Moștenibilă",
|
||||
"inheritable_title": "Atributele moștenibile vor fi moștenite de către toți descendenții acestei notițe.",
|
||||
@@ -177,7 +177,7 @@
|
||||
"render_note": "relație ce definește notița (de tip notiță de cod HTML sau script) ce trebuie randată pentru notițele de tip „Randare notiță HTML”",
|
||||
"run": "definește evenimentele la care să ruleze scriptul. Valori acceptate:\n<ul>\n<li>frontendStartup - când pornește interfața Trilium (sau este reîncărcată), dar nu pe mobil.</li>\n<li>mobileStartup - când pornește interfața Trilium (sau este reîncărcată), doar pe mobil.</li>\n<li>backendStartup - când pornește serverul Trilium</li>\n<li>hourly - o dată pe oră. Se poate utiliza adițional eticheta <code>runAtHour</code> pentru a specifica ora.</li>\n<li>daily - o dată pe zi</li>\n</ul>",
|
||||
"run_at_hour": "La ce oră ar trebui să ruleze. Trebuie folosit împreună cu <code>#run=hourly</code>. Poate fi definit de mai multe ori pentru a rula de mai multe ori în cadrul aceleași zile.",
|
||||
"run_on_attribute_change": " se execută atunci când atributele unei notițe care definește această relație se schimbă. Se apelează și atunci când un atribut este șters",
|
||||
"run_on_attribute_change": "se execută atunci când atributele unei notițe care definește această relație se schimbă. Se apelează și atunci când un atribut este șters",
|
||||
"run_on_attribute_creation": "se execută atunci când un nou atribut este creat pentru notița care definește această relație",
|
||||
"run_on_branch_change": "se execută atunci când o ramură este actualizată.",
|
||||
"run_on_branch_creation": "se execută când o ramură este creată. O ramură este o legătură dintre o notiță părinte și o notiță copil și este creată, spre exemplu, la clonarea sau mutarea unei notițe.",
|
||||
@@ -198,7 +198,7 @@
|
||||
"share_disallow_robot_indexing": "împiedică indexarea conținutului de către roboți utilizând antetul <code>X-Robots-Tag: noindex</code>",
|
||||
"share_external_link": "notița va funcționa drept o legătură către un site web extern în ierarhia de partajare",
|
||||
"share_favicon": "Notiță ce conține pictograma favicon pentru a fi setată în paginile partajate. De obicei se poate seta în rădăcina ierarhiei de partajare și se poate face moștenibilă. Notița ce conține favicon-ul trebuie să fie și ea în ierarhia de partajare. Considerați și utilizarea „share_hidden_from_tree”.",
|
||||
"share_hidden_from_tree": "notița este ascunsă din arborele de navigație din stânga, dar încă este accesibilă prin intermediul unui URL",
|
||||
"share_hidden_from_tree": "notița este ascunsă din arborele de navigație din stânga, dar încă este accesibilă prin intermediul unui URL.",
|
||||
"share_index": "notițele cu această etichetă vor afișa lista tuturor rădăcilor notițelor partajate",
|
||||
"share_js": "Notiță JavaScript ce va fi injectată în pagina de partajare. Notița respectivă trebuie să fie și ea în ierarhia de partajare. Considerați utilizarea 'share_hidden_from_tree'.",
|
||||
"share_omit_default_css": "CSS-ul implicit pentru pagina de partajare va fi omis. Se poate folosi atunci când se fac schimbări majore de stil la pagină.",
|
||||
@@ -214,7 +214,7 @@
|
||||
"target_note_title": "Relația este o conexiune numită dintre o notiță sursă și o notiță țintă.",
|
||||
"template": "Șablon",
|
||||
"text": "Text",
|
||||
"title_template": "titlul implicit al notițelor create în interiorul acestei notițe. Valoarea este evaluată ca un șir de caractere JavaScript\n și poate fi astfel îmbogățită cu un conținut dinamic prin intermediul variabilelor <code>now</code> și <code>parentNote</code>. Exemple:\n \n <ul>\n <li><code>Lucrările lui ${parentNote.getLabelValue('autor')}</code></li>\n <li><code>Jurnal pentru ${now.format('YYYY-MM-DD HH:mm:ss')}</code></li>\n </ul>\n \n A se vedea <a href=\"https://triliumnext.github.io/Docs/Wiki/default-note-title.html\">wiki-ul pentru detalii</a>, documentația API pentru <a href=\"https://zadam.github.io/trilium/backend_api/Note.html\">parentNote</a> și <a href=\"https://day.js.org/docs/en/display/format\">now</a> pentru mai multe informații.",
|
||||
"title_template": "titlul implicit al notițelor create în interiorul acestei notițe. Valoarea este evaluată ca un șir de caractere JavaScript\n și poate fi astfel îmbogățită cu un conținut dinamic prin intermediul variabilelow <code>now</code> și <code>parentNote</code>. Exemple:\n \n <ul>\n <li><code>Lucrările lui ${parentNote.getLabelValue('autor')}</code></li>\n <li><code>Jurnal pentru ${now.format('YYYY-MM-DD HH:mm:ss')}</code></li>\n </ul>\n \n A se vedea <a href=\"https://triliumnext.github.io/Docs/Wiki/default-note-title.html\">wiki-ul pentru detalii</a>, documentația API pentru <a href=\"https://zadam.github.io/trilium/backend_api/Note.html\">parentNote</a> și <a href=\"https://day.js.org/docs/en/display/format\">now</a> pentru mai multe informații",
|
||||
"toc": "<code>#toc</code> sau <code>#toc=show</code> forțează afișarea tabelei de conținut, <code>#toc=hide</code> forțează ascunderea ei. Dacă eticheta nu există, se utilizează setările globale",
|
||||
"top": "păstrează notița la începutul listei (se aplică doar pentru notițe sortate automat)",
|
||||
"url": "URL",
|
||||
@@ -369,7 +369,7 @@
|
||||
},
|
||||
"confirm": {
|
||||
"also_delete_note": "Șterge și notița",
|
||||
"are_you_sure_remove_note": "Doriți ștergerea notiței „{{title}}” din harta de relații? ",
|
||||
"are_you_sure_remove_note": "Doriți ștergerea notiței „{{title}}” din harta de relații?",
|
||||
"cancel": "Anulează",
|
||||
"confirmation": "Confirm",
|
||||
"if_you_dont_check": "Dacă această opțiune nu este bifată, notița va fi ștearsă doar din harta de relații.",
|
||||
@@ -519,8 +519,8 @@
|
||||
"export_status": "Starea exportului",
|
||||
"export_type_single": "Doar această notiță fără descendenții ei",
|
||||
"export_type_subtree": "Această notiță și toți descendenții ei",
|
||||
"format_html_zip": "HTML în arhivă ZIP - recomandat deoarece păstrează toată formatarea.",
|
||||
"format_markdown": "Markdown - păstrează majoritatea formatării.",
|
||||
"format_html_zip": "HTML în arhivă ZIP - recomandat deoarece păstrează toată formatarea",
|
||||
"format_markdown": "Markdown - păstrează majoritatea formatării",
|
||||
"format_opml": "OPML - format de interschimbare pentru editoare cu structură ierarhică (outline). Formatarea, imaginile și fișierele nu vor fi incluse.",
|
||||
"opml_version_1": "OPML v1.0 - text simplu",
|
||||
"opml_version_2": "OPML v2.0 - permite și HTML",
|
||||
@@ -640,7 +640,7 @@
|
||||
"newTabNoteLink": "pe o legătură către o notiță va deschide notița într-un tab nou",
|
||||
"notSet": "nesetat",
|
||||
"noteNavigation": "Navigarea printre notițe",
|
||||
"numberedList": "<code>1.</code> sau <code>1)</code> urmat de spațiu pentru o listă numerotată",
|
||||
"numberedList": "<kbd>1.</code> sau <code>1)</code> urmat de spațiu pentru o listă numerotată",
|
||||
"onlyInDesktop": "Doar pentru desktop (aplicația Electron)",
|
||||
"openEmptyTab": "deschide un tab nou",
|
||||
"other": "Altele",
|
||||
@@ -807,7 +807,7 @@
|
||||
"dialog_title": "Mută notițele în...",
|
||||
"error_no_path": "Nicio cale la care să poată fi mutate.",
|
||||
"move_button": "Mută la notița selectată",
|
||||
"move_success_message": "Notițele selectate au fost mutate în ",
|
||||
"move_success_message": "Notițele selectate au fost mutate în",
|
||||
"notes_to_move": "Notițe de mutat",
|
||||
"search_placeholder": "căutați notița după denumirea ei",
|
||||
"target_parent_note": "Notița părinte destinație"
|
||||
@@ -1058,7 +1058,7 @@
|
||||
"download_button": "Descarcă",
|
||||
"file_size": "Dimensiune fișier:",
|
||||
"help_title": "Informații despre reviziile notițelor",
|
||||
"mime": "MIME: ",
|
||||
"mime": "MIME:",
|
||||
"no_revisions": "Nu există încă nicio revizie pentru această notiță...",
|
||||
"note_revisions": "Revizii ale notiței",
|
||||
"preview": "Previzualizare:",
|
||||
@@ -1106,7 +1106,7 @@
|
||||
"limit_description": "Limitează numărul de rezultate",
|
||||
"order_by": "ordonează după",
|
||||
"save_to_note": "Salvează în notiță",
|
||||
"search_button": "Căutare",
|
||||
"search_button": "Căutare <kbd>Enter</kbd>",
|
||||
"search_execute": "Caută și execută acțiunile",
|
||||
"search_note_saved": "Notița de căutare a fost salvată în {{- notePathTitle}}",
|
||||
"search_parameters": "Parametrii de căutare",
|
||||
@@ -1193,7 +1193,7 @@
|
||||
"enable": "Activează corectorul ortografic",
|
||||
"language_code_label": "Codurile de limbă",
|
||||
"language_code_placeholder": "de exemplu „en-US”, „de-AT”",
|
||||
"multiple_languages_info": "Mai multe limbi pot fi separate prin virgulă, e.g. \"en-US, de-DE, cs\". ",
|
||||
"multiple_languages_info": "Mai multe limbi pot fi separate prin virgulă, e.g. \"en-US, de-DE, cs\".",
|
||||
"title": "Corector ortografic",
|
||||
"restart-required": "Schimbările asupra setărilor corectorului ortografic vor fi aplicate după restartarea aplicației."
|
||||
},
|
||||
@@ -1286,7 +1286,7 @@
|
||||
"update_relation_target": {
|
||||
"allowed_characters": "Sunt permise doar caractere alfanumerice, underline și două puncte.",
|
||||
"change_target_note": "schimbă notița-țintă a unei relații existente",
|
||||
"on_all_matched_notes": "Pentru toate notițele găsite",
|
||||
"on_all_matched_notes": "Pentru toate notițele găsite:",
|
||||
"relation_name": "denumirea relației",
|
||||
"target_note": "notița destinație",
|
||||
"to": "la",
|
||||
@@ -1314,7 +1314,7 @@
|
||||
"use_vim_keybindings_in_code_notes": "Combinații de taste Vim"
|
||||
},
|
||||
"web_view": {
|
||||
"create_label": "Pentru a începe, creați o etichetă cu adresa URL de încorporat, e.g. #webViewSrc=\"https://www.google.com\"",
|
||||
"create_label": "Pentru a începe, creați o etichetă cu adresa URL de încorporat, e.g. #webViewSrc=\"https://www.google.com\"",
|
||||
"embed_websites": "Notițele de tip „Vizualizare web” permit încorporarea site-urilor web în Trilium.",
|
||||
"web_view": "Vizualizare web"
|
||||
},
|
||||
@@ -1863,16 +1863,11 @@
|
||||
},
|
||||
"create_new_ai_chat": "Crează o nouă discuție cu AI-ul",
|
||||
"configuration_warnings": "Sunt câteva probleme la configurația AI-ului. Verificați setările.",
|
||||
"experimental_warning": "Funcția LLM este experimentală.",
|
||||
"experimental_warning": "Funcția LLM este experimentală!",
|
||||
"selected_provider": "Furnizor selectat",
|
||||
"selected_provider_description": "Selectați furnizorul de AI pentru funcțiile de discuție și completare",
|
||||
"select_model": "Selectați modelul...",
|
||||
"select_provider": "Selectați furnizorul...",
|
||||
"ai_enabled": "Funcționalitățile AI au fost activate",
|
||||
"ai_disabled": "Funcționalitățile AI au fost dezactivate",
|
||||
"no_models_found_online": "Nu s-a găsit niciun model. Verificați cheia API și configurația.",
|
||||
"no_models_found_ollama": "Nu s-a găsit niciun model Ollama. Verificați dacă Ollama rulează.",
|
||||
"error_fetching": "Eroare la obținerea modelelor: {{error}}"
|
||||
"select_provider": "Selectați furnizorul..."
|
||||
},
|
||||
"custom_date_time_format": {
|
||||
"title": "Format dată/timp personalizat",
|
||||
@@ -2003,26 +1998,6 @@
|
||||
"call_to_action": {
|
||||
"background_effects_title": "Efectele de fundal sunt acum stabile",
|
||||
"background_effects_message": "Pe dispozitive cu Windows, efectele de fundal sunt complet stabile. Acestea adaugă un strop de culoare interfeței grafice prin estomparea fundalului din spatele ferestrei. Această tehnică este folosită și în alte aplicații precum Windows Explorer.",
|
||||
"background_effects_button": "Activează efectele de fundal",
|
||||
"next_theme_title": "Încercați noua temă Trilium",
|
||||
"next_theme_message": "Utilizați tema clasică, doriți să încercați noua temă?",
|
||||
"next_theme_button": "Testează noua temă",
|
||||
"dismiss": "Treci peste"
|
||||
},
|
||||
"ui-performance": {
|
||||
"title": "Setări de performanță",
|
||||
"enable-motion": "Activează tranzițiile și animațiile",
|
||||
"enable-shadows": "Activează umbrirea elementelor",
|
||||
"enable-backdrop-effects": "Activează efectele de fundal pentru meniuri, popup-uri și panouri"
|
||||
},
|
||||
"settings": {
|
||||
"related_settings": "Setări similare"
|
||||
},
|
||||
"settings_appearance": {
|
||||
"related_code_blocks": "Tema de culori pentru blocuri de cod în notițe de tip text",
|
||||
"related_code_notes": "Tema de culori pentru notițele de tip cod"
|
||||
},
|
||||
"units": {
|
||||
"percentage": "%"
|
||||
"background_effects_button": "Activează efectele de fundal"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1060,7 +1060,7 @@
|
||||
"fast_search": "быстрый поиск",
|
||||
"include_archived": "включать архивированные",
|
||||
"order_by": "сортировать по",
|
||||
"search_button": "Поиск",
|
||||
"search_button": "Поиск <kbd>enter</kbd>",
|
||||
"search_parameters": "Параметры поиска",
|
||||
"ancestor": "предок",
|
||||
"action": "действие",
|
||||
|
||||
@@ -845,7 +845,7 @@
|
||||
"debug": "除錯",
|
||||
"debug_description": "除錯將顯示額外的除錯資訊至控制台,以幫助除錯複雜查詢",
|
||||
"action": "操作",
|
||||
"search_button": "搜尋",
|
||||
"search_button": "搜尋 <kbd>Enter</kbd>",
|
||||
"search_execute": "搜尋並執行操作",
|
||||
"save_to_note": "儲存至筆記",
|
||||
"search_parameters": "搜尋參數",
|
||||
|
||||
@@ -960,7 +960,7 @@
|
||||
"debug": "debug",
|
||||
"debug_description": "Debug виведе додаткову інформацію для налагодження в консоль, щоб допомогти у налагодженні складних запитів",
|
||||
"action": "дія",
|
||||
"search_button": "Пошук",
|
||||
"search_button": "Пошук <kbd>enter</kbd>",
|
||||
"search_execute": "Пошук & Виконання дій",
|
||||
"save_to_note": "Зберегти до нотатки",
|
||||
"search_parameters": "Параметри пошуку",
|
||||
|
||||
@@ -3,11 +3,7 @@ type DateTimeStyle = "full" | "long" | "medium" | "short" | "none" | undefined;
|
||||
/**
|
||||
* Formats the given date and time to a string based on the current locale.
|
||||
*/
|
||||
export function formatDateTime(date: string | Date | number | null | undefined, dateStyle: DateTimeStyle = "medium", timeStyle: DateTimeStyle = "medium") {
|
||||
if (!date) {
|
||||
return "";
|
||||
}
|
||||
|
||||
export function formatDateTime(date: string | Date | number, dateStyle: DateTimeStyle = "medium", timeStyle: DateTimeStyle = "medium") {
|
||||
const locale = navigator.language;
|
||||
|
||||
let parsedDate;
|
||||
|
||||
504
apps/client/src/widgets/attribute_widgets/attribute_editor.ts
Normal file
504
apps/client/src/widgets/attribute_widgets/attribute_editor.ts
Normal file
@@ -0,0 +1,504 @@
|
||||
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,11 +1,7 @@
|
||||
import { isValidElement, VNode } from "preact";
|
||||
import Component, { TypedComponent } from "../components/component.js";
|
||||
import froca from "../services/froca.js";
|
||||
import { t } from "../services/i18n.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> {
|
||||
protected attrs: Record<string, string>;
|
||||
@@ -26,14 +22,11 @@ export class TypedBasicWidget<T extends TypedComponent<any>> extends TypedCompon
|
||||
this.childPositionCounter = 10;
|
||||
}
|
||||
|
||||
child(..._components: (T | VNode)[]) {
|
||||
if (!_components) {
|
||||
child(...components: T[]) {
|
||||
if (!components) {
|
||||
return this;
|
||||
}
|
||||
|
||||
// Convert any React components to legacy wrapped components.
|
||||
const components = wrapReactWidgets(_components);
|
||||
|
||||
super.child(...components);
|
||||
|
||||
for (const component of components) {
|
||||
@@ -265,30 +258,3 @@ export class TypedBasicWidget<T extends TypedComponent<any>> extends TypedCompon
|
||||
* For information on using widgets, see the tutorial {@tutorial widget_basics}.
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
54
apps/client/src/widgets/bookmark_switch.ts
Normal file
54
apps/client/src/widgets/bookmark_switch.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
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,7 +1,6 @@
|
||||
import { ComponentChildren } from "preact";
|
||||
import { memo } from "preact/compat";
|
||||
import AbstractBulkAction from "./abstract_bulk_action";
|
||||
import HelpRemoveButtons from "../react/HelpRemoveButtons";
|
||||
|
||||
interface BulkActionProps {
|
||||
label: string | ComponentChildren;
|
||||
@@ -25,11 +24,19 @@ const BulkAction = memo(({ label, children, helpText, bulkAction }: BulkActionPr
|
||||
{children}
|
||||
</div>
|
||||
</td>
|
||||
<HelpRemoveButtons
|
||||
help={helpText}
|
||||
removeText="Delete"
|
||||
onRemove={() => bulkAction?.deleteAction()}
|
||||
/>
|
||||
<td className="button-column">
|
||||
{helpText && <div className="dropdown help-dropdown">
|
||||
<span className="bx bx-help-circle icon-action" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
|
||||
<div className="dropdown-menu dropdown-menu-right p-4">
|
||||
{helpText}
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
<span
|
||||
className="bx bx-x icon-action action-conf-del"
|
||||
onClick={() => bulkAction?.deleteAction()}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { t } from "../../../services/i18n.js";
|
||||
import AbstractBulkAction from "../abstract_bulk_action.js";
|
||||
import AbstractBulkAction, { ActionDefinition } from "../abstract_bulk_action.js";
|
||||
import BulkAction, { BulkActionText } from "../BulkAction.jsx";
|
||||
import NoteAutocomplete from "../../react/NoteAutocomplete.jsx";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { useSpacedUpdate } from "../../react/hooks.jsx";
|
||||
|
||||
function MoveNoteBulkActionComponent({ bulkAction }: { bulkAction: AbstractBulkAction }) {
|
||||
function MoveNoteBulkActionComponent({ bulkAction, actionDef }: { bulkAction: AbstractBulkAction, actionDef: ActionDefinition }) {
|
||||
const [ targetParentNoteId, setTargetParentNoteId ] = useState<string>();
|
||||
const spacedUpdate = useSpacedUpdate(() => {
|
||||
return bulkAction.saveAction({ targetParentNoteId: targetParentNoteId })
|
||||
@@ -45,6 +45,6 @@ export default class MoveNoteBulkAction extends AbstractBulkAction {
|
||||
}
|
||||
|
||||
doRender() {
|
||||
return <MoveNoteBulkActionComponent bulkAction={this} />
|
||||
return <MoveNoteBulkActionComponent bulkAction={this} actionDef={this.actionDef} />
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import SpacedUpdate from "../../../services/spaced_update.js";
|
||||
import AbstractBulkAction, { ActionDefinition } from "../abstract_bulk_action.js";
|
||||
import { t } from "../../../services/i18n.js";
|
||||
import BulkAction from "../BulkAction.jsx";
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import SpacedUpdate from "../../../services/spaced_update.js";
|
||||
import AbstractBulkAction, { ActionDefinition } from "../abstract_bulk_action.js";
|
||||
import noteAutocompleteService from "../../../services/note_autocomplete.js";
|
||||
import { t } from "../../../services/i18n.js";
|
||||
import BulkAction, { BulkActionText } from "../BulkAction.jsx";
|
||||
import NoteAutocomplete from "../../react/NoteAutocomplete.jsx";
|
||||
|
||||
252
apps/client/src/widgets/buttons/note_actions.ts
Normal file
252
apps/client/src/widgets/buttons/note_actions.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
14
apps/client/src/widgets/buttons/revisions_button.ts
Normal file
14
apps/client/src/widgets/buttons/revisions_button.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
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 ?? "");
|
||||
}
|
||||
}
|
||||
388
apps/client/src/widgets/containers/ribbon_container.ts
Normal file
388
apps/client/src/widgets/containers/ribbon_container.ts
Normal file
@@ -0,0 +1,388 @@
|
||||
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,3 +1,4 @@
|
||||
import ReactBasicWidget from "../react/ReactBasicWidget.js";
|
||||
import Modal from "../react/Modal.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import { formatDateTime } from "../../utils/formatters.js";
|
||||
@@ -7,11 +8,11 @@ import openService from "../../services/open.js";
|
||||
import { useState } from "preact/hooks";
|
||||
import type { CSSProperties } from "preact/compat";
|
||||
import type { AppInfo } from "@triliumnext/commons";
|
||||
import { useTriliumEvent } from "../react/hooks.jsx";
|
||||
import useTriliumEvent from "../react/hooks.jsx";
|
||||
|
||||
export default function AboutDialog() {
|
||||
const [appInfo, setAppInfo] = useState<AppInfo | null>(null);
|
||||
const [shown, setShown] = useState(false);
|
||||
function AboutDialogComponent() {
|
||||
let [appInfo, setAppInfo] = useState<AppInfo | null>(null);
|
||||
let [shown, setShown] = useState(false);
|
||||
const forceWordBreak: CSSProperties = { wordBreak: "break-all" };
|
||||
|
||||
useTriliumEvent("openAboutDialog", () => setShown(true));
|
||||
@@ -76,8 +77,16 @@ function DirectoryLink({ directory, style }: { directory: string, style?: CSSPro
|
||||
openService.openDirectory(directory);
|
||||
};
|
||||
|
||||
return <a className="tn-link" href="#" onClick={onClick} style={style}>{directory}</a>
|
||||
return <a className="tn-link" href="#" onClick={onClick} style={style}></a>
|
||||
} else {
|
||||
return <span style={style}>{directory}</span>;
|
||||
}
|
||||
}
|
||||
|
||||
export default class AboutDialog extends ReactBasicWidget {
|
||||
|
||||
get component() {
|
||||
return <AboutDialogComponent />;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { t } from "../../services/i18n";
|
||||
import Modal from "../react/Modal";
|
||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
||||
import Button from "../react/Button";
|
||||
import FormRadioGroup from "../react/FormRadioGroup";
|
||||
import NoteAutocomplete from "../react/NoteAutocomplete";
|
||||
@@ -10,11 +11,11 @@ import { default as TextTypeWidget } from "../type_widgets/editable_text.js";
|
||||
import { logError } from "../../services/ws";
|
||||
import FormGroup from "../react/FormGroup.js";
|
||||
import { refToJQuerySelector } from "../react/react_utils";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import useTriliumEvent from "../react/hooks";
|
||||
|
||||
type LinkType = "reference-link" | "external-link" | "hyper-link";
|
||||
|
||||
export default function AddLinkDialog() {
|
||||
function AddLinkDialogComponent() {
|
||||
const [ textTypeWidget, setTextTypeWidget ] = useState<TextTypeWidget>();
|
||||
const initialText = useRef<string>();
|
||||
const [ linkTitle, setLinkTitle ] = useState("");
|
||||
@@ -29,14 +30,6 @@ export default function AddLinkDialog() {
|
||||
setShown(true);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (hasSelection) {
|
||||
setLinkType("hyper-link");
|
||||
} else {
|
||||
setLinkType("reference-link");
|
||||
}
|
||||
}, [ hasSelection ])
|
||||
|
||||
async function setDefaultLinkTitle(noteId: string) {
|
||||
const noteTitle = await tree.getNoteTitle(noteId);
|
||||
setLinkTitle(noteTitle);
|
||||
@@ -159,3 +152,11 @@ export default function AddLinkDialog() {
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default class AddLinkDialog extends ReactBasicWidget {
|
||||
|
||||
get component() {
|
||||
return <AddLinkDialogComponent />;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,14 +4,15 @@ import { t } from "../../services/i18n.js";
|
||||
import server from "../../services/server.js";
|
||||
import toast from "../../services/toast.js";
|
||||
import Modal from "../react/Modal.jsx";
|
||||
import ReactBasicWidget from "../react/ReactBasicWidget.js";
|
||||
import froca from "../../services/froca.js";
|
||||
import tree from "../../services/tree.js";
|
||||
import Button from "../react/Button.jsx";
|
||||
import FormGroup from "../react/FormGroup.js";
|
||||
import { useTriliumEvent } from "../react/hooks.jsx";
|
||||
import useTriliumEvent from "../react/hooks.jsx";
|
||||
import FBranch from "../../entities/fbranch.js";
|
||||
|
||||
export default function BranchPrefixDialog() {
|
||||
function BranchPrefixDialogComponent() {
|
||||
const [ shown, setShown ] = useState(false);
|
||||
const [ branch, setBranch ] = useState<FBranch>();
|
||||
const [ prefix, setPrefix ] = useState(branch?.prefix ?? "");
|
||||
@@ -74,6 +75,14 @@ export default function BranchPrefixDialog() {
|
||||
);
|
||||
}
|
||||
|
||||
export default class BranchPrefixDialog extends ReactBasicWidget {
|
||||
|
||||
get component() {
|
||||
return <BranchPrefixDialogComponent />;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function savePrefix(branchId: string, prefix: string) {
|
||||
await server.put(`branches/${branchId}/set-prefix`, { prefix: prefix });
|
||||
toast.showMessage(t("branch_prefix.branch_prefix_saved"));
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState, useCallback } from "preact/hooks";
|
||||
import { t } from "../../services/i18n";
|
||||
import Modal from "../react/Modal";
|
||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
||||
import "./bulk_actions.css";
|
||||
import { BulkActionAffectedNotes } from "@triliumnext/commons";
|
||||
import server from "../../services/server";
|
||||
@@ -11,9 +12,9 @@ import toast from "../../services/toast";
|
||||
import RenameNoteBulkAction from "../bulk_actions/note/rename_note";
|
||||
import FNote from "../../entities/fnote";
|
||||
import froca from "../../services/froca";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import useTriliumEvent from "../react/hooks";
|
||||
|
||||
export default function BulkActionsDialog() {
|
||||
function BulkActionComponent() {
|
||||
const [ selectedOrActiveNoteIds, setSelectedOrActiveNoteIds ] = useState<string[]>();
|
||||
const [ bulkActionNote, setBulkActionNote ] = useState<FNote | null>();
|
||||
const [ includeDescendants, setIncludeDescendants ] = useState(false);
|
||||
@@ -50,7 +51,7 @@ export default function BulkActionsDialog() {
|
||||
row.type === "label" && row.name === "action" && row.noteId === "_bulkAction")) {
|
||||
refreshExistingActions();
|
||||
}
|
||||
});
|
||||
}, shown);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -116,3 +117,11 @@ function ExistingActionsList({ existingActions }: { existingActions?: RenameNote
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
export default class BulkActionsDialog extends ReactBasicWidget {
|
||||
|
||||
get component() {
|
||||
return <BulkActionComponent />
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
import { useMemo, useState } from "preact/hooks";
|
||||
import { useState } from "preact/hooks";
|
||||
import Button from "../react/Button";
|
||||
import Modal from "../react/Modal";
|
||||
import { dismissCallToAction, getCallToActions } from "./call_to_action_definitions";
|
||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
||||
import { CallToAction, dismissCallToAction, getCallToActions } from "./call_to_action_definitions";
|
||||
import { t } from "../../services/i18n";
|
||||
|
||||
export default function CallToActionDialog() {
|
||||
const activeCallToActions = useMemo(() => getCallToActions(), []);
|
||||
function CallToActionDialogComponent({ activeCallToActions }: { activeCallToActions: CallToAction[] }) {
|
||||
if (!activeCallToActions.length) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const [ activeIndex, setActiveIndex ] = useState(0);
|
||||
const [ shown, setShown ] = useState(true);
|
||||
const activeItem = activeCallToActions[activeIndex];
|
||||
@@ -18,7 +22,7 @@ export default function CallToActionDialog() {
|
||||
}
|
||||
}
|
||||
|
||||
return (activeCallToActions.length &&
|
||||
return (
|
||||
<Modal
|
||||
className="call-to-action"
|
||||
size="md"
|
||||
@@ -44,3 +48,11 @@ export default function CallToActionDialog() {
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export class CallToActionDialog extends ReactBasicWidget {
|
||||
|
||||
get component() {
|
||||
return <CallToActionDialogComponent activeCallToActions={getCallToActions()} />
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { useRef, useState } from "preact/hooks";
|
||||
import appContext from "../../components/app_context";
|
||||
import { t } from "../../services/i18n";
|
||||
import Modal from "../react/Modal";
|
||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
||||
import NoteAutocomplete from "../react/NoteAutocomplete";
|
||||
import froca from "../../services/froca";
|
||||
import FormGroup from "../react/FormGroup";
|
||||
@@ -13,9 +14,9 @@ import tree from "../../services/tree";
|
||||
import branches from "../../services/branches";
|
||||
import toast from "../../services/toast";
|
||||
import NoteList from "../react/NoteList";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import useTriliumEvent from "../react/hooks";
|
||||
|
||||
export default function CloneToDialog() {
|
||||
function CloneToDialogComponent() {
|
||||
const [ clonedNoteIds, setClonedNoteIds ] = useState<string[]>();
|
||||
const [ prefix, setPrefix ] = useState("");
|
||||
const [ suggestion, setSuggestion ] = useState<Suggestion | null>(null);
|
||||
@@ -82,6 +83,14 @@ export default function CloneToDialog() {
|
||||
)
|
||||
}
|
||||
|
||||
export default class CloneToDialog extends ReactBasicWidget {
|
||||
|
||||
get component() {
|
||||
return <CloneToDialogComponent />;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function cloneNotesTo(notePath: string, clonedNoteIds: string[], prefix?: string) {
|
||||
const { noteId, parentNoteId } = tree.getNoteIdAndParentIdFromUrl(notePath);
|
||||
if (!noteId || !parentNoteId) {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
||||
import Modal from "../react/Modal";
|
||||
import Button from "../react/Button";
|
||||
import { t } from "../../services/i18n";
|
||||
import { useState } from "preact/hooks";
|
||||
import FormCheckbox from "../react/FormCheckbox";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import useTriliumEvent from "../react/hooks";
|
||||
|
||||
interface ConfirmDialogProps {
|
||||
title?: string;
|
||||
@@ -12,7 +13,7 @@ interface ConfirmDialogProps {
|
||||
isConfirmDeleteNoteBox?: boolean;
|
||||
}
|
||||
|
||||
export default function ConfirmDialog() {
|
||||
function ConfirmDialogComponent() {
|
||||
const [ opts, setOpts ] = useState<ConfirmDialogProps>();
|
||||
const [ isDeleteNoteChecked, setIsDeleteNoteChecked ] = useState(false);
|
||||
const [ shown, setShown ] = useState(false);
|
||||
@@ -91,3 +92,11 @@ export interface ConfirmWithTitleOptions {
|
||||
title: string;
|
||||
callback: ConfirmDialogCallback;
|
||||
}
|
||||
|
||||
export default class ConfirmDialog extends ReactBasicWidget {
|
||||
|
||||
get component() {
|
||||
return <ConfirmDialogComponent />;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { useRef, useState, useEffect } from "preact/hooks";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import FormCheckbox from "../react/FormCheckbox.js";
|
||||
import Modal from "../react/Modal.js";
|
||||
import ReactBasicWidget from "../react/ReactBasicWidget.js";
|
||||
import type { DeleteNotesPreview } from "@triliumnext/commons";
|
||||
import server from "../../services/server.js";
|
||||
import froca from "../../services/froca.js";
|
||||
@@ -9,7 +10,7 @@ import FNote from "../../entities/fnote.js";
|
||||
import link from "../../services/link.js";
|
||||
import Button from "../react/Button.jsx";
|
||||
import Alert from "../react/Alert.jsx";
|
||||
import { useTriliumEvent } from "../react/hooks.jsx";
|
||||
import useTriliumEvent from "../react/hooks.jsx";
|
||||
|
||||
export interface ResolveOptions {
|
||||
proceed: boolean;
|
||||
@@ -29,7 +30,7 @@ interface BrokenRelationData {
|
||||
source: string;
|
||||
}
|
||||
|
||||
export default function DeleteNotesDialog() {
|
||||
function DeleteNotesDialogComponent() {
|
||||
const [ opts, setOpts ] = useState<ShowDeleteNotesDialogOpts>({});
|
||||
const [ deleteAllClones, setDeleteAllClones ] = useState(false);
|
||||
const [ eraseNotes, setEraseNotes ] = useState(!!opts.forceDeleteAllClones);
|
||||
@@ -139,7 +140,7 @@ function BrokenRelations({ brokenRelations }: { brokenRelations: DeleteNotesPrev
|
||||
const noteIds = brokenRelations
|
||||
.map(relation => relation.noteId)
|
||||
.filter(noteId => noteId) as string[];
|
||||
froca.getNotes(noteIds).then(async () => {
|
||||
froca.getNotes(noteIds).then(async (notes) => {
|
||||
const notesWithBrokenRelations: BrokenRelationData[] = [];
|
||||
for (const attr of brokenRelations) {
|
||||
notesWithBrokenRelations.push({
|
||||
@@ -170,3 +171,11 @@ function BrokenRelations({ brokenRelations }: { brokenRelations: DeleteNotesPrev
|
||||
return <></>;
|
||||
}
|
||||
}
|
||||
|
||||
export default class DeleteNotesDialog extends ReactBasicWidget {
|
||||
|
||||
get component() {
|
||||
return <DeleteNotesDialogComponent />;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,13 +4,14 @@ import tree from "../../services/tree";
|
||||
import Button from "../react/Button";
|
||||
import FormRadioGroup from "../react/FormRadioGroup";
|
||||
import Modal from "../react/Modal";
|
||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
||||
import "./export.css";
|
||||
import ws from "../../services/ws";
|
||||
import toastService, { ToastOptions } from "../../services/toast";
|
||||
import utils from "../../services/utils";
|
||||
import open from "../../services/open";
|
||||
import froca from "../../services/froca";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import useTriliumEvent from "../react/hooks";
|
||||
|
||||
interface ExportDialogProps {
|
||||
branchId?: string | null;
|
||||
@@ -18,7 +19,7 @@ interface ExportDialogProps {
|
||||
defaultType?: "subtree" | "single";
|
||||
}
|
||||
|
||||
export default function ExportDialog() {
|
||||
function ExportDialogComponent() {
|
||||
const [ opts, setOpts ] = useState<ExportDialogProps>();
|
||||
const [ exportType, setExportType ] = useState<string>(opts?.defaultType ?? "subtree");
|
||||
const [ subtreeFormat, setSubtreeFormat ] = useState("html");
|
||||
@@ -124,6 +125,14 @@ export default function ExportDialog() {
|
||||
);
|
||||
}
|
||||
|
||||
export default class ExportDialog extends ReactBasicWidget {
|
||||
|
||||
get component() {
|
||||
return <ExportDialogComponent />
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function exportBranch(branchId: string, type: string, format: string, version: string) {
|
||||
const taskId = utils.randomString(10);
|
||||
const url = open.getUrlForDownload(`api/branches/${branchId}/export/${type}/${format}/${version}/${taskId}`);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import ReactBasicWidget from "../react/ReactBasicWidget.js";
|
||||
import Modal from "../react/Modal.jsx";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import { ComponentChildren } from "preact";
|
||||
@@ -5,9 +6,9 @@ import { CommandNames } from "../../components/app_context.js";
|
||||
import RawHtml from "../react/RawHtml.jsx";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import keyboard_actions from "../../services/keyboard_actions.js";
|
||||
import { useTriliumEvent } from "../react/hooks.jsx";
|
||||
import useTriliumEvent from "../react/hooks.jsx";
|
||||
|
||||
export default function HelpDialog() {
|
||||
function HelpDialogComponent() {
|
||||
const [ shown, setShown ] = useState(false);
|
||||
useTriliumEvent("showCheatsheet", () => setShown(true));
|
||||
|
||||
@@ -160,3 +161,11 @@ function Card({ title, children }: { title: string, children: ComponentChildren
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default class HelpDialog extends ReactBasicWidget {
|
||||
|
||||
get component() {
|
||||
return <HelpDialogComponent />;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,10 +7,11 @@ import FormFileUpload from "../react/FormFileUpload";
|
||||
import FormGroup, { FormMultiGroup } from "../react/FormGroup";
|
||||
import Modal from "../react/Modal";
|
||||
import RawHtml from "../react/RawHtml";
|
||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
||||
import importService, { UploadFilesOptions } from "../../services/import";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import useTriliumEvent from "../react/hooks";
|
||||
|
||||
export default function ImportDialog() {
|
||||
function ImportDialogComponent() {
|
||||
const [ parentNoteId, setParentNoteId ] = useState<string>();
|
||||
const [ noteTitle, setNoteTitle ] = useState<string>();
|
||||
const [ files, setFiles ] = useState<FileList | null>(null);
|
||||
@@ -88,6 +89,14 @@ export default function ImportDialog() {
|
||||
);
|
||||
}
|
||||
|
||||
export default class ImportDialog extends ReactBasicWidget {
|
||||
|
||||
get component() {
|
||||
return <ImportDialogComponent />
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function boolToString(value: boolean) {
|
||||
return value ? "true" : "false";
|
||||
}
|
||||
@@ -4,14 +4,15 @@ import FormGroup from "../react/FormGroup";
|
||||
import FormRadioGroup from "../react/FormRadioGroup";
|
||||
import Modal from "../react/Modal";
|
||||
import NoteAutocomplete from "../react/NoteAutocomplete";
|
||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
||||
import Button from "../react/Button";
|
||||
import { Suggestion, triggerRecentNotes } from "../../services/note_autocomplete";
|
||||
import tree from "../../services/tree";
|
||||
import froca from "../../services/froca";
|
||||
import EditableTextTypeWidget from "../type_widgets/editable_text";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import useTriliumEvent from "../react/hooks";
|
||||
|
||||
export default function IncludeNoteDialog() {
|
||||
function IncludeNoteDialogComponent() {
|
||||
const [textTypeWidget, setTextTypeWidget] = useState<EditableTextTypeWidget>();
|
||||
const [suggestion, setSuggestion] = useState<Suggestion | null>(null);
|
||||
const [boxSize, setBoxSize] = useState("medium");
|
||||
@@ -69,6 +70,14 @@ export default function IncludeNoteDialog() {
|
||||
)
|
||||
}
|
||||
|
||||
export default class IncludeNoteDialog extends ReactBasicWidget {
|
||||
|
||||
get component() {
|
||||
return <IncludeNoteDialogComponent />;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function includeNote(notePath: string, textTypeWidget: EditableTextTypeWidget) {
|
||||
const noteId = tree.getNoteIdFromUrl(notePath);
|
||||
if (!noteId) {
|
||||
|
||||
@@ -3,10 +3,11 @@ import { t } from "../../services/i18n.js";
|
||||
import utils from "../../services/utils.js";
|
||||
import Button from "../react/Button.js";
|
||||
import Modal from "../react/Modal.js";
|
||||
import ReactBasicWidget from "../react/ReactBasicWidget.js";
|
||||
import { useState } from "preact/hooks";
|
||||
import { useTriliumEvent } from "../react/hooks.jsx";
|
||||
import useTriliumEvent from "../react/hooks.jsx";
|
||||
|
||||
export default function IncorrectCpuArchDialogComponent() {
|
||||
function IncorrectCpuArchDialogComponent() {
|
||||
const [ shown, setShown ] = useState(false);
|
||||
const downloadButtonRef = useRef<HTMLButtonElement>(null);
|
||||
useTriliumEvent("showCpuArchWarning", () => setShown(true));
|
||||
@@ -43,3 +44,11 @@ export default function IncorrectCpuArchDialogComponent() {
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default class IncorrectCpuArchDialog extends ReactBasicWidget {
|
||||
|
||||
get component() {
|
||||
return <IncorrectCpuArchDialogComponent />
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { EventData } from "../../components/app_context";
|
||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
||||
import Modal from "../react/Modal";
|
||||
import { t } from "../../services/i18n";
|
||||
import Button from "../react/Button";
|
||||
import { useRef, useState } from "preact/hooks";
|
||||
import { RawHtmlBlock } from "../react/RawHtml";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import useTriliumEvent from "../react/hooks";
|
||||
|
||||
export default function InfoDialog() {
|
||||
function ShowInfoDialogComponent() {
|
||||
const [ opts, setOpts ] = useState<EventData<"showInfoDialog">>();
|
||||
const [ shown, setShown ] = useState(false);
|
||||
const okButtonRef = useRef<HTMLButtonElement>(null);
|
||||
@@ -36,3 +37,11 @@ export default function InfoDialog() {
|
||||
<RawHtmlBlock className="info-dialog-content" html={opts?.message ?? ""} />
|
||||
</Modal>);
|
||||
}
|
||||
|
||||
export default class InfoDialog extends ReactBasicWidget {
|
||||
|
||||
get component() {
|
||||
return <ShowInfoDialogComponent />;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
||||
import Modal from "../react/Modal";
|
||||
import Button from "../react/Button";
|
||||
import NoteAutocomplete from "../react/NoteAutocomplete";
|
||||
@@ -7,14 +8,13 @@ import note_autocomplete, { Suggestion } from "../../services/note_autocomplete"
|
||||
import appContext from "../../components/app_context";
|
||||
import commandRegistry from "../../services/command_registry";
|
||||
import { refToJQuerySelector } from "../react/react_utils";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import shortcutService from "../../services/shortcuts";
|
||||
import useTriliumEvent from "../react/hooks";
|
||||
|
||||
const KEEP_LAST_SEARCH_FOR_X_SECONDS = 120;
|
||||
|
||||
type Mode = "last-search" | "recent-notes" | "commands";
|
||||
|
||||
export default function JumpToNoteDialogComponent() {
|
||||
function JumpToNoteDialogComponent() {
|
||||
const [ mode, setMode ] = useState<Mode>();
|
||||
const [ lastOpenedTs, setLastOpenedTs ] = useState<number>(0);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -26,12 +26,12 @@ export default function JumpToNoteDialogComponent() {
|
||||
|
||||
async function openDialog(commandMode: boolean) {
|
||||
let newMode: Mode;
|
||||
let initialText = "";
|
||||
let initialText: string = "";
|
||||
|
||||
if (commandMode) {
|
||||
newMode = "commands";
|
||||
initialText = ">";
|
||||
} else if (Date.now() - lastOpenedTs <= KEEP_LAST_SEARCH_FOR_X_SECONDS * 1000 && actualText.current) {
|
||||
} else if (Date.now() - lastOpenedTs <= KEEP_LAST_SEARCH_FOR_X_SECONDS * 1000 && actualText) {
|
||||
// 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)
|
||||
// so we'll keep the content.
|
||||
@@ -83,27 +83,6 @@ export default function JumpToNoteDialogComponent() {
|
||||
$autoComplete
|
||||
.trigger("focus")
|
||||
.trigger("select");
|
||||
|
||||
// Add keyboard shortcut for full search
|
||||
shortcutService.bindElShortcut($autoComplete, "ctrl+return", () => {
|
||||
if (!isCommandMode) {
|
||||
showInFullSearch();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function showInFullSearch() {
|
||||
try {
|
||||
setShown(false);
|
||||
const searchString = actualText.current?.trim();
|
||||
if (searchString && !searchString.startsWith(">")) {
|
||||
await appContext.triggerCommand("searchNotes", {
|
||||
searchString
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to trigger full search:", error);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -129,15 +108,18 @@ export default function JumpToNoteDialogComponent() {
|
||||
/>}
|
||||
onShown={onShown}
|
||||
onHidden={() => setShown(false)}
|
||||
footer={!isCommandMode && <Button
|
||||
className="show-in-full-text-button"
|
||||
text={t("jump_to_note.search_button")}
|
||||
keyboardShortcut="Ctrl+Enter"
|
||||
onClick={showInFullSearch}
|
||||
/>}
|
||||
footer={!isCommandMode && <Button className="show-in-full-text-button" text={t("jump_to_note.search_button")} keyboardShortcut="Ctrl+Enter" />}
|
||||
show={shown}
|
||||
>
|
||||
<div className="algolia-autocomplete-container jump-to-note-results" ref={containerRef}></div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default class JumpToNoteDialog extends ReactBasicWidget {
|
||||
|
||||
get component() {
|
||||
return <JumpToNoteDialogComponent />;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,17 +5,18 @@ import server from "../../services/server";
|
||||
import toast from "../../services/toast";
|
||||
import utils from "../../services/utils";
|
||||
import Modal from "../react/Modal";
|
||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
||||
import Button from "../react/Button";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import useTriliumEvent from "../react/hooks";
|
||||
|
||||
interface RenderMarkdownResponse {
|
||||
htmlContent: string;
|
||||
}
|
||||
|
||||
export default function MarkdownImportDialog() {
|
||||
function MarkdownImportDialogComponent() {
|
||||
const markdownImportTextArea = useRef<HTMLTextAreaElement>(null);
|
||||
const [ text, setText ] = useState("");
|
||||
const [ shown, setShown ] = useState(false);
|
||||
let [ text, setText ] = useState("");
|
||||
let [ shown, setShown ] = useState(false);
|
||||
|
||||
const triggerImport = useCallback(() => {
|
||||
if (appContext.tabManager.getActiveContextNoteType() !== "text") {
|
||||
@@ -63,6 +64,14 @@ export default function MarkdownImportDialog() {
|
||||
)
|
||||
}
|
||||
|
||||
export default class MarkdownImportDialog extends ReactBasicWidget {
|
||||
|
||||
get component() {
|
||||
return <MarkdownImportDialogComponent />;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function convertMarkdownToHtml(markdownContent: string) {
|
||||
const { htmlContent } = await server.post<RenderMarkdownResponse>("other/render-markdown", { markdownContent });
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
||||
import Modal from "../react/Modal";
|
||||
import { t } from "../../services/i18n";
|
||||
import NoteList from "../react/NoteList";
|
||||
@@ -10,9 +11,9 @@ import tree from "../../services/tree";
|
||||
import froca from "../../services/froca";
|
||||
import branches from "../../services/branches";
|
||||
import toast from "../../services/toast";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import useTriliumEvent from "../react/hooks";
|
||||
|
||||
export default function MoveToDialog() {
|
||||
function MoveToDialogComponent() {
|
||||
const [ movedBranchIds, setMovedBranchIds ] = useState<string[]>();
|
||||
const [ suggestion, setSuggestion ] = useState<Suggestion | null>(null);
|
||||
const [ shown, setShown ] = useState(false);
|
||||
@@ -66,6 +67,14 @@ export default function MoveToDialog() {
|
||||
)
|
||||
}
|
||||
|
||||
export default class MoveToDialog extends ReactBasicWidget {
|
||||
|
||||
get component() {
|
||||
return <MoveToDialogComponent />;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function moveNotesTo(movedBranchIds: string[] | undefined, parentBranchId: string) {
|
||||
if (movedBranchIds) {
|
||||
await branches.moveToParentNote(movedBranchIds, parentBranchId);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
||||
import Modal from "../react/Modal";
|
||||
import { t } from "../../services/i18n";
|
||||
import FormGroup from "../react/FormGroup";
|
||||
@@ -9,7 +10,7 @@ import { MenuCommandItem, MenuItem } from "../../menus/context_menu";
|
||||
import { TreeCommandNames } from "../../menus/tree_context_menu";
|
||||
import { Suggestion } from "../../services/note_autocomplete";
|
||||
import Badge from "../react/Badge";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import useTriliumEvent from "../react/hooks";
|
||||
|
||||
export interface ChooseNoteTypeResponse {
|
||||
success: boolean;
|
||||
@@ -25,7 +26,7 @@ const SEPARATOR_TITLE_REPLACEMENTS = [
|
||||
t("note_type_chooser.templates")
|
||||
];
|
||||
|
||||
export default function NoteTypeChooserDialogComponent() {
|
||||
function NoteTypeChooserDialogComponent() {
|
||||
const [ callback, setCallback ] = useState<ChooseNoteTypeCallback>();
|
||||
const [ shown, setShown ] = useState(false);
|
||||
const [ parentNote, setParentNote ] = useState<Suggestion | null>();
|
||||
@@ -36,23 +37,25 @@ export default function NoteTypeChooserDialogComponent() {
|
||||
setShown(true);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
note_types.getNoteTypeItems().then(noteTypes => {
|
||||
let index = -1;
|
||||
if (!noteTypes.length) {
|
||||
useEffect(() => {
|
||||
note_types.getNoteTypeItems().then(noteTypes => {
|
||||
let index = -1;
|
||||
|
||||
setNoteTypes((noteTypes ?? []).map((item) => {
|
||||
if (item.title === "----") {
|
||||
index++;
|
||||
return {
|
||||
title: SEPARATOR_TITLE_REPLACEMENTS[index],
|
||||
enabled: false
|
||||
setNoteTypes((noteTypes ?? []).map((item, _index) => {
|
||||
if (item.title === "----") {
|
||||
index++;
|
||||
return {
|
||||
title: SEPARATOR_TITLE_REPLACEMENTS[index],
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return item;
|
||||
}));
|
||||
return item;
|
||||
}));
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
}
|
||||
|
||||
function onNoteTypeSelected(value: string) {
|
||||
const [ noteType, templateNoteId ] = value.split(",");
|
||||
@@ -117,3 +120,11 @@ export default function NoteTypeChooserDialogComponent() {
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default class NoteTypeChooserDialog extends ReactBasicWidget {
|
||||
|
||||
get component() {
|
||||
return <NoteTypeChooserDialogComponent />
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
||||
import Modal from "../react/Modal";
|
||||
import { t } from "../../services/i18n";
|
||||
import Button from "../react/Button";
|
||||
import appContext from "../../components/app_context";
|
||||
import { useState } from "preact/hooks";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import useTriliumEvent from "../react/hooks";
|
||||
|
||||
export default function PasswordNotSetDialog() {
|
||||
function PasswordNotSetDialogComponent() {
|
||||
const [ shown, setShown ] = useState(false);
|
||||
useTriliumEvent("showPasswordNotSet", () => setShown(true));
|
||||
|
||||
@@ -26,3 +27,10 @@ export default function PasswordNotSetDialog() {
|
||||
);
|
||||
}
|
||||
|
||||
export default class PasswordNotSetDialog extends ReactBasicWidget {
|
||||
|
||||
get component() {
|
||||
return <PasswordNotSetDialogComponent />;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@ import { useRef, useState } from "preact/hooks";
|
||||
import { t } from "../../services/i18n";
|
||||
import Button from "../react/Button";
|
||||
import Modal from "../react/Modal";
|
||||
import { Modal as BootstrapModal } from "bootstrap";
|
||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
||||
import FormTextBox from "../react/FormTextBox";
|
||||
import FormGroup from "../react/FormGroup";
|
||||
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.
|
||||
interface ShownCallbackData {
|
||||
@@ -26,7 +28,7 @@ export interface PromptDialogOptions {
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
export default function PromptDialog() {
|
||||
function PromptDialogComponent() {
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
const labelRef = useRef<HTMLLabelElement>(null);
|
||||
@@ -82,3 +84,11 @@ export default function PromptDialog() {
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default class PromptDialog extends ReactBasicWidget {
|
||||
|
||||
get component() {
|
||||
return <PromptDialogComponent />;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,10 +3,11 @@ import { t } from "../../services/i18n";
|
||||
import Button from "../react/Button";
|
||||
import FormTextBox from "../react/FormTextBox";
|
||||
import Modal from "../react/Modal";
|
||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
||||
import protected_session from "../../services/protected_session";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import useTriliumEvent from "../react/hooks";
|
||||
|
||||
export default function ProtectedSessionPasswordDialog() {
|
||||
function ProtectedSessionPasswordDialogComponent() {
|
||||
const [ shown, setShown ] = useState(false);
|
||||
const [ password, setPassword ] = useState("");
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -37,3 +38,11 @@ export default function ProtectedSessionPasswordDialog() {
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default class ProtectedSessionPasswordDialog extends ReactBasicWidget {
|
||||
|
||||
get component() {
|
||||
return <ProtectedSessionPasswordDialogComponent />;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import server from "../../services/server";
|
||||
import toast from "../../services/toast";
|
||||
import Button from "../react/Button";
|
||||
import Modal from "../react/Modal";
|
||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
||||
import hoisted_note from "../../services/hoisted_note";
|
||||
import type { RecentChangeRow } from "@triliumnext/commons";
|
||||
import froca from "../../services/froca";
|
||||
@@ -13,32 +14,39 @@ import { formatDateTime } from "../../utils/formatters";
|
||||
import link from "../../services/link";
|
||||
import RawHtml from "../react/RawHtml";
|
||||
import ws from "../../services/ws";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import useTriliumEvent from "../react/hooks";
|
||||
|
||||
export default function RecentChangesDialog() {
|
||||
function RecentChangesDialogComponent() {
|
||||
const [ ancestorNoteId, setAncestorNoteId ] = useState<string>();
|
||||
const [ groupedByDate, setGroupedByDate ] = useState<Map<string, RecentChangeRow[]>>();
|
||||
const [ refreshCounter, setRefreshCounter ] = useState(0);
|
||||
const [ groupedByDate, setGroupedByDate ] = useState<Map<String, RecentChangeRow[]>>();
|
||||
const [ needsRefresh, setNeedsRefresh ] = useState(false);
|
||||
const [ shown, setShown ] = useState(false);
|
||||
|
||||
useTriliumEvent("showRecentChanges", ({ ancestorNoteId }) => {
|
||||
useTriliumEvent("showRecentChanges", ({ ancestorNoteId }) => {
|
||||
setNeedsRefresh(true);
|
||||
setAncestorNoteId(ancestorNoteId ?? hoisted_note.getHoistedNoteId());
|
||||
setShown(true);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
server.get<RecentChangeRow[]>(`recent-changes/${ancestorNoteId}`)
|
||||
.then(async (recentChanges) => {
|
||||
// preload all notes into cache
|
||||
await froca.getNotes(
|
||||
recentChanges.map((r) => r.noteId),
|
||||
true
|
||||
);
|
||||
if (!groupedByDate || needsRefresh) {
|
||||
useEffect(() => {
|
||||
if (needsRefresh) {
|
||||
setNeedsRefresh(false);
|
||||
}
|
||||
|
||||
const groupedByDate = groupByDate(recentChanges);
|
||||
setGroupedByDate(groupedByDate);
|
||||
});
|
||||
}, [ shown, refreshCounter ])
|
||||
server.get<RecentChangeRow[]>(`recent-changes/${ancestorNoteId}`)
|
||||
.then(async (recentChanges) => {
|
||||
// preload all notes into cache
|
||||
await froca.getNotes(
|
||||
recentChanges.map((r) => r.noteId),
|
||||
true
|
||||
);
|
||||
|
||||
const groupedByDate = groupByDate(recentChanges);
|
||||
setGroupedByDate(groupedByDate);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -53,7 +61,7 @@ export default function RecentChangesDialog() {
|
||||
style={{ padding: "0 10px" }}
|
||||
onClick={() => {
|
||||
server.post("notes/erase-deleted-notes-now").then(() => {
|
||||
setRefreshCounter(refreshCounter + 1);
|
||||
setNeedsRefresh(true);
|
||||
toast.showMessage(t("recent_changes.deleted_notes_message"));
|
||||
});
|
||||
}}
|
||||
@@ -71,7 +79,7 @@ export default function RecentChangesDialog() {
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<>
|
||||
{ Array.from(groupedByDate.entries()).map(([dateDay, dayChanges]) => {
|
||||
@@ -106,6 +114,10 @@ function RecentChangesTimeline({ groupedByDate, setShown }: { groupedByDate: Map
|
||||
}
|
||||
|
||||
function NoteLink({ notePath, title }: { notePath: string, title: string }) {
|
||||
if (!notePath || !title) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [ noteLink, setNoteLink ] = useState<JQuery<HTMLElement> | null>(null);
|
||||
useEffect(() => {
|
||||
link.createLink(notePath, {
|
||||
@@ -144,19 +156,25 @@ function DeletedNoteLink({ change, setShown }: { change: RecentChangeRow, setSho
|
||||
);
|
||||
}
|
||||
|
||||
export default class RecentChangesDialog extends ReactBasicWidget {
|
||||
|
||||
get component() {
|
||||
return <RecentChangesDialogComponent />
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function groupByDate(rows: RecentChangeRow[]) {
|
||||
const groupedByDate = new Map<string, RecentChangeRow[]>();
|
||||
const groupedByDate = new Map<String, RecentChangeRow[]>();
|
||||
|
||||
for (const row of rows) {
|
||||
const dateDay = row.date.substr(0, 10);
|
||||
|
||||
let dateDayArray = groupedByDate.get(dateDay);
|
||||
if (!dateDayArray) {
|
||||
dateDayArray = [];
|
||||
groupedByDate.set(dateDay, dateDayArray);
|
||||
if (!groupedByDate.has(dateDay)) {
|
||||
groupedByDate.set(dateDay, []);
|
||||
}
|
||||
|
||||
dateDayArray.push(row);
|
||||
groupedByDate.get(dateDay)!.push(row);
|
||||
}
|
||||
|
||||
return groupedByDate;
|
||||
|
||||
@@ -8,6 +8,7 @@ import server from "../../services/server";
|
||||
import toast from "../../services/toast";
|
||||
import Button from "../react/Button";
|
||||
import Modal from "../react/Modal";
|
||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
||||
import FormList, { FormListItem } from "../react/FormList";
|
||||
import utils from "../../services/utils";
|
||||
import { Dispatch, StateUpdater, useEffect, useRef, useState } from "preact/hooks";
|
||||
@@ -17,9 +18,9 @@ import type { CSSProperties } from "preact/compat";
|
||||
import open from "../../services/open";
|
||||
import ActionButton from "../react/ActionButton";
|
||||
import options from "../../services/options";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import useTriliumEvent from "../react/hooks";
|
||||
|
||||
export default function RevisionsDialog() {
|
||||
function RevisionsDialogComponent() {
|
||||
const [ note, setNote ] = useState<FNote>();
|
||||
const [ revisions, setRevisions ] = useState<RevisionItem[]>();
|
||||
const [ currentRevision, setCurrentRevision ] = useState<RevisionItem>();
|
||||
@@ -71,8 +72,6 @@ export default function RevisionsDialog() {
|
||||
onHidden={() => {
|
||||
setShown(false);
|
||||
setNote(undefined);
|
||||
setCurrentRevision(undefined);
|
||||
setRevisions(undefined);
|
||||
}}
|
||||
show={shown}
|
||||
>
|
||||
@@ -203,9 +202,17 @@ function RevisionContent({ revisionItem, fullRevision }: { revisionItem?: Revisi
|
||||
return <></>;
|
||||
}
|
||||
|
||||
|
||||
switch (revisionItem.type) {
|
||||
case "text":
|
||||
return <RevisionContentText content={content} />
|
||||
case "text": {
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
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":
|
||||
return <pre style={CODE_STYLE}>{content}</pre>;
|
||||
case "image":
|
||||
@@ -257,16 +264,6 @@ 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 }) {
|
||||
if (!note) {
|
||||
return <></>;
|
||||
@@ -294,6 +291,14 @@ function RevisionFooter({ note }: { note?: FNote }) {
|
||||
</>;
|
||||
}
|
||||
|
||||
export default class RevisionsDialog extends ReactBasicWidget {
|
||||
|
||||
get component() {
|
||||
return <RevisionsDialogComponent />
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function getNote(noteId?: string | null) {
|
||||
if (noteId) {
|
||||
return await froca.getNote(noteId);
|
||||
|
||||
@@ -5,11 +5,12 @@ import FormCheckbox from "../react/FormCheckbox";
|
||||
import FormRadioGroup from "../react/FormRadioGroup";
|
||||
import FormTextBox from "../react/FormTextBox";
|
||||
import Modal from "../react/Modal";
|
||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
||||
import server from "../../services/server";
|
||||
import FormGroup from "../react/FormGroup";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import useTriliumEvent from "../react/hooks";
|
||||
|
||||
export default function SortChildNotesDialog() {
|
||||
function SortChildNotesDialogComponent() {
|
||||
const [ parentNoteId, setParentNoteId ] = useState<string>();
|
||||
const [ sortBy, setSortBy ] = useState("title");
|
||||
const [ sortDirection, setSortDirection ] = useState("asc");
|
||||
@@ -88,3 +89,11 @@ export default function SortChildNotesDialog() {
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default class SortChildNotesDialog extends ReactBasicWidget {
|
||||
|
||||
get component() {
|
||||
return <SortChildNotesDialogComponent />;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,12 +5,13 @@ import FormCheckbox from "../react/FormCheckbox";
|
||||
import FormFileUpload from "../react/FormFileUpload";
|
||||
import FormGroup from "../react/FormGroup";
|
||||
import Modal from "../react/Modal";
|
||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
||||
import options from "../../services/options";
|
||||
import importService from "../../services/import.js";
|
||||
import tree from "../../services/tree";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import useTriliumEvent from "../react/hooks";
|
||||
|
||||
export default function UploadAttachmentsDialog() {
|
||||
function UploadAttachmentsDialogComponent() {
|
||||
const [ parentNoteId, setParentNoteId ] = useState<string>();
|
||||
const [ files, setFiles ] = useState<FileList | null>(null);
|
||||
const [ shrinkImages, setShrinkImages ] = useState(options.is("compressImages"));
|
||||
@@ -23,12 +24,12 @@ export default function UploadAttachmentsDialog() {
|
||||
setShown(true);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!parentNoteId) return;
|
||||
|
||||
tree.getNoteTitle(parentNoteId).then((noteTitle) =>
|
||||
setDescription(t("upload_attachments.files_will_be_uploaded", { noteTitle })));
|
||||
}, [parentNoteId]);
|
||||
if (parentNoteId) {
|
||||
useEffect(() => {
|
||||
tree.getNoteTitle(parentNoteId).then((noteTitle) =>
|
||||
setDescription(t("upload_attachments.files_will_be_uploaded", { noteTitle })));
|
||||
}, [parentNoteId]);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -63,3 +64,11 @@ export default function UploadAttachmentsDialog() {
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default class UploadAttachmentsDialog extends ReactBasicWidget {
|
||||
|
||||
get component() {
|
||||
return <UploadAttachmentsDialogComponent />;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
120
apps/client/src/widgets/editability_select.ts
Normal file
120
apps/client/src/widgets/editability_select.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
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/
|
||||
|
||||
export interface Category {
|
||||
interface Category {
|
||||
name: string;
|
||||
id: number;
|
||||
}
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
.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);
|
||||
}
|
||||
229
apps/client/src/widgets/note_icon.ts
Normal file
229
apps/client/src/widgets/note_icon.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
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));
|
||||
}
|
||||
166
apps/client/src/widgets/note_language.ts
Normal file
166
apps/client/src/widgets/note_language.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
.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%;
|
||||
}
|
||||
147
apps/client/src/widgets/note_title.ts
Normal file
147
apps/client/src/widgets/note_title.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -152,7 +152,7 @@ const TPL = /*html*/`
|
||||
const MAX_SEARCH_RESULTS_IN_TREE = 100;
|
||||
|
||||
// this has to be hanged on the actual elements to effectively intercept and stop click event
|
||||
const cancelClickPropagation: (e: JQuery.ClickEvent) => void = (e) => e.stopPropagation();
|
||||
const cancelClickPropagation: JQuery.TypeEventHandler<unknown, unknown, unknown, unknown, any> = (e) => e.stopPropagation();
|
||||
|
||||
// TODO: Fix once we remove Node.js API from public
|
||||
type Timeout = NodeJS.Timeout | string | number | undefined;
|
||||
|
||||
175
apps/client/src/widgets/note_type.ts
Normal file
175
apps/client/src/widgets/note_type.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
39
apps/client/src/widgets/protected_note_switch.ts
Normal file
39
apps/client/src/widgets/protected_note_switch.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
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,19 +1,13 @@
|
||||
import { CommandNames } from "../../components/app_context";
|
||||
|
||||
interface ActionButtonProps {
|
||||
text: string;
|
||||
titlePosition?: "bottom"; // TODO: Use it
|
||||
icon: string;
|
||||
className?: string;
|
||||
onClick?: (e: MouseEvent) => void;
|
||||
triggerCommand?: CommandNames;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export default function ActionButton({ text, icon, className, onClick, triggerCommand }: ActionButtonProps) {
|
||||
export default function ActionButton({ text, icon, onClick }: ActionButtonProps) {
|
||||
return <button
|
||||
class={`icon-action ${icon} ${className ?? ""}`}
|
||||
class={`icon-action ${icon}`}
|
||||
title={text}
|
||||
onClick={onClick}
|
||||
data-trigger-command={triggerCommand}
|
||||
onClick={onClick}
|
||||
/>;
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
import type { RefObject } from "preact";
|
||||
import type { CSSProperties } from "preact/compat";
|
||||
import { useMemo } from "preact/hooks";
|
||||
import { useRef, useMemo } from "preact/hooks";
|
||||
import { memo } from "preact/compat";
|
||||
import { CommandNames } from "../../components/app_context";
|
||||
|
||||
export interface ButtonProps {
|
||||
interface ButtonProps {
|
||||
name?: string;
|
||||
/** Reference to the button element. Mostly useful for requesting focus. */
|
||||
buttonRef?: RefObject<HTMLButtonElement>;
|
||||
@@ -18,11 +17,9 @@ export interface ButtonProps {
|
||||
disabled?: boolean;
|
||||
size?: "normal" | "small" | "micro";
|
||||
style?: CSSProperties;
|
||||
triggerCommand?: CommandNames;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const Button = memo(({ name, buttonRef, className, text, onClick, keyboardShortcut, icon, primary, disabled, size, style, triggerCommand, ...restProps }: ButtonProps) => {
|
||||
const Button = memo(({ name, buttonRef: _buttonRef, className, text, onClick, keyboardShortcut, icon, primary, disabled, size, style }: ButtonProps) => {
|
||||
// Memoize classes array to prevent recreation
|
||||
const classes = useMemo(() => {
|
||||
const classList: string[] = ["btn"];
|
||||
@@ -42,6 +39,8 @@ const Button = memo(({ name, buttonRef, className, text, onClick, keyboardShortc
|
||||
return classList.join(" ");
|
||||
}, [primary, className, size]);
|
||||
|
||||
const buttonRef = _buttonRef ?? useRef<HTMLButtonElement>(null);
|
||||
|
||||
// Memoize keyboard shortcut rendering
|
||||
const shortcutElements = useMemo(() => {
|
||||
if (!keyboardShortcut) return null;
|
||||
@@ -58,13 +57,11 @@ const Button = memo(({ name, buttonRef, className, text, onClick, keyboardShortc
|
||||
<button
|
||||
name={name}
|
||||
className={classes}
|
||||
type={onClick || triggerCommand ? "button" : "submit"}
|
||||
type={onClick ? "button" : "submit"}
|
||||
onClick={onClick}
|
||||
ref={buttonRef}
|
||||
disabled={disabled}
|
||||
style={style}
|
||||
data-trigger-command={triggerCommand}
|
||||
{...restProps}
|
||||
>
|
||||
{icon && <span className={`bx ${icon}`}></span>}
|
||||
{text} {shortcutElements}
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
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,31 +1,17 @@
|
||||
import { Dropdown as BootstrapDropdown } from "bootstrap";
|
||||
import { ComponentChildren } from "preact";
|
||||
import { CSSProperties } from "preact/compat";
|
||||
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
|
||||
import { useUniqueName } from "./hooks";
|
||||
import { useEffect, useRef } from "preact/hooks";
|
||||
|
||||
export interface DropdownProps {
|
||||
interface DropdownProps {
|
||||
className?: string;
|
||||
buttonClassName?: string;
|
||||
isStatic?: boolean;
|
||||
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, buttonClassName, isStatic, children, title, text, dropdownContainerStyle, dropdownContainerClassName, hideToggleArrow, iconAction, disabled, noSelectButtonStyle }: DropdownProps) {
|
||||
export default function Dropdown({ className, isStatic, children }: DropdownProps) {
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
const triggerRef = useRef<HTMLButtonElement | null>(null);
|
||||
|
||||
const [ shown, setShown ] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!triggerRef.current) return;
|
||||
|
||||
@@ -33,54 +19,33 @@ export default function Dropdown({ className, buttonClassName, isStatic, childre
|
||||
return () => dropdown.dispose();
|
||||
}, []); // Add dependency array
|
||||
|
||||
const onShown = useCallback(() => {
|
||||
setShown(true);
|
||||
}, [])
|
||||
|
||||
const onHidden = useCallback(() => {
|
||||
setShown(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dropdownRef.current) return;
|
||||
|
||||
const handleHide = () => {
|
||||
// Remove console.log from production code
|
||||
};
|
||||
|
||||
const $dropdown = $(dropdownRef.current);
|
||||
$dropdown.on("show.bs.dropdown", onShown);
|
||||
$dropdown.on("hide.bs.dropdown", onHidden);
|
||||
$dropdown.on("hide.bs.dropdown", handleHide);
|
||||
|
||||
// Add proper cleanup
|
||||
return () => {
|
||||
$dropdown.off("show.bs.dropdown", onShown);
|
||||
$dropdown.off("hide.bs.dropdown", onHidden);
|
||||
$dropdown.off("hide.bs.dropdown", handleHide);
|
||||
};
|
||||
}, []); // Add dependency array
|
||||
|
||||
const ariaId = useUniqueName("button");
|
||||
|
||||
return (
|
||||
<div ref={dropdownRef} class={`dropdown ${className ?? ""}`} style={{ display: "flex" }}>
|
||||
<div ref={dropdownRef} class="dropdown" style={{ display: "flex" }}>
|
||||
<button
|
||||
className={`${iconAction ? "icon-action" : "btn"} ${!noSelectButtonStyle ? "select-button" : ""} ${buttonClassName ?? ""} ${!hideToggleArrow ? "dropdown-toggle" : ""}`}
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
style={{ display: "none" }}
|
||||
data-bs-toggle="dropdown"
|
||||
data-bs-display={ isStatic ? "static" : undefined }
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
title={title}
|
||||
id={ariaId}
|
||||
disabled={disabled}
|
||||
>
|
||||
{text}
|
||||
<span className="caret"></span>
|
||||
</button>
|
||||
data-bs-display={ isStatic ? "static" : undefined } />
|
||||
|
||||
<div
|
||||
class={`dropdown-menu ${isStatic ? "static" : ""} ${dropdownContainerClassName ?? ""} tn-dropdown-list`}
|
||||
style={dropdownContainerStyle}
|
||||
aria-labelledby={ariaId}
|
||||
>
|
||||
{shown && children}
|
||||
<div class={`dropdown-menu ${className ?? ""} ${isStatic ? "static" : undefined}`}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ import { CSSProperties, memo } from "preact/compat";
|
||||
import { useUniqueName } from "./hooks";
|
||||
|
||||
interface FormCheckboxProps {
|
||||
id?: string;
|
||||
name?: string;
|
||||
label: string | ComponentChildren;
|
||||
/**
|
||||
@@ -18,9 +19,9 @@ interface FormCheckboxProps {
|
||||
containerStyle?: CSSProperties;
|
||||
}
|
||||
|
||||
const FormCheckbox = memo(({ name, disabled, label, currentValue, onChange, hint, containerStyle }: FormCheckboxProps) => {
|
||||
const FormCheckbox = memo(({ name, id: _id, disabled, label, currentValue, onChange, hint, containerStyle }: FormCheckboxProps) => {
|
||||
const id = _id ?? useUniqueName(name);
|
||||
const labelRef = useRef<HTMLLabelElement>(null);
|
||||
const id = useUniqueName(name);
|
||||
|
||||
// Fix: Move useEffect outside conditional
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
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,48 +1,13 @@
|
||||
import { Ref } from "preact";
|
||||
import Button, { ButtonProps } from "./Button";
|
||||
import { useRef } from "preact/hooks";
|
||||
|
||||
interface FormFileUploadProps {
|
||||
name?: string;
|
||||
onChange: (files: FileList | null) => void;
|
||||
multiple?: boolean;
|
||||
hidden?: boolean;
|
||||
inputRef?: Ref<HTMLInputElement>;
|
||||
}
|
||||
|
||||
export default function FormFileUpload({ inputRef, name, onChange, multiple, hidden }: FormFileUploadProps) {
|
||||
export default function FormFileUpload({ onChange, multiple }: FormFileUploadProps) {
|
||||
return (
|
||||
<label class="tn-file-input tn-input-field" style={hidden ? { display: "none" } : undefined}>
|
||||
<input
|
||||
ref={inputRef}
|
||||
name={name}
|
||||
type="file"
|
||||
class="form-control-file"
|
||||
multiple={multiple}
|
||||
<label class="tn-file-input tn-input-field">
|
||||
<input type="file" class="form-control-file" multiple={multiple}
|
||||
onChange={e => onChange((e.target as HTMLInputElement).files)} />
|
||||
</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,7 +8,6 @@ interface FormGroupProps {
|
||||
label?: string;
|
||||
title?: string;
|
||||
className?: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
children: VNode<any>;
|
||||
description?: string | ComponentChildren;
|
||||
disabled?: boolean;
|
||||
@@ -26,7 +25,7 @@ export default function FormGroup({ name, label, title, className, children, des
|
||||
|
||||
{childWithId}
|
||||
|
||||
{description && <div><small className="form-text">{description}</small></div>}
|
||||
{description && <small className="form-text">{description}</small>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
.dropdown-item .description {
|
||||
font-size: small;
|
||||
color: var(--muted-text-color);
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.dropdown-item span.bx {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -2,8 +2,6 @@ import { Dropdown as BootstrapDropdown } from "bootstrap";
|
||||
import { ComponentChildren } from "preact";
|
||||
import Icon from "./Icon";
|
||||
import { useEffect, useMemo, useRef, type CSSProperties } from "preact/compat";
|
||||
import "./FormList.css";
|
||||
import { CommandNames } from "../../components/app_context";
|
||||
|
||||
interface FormListOpts {
|
||||
children: ComponentChildren;
|
||||
@@ -35,7 +33,6 @@ export default function FormList({ children, onSelect, style, fullHeight }: Form
|
||||
const style: CSSProperties = {};
|
||||
if (fullHeight) {
|
||||
style.height = "100%";
|
||||
style.overflow = "auto";
|
||||
}
|
||||
return style;
|
||||
}, [ fullHeight ]);
|
||||
@@ -54,8 +51,7 @@ export default function FormList({ children, onSelect, style, fullHeight }: Form
|
||||
...builtinStyles,
|
||||
position: "relative",
|
||||
}} onClick={(e) => {
|
||||
const dropdownItem = (e.target as HTMLElement).closest(".dropdown-item") as HTMLElement | null;
|
||||
const value = dropdownItem?.dataset?.value;
|
||||
const value = (e.target as HTMLElement)?.dataset?.value;
|
||||
if (value && onSelect) {
|
||||
onSelect(value);
|
||||
}
|
||||
@@ -67,50 +63,23 @@ export default function FormList({ children, onSelect, style, fullHeight }: Form
|
||||
);
|
||||
}
|
||||
|
||||
export interface FormListBadge {
|
||||
className?: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface FormListItemOpts {
|
||||
children: ComponentChildren;
|
||||
icon?: string;
|
||||
value?: string;
|
||||
title?: string;
|
||||
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, badges, disabled, checked, onClick, description, selected, rtl, triggerCommand }: FormListItemOpts) {
|
||||
if (checked) {
|
||||
icon = "bx bx-check";
|
||||
}
|
||||
|
||||
export function FormListItem({ children, icon, value, title, active }: FormListItemOpts) {
|
||||
return (
|
||||
<a
|
||||
class={`dropdown-item ${active ? "active" : ""} ${disabled ? "disabled" : ""} ${selected ? "selected" : ""}`}
|
||||
class={`dropdown-item ${active ? "active" : ""}`}
|
||||
data-value={value} title={title}
|
||||
tabIndex={0}
|
||||
onClick={onClick}
|
||||
data-trigger-command={triggerCommand}
|
||||
dir={rtl ? "rtl" : undefined}
|
||||
>
|
||||
<Icon icon={icon} />
|
||||
<div>
|
||||
{children}
|
||||
{badges && badges.map(({ className, text }) => (
|
||||
<span className={`badge ${className ?? ""}`}>{text}</span>
|
||||
))}
|
||||
{description && <div className="description">{description}</div>}
|
||||
</div>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -126,7 +95,3 @@ export function FormListHeader({ text }: FormListHeaderOpts) {
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
export function FormDropdownDivider() {
|
||||
return <div className="dropdown-divider" />;
|
||||
}
|
||||
@@ -20,18 +20,16 @@ interface ValueConfig<T, Q> {
|
||||
|
||||
interface FormSelectProps<T, Q> extends ValueConfig<T, Q> {
|
||||
id?: string;
|
||||
name?: string;
|
||||
onChange: OnChangeListener;
|
||||
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.
|
||||
*/
|
||||
export default function FormSelect<T>({ name, id, onChange, style, className, ...restProps }: FormSelectProps<T, T>) {
|
||||
export default function FormSelect<T>({ id, onChange, style, ...restProps }: FormSelectProps<T, T>) {
|
||||
return (
|
||||
<FormSelectBody name={name} id={id} onChange={onChange} style={style} className={className}>
|
||||
<FormSelectBody id={id} onChange={onChange} style={style}>
|
||||
<FormSelectGroup {...restProps} />
|
||||
</FormSelectBody>
|
||||
);
|
||||
@@ -40,35 +38,27 @@ export default function FormSelect<T>({ name, id, onChange, style, className, ..
|
||||
/**
|
||||
* Similar to {@link FormSelect}, but the top-level elements are actually groups.
|
||||
*/
|
||||
export function FormSelectWithGroups<T>({ name, id, values, keyProperty, titleProperty, currentValue, onChange, ...restProps }: FormSelectProps<T, FormSelectGroup<T> | T>) {
|
||||
export function FormSelectWithGroups<T>({ id, values, keyProperty, titleProperty, currentValue, onChange }: FormSelectProps<T, FormSelectGroup<T>>) {
|
||||
return (
|
||||
<FormSelectBody name={name} id={id} onChange={onChange} {...restProps}>
|
||||
{values.map((item) => {
|
||||
if (!item) return <></>;
|
||||
if (typeof item === "object" && "items" in item) {
|
||||
return (
|
||||
<optgroup label={item.title}>
|
||||
<FormSelectGroup values={item.items} keyProperty={keyProperty} titleProperty={titleProperty} currentValue={currentValue} />
|
||||
</optgroup>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<FormSelectGroup values={[ item ]} keyProperty={keyProperty} titleProperty={titleProperty} currentValue={currentValue} />
|
||||
)
|
||||
}
|
||||
<FormSelectBody id={id} onChange={onChange}>
|
||||
{values.map(({ title, items }) => {
|
||||
return (
|
||||
<optgroup label={title}>
|
||||
<FormSelectGroup values={items} keyProperty={keyProperty} titleProperty={titleProperty} currentValue={currentValue} />
|
||||
</optgroup>
|
||||
);
|
||||
})}
|
||||
</FormSelectBody>
|
||||
)
|
||||
}
|
||||
|
||||
function FormSelectBody({ id, name, children, onChange, style, className }: { id?: string, name?: string, children: ComponentChildren, onChange: OnChangeListener, style?: CSSProperties, className?: string }) {
|
||||
function FormSelectBody({ id, children, onChange, style }: { id?: string, children: ComponentChildren, onChange: OnChangeListener, style?: CSSProperties }) {
|
||||
return (
|
||||
<select
|
||||
id={id}
|
||||
name={name}
|
||||
class="form-select"
|
||||
onChange={e => onChange((e.target as HTMLInputElement).value)}
|
||||
style={style}
|
||||
className={`form-select ${className ?? ""}`}
|
||||
>
|
||||
{children}
|
||||
</select>
|
||||
@@ -79,10 +69,10 @@ function FormSelectGroup<T>({ values, keyProperty, titleProperty, currentValue }
|
||||
return values.map(item => {
|
||||
return (
|
||||
<option
|
||||
value={item[keyProperty] as string | number}
|
||||
value={item[keyProperty] as any}
|
||||
selected={item[keyProperty] === currentValue}
|
||||
>
|
||||
{item[titleProperty ?? keyProperty] ?? item[keyProperty] as string | number}
|
||||
{item[titleProperty ?? keyProperty] ?? item[keyProperty] as any}
|
||||
</option>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,26 +1,18 @@
|
||||
import { RefObject, TextareaHTMLAttributes } from "preact/compat";
|
||||
|
||||
interface FormTextAreaProps extends Omit<TextareaHTMLAttributes, "onBlur" | "onChange"> {
|
||||
interface FormTextAreaProps {
|
||||
id?: string;
|
||||
currentValue: string;
|
||||
onChange?(newValue: string): void;
|
||||
onBlur?(newValue: string): void;
|
||||
inputRef?: RefObject<HTMLTextAreaElement>
|
||||
rows: number;
|
||||
}
|
||||
export default function FormTextArea({ inputRef, id, onBlur, onChange, currentValue, className, ...restProps }: FormTextAreaProps) {
|
||||
export default function FormTextArea({ id, onBlur, rows, currentValue }: FormTextAreaProps) {
|
||||
return (
|
||||
<textarea
|
||||
ref={inputRef}
|
||||
id={id}
|
||||
className={`form-control ${className ?? ""}`}
|
||||
onChange={(e) => {
|
||||
onChange?.(e.currentTarget.value);
|
||||
}}
|
||||
rows={rows}
|
||||
onBlur={(e) => {
|
||||
onBlur?.(e.currentTarget.value);
|
||||
}}
|
||||
style={{ width: "100%" }}
|
||||
{...restProps}
|
||||
>{currentValue}</textarea>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, type InputHTMLAttributes, type RefObject } from "preact/compat";
|
||||
import type { InputHTMLAttributes, RefObject } from "preact/compat";
|
||||
|
||||
interface FormTextBoxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "onChange" | "onBlur" | "value"> {
|
||||
id?: string;
|
||||
@@ -8,7 +8,7 @@ interface FormTextBoxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "
|
||||
inputRef?: RefObject<HTMLInputElement>;
|
||||
}
|
||||
|
||||
export default function FormTextBox({ inputRef, className, type, currentValue, onChange, onBlur, autoFocus, ...rest}: FormTextBoxProps) {
|
||||
export default function FormTextBox({ inputRef, className, type, currentValue, onChange, onBlur,...rest}: FormTextBoxProps) {
|
||||
if (type === "number" && currentValue) {
|
||||
const { min, max } = rest;
|
||||
const currentValueNum = parseInt(currentValue, 10);
|
||||
@@ -19,12 +19,6 @@ export default function FormTextBox({ inputRef, className, type, currentValue, o
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (autoFocus) {
|
||||
inputRef?.current?.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<input
|
||||
ref={inputRef}
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
.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);
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
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,7 +1,6 @@
|
||||
import { ActionKeyboardShortcut, KeyboardActionNames } from "@triliumnext/commons";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import keyboard_actions from "../../services/keyboard_actions";
|
||||
import { joinElements } from "./react_utils";
|
||||
|
||||
interface KeyboardShortcutProps {
|
||||
actionName: KeyboardActionNames;
|
||||
@@ -20,15 +19,15 @@ export default function KeyboardShortcut({ actionName }: KeyboardShortcutProps)
|
||||
|
||||
return (
|
||||
<>
|
||||
{action.effectiveShortcuts?.map((shortcut) => {
|
||||
{action.effectiveShortcuts?.map((shortcut, i) => {
|
||||
const keys = shortcut.split("+");
|
||||
return joinElements(keys
|
||||
return keys
|
||||
.map((key, i) => (
|
||||
<>
|
||||
<kbd>{key}</kbd> {i + 1 < keys.length && "+ "}
|
||||
</>
|
||||
)))
|
||||
})}
|
||||
))
|
||||
}).reduce<any>((acc, item) => (acc.length ? [...acc, ", ", item] : [item]), [])}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export default function LoadingSpinner() {
|
||||
return <span className="bx bx-loader bx-spin" />
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useEffect, useRef, useMemo } from "preact/hooks";
|
||||
import { useContext, useEffect, useRef, useMemo, useCallback } from "preact/hooks";
|
||||
import { t } from "../../services/i18n";
|
||||
import { ComponentChildren } from "preact";
|
||||
import type { CSSProperties, RefObject } from "preact/compat";
|
||||
import { openDialog } from "../../services/dialog";
|
||||
import { ParentComponent } from "./ReactBasicWidget";
|
||||
import { Modal as BootstrapModal } from "bootstrap";
|
||||
import { memo } from "preact/compat";
|
||||
import { useSyncedRef } from "./hooks";
|
||||
|
||||
interface ModalProps {
|
||||
className: string;
|
||||
@@ -64,43 +64,50 @@ interface ModalProps {
|
||||
stackable?: boolean;
|
||||
}
|
||||
|
||||
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 = useSyncedRef<HTMLDivElement>(externalModalRef);
|
||||
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) {
|
||||
const modalRef = _modalRef ?? useRef<HTMLDivElement>(null);
|
||||
const modalInstanceRef = useRef<BootstrapModal>();
|
||||
const formRef = _formRef ?? useRef<HTMLFormElement>(null);
|
||||
const parentWidget = useContext(ParentComponent);
|
||||
const elementToFocus = useRef<Element | null>();
|
||||
|
||||
useEffect(() => {
|
||||
const modalElement = modalRef.current;
|
||||
if (!modalElement) {
|
||||
return;
|
||||
}
|
||||
if (onShown) {
|
||||
modalElement.addEventListener("shown.bs.modal", onShown);
|
||||
}
|
||||
modalElement.addEventListener("hidden.bs.modal", () => {
|
||||
onHidden();
|
||||
if (elementToFocus.current && "focus" in elementToFocus.current) {
|
||||
(elementToFocus.current as HTMLElement).focus();
|
||||
if (onShown || onHidden) {
|
||||
useEffect(() => {
|
||||
const modalElement = modalRef.current;
|
||||
if (!modalElement) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
if (onShown) {
|
||||
modalElement.removeEventListener("shown.bs.modal", onShown);
|
||||
modalElement.addEventListener("shown.bs.modal", onShown);
|
||||
}
|
||||
modalElement.removeEventListener("hidden.bs.modal", onHidden);
|
||||
};
|
||||
}, [ onShown, onHidden ]);
|
||||
modalElement.addEventListener("hidden.bs.modal", () => {
|
||||
onHidden();
|
||||
if (elementToFocus.current && "focus" in elementToFocus.current) {
|
||||
(elementToFocus.current as HTMLElement).focus();
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
if (onShown) {
|
||||
modalElement.removeEventListener("shown.bs.modal", onShown);
|
||||
}
|
||||
modalElement.removeEventListener("hidden.bs.modal", onHidden);
|
||||
};
|
||||
}, [ ]);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (show && modalRef.current) {
|
||||
if (!parentWidget) {
|
||||
return;
|
||||
}
|
||||
if (show) {
|
||||
elementToFocus.current = document.activeElement;
|
||||
openDialog($(modalRef.current), !stackable).then(($widget) => {
|
||||
openDialog(parentWidget.$widget, !stackable).then(($widget) => {
|
||||
modalInstanceRef.current = BootstrapModal.getOrCreateInstance($widget[0]);
|
||||
})
|
||||
} else {
|
||||
modalInstanceRef.current?.hide();
|
||||
}
|
||||
}, [ show, modalRef.current ]);
|
||||
}, [ show ]);
|
||||
|
||||
// Memoize styles to prevent recreation on every render
|
||||
const dialogStyle = useMemo<CSSProperties>(() => {
|
||||
@@ -140,10 +147,10 @@ export default function Modal({ children, className, size, title, header, footer
|
||||
</div>
|
||||
|
||||
{onSubmit ? (
|
||||
<form ref={formRef} onSubmit={(e) => {
|
||||
<form ref={formRef} onSubmit={useCallback((e) => {
|
||||
e.preventDefault();
|
||||
onSubmit();
|
||||
}}>
|
||||
}, [onSubmit])}>
|
||||
<ModalInner footer={footer} bodyStyle={bodyStyle} footerStyle={footerStyle} footerAlignment={footerAlignment}>{children}</ModalInner>
|
||||
</form>
|
||||
) : (
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user