All files / lib/util input.ts

98.88% Statements 89/90
100% Branches 29/29
90% Functions 9/10
98.88% Lines 89/90

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 911x 1x 1x 68x 33x 1x 1x 35x   1x 1x 6x 6x 6x 1x 1x 1x 1x 1x 24x 24x 97x 97x 97x 24x 24x 53x 53x 53x 53x 53x 53x 53x 53x 24x 30x 30x 4x 4x 4x 30x 30x 24x 24x 24x 1x 1x 1x 1x 1x 35x 35x 35x 35x 35x 35x 35x 35x 35x 311x 311x 311x 311x 28x 28x 28x 28x 28x 28x 28x 28x 28x 25x 25x 24x 24x 28x 24x 24x 24x 24x 24x 24x 28x 311x 311x 287x 287x 35x 35x  
import { AcceptedInputType, ActiveTrigger, Item, Trigger } from '../types';
 
export function getInputSelectionStart(input: AcceptedInputType): number {
    return input.selectionStart;
}
 
export function getInputValue(input: AcceptedInputType) {
    return input.value;
}
 
export function setInputValue(input: AcceptedInputType, value: string) {
    input.value = value;
    input.dispatchEvent(new Event('input'));
}
 
/**
 * Search items by matching the search string with the item value.
 */
export function searchItems(trigger: Trigger, search: string): Item[] {
    return trigger.items
        .filter((item) =>
            (item.searchMatch ?? item.value)
                .toLowerCase()
                .includes(search.toLowerCase())
        )
        .map((item) => ({
            item,
            score: [
                (item.searchMatch ?? item.value)
                    .toLowerCase()
                    .indexOf(search.toLowerCase()),
                item.value.length,
            ],
        }))
        .sort((a, b) => {
            // Return item whose match is closer to the beginning of the string
            if (a.score[0] !== b.score[0]) {
                return a.score[0] - b.score[0];
            }
            // Return shorter item
            return a.score[1] - b.score[1];
        })
        .map(({ item }) => item)
        .slice(0, trigger.maxRenderedItems ?? Infinity);
}
 
/**
 * Get the current active trigger for a given input
 */
export function getActiveTrigger(
    input: AcceptedInputType,
    triggers: Trigger[],
    maxSearchLength: number
): null | ActiveTrigger {
    // Start from cursor position and search for trigger keys. When found, if search regex until cursor position matches, activate suggestions
    const inputValue = getInputValue(input);
    const cursorPosition = getInputSelectionStart(input);
    let index = cursorPosition;
    while (index >= 0 && index > cursorPosition - maxSearchLength) {
        // For each potential trigger key
        for (const trigger of triggers) {
            // If the current character is the trigger key
            if (inputValue[index] === trigger.char) {
                // If the following characters until cursor position match the search regex
                const search = inputValue.substring(index + 1, cursorPosition);
                const searchRegExp =
                    trigger.searchRegExp?.source ?? /\S*/.source;
                const regexp = new RegExp('^' + searchRegExp + '$');
                const { whitespaceBefore = true } = trigger;
                if (
                    // Matching regexp
                    search.match(regexp) &&
                    // Whitespace before trigger
                    (!whitespaceBefore ||
                        index === 0 ||
                        /\s/.test(inputValue[index - 1]))
                ) {
                    return {
                        trigger,
                        search,
                        index,
                    };
                }
            }
        }
        --index;
    }
 
    return null;
}