mirror of
https://github.com/zadam/trilium.git
synced 2026-03-13 11:53:38 +01:00
refactor: add new autocomplete registry
This commit is contained in:
parent
2dfc4514b0
commit
1b013e6888
@ -1,19 +1,135 @@
|
||||
import { autocomplete } from "@algolia/autocomplete-js";
|
||||
import type { AutocompleteApi } from "@algolia/autocomplete-js";
|
||||
import type { BaseItem } from "@algolia/autocomplete-core";
|
||||
import type { AttributeType } from "../entities/fattribute.js";
|
||||
import server from "./server.js";
|
||||
|
||||
interface InitOptions {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Global instance registry for "close all" functionality
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const activeInstances = new Set<AutocompleteApi<any>>();
|
||||
|
||||
export function closeAllAttributeAutocompletes(): void {
|
||||
for (const api of activeInstances) {
|
||||
api.setIsOpen(false);
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const instanceMap = new WeakMap<HTMLElement, AutocompleteApi<any>>();
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface NameItem extends BaseItem {
|
||||
name: string;
|
||||
}
|
||||
|
||||
/** New API: pass a container div, autocomplete-js creates its own input inside. */
|
||||
interface NewInitOptions {
|
||||
container: HTMLElement;
|
||||
attributeType?: AttributeType | (() => AttributeType);
|
||||
open: boolean;
|
||||
onValueChange?: (value: string) => void;
|
||||
}
|
||||
|
||||
/** Old API: pass a jQuery input element, uses legacy autocomplete.js plugin. */
|
||||
interface OldInitOptions {
|
||||
$el: JQuery<HTMLElement>;
|
||||
attributeType?: AttributeType | (() => AttributeType);
|
||||
open: boolean;
|
||||
nameCallback?: () => string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $el - element on which to init autocomplete
|
||||
* @param attributeType - "relation" or "label" or callback providing one of those values as a type of autocompleted attributes
|
||||
* @param open - should the autocomplete be opened after init?
|
||||
*/
|
||||
function initAttributeNameAutocomplete({ $el, attributeType, open }: InitOptions) {
|
||||
type InitAttributeNameOptions = NewInitOptions | OldInitOptions;
|
||||
|
||||
function isNewApi(opts: InitAttributeNameOptions): opts is NewInitOptions {
|
||||
return "container" in opts;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Attribute name autocomplete
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function initAttributeNameAutocomplete(opts: InitAttributeNameOptions) {
|
||||
if (isNewApi(opts)) {
|
||||
initAttributeNameNew(opts);
|
||||
} else {
|
||||
initAttributeNameLegacy(opts);
|
||||
}
|
||||
}
|
||||
|
||||
/** New implementation using @algolia/autocomplete-js */
|
||||
function initAttributeNameNew({ container, attributeType, open, onValueChange }: NewInitOptions) {
|
||||
// Only init once per container
|
||||
if (instanceMap.has(container)) {
|
||||
if (open) {
|
||||
const api = instanceMap.get(container)!;
|
||||
api.setIsOpen(true);
|
||||
api.refresh();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const api = autocomplete<NameItem>({
|
||||
container,
|
||||
panelContainer: document.body,
|
||||
openOnFocus: true,
|
||||
detachedMediaQuery: "none",
|
||||
placeholder: "",
|
||||
classNames: {
|
||||
input: "form-control",
|
||||
},
|
||||
|
||||
getSources({ query }) {
|
||||
return [
|
||||
{
|
||||
sourceId: "attribute-names",
|
||||
getItems() {
|
||||
const type = typeof attributeType === "function" ? attributeType() : attributeType;
|
||||
return server
|
||||
.get<string[]>(`attribute-names/?type=${type}&query=${encodeURIComponent(query)}`)
|
||||
.then((names) => names.map((name) => ({ name })));
|
||||
},
|
||||
getItemInputValue({ item }) {
|
||||
return item.name;
|
||||
},
|
||||
onSelect({ item }) {
|
||||
onValueChange?.(item.name);
|
||||
},
|
||||
templates: {
|
||||
item({ item, html }) {
|
||||
return html`<div>${item.name}</div>`;
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
onStateChange({ state, prevState }) {
|
||||
if (!state.isOpen && prevState.isOpen) {
|
||||
onValueChange?.(state.query);
|
||||
}
|
||||
},
|
||||
|
||||
shouldPanelOpen() {
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
instanceMap.set(container, api);
|
||||
activeInstances.add(api);
|
||||
|
||||
if (open) {
|
||||
api.setIsOpen(true);
|
||||
api.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
/** Legacy implementation using old autocomplete.js jQuery plugin */
|
||||
function initAttributeNameLegacy({ $el, attributeType, open }: OldInitOptions) {
|
||||
if (!$el.hasClass("aa-input")) {
|
||||
$el.autocomplete(
|
||||
{
|
||||
@ -26,14 +142,11 @@ function initAttributeNameAutocomplete({ $el, attributeType, open }: InitOptions
|
||||
[
|
||||
{
|
||||
displayKey: "name",
|
||||
// disabling cache is important here because otherwise cache can stay intact when switching between attribute type which will lead to autocomplete displaying attribute names for incorrect attribute type
|
||||
cache: false,
|
||||
source: async (term, cb) => {
|
||||
const type = typeof attributeType === "function" ? attributeType() : attributeType;
|
||||
|
||||
const names = await server.get<string[]>(`attribute-names/?type=${type}&query=${encodeURIComponent(term)}`);
|
||||
const result = names.map((name) => ({ name }));
|
||||
|
||||
cb(result);
|
||||
}
|
||||
}
|
||||
@ -52,10 +165,18 @@ function initAttributeNameAutocomplete({ $el, attributeType, open }: InitOptions
|
||||
}
|
||||
}
|
||||
|
||||
async function initLabelValueAutocomplete({ $el, open, nameCallback }: InitOptions) {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Label value autocomplete (still using old autocomplete.js)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface LabelValueInitOptions {
|
||||
$el: JQuery<HTMLElement>;
|
||||
open: boolean;
|
||||
nameCallback?: () => string;
|
||||
}
|
||||
|
||||
async function initLabelValueAutocomplete({ $el, open, nameCallback }: LabelValueInitOptions) {
|
||||
if ($el.hasClass("aa-input")) {
|
||||
// we reinit every time because autocomplete seems to have a bug where it retains state from last
|
||||
// open even though the value was reset
|
||||
$el.autocomplete("destroy");
|
||||
}
|
||||
|
||||
@ -78,7 +199,7 @@ async function initLabelValueAutocomplete({ $el, open, nameCallback }: InitOptio
|
||||
{
|
||||
appendTo: document.querySelector("body"),
|
||||
hint: false,
|
||||
openOnFocus: false, // handled manually
|
||||
openOnFocus: false,
|
||||
minLength: 0,
|
||||
tabAutocomplete: false
|
||||
},
|
||||
@ -88,9 +209,7 @@ async function initLabelValueAutocomplete({ $el, open, nameCallback }: InitOptio
|
||||
cache: false,
|
||||
source: async function (term, cb) {
|
||||
term = term.toLowerCase();
|
||||
|
||||
const filtered = attributeValues.filter((attr) => attr.value.toLowerCase().includes(term));
|
||||
|
||||
cb(filtered);
|
||||
}
|
||||
}
|
||||
@ -108,7 +227,35 @@ async function initLabelValueAutocomplete({ $el, open, nameCallback }: InitOptio
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Utilities for the new autocomplete-js containers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function getInput(container: HTMLElement): HTMLInputElement | null {
|
||||
return container.querySelector<HTMLInputElement>(".aa-Input");
|
||||
}
|
||||
|
||||
function setInputValue(container: HTMLElement, value: string): void {
|
||||
const input = getInput(container);
|
||||
if (input) {
|
||||
input.value = value;
|
||||
}
|
||||
const api = instanceMap.get(container);
|
||||
if (api) {
|
||||
api.setQuery(value);
|
||||
}
|
||||
}
|
||||
|
||||
function getInputValue(container: HTMLElement): string {
|
||||
const input = getInput(container);
|
||||
return input?.value ?? "";
|
||||
}
|
||||
|
||||
export default {
|
||||
initAttributeNameAutocomplete,
|
||||
initLabelValueAutocomplete
|
||||
initLabelValueAutocomplete,
|
||||
closeAllAttributeAutocompletes,
|
||||
getInput,
|
||||
setInputValue,
|
||||
getInputValue,
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user