Add more rigorous documentation to the Autocomplete feature.
This commit is contained in:
parent
6af9af9583
commit
ec46b3b373
|
@ -4,19 +4,45 @@ import {type UserLabelsData} from "../../storage/exports.js";
|
||||||
import {log, querySelectorAll} from "../../utilities/exports.js";
|
import {log, querySelectorAll} from "../../utilities/exports.js";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
/**
|
||||||
|
* Whether the Anonymize Usernames feature is enabled, in which case this
|
||||||
|
* feature needs to handle collecting usernames a little differently.
|
||||||
|
*/
|
||||||
anonymizeUsernamesEnabled: boolean;
|
anonymizeUsernamesEnabled: boolean;
|
||||||
|
|
||||||
|
/** The list of known groups to use for the group autocompletions. */
|
||||||
knownGroups: Set<string>;
|
knownGroups: Set<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All the User Labels the user has saved to use for additional username
|
||||||
|
* completions.
|
||||||
|
*/
|
||||||
userLabels: UserLabelsData;
|
userLabels: UserLabelsData;
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
|
/** All the groups without leading tildes. */
|
||||||
groups: Set<string>;
|
groups: Set<string>;
|
||||||
|
|
||||||
|
/** Whether the group autocompletion list is hidden or not. */
|
||||||
groupsHidden: boolean;
|
groupsHidden: boolean;
|
||||||
|
|
||||||
|
/** The current set of group matches. */
|
||||||
groupsMatches: Set<string>;
|
groupsMatches: Set<string>;
|
||||||
|
|
||||||
|
/** The position where the group autocompletion list should be shown. */
|
||||||
groupsPosition: Offset | undefined;
|
groupsPosition: Offset | undefined;
|
||||||
|
|
||||||
|
/** All the usernames without leading @-symbols. */
|
||||||
usernames: Set<string>;
|
usernames: Set<string>;
|
||||||
|
|
||||||
|
/** Whether the username autocompletion list is hidden or not. */
|
||||||
usernamesHidden: boolean;
|
usernamesHidden: boolean;
|
||||||
|
|
||||||
|
/** The current set of username matches. */
|
||||||
usernamesMatches: Set<string>;
|
usernamesMatches: Set<string>;
|
||||||
|
|
||||||
|
/** The position where the username autocompletion list should be shown. */
|
||||||
usernamesPosition: Offset | undefined;
|
usernamesPosition: Offset | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -29,17 +55,16 @@ export class AutocompleteFeature extends Component<Props, State> {
|
||||||
value.startsWith("~") ? value.slice(1) : value,
|
value.startsWith("~") ? value.slice(1) : value,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get all the usernames on the page without their leading @s, and get
|
|
||||||
// all the usernames from the saved user labels.
|
|
||||||
const usernameElements = querySelectorAll<HTMLElement>(".link-user");
|
|
||||||
const usernames = [
|
const usernames = [
|
||||||
...usernameElements.map((value) => {
|
// Get all the usernames on the page without their leading @-symbols.
|
||||||
|
...querySelectorAll<HTMLElement>(".link-user").map((value) => {
|
||||||
if (props.anonymizeUsernamesEnabled) {
|
if (props.anonymizeUsernamesEnabled) {
|
||||||
return (value.dataset.trxUsername ?? "<unknown>").toLowerCase();
|
return (value.dataset.trxUsername ?? "<unknown>").toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
return value.textContent!.replace(/^@/, "").toLowerCase();
|
return value.textContent!.replace(/^@/, "").toLowerCase();
|
||||||
}),
|
}),
|
||||||
|
// Get all the usernames from the saved User Labels.
|
||||||
...props.userLabels.map(({value}) => value.username),
|
...props.userLabels.map(({value}) => value.username),
|
||||||
].sort((a, b) => a.localeCompare(b));
|
].sort((a, b) => a.localeCompare(b));
|
||||||
|
|
||||||
|
@ -54,7 +79,6 @@ export class AutocompleteFeature extends Component<Props, State> {
|
||||||
usernamesPosition: undefined,
|
usernamesPosition: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add a keydown listener for the entire page.
|
|
||||||
document.addEventListener("keydown", this.globalInputHandler);
|
document.addEventListener("keydown", this.globalInputHandler);
|
||||||
document.addEventListener("compositionupdate", this.globalInputHandler);
|
document.addEventListener("compositionupdate", this.globalInputHandler);
|
||||||
|
|
||||||
|
@ -64,6 +88,12 @@ export class AutocompleteFeature extends Component<Props, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) => {
|
globalInputHandler = (event: CompositionEvent | KeyboardEvent) => {
|
||||||
const textarea = event.target;
|
const textarea = event.target;
|
||||||
|
|
||||||
|
@ -80,7 +110,9 @@ export class AutocompleteFeature extends Component<Props, State> {
|
||||||
) => {
|
) => {
|
||||||
const dataAttribute = `data-trx-autocomplete-${target}`;
|
const dataAttribute = `data-trx-autocomplete-${target}`;
|
||||||
|
|
||||||
|
// Get the key that was pressed.
|
||||||
const key = event instanceof KeyboardEvent ? event.key : event.data;
|
const key = event instanceof KeyboardEvent ? event.key : event.data;
|
||||||
|
|
||||||
if (key === prefix && !textarea.getAttribute(dataAttribute)) {
|
if (key === prefix && !textarea.getAttribute(dataAttribute)) {
|
||||||
textarea.setAttribute(dataAttribute, "true");
|
textarea.setAttribute(dataAttribute, "true");
|
||||||
textarea.addEventListener("keyup", (event) => {
|
textarea.addEventListener("keyup", (event) => {
|
||||||
|
@ -99,6 +131,7 @@ export class AutocompleteFeature extends Component<Props, State> {
|
||||||
createHandler("@", "usernames", this.state.usernames);
|
createHandler("@", "usernames", this.state.usernames);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** The input handler for any `<textarea>` elements. */
|
||||||
textareaInputHandler = (
|
textareaInputHandler = (
|
||||||
textarea: HTMLTextAreaElement,
|
textarea: HTMLTextAreaElement,
|
||||||
prefix: string,
|
prefix: string,
|
||||||
|
@ -124,14 +157,15 @@ export class AutocompleteFeature extends Component<Props, State> {
|
||||||
// what the user is currently typing.
|
// what the user is currently typing.
|
||||||
const input = text.slice(prefixIndex + prefix.length, position);
|
const input = text.slice(prefixIndex + prefix.length, position);
|
||||||
|
|
||||||
// If there is any whitespace in the input or there is no input at all,
|
// If there is any whitespace in the input, return early. Usernames and
|
||||||
// return early. Usernames cannot have whitespace in them.
|
// groups cannot have whitespace in them which means that the user has
|
||||||
if (/\s/.test(input) || input === "") {
|
// finished typing what the autocomplete should handle.
|
||||||
|
if (/\s/.test(input)) {
|
||||||
this.hide(target);
|
this.hide(target);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find all the values that match the input using `includes`.
|
// Find any values that match using case-insensitive includes.
|
||||||
const matches = new Set<string>(
|
const matches = new Set<string>(
|
||||||
[...values].filter((value) =>
|
[...values].filter((value) =>
|
||||||
value.toLowerCase().includes(input.toLowerCase()),
|
value.toLowerCase().includes(input.toLowerCase()),
|
||||||
|
@ -150,6 +184,7 @@ export class AutocompleteFeature extends Component<Props, State> {
|
||||||
this.update(target, matches);
|
this.update(target, matches);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Update the available matches. */
|
||||||
update = (target: string, matches: Set<string>) => {
|
update = (target: string, matches: Set<string>) => {
|
||||||
if (target === "groups") {
|
if (target === "groups") {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -162,6 +197,7 @@ export class AutocompleteFeature extends Component<Props, State> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Show the autocomplete list in the given position. */
|
||||||
show = (target: string, position: Offset) => {
|
show = (target: string, position: Offset) => {
|
||||||
if (target === "groups") {
|
if (target === "groups") {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -176,6 +212,7 @@ export class AutocompleteFeature extends Component<Props, State> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Hide the autocomplete list. */
|
||||||
hide = (target: string) => {
|
hide = (target: string) => {
|
||||||
if (target === "groups") {
|
if (target === "groups") {
|
||||||
this.setState({groupsHidden: true});
|
this.setState({groupsHidden: true});
|
||||||
|
@ -185,7 +222,7 @@ export class AutocompleteFeature extends Component<Props, State> {
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
// Create the list of groups and usernames.
|
// Create the `<li>` elements for groups and usernames.
|
||||||
const groups = [...this.state.groupsMatches].map((value) => (
|
const groups = [...this.state.groupsMatches].map((value) => (
|
||||||
<li>~{value}</li>
|
<li>~{value}</li>
|
||||||
));
|
));
|
||||||
|
@ -193,11 +230,11 @@ export class AutocompleteFeature extends Component<Props, State> {
|
||||||
<li>@{value}</li>
|
<li>@{value}</li>
|
||||||
));
|
));
|
||||||
|
|
||||||
// Create the CSS class whether or not to hide the autocomplete.
|
// Figure out which lists are hidden.
|
||||||
const groupsHidden = this.state.groupsHidden ? "trx-hidden" : "";
|
const groupsHidden = this.state.groupsHidden ? "trx-hidden" : "";
|
||||||
const usernamesHidden = this.state.usernamesHidden ? "trx-hidden" : "";
|
const usernamesHidden = this.state.usernamesHidden ? "trx-hidden" : "";
|
||||||
|
|
||||||
// Create the position for the group and usernames autocomplete.
|
// Calculate the position for the `<ul>` elements.
|
||||||
const groupsLeft = this.state.groupsPosition?.left ?? 0;
|
const groupsLeft = this.state.groupsPosition?.left ?? 0;
|
||||||
const groupsTop =
|
const groupsTop =
|
||||||
(this.state.groupsPosition?.top ?? 0) +
|
(this.state.groupsPosition?.top ?? 0) +
|
||||||
|
|
Loading…
Reference in New Issue