trilium/apps/client/src/services/note_autocomplete.ts
2026-03-11 16:48:57 +00:00

879 lines
28 KiB
TypeScript

import type { AutocompleteApi as CoreAutocompleteApi, BaseItem } from "@algolia/autocomplete-core";
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 commandRegistry from "./command_registry.js";
import froca from "./froca.js";
import { t } from "./i18n.js";
import noteCreateService from "./note_create.js";
import server from "./server.js";
// this key needs to have this value, so it's hit by the tooltip
const SELECTED_NOTE_PATH_KEY = "data-note-path";
const SELECTED_EXTERNAL_LINK_KEY = "data-external-link";
// To prevent search lag when there are a large number of notes, set a delay based on the number of notes to avoid jitter.
const notesCount = await server.get<number>(`autocomplete/notesCount`);
let debounceTimeoutId: ReturnType<typeof setTimeout>;
function getSearchDelay(notesCount: number): number {
const maxNotes = 20000;
const maxDelay = 1000;
const delay = Math.min(maxDelay, (notesCount / maxNotes) * maxDelay);
return delay;
}
let searchDelay = getSearchDelay(notesCount);
// TODO: Deduplicate with server.
export interface Suggestion extends BaseItem {
noteTitle?: string;
externalLink?: string;
notePathTitle?: string;
notePath?: string;
highlightedNotePathTitle?: string;
action?: string | "create-note" | "search-notes" | "external-link" | "command";
parentNoteId?: string;
icon?: string;
commandId?: string;
commandDescription?: string;
commandShortcut?: string;
attributeSnippet?: string;
highlightedAttributeSnippet?: string;
}
export interface Options {
container?: HTMLElement | null;
fastSearch?: boolean;
allowCreatingNotes?: boolean;
allowJumpToSearchNotes?: boolean;
allowExternalLinks?: boolean;
/** If set, hides the right-side button corresponding to go to selected note. */
hideGoToSelectedNoteButton?: boolean;
/** If set, hides all right-side buttons in the autocomplete dropdown */
hideAllButtons?: boolean;
/** If set, enables command palette mode */
isCommandPalette?: boolean;
}
// --- Headless Autocomplete Helpers ---
interface ManagedInstance {
autocomplete: CoreAutocompleteApi<Suggestion>;
panelEl: HTMLElement;
clearCursor: () => void;
isPanelOpen: () => boolean;
suppressNextClosedReset: () => void;
showQuery: (query: string) => void;
openRecentNotes: () => void;
cleanup: () => void;
}
const instanceMap = new WeakMap<HTMLElement, ManagedInstance>();
function escapeHtml(text: string): string {
return text
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
function sanitizeHighlightedHtml(text: string, { allowBreaks = false }: { allowBreaks?: boolean } = {}): string {
const sanitizedBreaks = allowBreaks
? text.replace(/<br\b[^>]*\/?>/gi, "<br>")
: text.replace(/<br\b[^>]*\/?>/gi, "");
return sanitizedBreaks
.replace(/<b\b[^>]*>/gi, "<b>")
.replace(/<\/b\s*>/gi, "</b>")
.replace(/<\/?[^>]+>/g, "");
}
function normalizeAttributeSnippet(snippet: string): string {
return sanitizeHighlightedHtml(snippet, { allowBreaks: true })
.replace(/<br\s*\/?>/gi, " <span class=\"aa-core-separator\">&middot;</span> ");
}
function getSuggestionIconClass(item: Suggestion): string {
if (item.action === "search-notes") {
return "bx bx-search";
}
if (item.action === "create-note") {
return "bx bx-plus";
}
if (item.action === "external-link") {
return "bx bx-link-external";
}
return item.icon || "bx bx-note";
}
function getSuggestionInputValue(item: Suggestion): string {
return item.noteTitle || item.notePathTitle || item.externalLink || "";
}
function renderCommandSuggestion(item: Suggestion): string {
const iconClass = escapeHtml(item.icon || "bx bx-terminal");
const titleHtml = item.highlightedNotePathTitle
? sanitizeHighlightedHtml(item.highlightedNotePathTitle)
: escapeHtml(item.noteTitle || "");
const descriptionHtml = item.commandDescription ? `<div class="command-description">${escapeHtml(item.commandDescription)}</div>` : "";
const shortcutHtml = item.commandShortcut ? `<kbd class="command-shortcut">${escapeHtml(item.commandShortcut)}</kbd>` : "";
return `
<div class="command-suggestion">
<span class="command-icon ${iconClass}"></span>
<div class="command-content">
<div class="command-name">${titleHtml}</div>
${descriptionHtml}
</div>
${shortcutHtml}
</div>
`;
}
function renderNoteSuggestion(item: Suggestion): string {
const iconClass = escapeHtml(getSuggestionIconClass(item));
const titleHtml = item.highlightedNotePathTitle
? sanitizeHighlightedHtml(item.highlightedNotePathTitle)
: escapeHtml(item.noteTitle || item.notePathTitle || item.externalLink || "");
const shortcutHtml = item.action === "search-notes"
? `<kbd class="aa-core-shortcut">Ctrl+Enter</kbd>`
: "";
const attributeHtml = item.highlightedAttributeSnippet
? `<div class="search-result-attributes">${normalizeAttributeSnippet(item.highlightedAttributeSnippet)}</div>`
: "";
const contentClass = item.action === "search-notes" ? "note-suggestion search-notes-action" : "note-suggestion";
return `
<div class="${contentClass}">
<span class="icon ${iconClass}"></span>
<span class="text">
<span class="aa-core-primary-row">
<span class="search-result-title">${titleHtml}</span>
${shortcutHtml}
</span>
${attributeHtml}
</span>
</div>
`;
}
function renderSuggestion(item: Suggestion): string {
if (item.action === "command") {
return renderCommandSuggestion(item);
}
return renderNoteSuggestion(item);
}
function createSuggestionSource(options: Options, onSelectItem: (item: Suggestion) => void) {
return withHeadlessSourceDefaults<Suggestion>({
sourceId: "note-suggestions",
async getItems({ query }: { query: string }) {
return await fetchResolvedSuggestions(query, options);
},
getItemInputValue({ item }: { item: Suggestion }) {
return getSuggestionInputValue(item);
},
onSelect({ item }: { item: Suggestion }) {
void onSelectItem(item);
}
});
}
function renderItems(
panelEl: HTMLElement,
items: Suggestion[],
activeId: number | null,
onSelect: (item: Suggestion) => void | Promise<void>,
onActivate: (index: number) => void,
onDeactivate: () => void
) {
if (items.length === 0) {
panelEl.style.display = "none";
return;
}
const list = document.createElement("div");
list.className = "aa-core-list aa-suggestions";
list.setAttribute("role", "listbox");
items.forEach((item, index) => {
const itemEl = document.createElement("div");
itemEl.className = "aa-core-item aa-suggestion";
itemEl.setAttribute("role", "option");
itemEl.setAttribute("aria-selected", index === activeId ? "true" : "false");
itemEl.dataset.index = String(index);
if (item.action) {
itemEl.classList.add(`${item.action}-action`);
}
if (index === activeId) {
itemEl.classList.add("aa-core-item--active", "aa-cursor");
}
itemEl.innerHTML = renderSuggestion(item);
itemEl.onmousemove = () => {
if (activeId === index) {
return;
}
onDeactivate();
window.setTimeout(() => {
onActivate(index);
}, 0);
};
itemEl.onmouseleave = (event) => {
const relatedTarget = event.relatedTarget;
if (relatedTarget instanceof HTMLElement && itemEl.contains(relatedTarget)) {
return;
}
onDeactivate();
};
itemEl.onmousedown = (e) => {
e.preventDefault();
void onSelect(item);
};
list.appendChild(itemEl);
});
panelEl.innerHTML = "";
panelEl.appendChild(list);
panelEl.style.display = "block";
}
async function autocompleteSourceForCKEditor(queryText: string) {
const rows = await fetchResolvedSuggestions(queryText, { allowCreatingNotes: true });
return rows.map((row) => {
return {
action: row.action,
noteTitle: row.noteTitle,
id: `@${row.notePathTitle}`,
name: row.notePathTitle || "",
link: `#${row.notePath}`,
notePath: row.notePath,
highlightedNotePathTitle: row.highlightedNotePathTitle
};
});
}
function getSearchingSuggestion(term: string): Suggestion[] {
if (term.trim().length === 0) {
return [];
}
return [
{
noteTitle: term,
highlightedNotePathTitle: t("quick-search.searching")
}
];
}
async function fetchResolvedSuggestions(term: string, options: Options = {}): Promise<Suggestion[]> {
// Check if we're in command mode
if (options.isCommandPalette && term.startsWith(">")) {
const commandQuery = term.substring(1).trim();
// Get commands (all if no query, filtered if query provided)
const commands = commandQuery.length === 0
? commandRegistry.getAllCommands()
: commandRegistry.searchCommands(commandQuery);
// Convert commands to suggestions
const commandSuggestions: Suggestion[] = commands.map(cmd => ({
action: "command",
commandId: cmd.id,
noteTitle: cmd.name,
notePathTitle: `>${cmd.name}`,
highlightedNotePathTitle: cmd.name,
commandDescription: cmd.description,
commandShortcut: cmd.shortcut,
icon: cmd.icon
}));
return commandSuggestions;
}
const fastSearch = options.fastSearch === false ? false : true;
if (fastSearch === false) {
if (term.trim().length === 0) {
return [];
}
}
const activeNoteId = appContext.tabManager.getActiveContextNoteId();
const length = term.trim().length;
let results = await server.get<Suggestion[]>(`autocomplete?query=${encodeURIComponent(term)}&activeNoteId=${activeNoteId}&fastSearch=${fastSearch}`);
options.fastSearch = true;
if (length >= 1 && options.allowCreatingNotes) {
results = [
{
action: "create-note",
noteTitle: term,
parentNoteId: activeNoteId || "root",
highlightedNotePathTitle: t("note_autocomplete.create-note", { term })
} as Suggestion
].concat(results);
}
if (length >= 1 && options.allowJumpToSearchNotes) {
results = results.concat([
{
action: "search-notes",
noteTitle: term,
highlightedNotePathTitle: t("note_autocomplete.search-for", { term })
}
]);
}
if (term.match(/^[a-z]+:\/\/.+/i) && options.allowExternalLinks) {
results = [
{
action: "external-link",
externalLink: term,
highlightedNotePathTitle: t("note_autocomplete.insert-external-link", { term })
} as Suggestion
].concat(results);
}
return results;
}
async function fetchSuggestionsWithDelay(term: string, options: Options): Promise<Suggestion[]> {
return await new Promise<Suggestion[]>((resolve) => {
clearTimeout(debounceTimeoutId);
debounceTimeoutId = setTimeout(async () => {
resolve(await fetchResolvedSuggestions(term, options));
}, searchDelay);
if (searchDelay === 0) {
searchDelay = getSearchDelay(notesCount);
}
});
}
function resetSelectionState($el: JQuery<HTMLElement>) {
$el.setSelectedNotePath("");
$el.setSelectedExternalLink(null);
}
function getManagedInstance($el: JQuery<HTMLElement>): ManagedInstance | null {
const inputEl = $el[0] as HTMLInputElement | undefined;
return inputEl ? (instanceMap.get(inputEl) ?? null) : null;
}
async function handleSuggestionSelection(
$el: JQuery<HTMLElement>,
autocomplete: CoreAutocompleteApi<Suggestion>,
inputEl: HTMLInputElement,
suggestion: Suggestion
) {
if (suggestion.action === "command") {
autocomplete.setIsOpen(false);
$el.trigger("autocomplete:commandselected", [suggestion]);
return;
}
if (suggestion.action === "external-link") {
$el.setSelectedNotePath(null);
$el.setSelectedExternalLink(suggestion.externalLink ?? null);
inputEl.value = suggestion.externalLink ?? "";
autocomplete.setIsOpen(false);
$el.trigger("autocomplete:externallinkselected", [suggestion]);
return;
}
if (suggestion.action === "create-note") {
const { success, noteType, templateNoteId, notePath } = await noteCreateService.chooseNoteType();
if (!success) {
return;
}
const { note } = await noteCreateService.createNote(notePath || suggestion.parentNoteId, {
title: suggestion.noteTitle,
activate: false,
type: noteType,
templateNoteId
});
const hoistedNoteId = appContext.tabManager.getActiveContext()?.hoistedNoteId;
suggestion.notePath = note?.getBestNotePathString(hoistedNoteId);
}
if (suggestion.action === "search-notes") {
const searchString = suggestion.noteTitle;
autocomplete.setIsOpen(false);
await appContext.triggerCommand("searchNotes", { searchString });
return;
}
$el.setSelectedNotePath(suggestion.notePath || "");
$el.setSelectedExternalLink(null);
inputEl.value = suggestion.noteTitle || getSuggestionInputValue(suggestion);
autocomplete.setIsOpen(false);
$el.trigger("autocomplete:noteselected", [suggestion]);
}
export function clearText($el: JQuery<HTMLElement>) {
searchDelay = 0;
resetSelectionState($el);
const inputEl = $el[0] as HTMLInputElement;
const instance = getManagedInstance($el);
if (instance) {
if (instance.isPanelOpen()) {
instance.suppressNextClosedReset();
}
inputEl.value = "";
instance.clearCursor();
instance.autocomplete.setQuery("");
instance.autocomplete.setIsOpen(false);
instance.autocomplete.refresh();
$el.trigger("change");
}
}
function setText($el: JQuery<HTMLElement>, text: string) {
resetSelectionState($el);
const instance = getManagedInstance($el);
if (instance) {
instance.showQuery(text.trim());
}
}
function showRecentNotes($el: JQuery<HTMLElement>) {
searchDelay = 0;
resetSelectionState($el);
const instance = getManagedInstance($el);
if (instance) {
instance.openRecentNotes();
}
$el.trigger("focus");
}
function showAllCommands($el: JQuery<HTMLElement>) {
searchDelay = 0;
resetSelectionState($el);
const instance = getManagedInstance($el);
if (instance) {
instance.showQuery(">");
}
}
function fullTextSearch($el: JQuery<HTMLElement>, options: Options) {
const inputEl = $el[0] as HTMLInputElement;
const searchString = inputEl.value;
if (options.fastSearch === false || searchString.trim().length === 0) {
return;
}
$el.trigger("focus");
options.fastSearch = false;
searchDelay = 0;
resetSelectionState($el);
const instance = getManagedInstance($el);
if (instance) {
instance.clearCursor();
instance.autocomplete.setQuery("");
inputEl.value = "";
instance.showQuery(searchString);
}
}
function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
$el.addClass("note-autocomplete-input");
const inputEl = $el[0] as HTMLInputElement;
if (instanceMap.has(inputEl)) {
$el
.off("autocomplete:noteselected")
.off("autocomplete:externallinkselected")
.off("autocomplete:commandselected");
return $el;
}
options = options || {};
let isComposingInput = false;
const panelController = createHeadlessPanelController({
inputEl,
container: options.container,
className: "aa-core-panel aa-dropdown-menu"
});
const { panelEl } = panelController;
let currentQuery = inputEl.value;
let shouldAutoselectTopItem = false;
let shouldMirrorActiveItemToInput = false;
let wasPanelOpen = false;
let suppressNextClosedEmptyReset = false;
let suggestionRequestId = 0;
const clearCursor = () => {
shouldMirrorActiveItemToInput = false;
autocomplete.setActiveItemId(null);
inputEl.value = currentQuery;
};
const suppressNextClosedReset = () => {
suppressNextClosedEmptyReset = true;
};
const prepareForQueryChange = () => {
shouldAutoselectTopItem = true;
shouldMirrorActiveItemToInput = false;
};
const rerunQuery = (query: string) => {
if (!query.trim().length) {
openRecentNotes();
return;
}
prepareForQueryChange();
currentQuery = "";
inputEl.value = "";
autocomplete.setQuery("");
showQuery(query);
};
const onSelectItem = async (item: Suggestion) => {
await handleSuggestionSelection($el, autocomplete, inputEl, item);
};
const source = createSuggestionSource(options, onSelectItem);
const showQuery = (query: string) => {
prepareForQueryChange();
inputEl.value = query;
autocomplete.setQuery(query);
autocomplete.setIsOpen(true);
autocomplete.refresh();
};
const openRecentNotes = () => {
resetSelectionState($el);
prepareForQueryChange();
inputEl.value = "";
autocomplete.setQuery("");
autocomplete.setActiveItemId(null);
fetchResolvedSuggestions("", options).then((items) => {
autocomplete.setCollections([{ source, items }]);
autocomplete.setActiveItemId(items.length > 0 ? 0 : null);
autocomplete.setIsOpen(items.length > 0);
});
};
const autocomplete = createAutocomplete<Suggestion>({
openOnFocus: false, // Wait until we explicitly focus or type
// Old autocomplete.js used `autoselect: true`, so the first item
// should be immediately selectable when the panel opens.
defaultActiveItemId: 0,
shouldPanelOpen() {
return true;
},
getSources({ query }) {
return [
{
...source,
async getItems() {
if (isComposingInput) {
return [];
}
if (options.fastSearch === false && query.trim().length > 0) {
const requestId = ++suggestionRequestId;
void fetchSuggestionsWithDelay(query, options).then((items) => {
if (requestId !== suggestionRequestId || currentQuery !== query) {
return;
}
autocomplete.setCollections([{ source, items }]);
autocomplete.setIsOpen(items.length > 0);
});
return getSearchingSuggestion(query);
}
return await fetchSuggestionsWithDelay(query, options);
}
},
];
},
onStateChange({ state }) {
const collections = state.collections;
const items = collections.length > 0 ? (collections[0].items as Suggestion[]) : [];
const activeId = state.activeItemId ?? null;
const activeItem = activeId !== null ? items[activeId] : null;
currentQuery = state.query;
const isPanelOpen = state.isOpen && items.length > 0;
if (isPanelOpen !== wasPanelOpen) {
wasPanelOpen = isPanelOpen;
if (isPanelOpen) {
$el.trigger("autocomplete:opened");
if (inputEl.readOnly) {
suppressNextClosedReset();
autocomplete.setIsOpen(false);
return;
}
} else {
$el.trigger("autocomplete:closed");
if (suppressNextClosedEmptyReset) {
suppressNextClosedEmptyReset = false;
} else if (!String(inputEl.value).trim()) {
searchDelay = 0;
resetSelectionState($el);
$el.trigger("change");
}
}
}
if (activeItem && shouldMirrorActiveItemToInput) {
inputEl.value = getSuggestionInputValue(activeItem);
} else {
inputEl.value = state.query;
}
if (isPanelOpen) {
renderItems(panelEl, items, activeId, (item) => {
void onSelectItem(item);
}, (index) => {
autocomplete.setActiveItemId(index);
}, () => {
clearCursor();
});
if (shouldAutoselectTopItem && activeId === null) {
shouldAutoselectTopItem = false;
shouldMirrorActiveItemToInput = false;
autocomplete.setActiveItemId(0);
return;
}
panelController.startPositioning();
} else {
shouldAutoselectTopItem = false;
panelController.hide();
}
},
});
const onCompositionStart = () => {
isComposingInput = true;
};
const onCompositionEnd = (e: CompositionEvent) => {
isComposingInput = false;
rerunQuery(inputEl.value);
};
const cleanupInputBindings = bindAutocompleteInput<Suggestion>({
inputEl,
autocomplete,
onInput(e, handlers) {
const value = (e.currentTarget as HTMLInputElement).value;
if (value.trim().length === 0) {
openRecentNotes();
return;
}
prepareForQueryChange();
handlers.onChange(e as any);
},
onFocus(e, handlers) {
if (inputEl.readOnly) {
autocomplete.setIsOpen(false);
panelController.hide();
return;
}
handlers.onFocus(e as any);
},
onBlur() {
if (options.container) {
return;
}
setTimeout(() => {
autocomplete.setIsOpen(false);
panelController.hide();
}, 50);
},
onKeyDown(e, handlers) {
if (options.allowJumpToSearchNotes && e.ctrlKey && e.key === "Enter") {
e.stopImmediatePropagation();
e.preventDefault();
void handleSuggestionSelection($el, autocomplete, inputEl, {
action: "search-notes",
noteTitle: inputEl.value
});
return;
}
if (e.shiftKey && e.key === "Enter") {
e.stopImmediatePropagation();
e.preventDefault();
fullTextSearch($el, options);
return;
}
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
shouldMirrorActiveItemToInput = true;
}
handlers.onKeyDown(e as any);
},
extraBindings: [
{ type: "compositionstart", listener: onCompositionStart },
{ type: "compositionend", listener: onCompositionEnd }
]
});
const cleanup = () => {
cleanupInputBindings();
autocomplete.destroy();
panelController.destroy();
};
instanceMap.set(inputEl, {
autocomplete,
panelEl,
clearCursor,
isPanelOpen: () => wasPanelOpen,
suppressNextClosedReset,
showQuery,
openRecentNotes,
cleanup
});
// Buttons UI logic
const $clearTextButton = $("<a>").addClass("input-group-text input-clearer-button bx bxs-tag-x").prop("title", t("note_autocomplete.clear-text-field"));
const $showRecentNotesButton = $("<a>").addClass("input-group-text show-recent-notes-button bx bx-time").prop("title", t("note_autocomplete.show-recent-notes"));
const $fullTextSearchButton = $("<a>").addClass("input-group-text full-text-search-button bx bx-search").prop("title", `${t("note_autocomplete.full-text-search")} (Shift+Enter)`);
const $goToSelectedNoteButton = $("<a>").addClass("input-group-text go-to-selected-note-button bx bx-arrow-to-right");
if (!options.hideAllButtons) {
$el.after($clearTextButton).after($showRecentNotesButton).after($fullTextSearchButton);
}
if (!options.hideGoToSelectedNoteButton && !options.hideAllButtons) {
$el.after($goToSelectedNoteButton);
}
$clearTextButton.on("click", () => clearText($el));
$showRecentNotesButton.on("click", (e) => {
showRecentNotes($el);
return false;
});
$fullTextSearchButton.on("click", (e) => {
fullTextSearch($el, options!);
return false;
});
return $el;
}
export function destroyAutocomplete($el: JQuery<HTMLElement> | HTMLElement) {
const inputEl = $el instanceof HTMLElement ? $el : $el[0] as HTMLInputElement;
const instance = instanceMap.get(inputEl);
if (instance) {
instance.cleanup();
instanceMap.delete(inputEl);
}
}
function init() {
$.fn.getSelectedNotePath = function () {
if (!String($(this).val())?.trim()) {
return "";
} else {
return $(this).attr(SELECTED_NOTE_PATH_KEY);
}
};
$.fn.getSelectedNoteId = function () {
const $el = $(this as unknown as HTMLElement);
const notePath = $el.getSelectedNotePath();
if (!notePath) {
return null;
}
const chunks = notePath.split("/");
return chunks.length >= 1 ? chunks[chunks.length - 1] : null;
};
$.fn.setSelectedNotePath = function (notePath) {
notePath = notePath || "";
$(this).attr(SELECTED_NOTE_PATH_KEY, notePath);
$(this).closest(".input-group").find(".go-to-selected-note-button").toggleClass("disabled", !notePath.trim()).attr("href", `#${notePath}`); // we also set href here so tooltip can be displayed
};
$.fn.getSelectedExternalLink = function () {
if (!String($(this).val())?.trim()) {
return "";
} else {
return $(this).attr(SELECTED_EXTERNAL_LINK_KEY);
}
};
$.fn.setSelectedExternalLink = function (externalLink: string | null) {
$(this).attr(SELECTED_EXTERNAL_LINK_KEY, externalLink);
$(this).closest(".input-group").find(".go-to-selected-note-button").toggleClass("disabled", true);
};
$.fn.setNote = async function (noteId) {
const note = noteId ? await froca.getNote(noteId, true) : null;
const $el = $(this as unknown as HTMLElement);
const instance = getManagedInstance($el);
const noteTitle = note ? note.title : "";
$el
.val(noteTitle)
.setSelectedNotePath(noteId);
if (instance) {
instance.clearCursor();
instance.autocomplete.setQuery(noteTitle);
instance.autocomplete.setIsOpen(false);
}
};
}
/**
* Convenience function which triggers the display of recent notes in the autocomplete input and focuses it.
*
* @param inputElement - The input element to trigger recent notes on.
*/
export function triggerRecentNotes(inputElement: HTMLInputElement | null | undefined) {
if (!inputElement) {
return;
}
const $el = $(inputElement);
showRecentNotes($el);
$el.trigger("focus").trigger("select");
}
export default {
autocompleteSourceForCKEditor,
clearText,
destroyAutocomplete,
initNoteAutocomplete,
showRecentNotes,
showAllCommands,
setText,
init
};