@wordpress/components
Version:
UI components for WordPress.
8 lines (7 loc) • 17.5 kB
Source Map (JSON)
{
"version": 3,
"sources": ["../../src/autocomplete/index.tsx"],
"sourcesContent": ["/**\n * WordPress dependencies\n */\nimport { renderToString, useEffect, useMemo, useReducer, useRef } from '@wordpress/element';\nimport { useInstanceId, useMergeRefs, useRefEffect } from '@wordpress/compose';\nimport { create, slice, insert, isCollapsed, getTextContent } from '@wordpress/rich-text';\nimport { speak } from '@wordpress/a11y';\nimport { isAppleOS } from '@wordpress/keycodes';\n\n/**\n * Internal dependencies\n */\nimport { AutocompleterUI } from './autocompleter-ui';\nimport { getAutocompleteMatch } from './get-autocomplete-match';\nimport { withIgnoreIMEEvents } from '../utils/with-ignore-ime-events';\nimport getNodeText from '../utils/get-node-text';\nimport { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from \"react/jsx-runtime\";\nconst EMPTY_FILTERED_OPTIONS = [];\n\n// Used for generating the instance ID\nconst AUTOCOMPLETE_HOOK_REFERENCE = {};\nfunction getCompletionObject(completion) {\n if (completion !== null && typeof completion === 'object' && 'action' in completion && completion.action !== undefined && 'value' in completion && completion.value !== undefined) {\n return completion;\n }\n return {\n action: 'insert-at-caret',\n value: completion\n };\n}\nconst initialState = {\n selectedIndex: 0,\n filteredOptions: EMPTY_FILTERED_OPTIONS,\n filterValue: '',\n autocompleter: null\n};\nfunction autocompleteReducer(state, action) {\n switch (action.type) {\n case 'RESET':\n return initialState;\n case 'SELECT':\n return {\n ...state,\n selectedIndex: action.index\n };\n case 'OPTIONS':\n return {\n ...state,\n filteredOptions: action.options,\n selectedIndex: action.options.length === state.filteredOptions.length ? state.selectedIndex : 0\n };\n case 'MATCH':\n return {\n ...state,\n autocompleter: action.completer,\n filterValue: action.query\n };\n }\n}\nexport function useAutocomplete({\n record,\n onChange,\n onReplace,\n completers,\n contentRef\n}) {\n const instanceId = useInstanceId(AUTOCOMPLETE_HOOK_REFERENCE);\n const [state, dispatch] = useReducer(autocompleteReducer, initialState);\n const {\n selectedIndex,\n filteredOptions,\n filterValue,\n autocompleter\n } = state;\n const backspacingRef = useRef(false);\n const prevRecordTextRef = useRef('');\n const lastCompletionRef = useRef(null);\n function insertCompletion(replacement) {\n if (autocompleter === null) {\n return '';\n }\n const end = record.start;\n const start = end - autocompleter.triggerPrefix.length - filterValue.length;\n const toInsert = create({\n html: renderToString(replacement)\n });\n onChange(insert(record, toInsert, start, end));\n return getTextContent(toInsert);\n }\n function select(option) {\n if (option.isDisabled || !autocompleter) {\n return;\n }\n const {\n getOptionCompletion\n } = autocompleter;\n if (!getOptionCompletion) {\n dispatch({\n type: 'RESET'\n });\n contentRef.current?.focus();\n return;\n }\n const completionObject = getCompletionObject(getOptionCompletion(option.value, filterValue));\n if ('replace' === completionObject.action) {\n onReplace([completionObject.value]);\n // When replacing, the component will unmount, so don't reset\n // state (below) on an unmounted component.\n return;\n }\n if ('insert-at-caret' === completionObject.action) {\n const completionText = insertCompletion(completionObject.value);\n // When the completion value starts with the trigger prefix\n // (e.g. @username), the trigger stays in the text and would\n // re-activate the autocompleter. Store the completed text so\n // the effect can suppress the stale re-match.\n if (completionText.startsWith(autocompleter.triggerPrefix)) {\n const afterPrefix = completionText.slice(autocompleter.triggerPrefix.length);\n if (afterPrefix) {\n lastCompletionRef.current = {\n name: autocompleter.name,\n value: afterPrefix\n };\n }\n }\n }\n\n // Reset autocomplete state after insertion rather than before\n // so insertion events don't cause the completion menu to redisplay.\n dispatch({\n type: 'RESET'\n });\n\n // Make sure that the content remains focused after making a selection\n // and that the text cursor position is not lost.\n contentRef.current?.focus();\n }\n function onChangeOptions(options) {\n dispatch({\n type: 'OPTIONS',\n options\n });\n }\n function handleKeyDown(event) {\n backspacingRef.current = event.key === 'Backspace';\n if (!autocompleter) {\n return;\n }\n if (filteredOptions.length === 0) {\n return;\n }\n if (event.defaultPrevented) {\n return;\n }\n switch (event.key) {\n case 'ArrowUp':\n case 'ArrowDown':\n {\n const offset = event.key === 'ArrowUp' ? -1 : 1;\n const newIndex = (selectedIndex + offset + filteredOptions.length) % filteredOptions.length;\n dispatch({\n type: 'SELECT',\n index: newIndex\n });\n // See the related PR as to why this is necessary: https://github.com/WordPress/gutenberg/pull/54902.\n if (isAppleOS()) {\n speak(getNodeText(filteredOptions[newIndex].label), 'assertive');\n }\n break;\n }\n case 'Escape':\n dispatch({\n type: 'RESET'\n });\n event.preventDefault();\n break;\n case 'Enter':\n select(filteredOptions[selectedIndex]);\n break;\n case 'ArrowLeft':\n case 'ArrowRight':\n dispatch({\n type: 'RESET'\n });\n return;\n default:\n return;\n }\n\n // Any handled key should prevent original behavior. This relies on\n // the early return in the default case.\n event.preventDefault();\n }\n\n // textContent is a primitive (string), memoizing is not strictly necessary\n // but this is a preemptive performance improvement, since the autocompleter\n // is a potential bottleneck for the editor type metric.\n const textContent = useMemo(() => {\n if (isCollapsed(record)) {\n return getTextContent(slice(record, 0));\n }\n return '';\n }, [record]);\n useEffect(() => {\n const isTextChange = record.text !== prevRecordTextRef.current;\n prevRecordTextRef.current = record.text;\n function getTextAfterSelection() {\n return textContent ? getTextContent(slice(record, undefined, getTextContent(record).length)) : '';\n }\n const match = getAutocompleteMatch(textContent, completers, {\n matchCount: filteredOptions.length,\n isBackspacing: backspacingRef.current,\n getTextAfterSelection,\n lastCompletion: lastCompletionRef.current\n });\n if (!match) {\n if (autocompleter) {\n dispatch({\n type: 'RESET'\n });\n }\n return;\n }\n const {\n completer,\n filterValue: query\n } = match;\n\n // Don't re-activate a dismissed autocompleter on cursor-only\n // movement. `textContent` (text before cursor) changes with the\n // caret, so the effect re-runs, but `record.text` does not.\n // Complements the render-time `didUserInput` gate in\n // `useAutocompleteProps` for callers using this hook directly.\n if (!autocompleter && !isTextChange) {\n return;\n }\n\n // Clear stale completion ref when the user types a new trigger\n // for the same completer (the previous completion is no longer\n // relevant). Must be after the cursor-only check so that mere\n // cursor movement doesn't discard the suppression state.\n if (lastCompletionRef.current && lastCompletionRef.current.name === completer.name) {\n lastCompletionRef.current = null;\n }\n dispatch({\n type: 'MATCH',\n completer,\n query\n });\n // We want to avoid introducing unexpected side effects.\n // See https://github.com/WordPress/gutenberg/pull/41820\n }, [textContent]);\n const {\n key: selectedKey = ''\n } = filteredOptions[selectedIndex] || {};\n const {\n className\n } = autocompleter || {};\n const isExpanded = !!autocompleter && filteredOptions.length > 0;\n const listBoxId = isExpanded ? `components-autocomplete-listbox-${instanceId}` : undefined;\n const activeId = isExpanded ? `components-autocomplete-item-${instanceId}-${selectedKey}` : null;\n const hasSelection = record.start !== undefined;\n const showPopover = !!textContent && hasSelection && !!autocompleter;\n return {\n listBoxId,\n activeId,\n onKeyDown: withIgnoreIMEEvents(handleKeyDown),\n popover: showPopover && /*#__PURE__*/_jsx(AutocompleterUI, {\n autocompleter: autocompleter,\n className: className,\n filterValue: filterValue,\n instanceId: instanceId,\n listBoxId: listBoxId,\n selectedIndex: selectedIndex,\n onChangeOptions: onChangeOptions,\n onSelect: select,\n contentRef: contentRef,\n reset: () => dispatch({\n type: 'RESET'\n })\n }, autocompleter.name + autocompleter.triggerPrefix)\n };\n}\n\n/**\n * Checks whether two records represent the same user-visible state\n * (same text content and cursor position).\n */\nfunction recordValuesMatch(a, b) {\n return a.text === b.text && a.start === b.start && a.end === b.end;\n}\n\n/**\n * Tracks the last record whose value differed from the current one.\n * Used to determine whether the user has actually typed something\n */\nexport function useLastDifferentValue(value) {\n const history = useRef([]);\n const lastEntry = history.current[history.current.length - 1];\n\n // Only add to history if the value is meaningfully different from\n // the most recent entry (analogous to Set.add being a no-op for\n // duplicate references in the original implementation).\n if (!lastEntry || !recordValuesMatch(value, lastEntry)) {\n history.current.push(value);\n }\n\n // Keep the history size to 2.\n if (history.current.length > 2) {\n history.current.shift();\n }\n return history.current[0];\n}\nexport function useAutocompleteProps(options) {\n const ref = useRef(null);\n const onKeyDownRef = useRef(undefined);\n const {\n record\n } = options;\n const previousRecord = useLastDifferentValue(record);\n const {\n popover,\n listBoxId,\n activeId,\n onKeyDown\n } = useAutocomplete({\n ...options,\n contentRef: ref\n });\n onKeyDownRef.current = onKeyDown;\n const mergedRefs = useMergeRefs([ref, useRefEffect(element => {\n function _onKeyDown(event) {\n onKeyDownRef.current?.(event);\n }\n element.addEventListener('keydown', _onKeyDown);\n return () => {\n element.removeEventListener('keydown', _onKeyDown);\n };\n }, [])]);\n\n // We only want to show the popover if the user has typed something.\n const didUserInput = record.text !== previousRecord?.text;\n if (!didUserInput) {\n return {\n ref: mergedRefs\n };\n }\n return {\n ref: mergedRefs,\n children: popover,\n 'aria-autocomplete': listBoxId ? 'list' : undefined,\n 'aria-owns': listBoxId,\n 'aria-activedescendant': activeId\n };\n}\nexport default function Autocomplete({\n children,\n isSelected,\n ...options\n}) {\n const {\n popover,\n ...props\n } = useAutocomplete(options);\n return /*#__PURE__*/_jsxs(_Fragment, {\n children: [children(props), isSelected && popover]\n });\n}"],
"mappings": "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",
"names": ["getNodeText", "_jsx", "_jsxs", "_Fragment"]
}