diff --git a/.editorconfig b/.editorconfig index c965ea8c0..cd301498e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,6 +1,6 @@ root = true -[*.{js,ts}] +[*.{js,ts,.tsx}] charset = utf-8 end_of_line = lf indent_size = 4 diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 90751b906..5084d2011 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -12,6 +12,7 @@ jobs: steps: - name: Check if PRs have conflicts uses: eps1lon/actions-label-merge-conflict@v3 + if: github.repository == ${{ vars.REPO_MAIN }} with: dirtyLabel: "merge-conflicts" repoToken: "${{ secrets.MERGE_CONFLICT_LABEL_PAT }}" diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 03a9b5262..f20c5a2d4 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -27,7 +27,7 @@ permissions: jobs: nightly-electron: - if: github.repository == 'TriliumNext/Trilium' + if: github.repository == ${{ vars.REPO_MAIN }} name: Deploy nightly strategy: fail-fast: false @@ -98,7 +98,7 @@ jobs: path: apps/desktop/upload nightly-server: - if: github.repository == 'TriliumNext/Trilium' + if: github.repository == ${{ vars.REPO_MAIN }} name: Deploy server nightly strategy: fail-fast: false diff --git a/.vscode/snippets.code-snippets b/.vscode/snippets.code-snippets index 77b251a4a..c7255af76 100644 --- a/.vscode/snippets.code-snippets +++ b/.vscode/snippets.code-snippets @@ -20,5 +20,10 @@ "scope": "typescript", "prefix": "jqf", "body": ["private $${1:name}!: JQuery;"] + }, + "region": { + "scope": "css", + "prefix": "region", + "body": ["/* #region ${1:name} */\n$0\n/* #endregion */"] } } diff --git a/_regroup/package.json b/_regroup/package.json index bd7bc8105..7d20e9fc4 100644 --- a/_regroup/package.json +++ b/_regroup/package.json @@ -35,13 +35,13 @@ "chore:generate-openapi": "tsx bin/generate-openapi.js" }, "devDependencies": { - "@playwright/test": "1.54.2", + "@playwright/test": "1.55.0", "@stylistic/eslint-plugin": "5.2.3", "@types/express": "5.0.3", - "@types/node": "22.17.2", + "@types/node": "22.18.0", "@types/yargs": "17.0.33", "@vitest/coverage-v8": "3.2.4", - "eslint": "9.33.0", + "eslint": "9.34.0", "eslint-plugin-simple-import-sort": "12.1.1", "esm": "3.2.25", "jsdoc": "4.0.4", @@ -49,7 +49,7 @@ "rcedit": "4.0.1", "rimraf": "6.0.1", "tslib": "2.8.1", - "typedoc": "0.28.10", + "typedoc": "0.28.11", "typedoc-plugin-missing-exports": "4.1.0" }, "optionalDependencies": { diff --git a/apps/client/package.json b/apps/client/package.json index ed6e72742..27811a06e 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -1,6 +1,6 @@ { "name": "@triliumnext/client", - "version": "0.98.0", + "version": "0.98.1", "description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)", "private": true, "license": "AGPL-3.0-only", @@ -10,7 +10,7 @@ "url": "https://github.com/TriliumNext/Notes" }, "dependencies": { - "@eslint/js": "9.33.0", + "@eslint/js": "9.34.0", "@excalidraw/excalidraw": "0.18.0", "@fullcalendar/core": "6.1.19", "@fullcalendar/daygrid": "6.1.19", @@ -19,7 +19,7 @@ "@fullcalendar/multimonth": "6.1.19", "@fullcalendar/timegrid": "6.1.19", "@maplibre/maplibre-gl-leaflet": "0.1.3", - "@mermaid-js/layout-elk": "0.1.8", + "@mermaid-js/layout-elk": "0.1.9", "@mind-elixir/node-menu": "5.0.0", "@popperjs/core": "2.11.8", "@triliumnext/ckeditor5": "workspace:*", @@ -28,15 +28,15 @@ "@triliumnext/highlightjs": "workspace:*", "@triliumnext/share-theme": "workspace:*", "autocomplete.js": "0.38.1", - "bootstrap": "5.3.7", + "bootstrap": "5.3.8", "boxicons": "2.1.4", - "dayjs": "1.11.13", + "dayjs": "1.11.14", "dayjs-plugin-utc": "0.1.2", "debounce": "2.2.0", "draggabilly": "3.0.0", "force-graph": "1.50.1", "globals": "16.3.0", - "i18next": "25.3.6", + "i18next": "25.4.2", "i18next-http-backend": "3.0.2", "jquery": "3.7.1", "jquery.fancytree": "2.38.5", @@ -46,12 +46,13 @@ "leaflet": "1.9.4", "leaflet-gpx": "2.2.0", "mark.js": "8.11.1", - "marked": "16.1.2", - "mermaid": "11.9.0", + "marked": "16.2.1", + "mermaid": "11.10.1", "mind-elixir": "5.0.6", "normalize.css": "8.0.1", "panzoom": "9.4.3", - "preact": "10.27.0", + "preact": "10.27.1", + "react-i18next": "15.7.2", "split.js": "1.6.5", "svg-pan-zoom": "3.6.2", "tabulator-tables": "6.3.1", @@ -61,7 +62,7 @@ "@ckeditor/ckeditor5-inspector": "5.0.0", "@preact/preset-vite": "2.10.2", "@types/bootstrap": "5.2.10", - "@types/jquery": "3.5.32", + "@types/jquery": "3.5.33", "@types/leaflet": "1.9.20", "@types/leaflet-gpx": "1.3.7", "@types/mark.js": "8.11.12", @@ -69,7 +70,7 @@ "copy-webpack-plugin": "13.0.1", "happy-dom": "18.0.1", "script-loader": "0.7.2", - "vite-plugin-static-copy": "3.1.1" + "vite-plugin-static-copy": "3.1.2" }, "nx": { "name": "client", diff --git a/apps/client/src/components/app_context.ts b/apps/client/src/components/app_context.ts index 4c750a544..d888eba6f 100644 --- a/apps/client/src/components/app_context.ts +++ b/apps/client/src/components/app_context.ts @@ -1,6 +1,6 @@ import froca from "../services/froca.js"; import RootCommandExecutor from "./root_command_executor.js"; -import Entrypoints, { type SqlExecuteResults } from "./entrypoints.js"; +import Entrypoints from "./entrypoints.js"; import options from "../services/options.js"; import utils, { hasTouchBar } from "../services/utils.js"; import zoomComponent from "./zoom.js"; @@ -31,16 +31,14 @@ 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"; +import { SqlExecuteResults } from "@triliumnext/commons"; interface Layout { - getRootWidget: (appContext: AppContext) => RootWidget; + getRootWidget: (appContext: AppContext) => RootContainer; } -interface RootWidget extends Component { - render: () => JQuery; -} - -interface BeforeUploadListener extends Component { +export interface BeforeUploadListener extends Component { beforeUnloadEvent(): boolean; } @@ -85,7 +83,6 @@ export type CommandMappings = { focusTree: CommandData; focusOnTitle: CommandData; focusOnDetail: CommandData; - focusOnSearchDefinition: Required; searchNotes: CommandData & { searchString?: string; ancestorNoteId?: string | null; @@ -93,6 +90,11 @@ export type CommandMappings = { closeTocCommand: CommandData; closeHlt: CommandData; showLaunchBarSubtree: CommandData; + showHiddenSubtree: CommandData; + showSQLConsoleHistory: CommandData; + logout: CommandData; + switchToMobileVersion: CommandData; + switchToDesktopVersion: CommandData; showRevisions: CommandData & { noteId?: string | null; }; @@ -138,6 +140,7 @@ export type CommandMappings = { showLeftPane: CommandData; showAttachments: CommandData; showSearchHistory: CommandData; + showShareSubtree: CommandData; hoistNote: CommandData & { noteId: string }; leaveProtectedSession: CommandData; enterProtectedSession: CommandData; @@ -323,6 +326,7 @@ export type CommandMappings = { printActiveNote: CommandData; exportAsPdf: CommandData; openNoteExternally: CommandData; + openNoteCustom: CommandData; renderActiveNote: CommandData; unhoist: CommandData; reloadFrontendApp: CommandData; @@ -526,7 +530,7 @@ export type FilteredCommandNames = keyof Pick[]; + beforeUnloadListeners: (WeakRef | (() => boolean))[]; tabManager!: TabManager; layout?: Layout; noteTreeWidget?: NoteTreeWidget; @@ -619,7 +623,7 @@ export class AppContext extends Component { component.triggerCommand(commandName, { $el: $(this) }); }); - this.child(rootWidget); + this.child(rootWidget as Component); this.triggerEvent("initialRenderComplete", {}); } @@ -649,13 +653,17 @@ export class AppContext extends Component { return $(el).closest(".component").prop("component"); } - addBeforeUnloadListener(obj: BeforeUploadListener) { + addBeforeUnloadListener(obj: BeforeUploadListener | (() => boolean)) { if (typeof WeakRef !== "function") { // older browsers don't support WeakRef return; } - this.beforeUnloadListeners.push(new WeakRef(obj)); + if (typeof obj === "object") { + this.beforeUnloadListeners.push(new WeakRef(obj)); + } else { + this.beforeUnloadListeners.push(obj); + } } } @@ -665,25 +673,29 @@ const appContext = new AppContext(window.glob.isMainWindow); $(window).on("beforeunload", () => { let allSaved = true; - appContext.beforeUnloadListeners = appContext.beforeUnloadListeners.filter((wr) => !!wr.deref()); + appContext.beforeUnloadListeners = appContext.beforeUnloadListeners.filter((wr) => typeof wr === "function" || !!wr.deref()); - for (const weakRef of appContext.beforeUnloadListeners) { - const component = weakRef.deref(); + for (const listener of appContext.beforeUnloadListeners) { + if (typeof listener === "object") { + const component = listener.deref(); - if (!component) { - continue; - } + if (!component) { + continue; + } - 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 (!component.beforeUnloadEvent()) { + console.log(`Component ${component.componentId} is not finished saving its state.`); + allSaved = false; + } + } else { + if (!listener()) { + allSaved = false; + } } } if (!allSaved) { + toast.showMessage(t("app_context.please_wait_for_save"), 10000); return "some string"; } }); diff --git a/apps/client/src/components/component.ts b/apps/client/src/components/component.ts index 8686a7bb9..9a59b96be 100644 --- a/apps/client/src/components/component.ts +++ b/apps/client/src/components/component.ts @@ -1,6 +1,8 @@ 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. * @@ -19,6 +21,7 @@ export class TypedComponent> { initialized: Promise | null; parent?: TypedComponent; _position!: number; + private listeners: Record | null = {}; constructor() { this.componentId = `${this.sanitizedClassName}-${utils.randomString(8)}`; @@ -76,6 +79,14 @@ export class TypedComponent> { handleEventInChildren(name: T, data: EventData): Promise | null { const promises: Promise[] = []; + // 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; @@ -120,6 +131,35 @@ export class TypedComponent> { return promise; } + + registerHandler(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(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 {} diff --git a/apps/client/src/components/entrypoints.ts b/apps/client/src/components/entrypoints.ts index 2e55a9b9d..7989960a6 100644 --- a/apps/client/src/components/entrypoints.ts +++ b/apps/client/src/components/entrypoints.ts @@ -10,22 +10,7 @@ import bundleService from "../services/bundle.js"; import froca from "../services/froca.js"; import linkService from "../services/link.js"; import { t } from "../services/i18n.js"; -import type FNote from "../entities/fnote.js"; - -// TODO: Move somewhere else nicer. -export type SqlExecuteResults = string[][][]; - -// TODO: Deduplicate with server. -interface SqlExecuteResponse { - success: boolean; - error?: string; - results: SqlExecuteResults; -} - -// TODO: Deduplicate with server. -interface CreateChildrenResponse { - note: FNote; -} +import { CreateChildrenResponse, SqlExecuteResponse } from "@triliumnext/commons"; export default class Entrypoints extends Component { constructor() { @@ -34,7 +19,7 @@ export default class Entrypoints extends Component { openDevToolsCommand() { if (utils.isElectron()) { - utils.dynamicRequire("@electron/remote").getCurrentWindow().toggleDevTools(); + utils.dynamicRequire("@electron/remote").getCurrentWindow().webContents.toggleDevTools(); } } @@ -124,7 +109,7 @@ export default class Entrypoints extends Component { if (utils.isElectron()) { // standard JS version does not work completely correctly in electron const webContents = utils.dynamicRequire("@electron/remote").getCurrentWebContents(); - const activeIndex = parseInt(webContents.navigationHistory.getActiveIndex()); + const activeIndex = webContents.navigationHistory.getActiveIndex(); webContents.goToIndex(activeIndex - 1); } else { @@ -136,7 +121,7 @@ export default class Entrypoints extends Component { if (utils.isElectron()) { // standard JS version does not work completely correctly in electron const webContents = utils.dynamicRequire("@electron/remote").getCurrentWebContents(); - const activeIndex = parseInt(webContents.navigationHistory.getActiveIndex()); + const activeIndex = webContents.navigationHistory.getActiveIndex(); webContents.goToIndex(activeIndex + 1); } else { diff --git a/apps/client/src/components/root_command_executor.ts b/apps/client/src/components/root_command_executor.ts index 8e7df9494..632eb0a88 100644 --- a/apps/client/src/components/root_command_executor.ts +++ b/apps/client/src/components/root_command_executor.ts @@ -43,8 +43,6 @@ 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">) { diff --git a/apps/client/src/desktop.ts b/apps/client/src/desktop.ts index 2791f0577..57ff4084e 100644 --- a/apps/client/src/desktop.ts +++ b/apps/client/src/desktop.ts @@ -8,7 +8,6 @@ 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"; diff --git a/apps/client/src/entities/fnote.ts b/apps/client/src/entities/fnote.ts index 6099cba35..b80e8e3fb 100644 --- a/apps/client/src/entities/fnote.ts +++ b/apps/client/src/entities/fnote.ts @@ -64,7 +64,7 @@ export interface NoteMetaData { /** * Note is the main node and concept in Trilium. */ -class FNote { +export default class FNote { private froca: Froca; noteId!: string; @@ -1020,6 +1020,14 @@ 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. */ @@ -1027,5 +1035,3 @@ class FNote { return await server.get(`notes/${this.noteId}/metadata`); } } - -export default FNote; diff --git a/apps/client/src/layouts/desktop_layout.ts b/apps/client/src/layouts/desktop_layout.tsx similarity index 51% rename from apps/client/src/layouts/desktop_layout.ts rename to apps/client/src/layouts/desktop_layout.tsx index 33c1135c8..530170282 100644 --- a/apps/client/src/layouts/desktop_layout.ts +++ b/apps/client/src/layouts/desktop_layout.tsx @@ -1,78 +1,47 @@ import FlexContainer from "../widgets/containers/flex_container.js"; -import GlobalMenuWidget from "../widgets/buttons/global_menu.js"; 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.js"; -import OwnedAttributeListWidget from "../widgets/ribbon_widgets/owned_attribute_list.js"; -import NoteActionsWidget from "../widgets/buttons/note_actions.js"; +import NoteTitleWidget from "../widgets/note_title.jsx"; import NoteDetailWidget from "../widgets/note_detail.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 PromotedAttributesWidget from "../widgets/promoted_attributes.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 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 NoteIconWidget from "../widgets/note_icon.jsx"; import ScrollingContainer from "../widgets/containers/scrolling_container.js"; import RootContainer from "../widgets/containers/root_container.js"; import WatchedFileUpdateStatusWidget from "../widgets/watched_file_update_status.js"; import SpacerWidget from "../widgets/spacer.js"; import QuickSearchWidget from "../widgets/quick_search.js"; 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"; -import BacklinksWidget from "../widgets/floating_buttons/zpetne_odkazy.js"; -import SharedInfoWidget from "../widgets/shared_info.js"; import FindWidget from "../widgets/find.js"; import TocWidget from "../widgets/toc.js"; import HighlightsListWidget from "../widgets/highlights_list.js"; import PasswordNoteSetDialog from "../widgets/dialogs/password_not_set.js"; -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 ScrollPadding from "../widgets/scroll_padding.js"; import options from "../services/options.js"; import utils from "../services/utils.js"; -import GeoMapButtons from "../widgets/floating_buttons/geo_map_button.js"; -import ContextualHelpButton from "../widgets/floating_buttons/help_button.js"; -import CloseZenButton from "../widgets/close_zen_button.js"; import type { AppContext } from "../components/app_context.js"; import type { WidgetsByParent } from "../services/bundle.js"; -import SwitchSplitOrientationButton from "../widgets/floating_buttons/switch_layout_button.js"; -import ToggleReadOnlyButton from "../widgets/floating_buttons/toggle_read_only_button.js"; -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"; +import FloatingButtons from "../widgets/FloatingButtons.jsx"; +import { DESKTOP_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx"; +import SearchResult from "../widgets/search_result.jsx"; +import GlobalMenu from "../widgets/buttons/global_menu.jsx"; +import SqlResults from "../widgets/sql_result.js"; +import SqlTableSchemas from "../widgets/sql_table_schemas.js"; +import TitleBarButtons from "../widgets/title_bar_buttons.jsx"; +import LeftPaneToggle from "../widgets/buttons/left_pane_toggle.js"; +import ApiLog from "../widgets/api_log.jsx"; +import CloseZenModeButton from "../widgets/close_zen_button.jsx"; +import SharedInfo from "../widgets/shared_info.jsx"; export default class DesktopLayout { @@ -107,9 +76,9 @@ export default class DesktopLayout { new FlexContainer("row") .class("tab-row-container") .child(new FlexContainer("row").id("tab-row-left-spacer")) - .optChild(launcherPaneIsHorizontal, new LeftPaneToggleWidget(true)) + .optChild(launcherPaneIsHorizontal, ) .child(new TabRowWidget().class("full-width")) - .optChild(customTitleBarButtons, new TitleBarButtonsWidget()) + .optChild(customTitleBarButtons, ) .css("height", "40px") .css("background-color", "var(--launcher-pane-background-color)") .setParent(appContext) @@ -130,7 +99,7 @@ export default class DesktopLayout { new FlexContainer("column") .id("rest-pane") .css("flex-grow", "1") - .optChild(!fullWidthTabBar, new FlexContainer("row").child(new TabRowWidget()).optChild(customTitleBarButtons, new TitleBarButtonsWidget()).css("height", "40px")) + .optChild(!fullWidthTabBar, new FlexContainer("row").child(new TabRowWidget()).optChild(customTitleBarButtons, ).css("height", "40px")) .child( new FlexContainer("row") .filling() @@ -151,69 +120,30 @@ export default class DesktopLayout { .css("min-height", "50px") .css("align-items", "center") .cssBlock(".title-row > * { margin: 5px; }") - .child(new NoteIconWidget()) - .child(new NoteTitleWidget()) + .child() + .child() .child(new SpacerWidget(0, 1)) .child(new MovePaneButton(true)) .child(new MovePaneButton(false)) .child(new ClosePaneButton()) .child(new CreatePaneButton()) ) - .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() + .child() .child(new WatchedFileUpdateStatusWidget()) - .child( - new FloatingButtons() - .child(new RefreshButton()) - .child(new SwitchSplitOrientationButton()) - .child(new ToggleReadOnlyButton()) - .child(new EditButton()) - .child(new ShowTocWidgetButton()) - .child(new ShowHighlightsListWidgetButton()) - .child(new CodeButtonsWidget()) - .child(new RelationMapButtons()) - .child(new GeoMapButtons()) - .child(new CopyImageReferenceButton()) - .child(new SvgExportButton()) - .child(new PngExportButton()) - .child(new BacklinksWidget()) - .child(new ContextualHelpButton()) - .child(new HideFloatingButtonsButton()) - ) + .child() .child( new ScrollingContainer() .filling() .child(new PromotedAttributesWidget()) - .child(new SqlTableSchemasWidget()) + .child() .child(new NoteDetailWidget()) .child(new NoteListWidget(false)) - .child(new SearchResultWidget()) - .child(new SqlResultWidget()) - .child(new ScrollPaddingWidget()) + .child() + .child() + .child() ) - .child(new ApiLogWidget()) + .child() .child(new FindWidget()) .child( ...this.customWidgets.get("node-detail-pane"), // typo, let's keep it for a while as BC @@ -232,11 +162,11 @@ export default class DesktopLayout { ) ) ) - .child(new CloseZenButton()) + .child() // Desktop-specific dialogs. - .child(new PasswordNoteSetDialog()) - .child(new UploadAttachmentsDialog()); + .child() + .child(); applyModals(rootContainer); return rootContainer; @@ -246,14 +176,18 @@ export default class DesktopLayout { let launcherPane; if (isHorizontal) { - launcherPane = new FlexContainer("row").css("height", "53px").class("horizontal").child(new LauncherContainer(true)).child(new GlobalMenuWidget(true)); + launcherPane = new FlexContainer("row") + .css("height", "53px") + .class("horizontal") + .child(new LauncherContainer(true)) + .child(); } else { launcherPane = new FlexContainer("column") .css("width", "53px") .class("vertical") - .child(new GlobalMenuWidget(false)) + .child() .child(new LauncherContainer(false)) - .child(new LeftPaneToggleWidget(false)); + .child(); } launcherPane.id("launcher-pane"); diff --git a/apps/client/src/layouts/layout_commons.ts b/apps/client/src/layouts/layout_commons.tsx similarity index 61% rename from apps/client/src/layouts/layout_commons.ts rename to apps/client/src/layouts/layout_commons.tsx index 5ee261317..02171db60 100644 --- a/apps/client/src/layouts/layout_commons.ts +++ b/apps/client/src/layouts/layout_commons.tsx @@ -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.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 NoteIconWidget from "../widgets/note_icon"; +import PromotedAttributesWidget from "../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 CallToActionDialog from "../widgets/dialogs/call_to_action.jsx"; +import NoteTitleWidget from "../widgets/note_title.jsx"; +import { PopupEditorFormattingToolbar } from "../widgets/ribbon/FormattingToolbar.js"; export function applyModals(rootContainer: RootContainer) { rootContainer - .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() + .child() + .child() + .child() + .child() + .child() + .child() + .child() + .child() + .child() + .child() + .child() + .child() + .child() + .child() + .child() + .child() + .child() + .child() + .child() + .child() + .child() .child(new PopupEditorDialog() .child(new FlexContainer("row") .class("title-row") .css("align-items", "center") .cssBlock(".title-row > * { margin: 5px; }") - .child(new NoteIconWidget()) - .child(new NoteTitleWidget())) - .child(new ClassicEditorToolbar()) + .child() + .child()) + .child() .child(new PromotedAttributesWidget()) .child(new NoteDetailWidget()) .child(new NoteListWidget(true))) - .child(new CallToActionDialog()); + .child(); } diff --git a/apps/client/src/layouts/mobile_layout.ts b/apps/client/src/layouts/mobile_layout.tsx similarity index 67% rename from apps/client/src/layouts/mobile_layout.ts rename to apps/client/src/layouts/mobile_layout.tsx index 10b6d2ebe..b7eceffa2 100644 --- a/apps/client/src/layouts/mobile_layout.ts +++ b/apps/client/src/layouts/mobile_layout.tsx @@ -3,30 +3,26 @@ import NoteTitleWidget from "../widgets/note_title.js"; import NoteDetailWidget from "../widgets/note_detail.js"; import QuickSearchWidget from "../widgets/quick_search.js"; import NoteTreeWidget from "../widgets/note_tree.js"; -import ToggleSidebarButtonWidget from "../widgets/mobile_widgets/toggle_sidebar_button.js"; -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"; -import SvgExportButton from "../widgets/floating_buttons/svg_export_button.js"; -import BacklinksWidget from "../widgets/floating_buttons/zpetne_odkazy.js"; -import HideFloatingButtonsButton from "../widgets/floating_buttons/hide_floating_buttons_button.js"; import NoteListWidget from "../widgets/note_list.js"; 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/ribbon_widgets/promoted_attributes.js"; +import PromotedAttributesWidget from "../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/ribbon_widgets/mobile_editor_toolbar.js"; +import MobileEditorToolbar from "../widgets/type_widgets/ckeditor/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 FloatingButtons from "../widgets/FloatingButtons.jsx"; +import { MOBILE_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx"; +import ToggleSidebarButton from "../widgets/mobile_widgets/toggle_sidebar_button.jsx"; +import CloseZenModeButton from "../widgets/close_zen_button.js"; +import MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js"; const MOBILE_CSS = ` - -
- -
-`; - -export default class ApiLogWidget extends NoteContextAwareWidget { - - private $logContainer!: JQuery; - private $closeButton!: JQuery; - - isEnabled() { - return !!this.note && this.note.mime.startsWith("application/javascript;env=") && super.isEnabled(); - } - - doRender() { - this.$widget = $(TPL); - this.toggle(false); - - this.$logContainer = this.$widget.find(".api-log-container"); - this.$closeButton = this.$widget.find(".close-api-log-button"); - this.$closeButton.on("click", () => this.toggle(false)); - } - - async refreshWithNote(note: FNote) { - this.$logContainer.empty(); - } - - apiLogMessagesEvent({ messages, noteId }: EventData<"apiLogMessages">) { - if (!this.isNote(noteId)) { - return; - } - - this.toggle(true); - - for (const message of messages) { - this.$logContainer.append(message).append($("
")); - } - } - - toggle(show: boolean) { - this.$widget.toggleClass("hidden-api-log", !show); - } -} diff --git a/apps/client/src/widgets/api_log.tsx b/apps/client/src/widgets/api_log.tsx new file mode 100644 index 000000000..ba0f8e095 --- /dev/null +++ b/apps/client/src/widgets/api_log.tsx @@ -0,0 +1,41 @@ +import { useEffect, useState } from "preact/hooks"; +import "./api_log.css"; +import { useNoteContext, useTriliumEvent } from "./react/hooks"; +import ActionButton from "./react/ActionButton"; +import { t } from "../services/i18n"; + +/** + * Displays the messages that are logged by the current note via `api.log`, for frontend and backend scripts. + */ +export default function ApiLog() { + const { note, noteId } = useNoteContext(); + const [ messages, setMessages ] = useState(); + + useTriliumEvent("apiLogMessages", ({ messages, noteId: eventNoteId }) => { + if (eventNoteId !== noteId) return; + setMessages(messages); + }); + + // Clear when navigating away. + useEffect(() => setMessages(undefined), [ note ]); + + const isEnabled = note?.mime.startsWith("application/javascript;env=") && messages?.length; + return ( +
+ {isEnabled && ( + <> + setMessages(undefined)} + /> + +
+ {messages.join("\n")} +
+ + )} +
+ ) +} diff --git a/apps/client/src/widgets/attribute_widgets/attribute_editor.ts b/apps/client/src/widgets/attribute_widgets/attribute_editor.ts deleted file mode 100644 index 3e97723a5..000000000 --- a/apps/client/src/widgets/attribute_widgets/attribute_editor.ts +++ /dev/null @@ -1,504 +0,0 @@ -import { t } from "../../services/i18n.js"; -import NoteContextAwareWidget from "../note_context_aware_widget.js"; -import noteAutocompleteService, { type Suggestion } from "../../services/note_autocomplete.js"; -import server from "../../services/server.js"; -import contextMenuService from "../../menus/context_menu.js"; -import attributeParser, { type Attribute } from "../../services/attribute_parser.js"; -import { AttributeEditor, type EditorConfig, type ModelElement, type MentionFeed, type ModelNode, type ModelPosition } from "@triliumnext/ckeditor5"; -import froca from "../../services/froca.js"; -import attributeRenderer from "../../services/attribute_renderer.js"; -import noteCreateService from "../../services/note_create.js"; -import attributeService from "../../services/attributes.js"; -import linkService from "../../services/link.js"; -import type AttributeDetailWidget from "./attribute_detail.js"; -import type { CommandData, EventData, EventListener, FilteredCommandNames } from "../../components/app_context.js"; -import type { default as FAttribute, AttributeType } from "../../entities/fattribute.js"; -import type FNote from "../../entities/fnote.js"; -import { escapeQuotes } from "../../services/utils.js"; - -const HELP_TEXT = ` -

${t("attribute_editor.help_text_body1")}

- -

${t("attribute_editor.help_text_body2")}

- -

${t("attribute_editor.help_text_body3")}

`; - -const TPL = /*html*/` -
- - -
- -
-
- - -
-`; - -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(`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(`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; - -export default class AttributeEditorWidget extends NoteContextAwareWidget implements EventListener<"entitiesReloaded">, EventListener<"addNewLabel">, EventListener<"addNewRelation"> { - private attributeDetailWidget: AttributeDetailWidget; - private $editor!: JQuery; - private $addNewAttributeButton!: JQuery; - private $saveAttributesButton!: JQuery; - private $errors!: JQuery; - - 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({ - 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(/]+href="(#[A-Za-z0-9_/]*)"[^>]*>[^<]*<\/a>/g, "$1") - .replace(/ /g, " "); // otherwise .text() below outputs non-breaking space in unicode - - return $("
").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, 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(); - } - } -} diff --git a/apps/client/src/widgets/basic_widget.ts b/apps/client/src/widgets/basic_widget.ts index be7b0bbd7..e33267daf 100644 --- a/apps/client/src/widgets/basic_widget.ts +++ b/apps/client/src/widgets/basic_widget.ts @@ -1,7 +1,11 @@ +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> extends TypedComponent { protected attrs: Record; @@ -22,11 +26,14 @@ export class TypedBasicWidget> extends TypedCompon this.childPositionCounter = 10; } - child(...components: T[]) { - if (!components) { + child(..._components: (T | VNode)[]) { + if (!_components) { return this; } + // Convert any React components to legacy wrapped components. + const components = wrapReactWidgets(_components); + super.child(...components); for (const component of components) { @@ -48,7 +55,7 @@ export class TypedBasicWidget> extends TypedCompon * @param components the components to be added as children to this component provided the condition is truthy. * @returns self for chaining. */ - optChild(condition: boolean, ...components: T[]) { + optChild(condition: boolean, ...components: (T | VNode)[]) { if (condition) { return this.child(...components); } else { @@ -258,3 +265,30 @@ export class TypedBasicWidget> extends TypedCompon * For information on using widgets, see the tutorial {@tutorial widget_basics}. */ export default class BasicWidget extends TypedBasicWidget {} + +export function wrapReactWidgets>(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); + } + +} diff --git a/apps/client/src/widgets/bookmark_switch.ts b/apps/client/src/widgets/bookmark_switch.ts deleted file mode 100644 index 93d4789aa..000000000 --- a/apps/client/src/widgets/bookmark_switch.ts +++ /dev/null @@ -1,54 +0,0 @@ -import SwitchWidget from "./switch.js"; -import server from "../services/server.js"; -import toastService from "../services/toast.js"; -import { t } from "../services/i18n.js"; -import type FNote from "../entities/fnote.js"; -import type { EventData } from "../components/app_context.js"; - -// TODO: Deduplicate -type Response = { - success: true; -} | { - success: false; - message: string; -} - -export default class BookmarkSwitchWidget extends SwitchWidget { - isEnabled() { - return ( - super.isEnabled() && - // it's not possible to bookmark root because that would clone it under bookmarks and thus create a cycle - !["root", "_hidden"].includes(this.noteId ?? "") - ); - } - - doRender() { - super.doRender(); - - this.switchOnName = t("bookmark_switch.bookmark"); - this.switchOnTooltip = t("bookmark_switch.bookmark_this_note"); - - this.switchOffName = t("bookmark_switch.bookmark"); - this.switchOffTooltip = t("bookmark_switch.remove_bookmark"); - } - - async toggle(state: boolean | null | undefined) { - const resp = await server.put(`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(); - } - } -} diff --git a/apps/client/src/widgets/bulk_actions/BulkAction.tsx b/apps/client/src/widgets/bulk_actions/BulkAction.tsx index 3389d59f5..8b562d0f0 100644 --- a/apps/client/src/widgets/bulk_actions/BulkAction.tsx +++ b/apps/client/src/widgets/bulk_actions/BulkAction.tsx @@ -1,6 +1,7 @@ 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; @@ -24,19 +25,11 @@ const BulkAction = memo(({ label, children, helpText, bulkAction }: BulkActionPr {children}
- - {helpText &&
- -
- {helpText} -
-
} - - bulkAction?.deleteAction()} - /> - + bulkAction?.deleteAction()} + /> ); }); diff --git a/apps/client/src/widgets/bulk_actions/note/move_note.tsx b/apps/client/src/widgets/bulk_actions/note/move_note.tsx index ac5829305..4290726ce 100644 --- a/apps/client/src/widgets/bulk_actions/note/move_note.tsx +++ b/apps/client/src/widgets/bulk_actions/note/move_note.tsx @@ -1,11 +1,11 @@ import { t } from "../../../services/i18n.js"; -import AbstractBulkAction, { ActionDefinition } from "../abstract_bulk_action.js"; +import AbstractBulkAction from "../abstract_bulk_action.js"; import BulkAction, { BulkActionText } from "../BulkAction.jsx"; import NoteAutocomplete from "../../react/NoteAutocomplete.jsx"; import { useEffect, useState } from "preact/hooks"; import { useSpacedUpdate } from "../../react/hooks.jsx"; -function MoveNoteBulkActionComponent({ bulkAction, actionDef }: { bulkAction: AbstractBulkAction, actionDef: ActionDefinition }) { +function MoveNoteBulkActionComponent({ bulkAction }: { bulkAction: AbstractBulkAction }) { const [ targetParentNoteId, setTargetParentNoteId ] = useState(); const spacedUpdate = useSpacedUpdate(() => { return bulkAction.saveAction({ targetParentNoteId: targetParentNoteId }) @@ -45,6 +45,6 @@ export default class MoveNoteBulkAction extends AbstractBulkAction { } doRender() { - return + return } } diff --git a/apps/client/src/widgets/bulk_actions/note/rename_note.tsx b/apps/client/src/widgets/bulk_actions/note/rename_note.tsx index 5fe9b8912..682494cd2 100644 --- a/apps/client/src/widgets/bulk_actions/note/rename_note.tsx +++ b/apps/client/src/widgets/bulk_actions/note/rename_note.tsx @@ -1,4 +1,3 @@ -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"; diff --git a/apps/client/src/widgets/bulk_actions/relation/add_relation.tsx b/apps/client/src/widgets/bulk_actions/relation/add_relation.tsx index 5e10933a8..59d222ee8 100644 --- a/apps/client/src/widgets/bulk_actions/relation/add_relation.tsx +++ b/apps/client/src/widgets/bulk_actions/relation/add_relation.tsx @@ -1,6 +1,4 @@ -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"; diff --git a/apps/client/src/widgets/buttons/global_menu.css b/apps/client/src/widgets/buttons/global_menu.css new file mode 100644 index 000000000..920308dea --- /dev/null +++ b/apps/client/src/widgets/buttons/global_menu.css @@ -0,0 +1,102 @@ +.global-menu { + width: 53px; + height: 53px; + flex-shrink: 0; +} + +.global-menu .dropdown-menu { + min-width: 20em; +} + +.global-menu-button { + width: 100% !important; + height: 100% !important; + position: relative; + padding: 6px; + border: 0; +} + +.global-menu-button svg path { + fill: var(--launcher-pane-text-color); +} + +.global-menu-button:hover { border: 0; } +.global-menu-button:hover svg path { + transition: 200ms ease-in-out fill; +} +.global-menu-button:hover svg path.st0 { fill:#95C980; } +.global-menu-button:hover svg path.st1 { fill:#72B755; } +.global-menu-button:hover svg path.st2 { fill:#4FA52B; } +.global-menu-button:hover svg path.st3 { fill:#EE8C89; } +.global-menu-button:hover svg path.st4 { fill:#E96562; } +.global-menu-button:hover svg path.st5 { fill:#E33F3B; } +.global-menu-button:hover svg path.st6 { fill:#EFB075; } +.global-menu-button:hover svg path.st7 { fill:#E99547; } +.global-menu-button:hover svg path.st8 { fill:#E47B19; } + +.global-menu-button-update-available { + position: absolute; + right: -30px; + bottom: -30px; + width: 100%; + height: 100%; + pointer-events: none; +} + +.global-menu .zoom-container { + display: flex; + flex-direction: row; + align-items: baseline; +} + +.global-menu .zoom-buttons { + margin-left: 2em; +} + +.global-menu .zoom-buttons a { + display: inline-block; + border: 1px solid var(--button-border-color); + border-radius: var(--button-border-radius); + color: var(--button-text-color); + background-color: var(--button-background-color); + padding: 3px; + margin-left: 3px; + text-decoration: none; +} + +.global-menu .zoom-buttons a:hover { + text-decoration: none; +} + +.global-menu .zoom-state { + margin-left: 5px; + margin-right: 5px; +} + +.global-menu .dropdown-item .bx { + position: relative; + top: 3px; + font-size: 120%; + margin-right: 6px; +} + +/* #region Update available */ +.global-menu-button-update-available-button { + width: 21px !important; + height: 21px !important; + padding: 0 !important; + + border-radius: var(--button-border-radius); + transform: scale(0.9); + border: none; + opacity: 0.8; + + display: flex; + align-items: center; + justify-content: center; +} + +.global-menu-button-wrapper:hover .global-menu-button-update-available-button { + opacity: 1; +} +/* #endregion */ \ No newline at end of file diff --git a/apps/client/src/widgets/buttons/global_menu.ts b/apps/client/src/widgets/buttons/global_menu.ts deleted file mode 100644 index 9d6493b89..000000000 --- a/apps/client/src/widgets/buttons/global_menu.ts +++ /dev/null @@ -1,436 +0,0 @@ -import { t } from "../../services/i18n.js"; -import BasicWidget from "../basic_widget.js"; -import utils from "../../services/utils.js"; -import UpdateAvailableWidget from "./update_available.js"; -import options from "../../services/options.js"; -import { Tooltip, Dropdown } from "bootstrap"; - -const TPL = /*html*/` - -`; - -export default class GlobalMenuWidget extends BasicWidget { - private updateAvailableWidget: UpdateAvailableWidget; - private isHorizontalLayout: boolean; - private tooltip!: Tooltip; - private dropdown!: Dropdown; - - private $updateToLatestVersionButton!: JQuery; - private $zoomState!: JQuery; - private $toggleZenMode!: JQuery; - - constructor(isHorizontalLayout: boolean) { - super(); - - this.updateAvailableWidget = new UpdateAvailableWidget(); - this.isHorizontalLayout = isHorizontalLayout; - } - - doRender() { - this.$widget = $(TPL); - - if (!this.isHorizontalLayout) { - this.$widget.addClass("dropend"); - } - - const $globalMenuButton = this.$widget.find(".global-menu-button"); - if (!this.isHorizontalLayout) { - $globalMenuButton.prepend( - $(`\ - - - - - - - - - - - - - - - `) - ); - - this.tooltip = new Tooltip(this.$widget.find("[data-bs-toggle='tooltip']")[0], { trigger: "hover" }); - } else { - $globalMenuButton.toggleClass("bx bx-menu"); - } - - this.dropdown = Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']")[0], { - popperConfig: { - placement: "bottom" - } - }); - - this.$widget.find(".show-about-dialog-button").on("click", () => this.triggerCommand("openAboutDialog")); - - const isElectron = utils.isElectron(); - - this.$widget.find(".toggle-pin").toggle(isElectron); - if (isElectron) { - this.$widget.on("click", ".toggle-pin", (e) => { - const $el = $(e.target); - const remote = utils.dynamicRequire("@electron/remote"); - const focusedWindow = remote.BrowserWindow.getFocusedWindow(); - const isAlwaysOnTop = focusedWindow.isAlwaysOnTop(); - if (isAlwaysOnTop) { - focusedWindow.setAlwaysOnTop(false); - $el.removeClass("active"); - } else { - focusedWindow.setAlwaysOnTop(true); - $el.addClass("active"); - } - }); - } - - this.$widget.find(".logout-button").toggle(!isElectron); - this.$widget.find(".logout-button-separator").toggle(!isElectron); - - this.$widget.find(".open-dev-tools-button").toggle(isElectron); - this.$widget.find(".switch-to-mobile-version-button").toggle(!isElectron && utils.isDesktop()); - this.$widget.find(".switch-to-desktop-version-button").toggle(!isElectron && utils.isMobile()); - - this.$widget.on("click", ".dropdown-item", (e) => { - if ($(e.target).parent(".zoom-buttons")) { - return; - } - - this.dropdown.toggle(); - }); - if (utils.isMobile()) { - this.$widget.on("click", ".dropdown-submenu .dropdown-toggle", (e) => { - const $submenu = $(e.target).closest(".dropdown-item"); - $submenu.toggleClass("submenu-open"); - $submenu.find("ul.dropdown-menu").toggleClass("show"); - e.stopPropagation(); - return; - }); - } - this.$widget.on("click", ".dropdown-submenu", (e) => { - if ($(e.target).children(".dropdown-menu").length === 1 || $(e.target).hasClass("dropdown-toggle")) { - e.stopPropagation(); - } - }); - - this.$widget.find(".global-menu-button-update-available").append(this.updateAvailableWidget.render()); - - this.$updateToLatestVersionButton = this.$widget.find(".update-to-latest-version-button"); - - if (!utils.isElectron()) { - this.$widget.find(".zoom-container").hide(); - } - - this.$zoomState = this.$widget.find(".zoom-state"); - this.$toggleZenMode = this.$widget.find('[data-trigger-command="toggleZenMode"'); - this.$widget.on("show.bs.dropdown", () => this.#onShown()); - if (this.tooltip) { - this.$widget.on("hide.bs.dropdown", () => this.tooltip.enable()); - } - - this.$widget.find(".zoom-buttons").on( - "click", - // delay to wait for the actual zoom change - () => setTimeout(() => this.updateZoomState(), 300) - ); - - this.updateVersionStatus(); - - setInterval(() => this.updateVersionStatus(), 8 * 60 * 60 * 1000); - } - - #onShown() { - this.$toggleZenMode.toggleClass("active", $("body").hasClass("zen")); - this.updateZoomState(); - if (this.tooltip) { - this.tooltip.hide(); - this.tooltip.disable(); - } - } - - updateZoomState() { - if (!utils.isElectron()) { - return; - } - - const zoomFactor = utils.dynamicRequire("electron").webFrame.getZoomFactor(); - const zoomPercent = Math.round(zoomFactor * 100); - - this.$zoomState.text(`${zoomPercent}%`); - } - - async updateVersionStatus() { - await options.initializedPromise; - - if (options.get("checkForUpdates") !== "true") { - return; - } - - const latestVersion = await this.fetchLatestVersion(); - this.updateAvailableWidget.updateVersionStatus(latestVersion); - // Show "click to download" button in options menu if there's a new version available - this.$updateToLatestVersionButton.toggle(utils.isUpdateAvailable(latestVersion, glob.triliumVersion)); - this.$updateToLatestVersionButton.find(".version-text").text(`Version ${latestVersion} is available, click to download.`); - } - - async fetchLatestVersion() { - const RELEASES_API_URL = "https://api.github.com/repos/TriliumNext/Trilium/releases/latest"; - - const resp = await fetch(RELEASES_API_URL); - const data = await resp.json(); - - return data?.tag_name?.substring(1); - } - - downloadLatestVersionCommand() { - window.open("https://github.com/TriliumNext/Trilium/releases/latest"); - } - - activeContextChangedEvent() { - this.dropdown.hide(); - } - - noteSwitchedEvent() { - this.dropdown.hide(); - } -} diff --git a/apps/client/src/widgets/buttons/global_menu.tsx b/apps/client/src/widgets/buttons/global_menu.tsx new file mode 100644 index 000000000..fc74166ba --- /dev/null +++ b/apps/client/src/widgets/buttons/global_menu.tsx @@ -0,0 +1,239 @@ +import Dropdown from "../react/Dropdown"; +import "./global_menu.css"; +import { useStaticTooltip, useStaticTooltipWithKeyboardShortcut, useTriliumOption, useTriliumOptionBool } from "../react/hooks"; +import { useContext, useEffect, useRef, useState } from "preact/hooks"; +import { t } from "../../services/i18n"; +import { FormDropdownDivider, FormDropdownSubmenu, FormListItem } from "../react/FormList"; +import { CommandNames } from "../../components/app_context"; +import KeyboardShortcut from "../react/KeyboardShortcut"; +import { KeyboardActionNames } from "@triliumnext/commons"; +import { ComponentChildren } from "preact"; +import Component from "../../components/component"; +import { ParentComponent } from "../react/react_utils"; +import utils, { dynamicRequire, isElectron, isMobile } from "../../services/utils"; + +interface MenuItemProps { + icon: string, + text: ComponentChildren, + title?: string, + command: T, + disabled?: boolean + active?: boolean; + outsideChildren?: ComponentChildren; +} + +export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout: boolean }) { + const isVerticalLayout = !isHorizontalLayout; + const parentComponent = useContext(ParentComponent); + const { isUpdateAvailable, latestVersion } = useTriliumUpdateStatus(); + + return ( + + {isVerticalLayout && } + {isUpdateAvailable && } + } + > + + + + + + + + + + + + + + + + + + + + {isUpdateAvailable && window.open("https://github.com/TriliumNext/Trilium/releases/latest")} icon="bx bx-sync" text={`Version ${latestVersion} is available, click to download.`} /> } + {!isElectron() && } + + ) +} + +function AdvancedMenu() { + return ( + + + + + + + + + + + {isElectron() && } + + + ) +} + +function BrowserOnlyOptions() { + return <> + + + ; +} + +function SwitchToOptions() { + if (isElectron()) { + return; + } else if (!isMobile()) { + return + } else { + return + } +} + +function MenuItem({ icon, text, title, command, disabled, active }: MenuItemProps void)>) { + return {text} +} + +function KeyboardActionMenuItem({ text, command, ...props }: MenuItemProps) { + return {text} } + /> +} + +function VerticalLayoutIcon() { + const logoRef = useRef(null); + useStaticTooltip(logoRef); + + return ( + + + + + + + + + + + + + + + + ) +} + +function ZoomControls({ parentComponent }: { parentComponent?: Component | null }) { + const [ zoomLevel, setZoomLevel ] = useState(100); + + function updateZoomState() { + if (!isElectron()) { + return; + } + + const zoomFactor = dynamicRequire("electron").webFrame.getZoomFactor(); + setZoomLevel(Math.round(zoomFactor * 100)); + } + + useEffect(updateZoomState, []); + + function ZoomControlButton({ command, title, icon, children }: { command: KeyboardActionNames, title: string, icon?: string, children?: ComponentChildren }) { + const linkRef = useRef(null); + useStaticTooltipWithKeyboardShortcut(linkRef, title, command); + return ( + { + parentComponent?.triggerCommand(command); + setTimeout(() => updateZoomState(), 300) + e.stopPropagation(); + }} + className={icon} + >{children} + ) + } + + return isElectron() ? ( + + {t("global_menu.zoom")} + <> +
+ +   + + {zoomLevel}{t("units.percentage")} + +
+ +
+ ) : ( + + ); +} + +function ToggleWindowOnTop() { + const focusedWindow = isElectron() ? dynamicRequire("@electron/remote").BrowserWindow.getFocusedWindow() : null; + const [ isAlwaysOnTop, setIsAlwaysOnTop ] = useState(focusedWindow?.isAlwaysOnTop()); + + return (isElectron() && + { + const newState = !isAlwaysOnTop; + focusedWindow?.setAlwaysOnTop(newState); + setIsAlwaysOnTop(newState); + }} + /> + ) +} + +function useTriliumUpdateStatus() { + const [ latestVersion, setLatestVersion ] = useState(); + const [ checkForUpdates ] = useTriliumOptionBool("checkForUpdates"); + const isUpdateAvailable = utils.isUpdateAvailable(latestVersion, glob.triliumVersion); + + async function updateVersionStatus() { + const RELEASES_API_URL = "https://api.github.com/repos/TriliumNext/Trilium/releases/latest"; + + const resp = await fetch(RELEASES_API_URL); + const data = await resp.json(); + const latestVersion = data?.tag_name?.substring(1); + setLatestVersion(latestVersion); + } + + useEffect(() => { + if (!checkForUpdates) { + setLatestVersion(undefined); + return; + } + + updateVersionStatus(); + + const interval = setInterval(() => updateVersionStatus(), 8 * 60 * 60 * 1000); + return () => clearInterval(interval); + }, [ checkForUpdates ]); + + return { isUpdateAvailable, latestVersion }; +} \ No newline at end of file diff --git a/apps/client/src/widgets/buttons/history_navigation.ts b/apps/client/src/widgets/buttons/history_navigation.ts index 9fa54c60a..74eaf6acc 100644 --- a/apps/client/src/widgets/buttons/history_navigation.ts +++ b/apps/client/src/widgets/buttons/history_navigation.ts @@ -1,24 +1,11 @@ import utils from "../../services/utils.js"; -import contextMenu from "../../menus/context_menu.js"; +import contextMenu, { MenuCommandItem } from "../../menus/context_menu.js"; import treeService from "../../services/tree.js"; import ButtonFromNoteWidget from "./button_from_note.js"; import type FNote from "../../entities/fnote.js"; import type { CommandNames } from "../../components/app_context.js"; - -interface WebContents { - history: string[]; - getActiveIndex(): number; - clearHistory(): void; - canGoBack(): boolean; - canGoForward(): boolean; - goToIndex(index: string): void; -} - -interface ContextMenuItem { - title: string; - idx: string; - uiIcon: string; -} +import type { WebContents } from "electron"; +import link from "../../services/link.js"; export default class HistoryNavigationButton extends ButtonFromNoteWidget { private webContents?: WebContents; @@ -51,28 +38,24 @@ export default class HistoryNavigationButton extends ButtonFromNoteWidget { async showContextMenu(e: JQuery.ContextMenuEvent) { e.preventDefault(); - if (!this.webContents || this.webContents.history.length < 2) { + if (!this.webContents || this.webContents.navigationHistory.length() < 2) { return; } - let items: ContextMenuItem[] = []; + let items: MenuCommandItem[] = []; - const activeIndex = this.webContents.getActiveIndex(); - const history = this.webContents.history; + const history = this.webContents.navigationHistory.getAllEntries(); + const activeIndex = this.webContents.navigationHistory.getActiveIndex(); for (const idx in history) { - const url = history[idx]; - const parts = url.split("#"); - if (parts.length < 2) continue; - - const notePathWithTab = parts[1]; - const notePath = notePathWithTab.split("-")[0]; + const { notePath } = link.parseNavigationStateFromUrl(history[idx].url); + if (!notePath) continue; const title = await treeService.getNotePathTitle(notePath); items.push({ title, - idx, + command: idx, uiIcon: parseInt(idx) === activeIndex ? "bx bx-radio-circle-marked" // compare with type coercion! @@ -92,9 +75,10 @@ export default class HistoryNavigationButton extends ButtonFromNoteWidget { x: e.pageX, y: e.pageY, items, - selectMenuItemHandler: (item: any) => { - if (item && item.idx && this.webContents) { - this.webContents.goToIndex(item.idx); + selectMenuItemHandler: (item: MenuCommandItem) => { + if (item && item.command && this.webContents) { + const idx = parseInt(item.command, 10); + this.webContents.navigationHistory.goToIndex(idx); } } }); diff --git a/apps/client/src/widgets/buttons/left_pane_toggle.ts b/apps/client/src/widgets/buttons/left_pane_toggle.ts deleted file mode 100644 index 22a902622..000000000 --- a/apps/client/src/widgets/buttons/left_pane_toggle.ts +++ /dev/null @@ -1,43 +0,0 @@ -import options from "../../services/options.js"; -import splitService from "../../services/resizer.js"; -import CommandButtonWidget from "./command_button.js"; -import { t } from "../../services/i18n.js"; -import type { EventData } from "../../components/app_context.js"; - -export default class LeftPaneToggleWidget extends CommandButtonWidget { - private currentLeftPaneVisible: boolean; - - constructor(isHorizontalLayout: boolean) { - super(); - - this.currentLeftPaneVisible = options.is("leftPaneVisible"); - - this.class(isHorizontalLayout ? "toggle-button" : "launcher-button"); - - this.settings.icon = () => { - if (options.get("layoutOrientation") === "horizontal") { - return "bx-sidebar"; - } - - return this.currentLeftPaneVisible ? "bx-chevrons-left" : "bx-chevrons-right"; - }; - - this.settings.title = () => (this.currentLeftPaneVisible ? t("left_pane_toggle.hide_panel") : t("left_pane_toggle.show_panel")); - - this.settings.command = () => (this.currentLeftPaneVisible ? "hideLeftPane" : "showLeftPane"); - - if (isHorizontalLayout) { - this.settings.titlePlacement = "bottom"; - } - } - - refreshIcon() { - super.refreshIcon(); - splitService.setupLeftPaneResizer(this.currentLeftPaneVisible); - } - - setLeftPaneVisibilityEvent({ leftPaneVisible }: EventData<"setLeftPaneVisibility">) { - this.currentLeftPaneVisible = leftPaneVisible ?? !this.currentLeftPaneVisible; - this.refreshIcon(); - } -} diff --git a/apps/client/src/widgets/buttons/left_pane_toggle.tsx b/apps/client/src/widgets/buttons/left_pane_toggle.tsx new file mode 100644 index 000000000..f6c4b3ab8 --- /dev/null +++ b/apps/client/src/widgets/buttons/left_pane_toggle.tsx @@ -0,0 +1,29 @@ +import { useEffect, useState } from "preact/hooks"; +import ActionButton from "../react/ActionButton"; +import options from "../../services/options"; +import { t } from "../../services/i18n"; +import { useTriliumEvent } from "../react/hooks"; +import resizer from "../../services/resizer"; + +export default function LeftPaneToggle({ isHorizontalLayout }: { isHorizontalLayout: boolean }) { + const [ currentLeftPaneVisible, setCurrentLeftPaneVisible ] = useState(options.is("leftPaneVisible")); + + useTriliumEvent("setLeftPaneVisibility", ({ leftPaneVisible }) => { + setCurrentLeftPaneVisible(leftPaneVisible ?? !currentLeftPaneVisible); + }); + + useEffect(() => { + resizer.setupLeftPaneResizer(currentLeftPaneVisible); + }, [ currentLeftPaneVisible ]); + + return ( + + ) +} \ No newline at end of file diff --git a/apps/client/src/widgets/buttons/note_actions.ts b/apps/client/src/widgets/buttons/note_actions.ts deleted file mode 100644 index 069253bfe..000000000 --- a/apps/client/src/widgets/buttons/note_actions.ts +++ /dev/null @@ -1,252 +0,0 @@ -import NoteContextAwareWidget from "../note_context_aware_widget.js"; -import utils from "../../services/utils.js"; -import branchService from "../../services/branches.js"; -import dialogService from "../../services/dialog.js"; -import server from "../../services/server.js"; -import toastService from "../../services/toast.js"; -import ws from "../../services/ws.js"; -import appContext, { type EventData } from "../../components/app_context.js"; -import { t } from "../../services/i18n.js"; -import type FNote from "../../entities/fnote.js"; -import type { FAttachmentRow } from "../../entities/fattachment.js"; - -// TODO: Deduplicate with server -interface ConvertToAttachmentResponse { - attachment: FAttachmentRow; -} - -const TPL = /*html*/` -`; - -export default class NoteActionsWidget extends NoteContextAwareWidget { - - private $convertNoteIntoAttachmentButton!: JQuery; - private $findInTextButton!: JQuery; - private $printActiveNoteButton!: JQuery; - private $exportAsPdfButton!: JQuery; - private $showSourceButton!: JQuery; - private $showAttachmentsButton!: JQuery; - private $renderNoteButton!: JQuery; - private $saveRevisionButton!: JQuery; - private $exportNoteButton!: JQuery; - private $importNoteButton!: JQuery; - private $openNoteExternallyButton!: JQuery; - private $openNoteCustomButton!: JQuery; - private $deleteNoteButton!: JQuery; - - 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(`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, enable: boolean) { - if (enable) { - $el.removeAttr("disabled"); - } else { - $el.attr("disabled", "disabled"); - } - } - - entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { - if (loadResults.isNoteReloaded(this.noteId)) { - this.refresh(); - } - } -} diff --git a/apps/client/src/widgets/buttons/revisions_button.ts b/apps/client/src/widgets/buttons/revisions_button.ts deleted file mode 100644 index 089c6a4c6..000000000 --- a/apps/client/src/widgets/buttons/revisions_button.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { t } from "../../services/i18n.js"; -import CommandButtonWidget from "./command_button.js"; - -export default class RevisionsButton extends CommandButtonWidget { - constructor() { - super(); - - this.icon("bx-history").title(t("revisions_button.note_revisions")).command("showRevisions").titlePlacement("bottom").class("icon-action"); - } - - isEnabled() { - return super.isEnabled() && !["launcher", "doc"].includes(this.note?.type ?? ""); - } -} diff --git a/apps/client/src/widgets/buttons/show_highlights_list_widget_button.ts b/apps/client/src/widgets/buttons/show_highlights_list_widget_button.ts deleted file mode 100644 index b5c641a07..000000000 --- a/apps/client/src/widgets/buttons/show_highlights_list_widget_button.ts +++ /dev/null @@ -1,62 +0,0 @@ -import OnClickButtonWidget from "./onclick_button.js"; -import appContext from "../../components/app_context.js"; -import attributeService from "../../services/attributes.js"; -import { t } from "../../services/i18n.js"; -import LoadResults from "../../services/load_results.js"; -import type { AttributeRow } from "../../services/load_results.js"; - -export default class ShowHighlightsListWidgetButton extends OnClickButtonWidget { - isEnabled(): boolean { - return Boolean(super.isEnabled() && this.note && this.note.type === "text" && this.noteContext?.viewScope?.viewMode === "default"); - } - - constructor() { - super(); - - this.icon("bx-bookmarks") - .title(t("show_highlights_list_widget_button.show_highlights_list")) - .titlePlacement("bottom") - .onClick(() => { - if (this.noteContext?.viewScope && this.noteId) { - this.noteContext.viewScope.highlightsListTemporarilyHidden = false; - appContext.triggerEvent("showHighlightsListWidget", { noteId: this.noteId }); - } - this.toggleInt(false); - }); - } - - async refreshWithNote(): Promise { - if (this.noteContext?.viewScope) { - this.toggleInt(this.noteContext.viewScope.highlightsListTemporarilyHidden); - } - } - - async reEvaluateHighlightsListWidgetVisibilityEvent({ noteId }: { noteId: string }): Promise { - if (noteId === this.noteId) { - await this.refresh(); - } - } - - async entitiesReloadedEvent({ loadResults }: { loadResults: LoadResults }): Promise { - if (this.noteId && loadResults.isNoteContentReloaded(this.noteId)) { - await this.refresh(); - } else if ( - loadResults - .getAttributeRows() - .find((attr: AttributeRow) => - attr.type === "label" && - (attr.name?.toLowerCase().includes("readonly") || attr.name === "hideHighlightWidget") && - this.note && - attributeService.isAffecting(attr, this.note) - ) - ) { - await this.refresh(); - } - } - - async noteTypeMimeChangedEvent({ noteId }: { noteId: string }): Promise { - if (this.isNote(noteId)) { - await this.refresh(); - } - } -} diff --git a/apps/client/src/widgets/buttons/show_toc_widget_button.ts b/apps/client/src/widgets/buttons/show_toc_widget_button.ts deleted file mode 100644 index 8a4a8c851..000000000 --- a/apps/client/src/widgets/buttons/show_toc_widget_button.ts +++ /dev/null @@ -1,62 +0,0 @@ -import OnClickButtonWidget from "./onclick_button.js"; -import appContext from "../../components/app_context.js"; -import attributeService from "../../services/attributes.js"; -import { t } from "../../services/i18n.js"; -import LoadResults from "../../services/load_results.js"; -import type { AttributeRow } from "../../services/load_results.js"; - -export default class ShowTocWidgetButton extends OnClickButtonWidget { - isEnabled(): boolean { - return Boolean(super.isEnabled() && this.note && this.note.type === "text" && this.noteContext?.viewScope?.viewMode === "default"); - } - - constructor() { - super(); - - this.icon("bx-tn-toc") - .title(t("show_toc_widget_button.show_toc")) - .titlePlacement("bottom") - .onClick(() => { - if (this.noteContext?.viewScope && this.noteId) { - this.noteContext.viewScope.tocTemporarilyHidden = false; - appContext.triggerEvent("showTocWidget", { noteId: this.noteId }); - } - this.toggleInt(false); - }); - } - - async refreshWithNote(): Promise { - if (this.noteContext?.viewScope) { - this.toggleInt(this.noteContext.viewScope.tocTemporarilyHidden); - } - } - - async reEvaluateTocWidgetVisibilityEvent({ noteId }: { noteId: string }): Promise { - if (noteId === this.noteId) { - await this.refresh(); - } - } - - async entitiesReloadedEvent({ loadResults }: { loadResults: LoadResults }): Promise { - if (this.noteId && loadResults.isNoteContentReloaded(this.noteId)) { - await this.refresh(); - } else if ( - loadResults - .getAttributeRows() - .find((attr: AttributeRow) => - attr.type === "label" && - (attr.name?.toLowerCase().includes("readonly") || attr.name === "toc") && - this.note && - attributeService.isAffecting(attr, this.note) - ) - ) { - await this.refresh(); - } - } - - async noteTypeMimeChangedEvent({ noteId }: { noteId: string }): Promise { - if (this.isNote(noteId)) { - await this.refresh(); - } - } -} diff --git a/apps/client/src/widgets/buttons/update_available.ts b/apps/client/src/widgets/buttons/update_available.ts deleted file mode 100644 index 2f2535cc0..000000000 --- a/apps/client/src/widgets/buttons/update_available.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { t } from "../../services/i18n.js"; -import BasicWidget from "../basic_widget.js"; -import utils from "../../services/utils.js"; - -const TPL = /*html*/` -
- - - -
-`; - -export default class UpdateAvailableWidget extends BasicWidget { - doRender() { - this.$widget = $(TPL); - } - - updateVersionStatus(latestVersion: string) { - this.$widget.toggle(utils.isUpdateAvailable(latestVersion, glob.triliumVersion)); - } -} diff --git a/apps/client/src/widgets/close_zen_button.css b/apps/client/src/widgets/close_zen_button.css new file mode 100644 index 000000000..3ec9f9315 --- /dev/null +++ b/apps/client/src/widgets/close_zen_button.css @@ -0,0 +1,26 @@ +:root { + --zen-button-size: 32px; +} + +.close-zen-container { + width: var(--zen-button-size); + height: var(--zen-button-size); +} + +body.zen .close-zen-container { + display: block; + position: fixed; + top: 2px; + right: 2px; + z-index: 9999; + -webkit-app-region: no-drag; +} + +body.zen.mobile .close-zen-container { + top: -2px; +} + +body.zen.electron:not(.platform-darwin):not(.native-titlebar) .close-zen-container { + left: calc(env(titlebar-area-width) - var(--zen-button-size) - 2px); + right: unset; +} \ No newline at end of file diff --git a/apps/client/src/widgets/close_zen_button.ts b/apps/client/src/widgets/close_zen_button.ts deleted file mode 100644 index 7ee4fcc28..000000000 --- a/apps/client/src/widgets/close_zen_button.ts +++ /dev/null @@ -1,54 +0,0 @@ -import BasicWidget from "./basic_widget.js"; -import { t } from "../services/i18n.js"; -import utils from "../services/utils.js"; - -const TPL = /*html*/`\ -
-
-`; - -export default class CloseZenButton extends BasicWidget { - - doRender(): void { - this.$widget = $(TPL); - } - - zenChangedEvent() { - this.toggleInt(true); - } - -} diff --git a/apps/client/src/widgets/close_zen_button.tsx b/apps/client/src/widgets/close_zen_button.tsx new file mode 100644 index 000000000..21b364da4 --- /dev/null +++ b/apps/client/src/widgets/close_zen_button.tsx @@ -0,0 +1,25 @@ +import { useState } from "preact/hooks"; +import { t } from "../services/i18n"; +import ActionButton from "./react/ActionButton"; +import { useTriliumEvent } from "./react/hooks"; +import "./close_zen_button.css"; + +export default function CloseZenModeButton() { + const [ zenModeEnabled, setZenModeEnabled ] = useState(false); + + useTriliumEvent("zenModeChanged", ({ isEnabled }) => { + setZenModeEnabled(isEnabled); + }); + + return ( +
+ {zenModeEnabled && ( + + )} +
+ ) +} diff --git a/apps/client/src/widgets/containers/ribbon_container.ts b/apps/client/src/widgets/containers/ribbon_container.ts deleted file mode 100644 index 9aee7bb67..000000000 --- a/apps/client/src/widgets/containers/ribbon_container.ts +++ /dev/null @@ -1,388 +0,0 @@ -import NoteContextAwareWidget from "../note_context_aware_widget.js"; -import keyboardActionsService from "../../services/keyboard_actions.js"; -import attributeService from "../../services/attributes.js"; -import type CommandButtonWidget from "../buttons/command_button.js"; -import type FNote from "../../entities/fnote.js"; -import type { NoteType } from "../../entities/fnote.js"; -import type { EventData, EventNames } from "../../components/app_context.js"; -import type NoteActionsWidget from "../buttons/note_actions.js"; - -const TPL = /*html*/` -
- - -
-
-
-
- -
-
`; - -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; - private $buttonContainer!: JQuery; - private $bodyContainer!: JQuery; - - 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($('
').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, 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 = $('
') - .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( - $('') - .addClass(ret.icon) - .attr("title", ret.title) - .attr("data-toggle-command", (ribbonWidget as any).toggleCommand) - ) // TODO: base class - .append(" ") - .append($('').text(ret.title)); - - this.$tabContainer.append($ribbonTitle); - this.$tabContainer.append('
'); - - 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(name: T, data: EventData) { - 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(name: T, data: EventData) { - 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. - * - *

- * 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); - } -} diff --git a/apps/client/src/widgets/containers/root_container.ts b/apps/client/src/widgets/containers/root_container.ts index c941cdd88..6c2d87521 100644 --- a/apps/client/src/widgets/containers/root_container.ts +++ b/apps/client/src/widgets/containers/root_container.ts @@ -1,6 +1,8 @@ -import utils from "../../services/utils.js"; -import type BasicWidget from "../basic_widget.js"; +import { EventData } from "../../components/app_context.js"; import FlexContainer from "./flex_container.js"; +import options from "../../services/options.js"; +import type BasicWidget from "../basic_widget.js"; +import utils from "../../services/utils.js"; /** * The root container is the top-most widget/container, from which the entire layout derives. @@ -27,15 +29,45 @@ export default class RootContainer extends FlexContainer { window.visualViewport?.addEventListener("resize", () => this.#onMobileResize()); } + this.#setMotion(options.is("motionEnabled")); + this.#setShadows(options.is("shadowsEnabled")); + this.#setBackdropEffects(options.is("backdropEffectsEnabled")); + return super.render(); } + entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { + if (loadResults.isOptionReloaded("motionEnabled")) { + this.#setMotion(options.is("motionEnabled")); + } + + if (loadResults.isOptionReloaded("shadowsEnabled")) { + this.#setShadows(options.is("shadowsEnabled")); + } + + if (loadResults.isOptionReloaded("backdropEffectsEnabled")) { + this.#setBackdropEffects(options.is("backdropEffectsEnabled")); + } + } + #onMobileResize() { const currentViewportHeight = getViewportHeight(); const isKeyboardOpened = (currentViewportHeight < this.originalViewportHeight); this.$widget.toggleClass("virtual-keyboard-opened", isKeyboardOpened); } + #setMotion(enabled: boolean) { + document.body.classList.toggle("motion-disabled", !enabled); + jQuery.fx.off = !enabled; + } + + #setShadows(enabled: boolean) { + document.body.classList.toggle("shadows-disabled", !enabled); + } + + #setBackdropEffects(enabled: boolean) { + document.body.classList.toggle("backdrop-effects-disabled", !enabled); + } } function getViewportHeight() { diff --git a/apps/client/src/widgets/containers/split_note_container.ts b/apps/client/src/widgets/containers/split_note_container.ts index 9336de214..6e5d71a01 100644 --- a/apps/client/src/widgets/containers/split_note_container.ts +++ b/apps/client/src/widgets/containers/split_note_container.ts @@ -2,8 +2,8 @@ import FlexContainer from "./flex_container.js"; import appContext, { type CommandData, type CommandListenerData, type EventData, type EventNames, type NoteSwitchedContext } from "../../components/app_context.js"; import type BasicWidget from "../basic_widget.js"; import type NoteContext from "../../components/note_context.js"; +import Component from "../../components/component.js"; import splitService from "../../services/resizer.js"; - interface NoteContextEvent { noteContext: NoteContext; } @@ -161,6 +161,8 @@ export default class SplitNoteContainer extends FlexContainer { for (const ntxId of ntxIds) { this.$widget.find(`[data-ntx-id="${ntxId}"]`).remove(); + const widget = this.widgets[ntxId]; + recursiveCleanup(widget); delete this.widgets[ntxId]; } @@ -248,3 +250,12 @@ export default class SplitNoteContainer extends FlexContainer { return Promise.all(promises); } } + +function recursiveCleanup(widget: Component) { + for (const child of widget.children) { + recursiveCleanup(child); + } + if ("cleanup" in widget && typeof widget.cleanup === "function") { + widget.cleanup(); + } +} diff --git a/apps/client/src/widgets/dialogs/about.tsx b/apps/client/src/widgets/dialogs/about.tsx index 2b49add70..7fa9c2390 100644 --- a/apps/client/src/widgets/dialogs/about.tsx +++ b/apps/client/src/widgets/dialogs/about.tsx @@ -1,4 +1,3 @@ -import ReactBasicWidget from "../react/ReactBasicWidget.js"; import Modal from "../react/Modal.js"; import { t } from "../../services/i18n.js"; import { formatDateTime } from "../../utils/formatters.js"; @@ -8,11 +7,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"; -function AboutDialogComponent() { - let [appInfo, setAppInfo] = useState(null); - let [shown, setShown] = useState(false); +export default function AboutDialog() { + const [appInfo, setAppInfo] = useState(null); + const [shown, setShown] = useState(false); const forceWordBreak: CSSProperties = { wordBreak: "break-all" }; useTriliumEvent("openAboutDialog", () => setShown(true)); @@ -77,16 +76,8 @@ function DirectoryLink({ directory, style }: { directory: string, style?: CSSPro openService.openDirectory(directory); }; - return + return {directory} } else { return {directory}; } } - -export default class AboutDialog extends ReactBasicWidget { - - get component() { - return ; - } - -} \ No newline at end of file diff --git a/apps/client/src/widgets/dialogs/add_link.tsx b/apps/client/src/widgets/dialogs/add_link.tsx index 78867304c..97440491d 100644 --- a/apps/client/src/widgets/dialogs/add_link.tsx +++ b/apps/client/src/widgets/dialogs/add_link.tsx @@ -1,6 +1,5 @@ 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"; @@ -11,11 +10,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"; -function AddLinkDialogComponent() { +export default function AddLinkDialog() { const [ textTypeWidget, setTextTypeWidget ] = useState(); const initialText = useRef(); const [ linkTitle, setLinkTitle ] = useState(""); @@ -30,6 +29,14 @@ function AddLinkDialogComponent() { 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); @@ -107,7 +114,7 @@ function AddLinkDialogComponent() { }} show={shown} > - + ); } - -export default class AddLinkDialog extends ReactBasicWidget { - - get component() { - return ; - } - -} diff --git a/apps/client/src/widgets/dialogs/branch_prefix.tsx b/apps/client/src/widgets/dialogs/branch_prefix.tsx index 67ab3d62b..f04280748 100644 --- a/apps/client/src/widgets/dialogs/branch_prefix.tsx +++ b/apps/client/src/widgets/dialogs/branch_prefix.tsx @@ -4,15 +4,14 @@ 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"; -function BranchPrefixDialogComponent() { +export default function BranchPrefixDialog() { const [ shown, setShown ] = useState(false); const [ branch, setBranch ] = useState(); const [ prefix, setPrefix ] = useState(branch?.prefix ?? ""); @@ -64,7 +63,7 @@ function BranchPrefixDialogComponent() { footer={ -

-
-`; - -export default class EditabilitySelectWidget extends NoteContextAwareWidget { - - private dropdown!: Dropdown; - private $editabilityActiveDesc!: JQuery; - - 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(); - } - } -} diff --git a/apps/client/src/widgets/find.ts b/apps/client/src/widgets/find.ts index cb49bf487..e2f52d58a 100644 --- a/apps/client/src/widgets/find.ts +++ b/apps/client/src/widgets/find.ts @@ -87,7 +87,7 @@ const TPL = /*html*/`
-
+