diff --git a/_regroup/package.json b/_regroup/package.json index c901824817..6f86b5d123 100644 --- a/_regroup/package.json +++ b/_regroup/package.json @@ -35,7 +35,7 @@ "chore:generate-openapi": "tsx bin/generate-openapi.js" }, "devDependencies": { - "@playwright/test": "1.53.0", + "@playwright/test": "1.53.1", "@stylistic/eslint-plugin": "4.4.1", "@types/express": "5.0.3", "@types/node": "22.15.32", diff --git a/apps/client/package.json b/apps/client/package.json index 7f1786fece..6dfde853f1 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -75,6 +75,9 @@ "dependsOn": [ "^build" ] + }, + "circular-deps": { + "command": "pnpx dpdm -T {projectRoot}/src/**/*.ts --tree=false --warning=false" } } } diff --git a/apps/client/src-example/app/app.element.css b/apps/client/src-example/app/app.element.css deleted file mode 100644 index 27d0984040..0000000000 --- a/apps/client/src-example/app/app.element.css +++ /dev/null @@ -1,424 +0,0 @@ -/* - * Remove template code below - */ - html { - -webkit-text-size-adjust: 100%; - font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, - 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, - 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', - 'Noto Color Emoji'; - line-height: 1.5; - tab-size: 4; - scroll-behavior: smooth; - } - body { - font-family: inherit; - line-height: inherit; - margin: 0; - } - h1, - h2, - p, - pre { - margin: 0; - } - *, - ::before, - ::after { - box-sizing: border-box; - border-width: 0; - border-style: solid; - border-color: currentColor; - } - h1, - h2 { - font-size: inherit; - font-weight: inherit; - } - a { - color: inherit; - text-decoration: inherit; - } - pre { - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - 'Liberation Mono', 'Courier New', monospace; - } - svg { - display: block; - vertical-align: middle; - } - - svg { - shape-rendering: auto; - text-rendering: optimizeLegibility; - } - pre { - background-color: rgba(55, 65, 81, 1); - border-radius: 0.25rem; - color: rgba(229, 231, 235, 1); - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - 'Liberation Mono', 'Courier New', monospace; - overflow: scroll; - padding: 0.5rem 0.75rem; - } - - .shadow { - box-shadow: 0 0 #0000, 0 0 #0000, 0 10px 15px -3px rgba(0, 0, 0, 0.1), - 0 4px 6px -2px rgba(0, 0, 0, 0.05); - } - .rounded { - border-radius: 1.5rem; - } - - .wrapper { - width: 100%; - } - .container { - margin-left: auto; - margin-right: auto; - max-width: 768px; - padding-bottom: 3rem; - padding-left: 1rem; - padding-right: 1rem; - color: rgba(55, 65, 81, 1); - width: 100%; - } - #welcome { - margin-top: 2.5rem; - } - #welcome h1 { - font-size: 3rem; - font-weight: 500; - letter-spacing: -0.025em; - line-height: 1; - } - #welcome span { - display: block; - font-size: 1.875rem; - font-weight: 300; - line-height: 2.25rem; - margin-bottom: 0.5rem; - } - #hero { - align-items: center; - background-color: hsla(214, 62%, 21%, 1); - border: none; - box-sizing: border-box; - color: rgba(55, 65, 81, 1); - display: grid; - grid-template-columns: 1fr; - margin-top: 3.5rem; - } - #hero .text-container { - color: rgba(255, 255, 255, 1); - padding: 3rem 2rem; - } - #hero .text-container h2 { - font-size: 1.5rem; - line-height: 2rem; - position: relative; - } - #hero .text-container h2 svg { - color: hsla(162, 47%, 50%, 1); - height: 2rem; - left: -0.25rem; - position: absolute; - top: 0; - width: 2rem; - } - #hero .text-container h2 span { - margin-left: 2.5rem; - } - #hero .text-container a { - background-color: rgba(255, 255, 255, 1); - border-radius: 0.75rem; - color: rgba(55, 65, 81, 1); - display: inline-block; - margin-top: 1.5rem; - padding: 1rem 2rem; - text-decoration: inherit; - } - #hero .logo-container { - display: none; - justify-content: center; - padding-left: 2rem; - padding-right: 2rem; - } - #hero .logo-container svg { - color: rgba(255, 255, 255, 1); - width: 66.666667%; - } - - #middle-content { - align-items: flex-start; - display: grid; - gap: 4rem; - grid-template-columns: 1fr; - margin-top: 3.5rem; - } - - #learning-materials { - padding: 2.5rem 2rem; - } - #learning-materials h2 { - font-weight: 500; - font-size: 1.25rem; - letter-spacing: -0.025em; - line-height: 1.75rem; - padding-left: 1rem; - padding-right: 1rem; - } - .list-item-link { - align-items: center; - border-radius: 0.75rem; - display: flex; - margin-top: 1rem; - padding: 1rem; - transition-property: background-color, border-color, color, fill, stroke, - opacity, box-shadow, transform, filter, backdrop-filter, - -webkit-backdrop-filter; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; - width: 100%; - } - .list-item-link svg:first-child { - margin-right: 1rem; - height: 1.5rem; - transition-property: background-color, border-color, color, fill, stroke, - opacity, box-shadow, transform, filter, backdrop-filter, - -webkit-backdrop-filter; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; - width: 1.5rem; - } - .list-item-link > span { - flex-grow: 1; - font-weight: 400; - transition-property: background-color, border-color, color, fill, stroke, - opacity, box-shadow, transform, filter, backdrop-filter, - -webkit-backdrop-filter; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; - } - .list-item-link > span > span { - color: rgba(107, 114, 128, 1); - display: block; - flex-grow: 1; - font-size: 0.75rem; - font-weight: 300; - line-height: 1rem; - transition-property: background-color, border-color, color, fill, stroke, - opacity, box-shadow, transform, filter, backdrop-filter, - -webkit-backdrop-filter; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; - } - .list-item-link svg:last-child { - height: 1rem; - transition-property: all; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; - width: 1rem; - } - .list-item-link:hover { - color: rgba(255, 255, 255, 1); - background-color: hsla(162, 47%, 50%, 1); - } - .list-item-link:hover > span { - } - .list-item-link:hover > span > span { - color: rgba(243, 244, 246, 1); - } - .list-item-link:hover svg:last-child { - transform: translateX(0.25rem); - } - - #other-links { - } - .button-pill { - padding: 1.5rem 2rem; - transition-duration: 300ms; - transition-property: background-color, border-color, color, fill, stroke, - opacity, box-shadow, transform, filter, backdrop-filter, - -webkit-backdrop-filter; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - align-items: center; - display: flex; - } - .button-pill svg { - transition-property: background-color, border-color, color, fill, stroke, - opacity, box-shadow, transform, filter, backdrop-filter, - -webkit-backdrop-filter; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; - flex-shrink: 0; - width: 3rem; - } - .button-pill > span { - letter-spacing: -0.025em; - font-weight: 400; - font-size: 1.125rem; - line-height: 1.75rem; - padding-left: 1rem; - padding-right: 1rem; - } - .button-pill span span { - display: block; - font-size: 0.875rem; - font-weight: 300; - line-height: 1.25rem; - } - .button-pill:hover svg, - .button-pill:hover { - color: rgba(255, 255, 255, 1) !important; - } - #nx-console:hover { - background-color: rgba(0, 122, 204, 1); - } - #nx-console svg { - color: rgba(0, 122, 204, 1); - } - #nx-console-jetbrains { - margin-top: 2rem; - } - #nx-console-jetbrains:hover { - background-color: rgba(255, 49, 140, 1); - } - #nx-console-jetbrains svg { - color: rgba(255, 49, 140, 1); - } - #nx-repo:hover { - background-color: rgba(24, 23, 23, 1); - } - #nx-repo svg { - color: rgba(24, 23, 23, 1); - } - - #nx-cloud { - margin-bottom: 2rem; - margin-top: 2rem; - padding: 2.5rem 2rem; - } - #nx-cloud > div { - align-items: center; - display: flex; - } - #nx-cloud > div svg { - border-radius: 0.375rem; - flex-shrink: 0; - width: 3rem; - } - #nx-cloud > div h2 { - font-size: 1.125rem; - font-weight: 400; - letter-spacing: -0.025em; - line-height: 1.75rem; - padding-left: 1rem; - padding-right: 1rem; - } - #nx-cloud > div h2 span { - display: block; - font-size: 0.875rem; - font-weight: 300; - line-height: 1.25rem; - } - #nx-cloud p { - font-size: 1rem; - line-height: 1.5rem; - margin-top: 1rem; - } - #nx-cloud pre { - margin-top: 1rem; - } - #nx-cloud a { - color: rgba(107, 114, 128, 1); - display: block; - font-size: 0.875rem; - line-height: 1.25rem; - margin-top: 1.5rem; - text-align: right; - } - #nx-cloud a:hover { - text-decoration: underline; - } - - #commands { - padding: 2.5rem 2rem; - - margin-top: 3.5rem; - } - #commands h2 { - font-size: 1.25rem; - font-weight: 400; - letter-spacing: -0.025em; - line-height: 1.75rem; - padding-left: 1rem; - padding-right: 1rem; - } - #commands p { - font-size: 1rem; - font-weight: 300; - line-height: 1.5rem; - margin-top: 1rem; - padding-left: 1rem; - padding-right: 1rem; - } - details { - align-items: center; - display: flex; - margin-top: 1rem; - padding-left: 1rem; - padding-right: 1rem; - width: 100%; - } - details pre > span { - color: rgba(181, 181, 181, 1); - } - summary { - border-radius: 0.5rem; - display: flex; - font-weight: 400; - padding: 0.5rem; - cursor: pointer; - transition-property: background-color, border-color, color, fill, stroke, - opacity, box-shadow, transform, filter, backdrop-filter, - -webkit-backdrop-filter; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; - } - summary:hover { - background-color: rgba(243, 244, 246, 1); - } - summary svg { - height: 1.5rem; - margin-right: 1rem; - width: 1.5rem; - } - - #love { - color: rgba(107, 114, 128, 1); - font-size: 0.875rem; - line-height: 1.25rem; - margin-top: 3.5rem; - opacity: 0.6; - text-align: center; - } - #love svg { - color: rgba(252, 165, 165, 1); - width: 1.25rem; - height: 1.25rem; - display: inline; - margin-top: -0.25rem; - } - - @media screen and (min-width: 768px) { - #hero { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - #hero .logo-container { - display: flex; - } - #middle-content { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - } diff --git a/apps/client/src-example/app/app.element.spec.ts b/apps/client/src-example/app/app.element.spec.ts deleted file mode 100644 index 2c12da1846..0000000000 --- a/apps/client/src-example/app/app.element.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { AppElement } from './app.element'; - -describe('AppElement', () => { - let app: AppElement; - - beforeEach(() => { - app = new AppElement(); - }); - - it('should create successfully', () => { - expect(app).toBeTruthy(); - }); - - it('should have a greeting', () => { - app.connectedCallback(); - - expect(app.querySelector('h1').innerHTML).toContain( - 'Welcome @triliumnext/client' - ); - }); -}); diff --git a/apps/client/src-example/app/app.element.ts b/apps/client/src-example/app/app.element.ts deleted file mode 100644 index fab80aa494..0000000000 --- a/apps/client/src-example/app/app.element.ts +++ /dev/null @@ -1,409 +0,0 @@ -import './app.element.css'; - -export class AppElement extends HTMLElement { - public static observedAttributes = [ - - ]; - - connectedCallback() { - const title = '@triliumnext/client'; - this.innerHTML = ` -
-
- -
-

- Hello there, - Welcome ${title} 👋 -

-
- - -
-
-

- - - - You're up and running -

- What's next? -
-
- - - -
-
- - - - - -
-

Next steps

-

Here are some things you can do with Nx:

-
- - - - - Add UI library - -
# Generate UI lib
-nx g @nx/angular:lib ui
-
-# Add a component
-nx g @nx/angular:component ui/src/lib/button
-
-
- - - - - View interactive project graph - -
nx graph
-
-
- - - - - Run affected commands - -
# see what's been affected by changes
-nx affected:graph
-
-# run tests for current changes
-nx affected:test
-
-# run e2e tests for current changes
-nx affected:e2e
-
-
- -

- Carefully crafted with - - - -

-
-
- `; - } -} -customElements.define('triliumnext-root', AppElement); diff --git a/apps/client/src-example/assets/.gitkeep b/apps/client/src-example/assets/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/apps/client/src-example/favicon.ico b/apps/client/src-example/favicon.ico deleted file mode 100644 index 317ebcb233..0000000000 Binary files a/apps/client/src-example/favicon.ico and /dev/null differ diff --git a/apps/client/src-example/index.html b/apps/client/src-example/index.html deleted file mode 100644 index e206d48372..0000000000 --- a/apps/client/src-example/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - Client - - - - - - - - - diff --git a/apps/client/src-example/main.ts b/apps/client/src-example/main.ts deleted file mode 100644 index fdb879ded5..0000000000 --- a/apps/client/src-example/main.ts +++ /dev/null @@ -1 +0,0 @@ -import './app/app.element'; diff --git a/apps/client/src-example/styles.css b/apps/client/src-example/styles.css deleted file mode 100644 index 90d4ee0072..0000000000 --- a/apps/client/src-example/styles.css +++ /dev/null @@ -1 +0,0 @@ -/* You can add global styles to this file, and also import other style files */ diff --git a/apps/client/src/components/app_context.ts b/apps/client/src/components/app_context.ts index 0cf5058ed6..4430145720 100644 --- a/apps/client/src/components/app_context.ts +++ b/apps/client/src/components/app_context.ts @@ -1,5 +1,4 @@ import froca from "../services/froca.js"; -import bundleService from "../services/bundle.js"; import RootCommandExecutor from "./root_command_executor.js"; import Entrypoints, { type SqlExecuteResults } from "./entrypoints.js"; import options from "../services/options.js"; @@ -470,6 +469,7 @@ export class AppContext extends Component { this.tabManager.loadTabs(); + const bundleService = (await import("../services/bundle.js")).default; setTimeout(() => bundleService.executeStartupBundles(), 2000); } diff --git a/apps/client/src/components/note_context.ts b/apps/client/src/components/note_context.ts index 11d32cf0db..3a8a543100 100644 --- a/apps/client/src/components/note_context.ts +++ b/apps/client/src/components/note_context.ts @@ -12,6 +12,7 @@ import type FNote from "../entities/fnote.js"; import type TypeWidget from "../widgets/type_widgets/type_widget.js"; import type { CKTextEditor } from "@triliumnext/ckeditor5"; import type CodeMirror from "@triliumnext/codemirror"; +import { closeActiveDialog } from "../services/dialog.js"; export interface SetNoteOpts { triggerSwitchEvent?: unknown; @@ -83,7 +84,7 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded"> await this.triggerEvent("beforeNoteSwitch", { noteContext: this }); - utils.closeActiveDialog(); + closeActiveDialog(); this.notePath = resolvedNotePath; this.viewScope = opts.viewScope; diff --git a/apps/client/src/entities/fnote.ts b/apps/client/src/entities/fnote.ts index 9e215821dd..fd35c09b8a 100644 --- a/apps/client/src/entities/fnote.ts +++ b/apps/client/src/entities/fnote.ts @@ -1,7 +1,6 @@ import server from "../services/server.js"; import noteAttributeCache from "../services/note_attribute_cache.js"; import ws from "../services/ws.js"; -import froca from "../services/froca.js"; import protectedSessionHolder from "../services/protected_session_holder.js"; import cssClassManager from "../services/css_class_manager.js"; import type { Froca } from "../services/froca-interface.js"; @@ -410,8 +409,8 @@ class FNote { const notePaths: NotePathRecord[] = this.getAllNotePaths().map((path) => ({ notePath: path, isInHoistedSubTree: isHoistedRoot || path.includes(hoistedNoteId), - isArchived: path.some((noteId) => froca.notes[noteId].isArchived), - isSearch: path.some((noteId) => froca.notes[noteId].type === "search"), + isArchived: path.some((noteId) => this.froca.notes[noteId].isArchived), + isSearch: path.some((noteId) => this.froca.notes[noteId].type === "search"), isHidden: path.includes("_hidden") })); @@ -982,7 +981,7 @@ class FNote { continue; } - const parentNote = froca.notes[parentNoteId]; + const parentNote = this.froca.notes[parentNoteId]; if (!parentNote || parentNote.type === "search") { continue; diff --git a/apps/client/src/services/bundle.ts b/apps/client/src/services/bundle.ts index e6eea7ef1c..e7b88a3433 100644 --- a/apps/client/src/services/bundle.ts +++ b/apps/client/src/services/bundle.ts @@ -1,6 +1,6 @@ import ScriptContext from "./script_context.js"; import server from "./server.js"; -import toastService from "./toast.js"; +import toastService, { showError } from "./toast.js"; import froca from "./froca.js"; import utils from "./utils.js"; import { t } from "./i18n.js"; @@ -37,7 +37,9 @@ async function executeBundle(bundle: Bundle, originEntity?: Entity | null, $cont } catch (e: any) { const note = await froca.getNote(bundle.noteId); - toastService.showAndLogError(`Execution of JS note "${note?.title}" with ID ${bundle.noteId} failed with error: ${e?.message}`); + const message = `Execution of JS note "${note?.title}" with ID ${bundle.noteId} failed with error: ${e?.message}`; + showError(message); + logError(message); } } diff --git a/apps/client/src/services/clipboard.ts b/apps/client/src/services/clipboard.ts index 952ac2e222..9ca5f9d092 100644 --- a/apps/client/src/services/clipboard.ts +++ b/apps/client/src/services/clipboard.ts @@ -4,7 +4,7 @@ import froca from "./froca.js"; import linkService from "./link.js"; import utils from "./utils.js"; import { t } from "./i18n.js"; -import toast from "./toast.js"; +import { throwError } from "./ws.js"; let clipboardBranchIds: string[] = []; let clipboardMode: string | null = null; @@ -37,7 +37,7 @@ async function pasteAfter(afterBranchId: string) { // copy will keep clipboardBranchIds and clipboardMode, so it's possible to paste into multiple places } else { - toastService.throwError(`Unrecognized clipboard mode=${clipboardMode}`); + throwError(`Unrecognized clipboard mode=${clipboardMode}`); } } @@ -69,7 +69,7 @@ async function pasteInto(parentBranchId: string) { // copy will keep clipboardBranchIds and clipboardMode, so it's possible to paste into multiple places } else { - toastService.throwError(`Unrecognized clipboard mode=${clipboardMode}`); + throwError(`Unrecognized clipboard mode=${clipboardMode}`); } } diff --git a/apps/client/src/services/dialog.ts b/apps/client/src/services/dialog.ts index e2d93250f5..240172f491 100644 --- a/apps/client/src/services/dialog.ts +++ b/apps/client/src/services/dialog.ts @@ -1,6 +1,41 @@ +import { Modal } from "bootstrap"; import appContext from "../components/app_context.js"; import type { ConfirmDialogOptions, ConfirmDialogResult, ConfirmWithMessageOptions } from "../widgets/dialogs/confirm.js"; import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js"; +import { focusSavedElement, saveFocusedElement } from "./focus.js"; + +export async function openDialog($dialog: JQuery, closeActDialog = true) { + if (closeActDialog) { + closeActiveDialog(); + glob.activeDialog = $dialog; + } + + saveFocusedElement(); + Modal.getOrCreateInstance($dialog[0]).show(); + + $dialog.on("hidden.bs.modal", () => { + const $autocompleteEl = $(".aa-input"); + if ("autocomplete" in $autocompleteEl) { + $autocompleteEl.autocomplete("close"); + } + + if (!glob.activeDialog || glob.activeDialog === $dialog) { + focusSavedElement(); + } + }); + + const keyboardActionsService = (await import("./keyboard_actions.js")).default; + keyboardActionsService.updateDisplayedShortcuts($dialog); + + return $dialog; +} + +export function closeActiveDialog() { + if (glob.activeDialog) { + Modal.getOrCreateInstance(glob.activeDialog[0]).hide(); + glob.activeDialog = null; + } +} async function info(message: string) { return new Promise((res) => appContext.triggerCommand("showInfoDialog", { message, callback: res })); diff --git a/apps/client/src/services/focus.ts b/apps/client/src/services/focus.ts new file mode 100644 index 0000000000..066c745581 --- /dev/null +++ b/apps/client/src/services/focus.ts @@ -0,0 +1,29 @@ +let $lastFocusedElement: JQuery | null; + +// perhaps there should be saved focused element per tab? +export function saveFocusedElement() { + $lastFocusedElement = $(":focus"); +} + +export function focusSavedElement() { + if (!$lastFocusedElement) { + return; + } + + if ($lastFocusedElement.hasClass("ck")) { + // must handle CKEditor separately because of this bug: https://github.com/ckeditor/ckeditor5/issues/607 + // the bug manifests itself in resetting the cursor position to the first character - jumping above + + const editor = $lastFocusedElement.closest(".ck-editor__editable").prop("ckeditorInstance"); + + if (editor) { + editor.editing.view.focus(); + } else { + console.log("Could not find CKEditor instance to focus last element"); + } + } else { + $lastFocusedElement.focus(); + } + + $lastFocusedElement = null; +} diff --git a/apps/client/src/services/i18n.spec.ts b/apps/client/src/services/i18n.spec.ts index 9143097bed..e64605949a 100644 --- a/apps/client/src/services/i18n.spec.ts +++ b/apps/client/src/services/i18n.spec.ts @@ -1,6 +1,7 @@ import { LOCALES } from "@triliumnext/commons"; import { readFileSync } from "fs"; import { join } from "path"; +import { describe, expect, it } from "vitest"; describe("i18n", () => { it("translations are valid JSON", () => { diff --git a/apps/client/src/services/image.ts b/apps/client/src/services/image.ts index 3cf1424d50..f13a9a3c71 100644 --- a/apps/client/src/services/image.ts +++ b/apps/client/src/services/image.ts @@ -1,5 +1,5 @@ import { t } from "./i18n.js"; -import toastService from "./toast.js"; +import toastService, { showError } from "./toast.js"; function copyImageReferenceToClipboard($imageWrapper: JQuery) { try { @@ -11,7 +11,9 @@ function copyImageReferenceToClipboard($imageWrapper: JQuery) { if (success) { toastService.showMessage(t("image.copied-to-clipboard")); } else { - toastService.showAndLogError(t("image.cannot-copy")); + const message = t("image.cannot-copy"); + showError(message); + logError(message); } } finally { window.getSelection()?.removeAllRanges(); diff --git a/apps/client/src/services/script_context.ts b/apps/client/src/services/script_context.ts index 27a8a7d441..7c15db1aef 100644 --- a/apps/client/src/services/script_context.ts +++ b/apps/client/src/services/script_context.ts @@ -1,4 +1,4 @@ -import FrontendScriptApi, { type Entity } from "./frontend_script_api.js"; +import type { Entity } from "./frontend_script_api.js"; import utils from "./utils.js"; import froca from "./froca.js"; @@ -14,6 +14,8 @@ async function ScriptContext(startNoteId: string, allNoteIds: string[], originEn throw new Error(`Could not find start note ${startNoteId}.`); } + const FrontendScriptApi = (await import("./frontend_script_api.js")).default; + return { modules: modules, notes: utils.toObject(allNotes, (note) => [note.noteId, note]), diff --git a/apps/client/src/services/server.ts b/apps/client/src/services/server.ts index f577fbfb42..861a20e602 100644 --- a/apps/client/src/services/server.ts +++ b/apps/client/src/services/server.ts @@ -1,5 +1,6 @@ import utils, { isShare } from "./utils.js"; import ValidationError from "./validation_error.js"; +import { throwError } from "./ws.js"; type Headers = Record; @@ -276,7 +277,7 @@ async function reportError(method: string, url: string, statusCode: number, resp } else { const title = `${statusCode} ${method} ${url}`; toastService.showErrorTitleAndMessage(title, messageStr); - toastService.throwError(`${title} - ${message}`); + throwError(`${title} - ${message}`); } } diff --git a/apps/client/src/services/toast.ts b/apps/client/src/services/toast.ts index 18cc876a9b..66b3f7f50b 100644 --- a/apps/client/src/services/toast.ts +++ b/apps/client/src/services/toast.ts @@ -78,13 +78,7 @@ function showMessage(message: string, delay = 2000) { }); } -function showAndLogError(message: string, delay = 10000) { - showError(message, delay); - - ws.logError(message); -} - -function showError(message: string, delay = 10000) { +export function showError(message: string, delay = 10000) { console.log(utils.now(), "error: ", message); toast({ @@ -108,18 +102,10 @@ function showErrorTitleAndMessage(title: string, message: string, delay = 10000) }); } -function throwError(message: string) { - ws.logError(message); - - throw new Error(message); -} - export default { showMessage, showError, showErrorTitleAndMessage, - showAndLogError, - throwError, showPersistent, closePersistent }; diff --git a/apps/client/src/services/utils.ts b/apps/client/src/services/utils.ts index 4126506052..c7d37e4d9a 100644 --- a/apps/client/src/services/utils.ts +++ b/apps/client/src/services/utils.ts @@ -1,5 +1,4 @@ import dayjs from "dayjs"; -import { Modal } from "bootstrap"; import type { ViewScope } from "./link.js"; const SVG_MIME = "image/svg+xml"; @@ -275,69 +274,6 @@ function getMimeTypeClass(mime: string) { return `mime-${mime.toLowerCase().replace(/[\W_]+/g, "-")}`; } -function closeActiveDialog() { - if (glob.activeDialog) { - Modal.getOrCreateInstance(glob.activeDialog[0]).hide(); - glob.activeDialog = null; - } -} - -let $lastFocusedElement: JQuery | null; - -// perhaps there should be saved focused element per tab? -function saveFocusedElement() { - $lastFocusedElement = $(":focus"); -} - -function focusSavedElement() { - if (!$lastFocusedElement) { - return; - } - - if ($lastFocusedElement.hasClass("ck")) { - // must handle CKEditor separately because of this bug: https://github.com/ckeditor/ckeditor5/issues/607 - // the bug manifests itself in resetting the cursor position to the first character - jumping above - - const editor = $lastFocusedElement.closest(".ck-editor__editable").prop("ckeditorInstance"); - - if (editor) { - editor.editing.view.focus(); - } else { - console.log("Could not find CKEditor instance to focus last element"); - } - } else { - $lastFocusedElement.focus(); - } - - $lastFocusedElement = null; -} - -async function openDialog($dialog: JQuery, closeActDialog = true) { - if (closeActDialog) { - closeActiveDialog(); - glob.activeDialog = $dialog; - } - - saveFocusedElement(); - Modal.getOrCreateInstance($dialog[0]).show(); - - $dialog.on("hidden.bs.modal", () => { - const $autocompleteEl = $(".aa-input"); - if ("autocomplete" in $autocompleteEl) { - $autocompleteEl.autocomplete("close"); - } - - if (!glob.activeDialog || glob.activeDialog === $dialog) { - focusSavedElement(); - } - }); - - const keyboardActionsService = (await import("./keyboard_actions.js")).default; - keyboardActionsService.updateDisplayedShortcuts($dialog); - - return $dialog; -} - function isHtmlEmpty(html: string) { if (!html) { return true; @@ -823,10 +759,6 @@ export default { setCookie, getNoteTypeClass, getMimeTypeClass, - closeActiveDialog, - openDialog, - saveFocusedElement, - focusSavedElement, isHtmlEmpty, clearBrowserCache, copySelectionToClipboard, diff --git a/apps/client/src/services/ws.ts b/apps/client/src/services/ws.ts index ccfd19592d..dcd63e5772 100644 --- a/apps/client/src/services/ws.ts +++ b/apps/client/src/services/ws.ts @@ -17,7 +17,7 @@ let lastProcessedEntityChangeId = window.glob.maxEntityChangeIdAtLoad; let lastPingTs: number; let frontendUpdateDataQueue: EntityChange[] = []; -function logError(message: string) { +export function logError(message: string) { console.error(utils.now(), message); // needs to be separate from .trace() if (ws && ws.readyState === 1) { @@ -301,6 +301,12 @@ setTimeout(() => { setInterval(sendPing, 1000); }, 0); +export function throwError(message: string) { + logError(message); + + throw new Error(message); +} + export default { logError, subscribeToMessages, diff --git a/apps/client/src/widgets/attribute_widgets/attribute_detail.ts b/apps/client/src/widgets/attribute_widgets/attribute_detail.ts index 08bb764fc1..9017673f32 100644 --- a/apps/client/src/widgets/attribute_widgets/attribute_detail.ts +++ b/apps/client/src/widgets/attribute_widgets/attribute_detail.ts @@ -11,6 +11,7 @@ import utils from "../../services/utils.js"; import shortcutService from "../../services/shortcuts.js"; import appContext from "../../components/app_context.js"; import type { Attribute } from "../../services/attribute_parser.js"; +import { focusSavedElement, saveFocusedElement } from "../../services/focus.js"; const TPL = /*html*/`
@@ -483,7 +484,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget { return; } - utils.saveFocusedElement(); + saveFocusedElement(); this.attrType = this.getAttrType(attribute); @@ -605,7 +606,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget { this.hide(); - utils.focusSavedElement(); + focusSavedElement(); } async cancelAndClose() { @@ -613,7 +614,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget { this.hide(); - utils.focusSavedElement(); + focusSavedElement(); } userEditedAttribute() { diff --git a/apps/client/src/widgets/dialogs/about.ts b/apps/client/src/widgets/dialogs/about.ts index 2c364d7568..06cf118ecd 100644 --- a/apps/client/src/widgets/dialogs/about.ts +++ b/apps/client/src/widgets/dialogs/about.ts @@ -4,6 +4,7 @@ import BasicWidget from "../basic_widget.js"; import openService from "../../services/open.js"; import server from "../../services/server.js"; import utils from "../../services/utils.js"; +import { openDialog } from "../../services/dialog.js"; interface AppInfo { appVersion: string; @@ -111,6 +112,6 @@ export default class AboutDialog extends BasicWidget { async openAboutDialogEvent() { await this.refresh(); - utils.openDialog(this.$widget); + openDialog(this.$widget); } } diff --git a/apps/client/src/widgets/dialogs/add_link.ts b/apps/client/src/widgets/dialogs/add_link.ts index fe22954425..d7758c92d7 100644 --- a/apps/client/src/widgets/dialogs/add_link.ts +++ b/apps/client/src/widgets/dialogs/add_link.ts @@ -1,11 +1,11 @@ import { t } from "../../services/i18n.js"; import treeService from "../../services/tree.js"; import noteAutocompleteService from "../../services/note_autocomplete.js"; -import utils from "../../services/utils.js"; import BasicWidget from "../basic_widget.js"; import type { Suggestion } from "../../services/note_autocomplete.js"; import type { default as TextTypeWidget } from "../type_widgets/editable_text.js"; import type { EventData } from "../../components/app_context.js"; +import { openDialog } from "../../services/dialog.js"; const TPL = /*html*/`