From e220e4ec9d7394bda46cb6efe17e77b4048dd42e Mon Sep 17 00:00:00 2001 From: JYC333 <22962980+JYC333@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:48:20 +0000 Subject: [PATCH] refactor: migrate cleanup function --- apps/client/src/components/entrypoints.ts | 3 +- apps/client/src/components/tab_manager.ts | 28 +++++++++---------- .../src/services/attribute_autocomplete.ts | 15 +++++++++- apps/client/src/services/autocomplete_core.ts | 18 ++++++++++++ apps/client/src/services/dialog.ts | 11 ++++---- apps/client/src/services/keyboard_actions.ts | 14 ++++++---- apps/client/src/services/note_autocomplete.ts | 9 +++++- .../attribute_widgets/attribute_detail.ts | 27 ++++++++++-------- 8 files changed, 84 insertions(+), 41 deletions(-) diff --git a/apps/client/src/components/entrypoints.ts b/apps/client/src/components/entrypoints.ts index 8fc4e1b3d5..6c880fa2c5 100644 --- a/apps/client/src/components/entrypoints.ts +++ b/apps/client/src/components/entrypoints.ts @@ -1,5 +1,6 @@ import { CreateChildrenResponse, SqlExecuteResponse } from "@triliumnext/commons"; +import { closeAllHeadlessAutocompletes } from "../services/autocomplete_core.js"; import bundleService from "../services/bundle.js"; import dateNoteService from "../services/date_notes.js"; import froca from "../services/froca.js"; @@ -197,7 +198,7 @@ export default class Entrypoints extends Component { hideAllPopups() { if (utils.isDesktop()) { - $(".aa-input").autocomplete("close"); + closeAllHeadlessAutocompletes(); } } diff --git a/apps/client/src/components/tab_manager.ts b/apps/client/src/components/tab_manager.ts index 3cf06a7793..6ba938d2db 100644 --- a/apps/client/src/components/tab_manager.ts +++ b/apps/client/src/components/tab_manager.ts @@ -1,15 +1,16 @@ -import Component from "./component.js"; -import SpacedUpdate from "../services/spaced_update.js"; -import server from "../services/server.js"; -import options from "../services/options.js"; -import froca from "../services/froca.js"; -import treeService from "../services/tree.js"; -import NoteContext from "./note_context.js"; -import appContext from "./app_context.js"; -import Mutex from "../utils/mutex.js"; -import linkService from "../services/link.js"; -import type { EventData } from "./app_context.js"; import type FNote from "../entities/fnote.js"; +import { closeAllHeadlessAutocompletes } from "../services/autocomplete_core.js"; +import froca from "../services/froca.js"; +import linkService from "../services/link.js"; +import options from "../services/options.js"; +import server from "../services/server.js"; +import SpacedUpdate from "../services/spaced_update.js"; +import treeService from "../services/tree.js"; +import Mutex from "../utils/mutex.js"; +import type { EventData } from "./app_context.js"; +import appContext from "./app_context.js"; +import Component from "./component.js"; +import NoteContext from "./note_context.js"; interface TabState { contexts: NoteContext[]; @@ -429,10 +430,7 @@ export default class TabManager extends Component { } // close dangling autocompletes after closing the tab - const $autocompleteEl = $(".aa-input"); - if ("autocomplete" in $autocompleteEl) { - $autocompleteEl.autocomplete("close"); - } + closeAllHeadlessAutocompletes(); // close dangling tooltips $("body > div.tooltip").remove(); diff --git a/apps/client/src/services/attribute_autocomplete.ts b/apps/client/src/services/attribute_autocomplete.ts index 37b84c6e28..8784771d31 100644 --- a/apps/client/src/services/attribute_autocomplete.ts +++ b/apps/client/src/services/attribute_autocomplete.ts @@ -2,7 +2,7 @@ import type { AutocompleteApi as CoreAutocompleteApi, BaseItem } from "@algolia/ import { createAutocomplete } from "@algolia/autocomplete-core"; import type { AttributeType } from "../entities/fattribute.js"; -import { bindAutocompleteInput, createHeadlessPanelController, withHeadlessSourceDefaults } from "./autocomplete_core.js"; +import { bindAutocompleteInput, createHeadlessPanelController, registerHeadlessAutocompleteCloser, withHeadlessSourceDefaults } from "./autocomplete_core.js"; import server from "./server.js"; // --------------------------------------------------------------------------- @@ -51,6 +51,7 @@ function renderItems(panelEl: HTMLElement, items: NameItem[], activeItemId: numb li.textContent = item.name; li.addEventListener("mousedown", (e) => { e.preventDefault(); // prevent input blur + e.stopPropagation(); onSelect(item); }); list.appendChild(li); @@ -142,6 +143,11 @@ function initAttributeNameAutocomplete({ $el, attributeType, open, onValueChange }, }); + const unregisterGlobalCloser = registerHeadlessAutocompleteCloser(() => { + autocomplete.setIsOpen(false); + panelController.hide(); + }); + const cleanupInputBindings = bindAutocompleteInput({ inputEl, autocomplete, @@ -171,6 +177,7 @@ function initAttributeNameAutocomplete({ $el, attributeType, open, onValueChange }); const cleanup = () => { + unregisterGlobalCloser(); cleanupInputBindings(); panelController.destroy(); }; @@ -305,6 +312,11 @@ function initLabelValueAutocomplete({ $el, open, nameCallback, onValueChange }: }, }); + const unregisterGlobalCloser = registerHeadlessAutocompleteCloser(() => { + autocomplete.setIsOpen(false); + panelController.hide(); + }); + const cleanupInputBindings = bindAutocompleteInput({ inputEl, autocomplete, @@ -338,6 +350,7 @@ function initLabelValueAutocomplete({ $el, open, nameCallback, onValueChange }: }); const cleanup = () => { + unregisterGlobalCloser(); cleanupInputBindings(); panelController.destroy(); }; diff --git a/apps/client/src/services/autocomplete_core.ts b/apps/client/src/services/autocomplete_core.ts index c58ff68518..f06227cece 100644 --- a/apps/client/src/services/autocomplete_core.ts +++ b/apps/client/src/services/autocomplete_core.ts @@ -1,5 +1,9 @@ import type { AutocompleteApi, AutocompleteSource, BaseItem } from "@algolia/autocomplete-core"; +export const HEADLESS_AUTOCOMPLETE_PANEL_SELECTOR = ".aa-core-panel"; + +const headlessAutocompleteClosers = new Set<() => void>(); + export function withHeadlessSourceDefaults( source: AutocompleteSource ): AutocompleteSource { @@ -14,6 +18,20 @@ export function withHeadlessSourceDefaults( }; } +export function registerHeadlessAutocompleteCloser(close: () => void) { + headlessAutocompleteClosers.add(close); + + return () => { + headlessAutocompleteClosers.delete(close); + }; +} + +export function closeAllHeadlessAutocompletes() { + for (const close of Array.from(headlessAutocompleteClosers)) { + close(); + } +} + interface HeadlessPanelControllerOptions { inputEl: HTMLElement; container?: HTMLElement | null; diff --git a/apps/client/src/services/dialog.ts b/apps/client/src/services/dialog.ts index 8711ec1751..6c301cf125 100644 --- a/apps/client/src/services/dialog.ts +++ b/apps/client/src/services/dialog.ts @@ -1,9 +1,11 @@ import { Modal } from "bootstrap"; + import appContext from "../components/app_context.js"; import type { ConfirmDialogOptions, ConfirmDialogResult, ConfirmWithMessageOptions, MessageType } from "../widgets/dialogs/confirm.js"; -import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js"; -import { focusSavedElement, saveFocusedElement } from "./focus.js"; import { InfoExtraProps } from "../widgets/dialogs/info.jsx"; +import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js"; +import { closeAllHeadlessAutocompletes } from "./autocomplete_core.js"; +import { focusSavedElement, saveFocusedElement } from "./focus.js"; export async function openDialog($dialog: JQuery, closeActDialog = true, config?: Partial) { if (closeActDialog) { @@ -15,10 +17,7 @@ export async function openDialog($dialog: JQuery, closeActDialog = Modal.getOrCreateInstance($dialog[0], config).show(); $dialog.on("hidden.bs.modal", () => { - const $autocompleteEl = $(".aa-input"); - if ("autocomplete" in $autocompleteEl) { - $autocompleteEl.autocomplete("close"); - } + closeAllHeadlessAutocompletes(); if (!glob.activeDialog || glob.activeDialog === $dialog) { focusSavedElement(); diff --git a/apps/client/src/services/keyboard_actions.ts b/apps/client/src/services/keyboard_actions.ts index d447a90019..712cf3de1d 100644 --- a/apps/client/src/services/keyboard_actions.ts +++ b/apps/client/src/services/keyboard_actions.ts @@ -1,9 +1,10 @@ -import server from "./server.js"; -import appContext from "../components/app_context.js"; -import shortcutService, { ShortcutBinding } from "./shortcuts.js"; -import type Component from "../components/component.js"; import type { ActionKeyboardShortcut } from "@triliumnext/commons"; +import appContext from "../components/app_context.js"; +import type Component from "../components/component.js"; +import server from "./server.js"; +import shortcutService, { ShortcutBinding } from "./shortcuts.js"; + const keyboardActionRepo: Record = {}; const keyboardActionsLoaded = server.get("keyboard-actions").then((actions) => { @@ -51,7 +52,10 @@ async function setupActionsForElement(scope: string, $el: JQuery, c getActionsForScope("window").then((actions) => { for (const action of actions) { for (const shortcut of action.effectiveShortcuts ?? []) { - shortcutService.bindGlobalShortcut(shortcut, () => appContext.triggerCommand(action.actionName, { ntxId: appContext.tabManager.activeNtxId })); + shortcutService.bindGlobalShortcut(shortcut, () => { + const ntxId = appContext.tabManager?.activeNtxId ?? null; + appContext.triggerCommand(action.actionName, { ntxId }); + }); } } }); diff --git a/apps/client/src/services/note_autocomplete.ts b/apps/client/src/services/note_autocomplete.ts index a0fb1e2d3a..63a18a0952 100644 --- a/apps/client/src/services/note_autocomplete.ts +++ b/apps/client/src/services/note_autocomplete.ts @@ -3,7 +3,7 @@ import { createAutocomplete } from "@algolia/autocomplete-core"; import type { MentionFeedObjectItem } from "@triliumnext/ckeditor5"; import appContext from "../components/app_context.js"; -import { bindAutocompleteInput, createHeadlessPanelController, withHeadlessSourceDefaults } from "./autocomplete_core.js"; +import { bindAutocompleteInput, createHeadlessPanelController, registerHeadlessAutocompleteCloser, withHeadlessSourceDefaults } from "./autocomplete_core.js"; import commandRegistry from "./command_registry.js"; import froca from "./froca.js"; import { t } from "./i18n.js"; @@ -237,6 +237,7 @@ function renderItems( }; itemEl.onmousedown = (e) => { e.preventDefault(); + e.stopPropagation(); void onSelect(item); }; @@ -674,6 +675,11 @@ function initNoteAutocomplete($el: JQuery, options?: Options) { }, }); + const unregisterGlobalCloser = registerHeadlessAutocompleteCloser(() => { + autocomplete.setIsOpen(false); + panelController.hide(); + }); + const onCompositionStart = () => { isComposingInput = true; }; @@ -742,6 +748,7 @@ function initNoteAutocomplete($el: JQuery, options?: Options) { }); const cleanup = () => { + unregisterGlobalCloser(); cleanupInputBindings(); autocomplete.destroy(); panelController.destroy(); diff --git a/apps/client/src/widgets/attribute_widgets/attribute_detail.ts b/apps/client/src/widgets/attribute_widgets/attribute_detail.ts index 46514aa38f..cd4427e893 100644 --- a/apps/client/src/widgets/attribute_widgets/attribute_detail.ts +++ b/apps/client/src/widgets/attribute_widgets/attribute_detail.ts @@ -1,18 +1,19 @@ -import { t } from "../../services/i18n.js"; -import server from "../../services/server.js"; -import froca from "../../services/froca.js"; -import linkService from "../../services/link.js"; +import appContext from "../../components/app_context.js"; import attributeAutocompleteService from "../../services/attribute_autocomplete.js"; +import type { Attribute } from "../../services/attribute_parser.js"; +import { HEADLESS_AUTOCOMPLETE_PANEL_SELECTOR } from "../../services/autocomplete_core.js"; +import { isExperimentalFeatureEnabled } from "../../services/experimental_features.js"; +import { focusSavedElement, saveFocusedElement } from "../../services/focus.js"; +import froca from "../../services/froca.js"; +import { t } from "../../services/i18n.js"; +import linkService from "../../services/link.js"; import noteAutocompleteService from "../../services/note_autocomplete.js"; import promotedAttributeDefinitionParser from "../../services/promoted_attribute_definition_parser.js"; -import NoteContextAwareWidget from "../note_context_aware_widget.js"; +import server from "../../services/server.js"; +import shortcutService from "../../services/shortcuts.js"; import SpacedUpdate from "../../services/spaced_update.js"; 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"; -import { isExperimentalFeatureEnabled } from "../../services/experimental_features.js"; +import NoteContextAwareWidget from "../note_context_aware_widget.js"; const TPL = /*html*/`
@@ -372,7 +373,6 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget { } }); this.$inputName.on("change", () => this.userEditedAttribute()); - this.$inputName.on("autocomplete:closed", () => this.userEditedAttribute()); this.$inputName.on("focus", () => { attributeAutocompleteService.initAttributeNameAutocomplete({ @@ -478,7 +478,10 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget { this.$relatedNotesMoreNotes = this.$relatedNotesContainer.find(".related-notes-more-notes"); $(window).on("mousedown", (e) => { - if (!$(e.target).closest(this.$widget[0]).length && !$(e.target).closest(".algolia-autocomplete").length && !$(e.target).closest("#context-menu-container").length) { + if (!$(e.target).closest(this.$widget[0]).length + && !$(e.target).closest(".algolia-autocomplete").length + && !$(e.target).closest(HEADLESS_AUTOCOMPLETE_PANEL_SELECTOR).length + && !$(e.target).closest("#context-menu-container").length) { this.hide(); } });