obsidian#PopoverSuggest TypeScript Examples

The following examples show how to use obsidian#PopoverSuggest. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: query.ts    From obsidian-map-view with GNU General Public License v3.0 4 votes vote down vote up
export class QuerySuggest extends PopoverSuggest<Suggestion> {
    suggestionsDiv: HTMLDivElement;
    app: App;
    sourceElement: TextComponent;
    selection: Suggestion = null;
    lastSuggestions: Suggestion[];
    // Event handers that were registered, in the format of [name, lambda]
    eventHandlers: [string, any][] = [];

    constructor(app: App, sourceElement: TextComponent, scope?: Scope) {
        super(app, scope);
        this.app = app;
        this.sourceElement = sourceElement;
    }

    open() {
        this.suggestionsDiv = this.app.workspace.containerEl.createDiv({
            cls: 'suggestion-container mod-search-suggestion',
        });
        this.suggestionsDiv.style.position = 'fixed';
        this.suggestionsDiv.style.top =
            this.sourceElement.inputEl.getClientRects()[0].bottom + 'px';
        this.suggestionsDiv.style.left =
            this.sourceElement.inputEl.getClientRects()[0].left + 'px';
        const keyUp = async () => {
            // We do this in keyup because we want the selection to update first
            this.doSuggestIfNeeded();
        };
        const mouseUp = async () => {
            // We do this in keyup because we want the selection to update first
            this.doSuggestIfNeeded();
        };
        const keyDown = async (ev: KeyboardEvent) => {
            if (ev.key == 'Enter' && this.selection) {
                this.selectSuggestion(this.selection, ev);
                this.doSuggestIfNeeded();
            } else if (ev.key == 'ArrowDown' || ev.key == 'ArrowUp') {
                if (this.lastSuggestions.length == 0) return;
                let index = this.lastSuggestions.findIndex(
                    (value) => value == this.selection
                );
                const direction = ev.key == 'ArrowDown' ? 1 : -1;
                do {
                    index += direction;
                    if (index >= this.lastSuggestions.length) index = 0;
                    if (index < 0) index = this.lastSuggestions.length - 1;
                } while (this.lastSuggestions[index].group);
                this.updateSelection(this.lastSuggestions[index]);
                ev.preventDefault();
            }
        };
        this.eventHandlers.push(
            ['keyup', keyUp],
            ['mouseup', mouseUp],
            ['keydown', keyDown]
        );
        this.sourceElement.inputEl.addEventListener('keyup', keyUp);
        this.sourceElement.inputEl.addEventListener('mouseup', mouseUp);
        this.sourceElement.inputEl.addEventListener('keydown', keyDown);
        this.doSuggestIfNeeded();
    }

    doSuggestIfNeeded() {
        const suggestions = this.createSuggestions();
        suggestions.splice(consts.MAX_QUERY_SUGGESTIONS);
        if (!this.compareSuggestions(suggestions, this.lastSuggestions)) {
            this.clear();
            this.lastSuggestions = suggestions;
            this.renderSuggestions(suggestions, this.suggestionsDiv);
        }
    }

    compareSuggestions(suggestions1: Suggestion[], suggestions2: Suggestion[]) {
        if (!suggestions1 && !suggestions2) return true;
        if (!suggestions1 || !suggestions2) return false;
        if (suggestions1.length != suggestions2.length) return false;
        for (const [i, s1] of suggestions1.entries()) {
            const s2 = suggestions2[i];
            if (
                s1.text != s2.text ||
                s1.textToInsert != s2.textToInsert ||
                s1.append != s2.append ||
                s1.insertAt != s2.insertAt ||
                s1.insertSkip != s2.insertSkip
            )
                return false;
        }
        return true;
    }

    clear() {
        while (this.suggestionsDiv.firstChild)
            this.suggestionsDiv.removeChild(this.suggestionsDiv.firstChild);
        this.selection = null;
        this.lastSuggestions = [];
    }

    createSuggestions(): Suggestion[] {
        const cursorPos = this.sourceElement.inputEl.selectionStart;
        const input = this.sourceElement.getValue();
        const tagMatch = getTagUnderCursor(input, cursorPos);
        // Doesn't include a closing parenthesis
        const pathMatch = matchByPosition(
            input,
            /path:((\"([\w\s\/\-\\\.]*)\")|([\w\/\-\\\.]*))/g,
            cursorPos
        );
        const linkedToMatch = matchByPosition(
            input,
            /linkedto:((\"([\w\s\/\-\\\.]*)\")|([\w\/\-\\\.]*))/g,
            cursorPos
        );
        const linkedFromMatch = matchByPosition(
            input,
            /linkedfrom:((\"([\w\s\/\-\\\.]*)\")|([\w\/\-\\\.]*))/g,
            cursorPos
        );
        if (tagMatch) {
            const tagQuery = tagMatch[1] ?? '';
            // Return a tag name with the pound (#) sign removed if any
            const noPound = (tagName: string) => {
                return tagName.startsWith('#') ? tagName.substring(1) : tagName;
            };
            // Find all tags that include the query, with the pound sign removed, case insensitive
            const allTagNames = utils
                .getAllTagNames(this.app)
                .filter((value) =>
                    value
                        .toLowerCase()
                        .includes(noPound(tagQuery).toLowerCase())
                );
            let toReturn: Suggestion[] = [{ text: 'TAGS', group: true }];
            for (const tagName of allTagNames) {
                toReturn.push({
                    text: tagName,
                    textToInsert: `tag:${tagName} `,
                    insertAt: tagMatch.index,
                    insertSkip: tagMatch[0].length,
                });
            }
            return toReturn;
        } else if (pathMatch)
            return this.createPathSuggestions(pathMatch, 'path');
        else if (linkedToMatch)
            return this.createPathSuggestions(linkedToMatch, 'linkedto');
        else if (linkedFromMatch)
            return this.createPathSuggestions(linkedFromMatch, 'linkedfrom');
        else {
            return [
                { text: 'SEARCH OPERATORS', group: true },
                { text: 'tag:', append: '#' },
                { text: 'path:', textToInsert: 'path:""', cursorOffset: -1 },
                {
                    text: 'linkedto:',
                    textToInsert: 'linkedto:""',
                    cursorOffset: -1,
                },
                {
                    text: 'linkedfrom:',
                    textToInsert: 'linkedfrom:""',
                    cursorOffset: -1,
                },
                { text: 'LOGICAL OPERATORS', group: true },
                { text: 'AND', append: ' ' },
                { text: 'OR', append: ' ' },
                { text: 'NOT', append: ' ' },
            ];
        }
    }

    createPathSuggestions(
        pathMatch: RegExpMatchArray,
        operator: string
    ): Suggestion[] {
        const pathQuery = pathMatch[3] ?? pathMatch[4];
        const allPathNames = this.getAllPathNames(pathQuery);
        let toReturn: Suggestion[] = [{ text: 'PATHS', group: true }];
        for (const pathName of allPathNames) {
            toReturn.push({
                text: pathName,
                textToInsert: `${operator}:"${pathName}" `,
                insertAt: pathMatch.index,
                insertSkip: pathMatch[0].length,
            });
        }
        return toReturn;
    }

    renderSuggestions(suggestions: Suggestion[], el: HTMLElement) {
        for (const suggestion of suggestions) {
            const element = el.createDiv({
                cls: 'suggestion-item search-suggest-item',
            });
            if (suggestion.group) element.addClass('mod-group');
            suggestion.element = element;
            if (this.selection == suggestion) {
                element.addClass('is-selected');
                this.selection = suggestion;
            }
            element.addEventListener('mousedown', (event) => {
                this.selectSuggestion(suggestion, event);
            });
            element.addEventListener('mouseover', () => {
                this.updateSelection(suggestion);
            });
            this.renderSuggestion(suggestion, element);
        }
    }

    close() {
        this.suggestionsDiv.remove();
        this.clear();
        for (const [eventName, handler] of this.eventHandlers)
            this.sourceElement.inputEl.removeEventListener(eventName, handler);
    }

    updateSelection(newSelection: Suggestion) {
        if (this.selection && this.selection.element) {
            this.selection.element.removeClass('is-selected');
        }
        if (!newSelection.group) {
            newSelection.element?.addClass('is-selected');
            this.selection = newSelection;
        }
    }

    renderSuggestion(value: Suggestion, el: HTMLElement) {
        el.setText(value.text);
    }

    selectSuggestion(
        suggestion: Suggestion,
        event: MouseEvent | KeyboardEvent
    ) {
        // We don't use it, but need it here to inherit from QuerySuggest
        if (!suggestion.group) {
            const insertAt =
                suggestion.insertAt != null
                    ? suggestion.insertAt
                    : this.sourceElement.inputEl.selectionStart;
            const insertSkip = suggestion.insertSkip ?? 0;
            let addedText = suggestion.textToInsert ?? suggestion.text;
            addedText += suggestion.append ?? '';
            const currentText = this.sourceElement.getValue();
            const newText =
                currentText.substring(0, insertAt) +
                addedText +
                currentText.substring(insertAt + insertSkip);
            this.sourceElement.setValue(newText);
            this.sourceElement.inputEl.selectionEnd =
                this.sourceElement.inputEl.selectionStart =
                    insertAt +
                    addedText.length +
                    (suggestion?.cursorOffset ?? 0);
            // Don't allow a click to steal the focus from the text box
            event.preventDefault();
            // This causes the text area to scroll to the new cursor position
            this.sourceElement.inputEl.blur();
            this.sourceElement.inputEl.focus();
            // Refresh the suggestion box
            this.doSuggestIfNeeded();
            // Make the UI react to the change
            this.sourceElement.onChanged();
        }
    }

    getAllPathNames(search: string): string[] {
        const allFiles = this.app.vault.getFiles();
        let toReturn: string[] = [];
        for (const file of allFiles) {
            if (
                !search ||
                (search &&
                    file.path.toLowerCase().includes(search.toLowerCase()))
            )
                toReturn.push(file.path);
        }
        return toReturn;
    }
}