UNPKG

@wordpress/components

Version:
353 lines (351 loc) 11.2 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // packages/components/src/autocomplete/index.tsx var autocomplete_exports = {}; __export(autocomplete_exports, { default: () => Autocomplete, useAutocomplete: () => useAutocomplete, useAutocompleteProps: () => useAutocompleteProps, useLastDifferentValue: () => useLastDifferentValue }); module.exports = __toCommonJS(autocomplete_exports); var import_element = require("@wordpress/element"); var import_compose = require("@wordpress/compose"); var import_rich_text = require("@wordpress/rich-text"); var import_a11y = require("@wordpress/a11y"); var import_keycodes = require("@wordpress/keycodes"); var import_autocompleter_ui = require("./autocompleter-ui.cjs"); var import_get_autocomplete_match = require("./get-autocomplete-match.cjs"); var import_with_ignore_ime_events = require("../utils/with-ignore-ime-events.cjs"); var import_get_node_text = __toESM(require("../utils/get-node-text.cjs")); var import_jsx_runtime = require("react/jsx-runtime"); var EMPTY_FILTERED_OPTIONS = []; var AUTOCOMPLETE_HOOK_REFERENCE = {}; function getCompletionObject(completion) { if (completion !== null && typeof completion === "object" && "action" in completion && completion.action !== void 0 && "value" in completion && completion.value !== void 0) { return completion; } return { action: "insert-at-caret", value: completion }; } var initialState = { selectedIndex: 0, filteredOptions: EMPTY_FILTERED_OPTIONS, filterValue: "", autocompleter: null }; function autocompleteReducer(state, action) { switch (action.type) { case "RESET": return initialState; case "SELECT": return { ...state, selectedIndex: action.index }; case "OPTIONS": return { ...state, filteredOptions: action.options, selectedIndex: action.options.length === state.filteredOptions.length ? state.selectedIndex : 0 }; case "MATCH": return { ...state, autocompleter: action.completer, filterValue: action.query }; } } function useAutocomplete({ record, onChange, onReplace, completers, contentRef }) { const instanceId = (0, import_compose.useInstanceId)(AUTOCOMPLETE_HOOK_REFERENCE); const [state, dispatch] = (0, import_element.useReducer)(autocompleteReducer, initialState); const { selectedIndex, filteredOptions, filterValue, autocompleter } = state; const backspacingRef = (0, import_element.useRef)(false); const prevRecordTextRef = (0, import_element.useRef)(""); const lastCompletionRef = (0, import_element.useRef)(null); function insertCompletion(replacement) { if (autocompleter === null) { return ""; } const end = record.start; const start = end - autocompleter.triggerPrefix.length - filterValue.length; const toInsert = (0, import_rich_text.create)({ html: (0, import_element.renderToString)(replacement) }); onChange((0, import_rich_text.insert)(record, toInsert, start, end)); return (0, import_rich_text.getTextContent)(toInsert); } function select(option) { if (option.isDisabled || !autocompleter) { return; } const { getOptionCompletion } = autocompleter; if (!getOptionCompletion) { dispatch({ type: "RESET" }); contentRef.current?.focus(); return; } const completionObject = getCompletionObject(getOptionCompletion(option.value, filterValue)); if ("replace" === completionObject.action) { onReplace([completionObject.value]); return; } if ("insert-at-caret" === completionObject.action) { const completionText = insertCompletion(completionObject.value); if (completionText.startsWith(autocompleter.triggerPrefix)) { const afterPrefix = completionText.slice(autocompleter.triggerPrefix.length); if (afterPrefix) { lastCompletionRef.current = { name: autocompleter.name, value: afterPrefix }; } } } dispatch({ type: "RESET" }); contentRef.current?.focus(); } function onChangeOptions(options) { dispatch({ type: "OPTIONS", options }); } function handleKeyDown(event) { backspacingRef.current = event.key === "Backspace"; if (!autocompleter) { return; } if (filteredOptions.length === 0) { return; } if (event.defaultPrevented) { return; } switch (event.key) { case "ArrowUp": case "ArrowDown": { const offset = event.key === "ArrowUp" ? -1 : 1; const newIndex = (selectedIndex + offset + filteredOptions.length) % filteredOptions.length; dispatch({ type: "SELECT", index: newIndex }); if ((0, import_keycodes.isAppleOS)()) { (0, import_a11y.speak)((0, import_get_node_text.default)(filteredOptions[newIndex].label), "assertive"); } break; } case "Escape": dispatch({ type: "RESET" }); event.preventDefault(); break; case "Enter": select(filteredOptions[selectedIndex]); break; case "ArrowLeft": case "ArrowRight": dispatch({ type: "RESET" }); return; default: return; } event.preventDefault(); } const textContent = (0, import_element.useMemo)(() => { if ((0, import_rich_text.isCollapsed)(record)) { return (0, import_rich_text.getTextContent)((0, import_rich_text.slice)(record, 0)); } return ""; }, [record]); (0, import_element.useEffect)(() => { const isTextChange = record.text !== prevRecordTextRef.current; prevRecordTextRef.current = record.text; function getTextAfterSelection() { return textContent ? (0, import_rich_text.getTextContent)((0, import_rich_text.slice)(record, void 0, (0, import_rich_text.getTextContent)(record).length)) : ""; } const match = (0, import_get_autocomplete_match.getAutocompleteMatch)(textContent, completers, { matchCount: filteredOptions.length, isBackspacing: backspacingRef.current, getTextAfterSelection, lastCompletion: lastCompletionRef.current }); if (!match) { if (autocompleter) { dispatch({ type: "RESET" }); } return; } const { completer, filterValue: query } = match; if (!autocompleter && !isTextChange) { return; } if (lastCompletionRef.current && lastCompletionRef.current.name === completer.name) { lastCompletionRef.current = null; } dispatch({ type: "MATCH", completer, query }); }, [textContent]); const { key: selectedKey = "" } = filteredOptions[selectedIndex] || {}; const { className } = autocompleter || {}; const isExpanded = !!autocompleter && filteredOptions.length > 0; const listBoxId = isExpanded ? `components-autocomplete-listbox-${instanceId}` : void 0; const activeId = isExpanded ? `components-autocomplete-item-${instanceId}-${selectedKey}` : null; const hasSelection = record.start !== void 0; const showPopover = !!textContent && hasSelection && !!autocompleter; return { listBoxId, activeId, onKeyDown: (0, import_with_ignore_ime_events.withIgnoreIMEEvents)(handleKeyDown), popover: showPopover && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_autocompleter_ui.AutocompleterUI, { autocompleter, className, filterValue, instanceId, listBoxId, selectedIndex, onChangeOptions, onSelect: select, contentRef, reset: () => dispatch({ type: "RESET" }) }, autocompleter.name + autocompleter.triggerPrefix) }; } function recordValuesMatch(a, b) { return a.text === b.text && a.start === b.start && a.end === b.end; } function useLastDifferentValue(value) { const history = (0, import_element.useRef)([]); const lastEntry = history.current[history.current.length - 1]; if (!lastEntry || !recordValuesMatch(value, lastEntry)) { history.current.push(value); } if (history.current.length > 2) { history.current.shift(); } return history.current[0]; } function useAutocompleteProps(options) { const ref = (0, import_element.useRef)(null); const onKeyDownRef = (0, import_element.useRef)(void 0); const { record } = options; const previousRecord = useLastDifferentValue(record); const { popover, listBoxId, activeId, onKeyDown } = useAutocomplete({ ...options, contentRef: ref }); onKeyDownRef.current = onKeyDown; const mergedRefs = (0, import_compose.useMergeRefs)([ref, (0, import_compose.useRefEffect)((element) => { function _onKeyDown(event) { onKeyDownRef.current?.(event); } element.addEventListener("keydown", _onKeyDown); return () => { element.removeEventListener("keydown", _onKeyDown); }; }, [])]); const didUserInput = record.text !== previousRecord?.text; if (!didUserInput) { return { ref: mergedRefs }; } return { ref: mergedRefs, children: popover, "aria-autocomplete": listBoxId ? "list" : void 0, "aria-owns": listBoxId, "aria-activedescendant": activeId }; } function Autocomplete({ children, isSelected, ...options }) { const { popover, ...props } = useAutocomplete(options); return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [children(props), isSelected && popover] }); } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { useAutocomplete, useAutocompleteProps, useLastDifferentValue }); //# sourceMappingURL=index.cjs.map