All files / lib/util input.ts

99.04% Statements 104/105
96.15% Branches 25/26
90% Functions 9/10
99.04% Lines 104/105

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 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 1061x 1x 1x 1x 1x 1x 1x 1x 50x 27x 1x 1x 23x   1x 1x 6x 6x 6x 6x 6x 6x 6x 6x 1x 1x 1x 1x 1x 14x 14x 56x 56x 56x 14x 14x 26x 26x 26x 26x 26x 26x 26x 26x 14x 13x 13x 2x 2x 2x 13x 13x 14x 14x 14x 1x 1x 1x 1x 1x 23x 23x 23x 23x 23x 23x 23x 23x 23x 2x 2x 2x 23x 23x 195x 195x 195x 195x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 14x 14x 14x 14x 14x 14x 15x 195x 195x 181x 181x 22x 23x  
import { AcceptedInputType, ActiveTrigger, Item, Trigger } from '../types';
 
/**
 * @returns null for input types that expose no caret, such as email or number
 */
export function getInputSelectionStart(
    input: AcceptedInputType
): number | null {
    return input.selectionStart;
}
 
export function getInputValue(input: AcceptedInputType) {
    return input.value;
}
 
export function setInputValue(
    input: AcceptedInputType,
    value: string,
    selectionStart: number
) {
    input.value = value;
    input.setSelectionRange(selectionStart, selectionStart);
    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);
    // No caret means no trigger, which also keeps `setInputValue` (and its `setSelectionRange`, which those types reject) out of reach
    if (cursorPosition === null) {
        return null;
    }
 
    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;
}