import {offset, type Offset} from "caret-pos"; import {Component} from "preact"; import {type UserLabelsData} from "../../storage/exports.js"; import {log, querySelectorAll} from "../../utilities/exports.js"; type Props = { /** * Whether the Anonymize Usernames feature is enabled, in which case this * feature needs to handle collecting usernames a little differently. */ anonymizeUsernamesEnabled: boolean; /** The list of known groups to use for the group autocompletions. */ knownGroups: Set; /** * All the User Labels the user has saved to use for additional username * completions. */ userLabels: UserLabelsData; }; type State = { /** All the groups without leading tildes. */ groups: Set; /** Whether the group autocompletion list is hidden or not. */ groupsHidden: boolean; /** The current set of group matches. */ groupsMatches: Set; /** The position where the group autocompletion list should be shown. */ groupsPosition: Offset | undefined; /** All the usernames without leading @-symbols. */ usernames: Set; /** Whether the username autocompletion list is hidden or not. */ usernamesHidden: boolean; /** The current set of username matches. */ usernamesMatches: Set; /** The position where the username autocompletion list should be shown. */ usernamesPosition: Offset | undefined; }; export class AutocompleteFeature extends Component { constructor(props: Props) { super(props); // Get all the groups without their leading tildes. const groups = Array.from(props.knownGroups).map((value) => value.startsWith("~") ? value.slice(1) : value, ); const usernames = [ // Get all the usernames on the page without their leading @-symbols. ...querySelectorAll(".link-user").map((value) => { if (props.anonymizeUsernamesEnabled) { return (value.dataset.trxUsername ?? "").toLowerCase(); } return value.textContent!.replace(/^@/, "").toLowerCase(); }), // Get all the usernames from the saved User Labels. ...props.userLabels.map(({value}) => value.username), ].sort((a, b) => a.localeCompare(b)); this.state = { groups: new Set(groups), groupsHidden: true, groupsMatches: new Set(groups), groupsPosition: undefined, usernames: new Set(usernames), usernamesHidden: true, usernamesMatches: new Set(usernames), usernamesPosition: undefined, }; document.addEventListener("keydown", this.globalInputHandler); document.addEventListener("compositionupdate", this.globalInputHandler); log( `Autocomplete: Initialized with ${this.state.groups.size} groups and ` + `${this.state.usernames.size} usernames.`, ); } /** * The global input handler for `keydown` and `compositionupdate` events. * * See https://gitlab.com/tildes-community/tildes-reextended/-/issues/31 for * why we also need to listen for `compositionupdate`. */ globalInputHandler = (event: CompositionEvent | KeyboardEvent) => { const textarea = event.target; // Only add the autocompletes to textareas. if (!(textarea instanceof HTMLTextAreaElement)) { return; } // Helper function to create autocompletes with. const createHandler = ( prefix: string, target: string, values: Set, ) => { const dataAttribute = `data-trx-autocomplete-${target}`; // Get the key that was pressed. const key = event instanceof KeyboardEvent ? event.key : event.data; if (key === prefix && !textarea.getAttribute(dataAttribute)) { textarea.setAttribute(dataAttribute, "true"); textarea.addEventListener("keyup", (event) => { if (!(event.target instanceof HTMLTextAreaElement)) { return; } this.textareaInputHandler(event.target, prefix, target, values); }); this.textareaInputHandler(textarea, prefix, target, values); } }; createHandler("~", "groups", this.state.groups); createHandler("@", "usernames", this.state.usernames); }; /** The input handler for any `