fix: prevent asset conflicts between React and Grid.js versions

Add coexistence checks to all enqueue methods to prevent loading
both React and Grid.js assets simultaneously.

Changes:
- ReactAdmin.php: Only enqueue React assets when ?react=1
- Init.php: Skip Grid.js when React active on admin pages
- Form.php, Coupon.php, Access.php: Restore classic assets when ?react=0
- Customer.php, Product.php, License.php: Add coexistence checks

Now the toggle between Classic and React versions works correctly.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
dwindown
2026-04-18 17:02:14 +07:00
parent bd9cdac02e
commit e8fbfb14c1
74973 changed files with 6658406 additions and 71 deletions

View File

@@ -0,0 +1,139 @@
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.getAutoCompleterUI = getAutoCompleterUI;
var _react = require("react");
var _classnames = _interopRequireDefault(require("classnames"));
var _element = require("@wordpress/element");
var _richText = require("@wordpress/rich-text");
var _compose = require("@wordpress/compose");
var _a11y = require("@wordpress/a11y");
var _i18n = require("@wordpress/i18n");
var _getDefaultUseItems = _interopRequireDefault(require("./get-default-use-items"));
var _button = _interopRequireDefault(require("../button"));
var _popover = _interopRequireDefault(require("../popover"));
var _visuallyHidden = require("../visually-hidden");
var _reactDom = require("react-dom");
/**
* External dependencies
*/
/**
* WordPress dependencies
*/
/**
* Internal dependencies
*/
function getAutoCompleterUI(autocompleter) {
const useItems = autocompleter.useItems ? autocompleter.useItems : (0, _getDefaultUseItems.default)(autocompleter);
function AutocompleterUI({
filterValue,
instanceId,
listBoxId,
className,
selectedIndex,
onChangeOptions,
onSelect,
onReset,
reset,
contentRef
}) {
const [items] = useItems(filterValue);
const popoverAnchor = (0, _richText.useAnchor)({
editableContentElement: contentRef.current
});
const [needsA11yCompat, setNeedsA11yCompat] = (0, _element.useState)(false);
const popoverRef = (0, _element.useRef)(null);
const popoverRefs = (0, _compose.useMergeRefs)([popoverRef, (0, _compose.useRefEffect)(node => {
if (!contentRef.current) return;
// If the popover is rendered in a different document than
// the content, we need to duplicate the options list in the
// content document so that it's available to the screen
// readers, which check the DOM ID based aira-* attributes.
setNeedsA11yCompat(node.ownerDocument !== contentRef.current.ownerDocument);
}, [contentRef])]);
useOnClickOutside(popoverRef, reset);
const debouncedSpeak = (0, _compose.useDebounce)(_a11y.speak, 500);
function announce(options) {
if (!debouncedSpeak) {
return;
}
if (!!options.length) {
if (filterValue) {
debouncedSpeak((0, _i18n.sprintf)( /* translators: %d: number of results. */
(0, _i18n._n)('%d result found, use up and down arrow keys to navigate.', '%d results found, use up and down arrow keys to navigate.', options.length), options.length), 'assertive');
} else {
debouncedSpeak((0, _i18n.sprintf)( /* translators: %d: number of results. */
(0, _i18n._n)('Initial %d result loaded. Type to filter all available results. Use up and down arrow keys to navigate.', 'Initial %d results loaded. Type to filter all available results. Use up and down arrow keys to navigate.', options.length), options.length), 'assertive');
}
} else {
debouncedSpeak((0, _i18n.__)('No results.'), 'assertive');
}
}
(0, _element.useLayoutEffect)(() => {
onChangeOptions(items);
announce(items);
// Temporarily disabling exhaustive-deps to avoid introducing unexpected side effecst.
// See https://github.com/WordPress/gutenberg/pull/41820
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [items]);
if (items.length === 0) {
return null;
}
const ListBox = ({
Component = 'div'
}) => (0, _react.createElement)(Component, {
id: listBoxId,
role: "listbox",
className: "components-autocomplete__results"
}, items.map((option, index) => (0, _react.createElement)(_button.default, {
key: option.key,
id: `components-autocomplete-item-${instanceId}-${option.key}`,
role: "option",
"aria-selected": index === selectedIndex,
disabled: option.isDisabled,
className: (0, _classnames.default)('components-autocomplete__result', className, {
'is-selected': index === selectedIndex
}),
onClick: () => onSelect(option)
}, option.label)));
return (0, _react.createElement)(_react.Fragment, null, (0, _react.createElement)(_popover.default, {
focusOnMount: false,
onClose: onReset,
placement: "top-start",
className: "components-autocomplete__popover",
anchor: popoverAnchor,
ref: popoverRefs
}, (0, _react.createElement)(ListBox, null)), contentRef.current && needsA11yCompat && (0, _reactDom.createPortal)((0, _react.createElement)(ListBox, {
Component: _visuallyHidden.VisuallyHidden
}), contentRef.current.ownerDocument.body));
}
return AutocompleterUI;
}
function useOnClickOutside(ref, handler) {
(0, _element.useEffect)(() => {
const listener = event => {
// Do nothing if clicking ref's element or descendent elements, or if the ref is not referencing an element
if (!ref.current || ref.current.contains(event.target)) {
return;
}
handler(event);
};
document.addEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);
return () => {
document.removeEventListener('mousedown', listener);
document.removeEventListener('touchstart', listener);
};
// Disable reason: `ref` is a ref object and should not be included in a
// hook's dependency list.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [handler]);
}
//# sourceMappingURL=autocompleter-ui.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,148 @@
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
exports.getAutoCompleterUI = getAutoCompleterUI;
var _react = require("react");
var _reactNative = require("react-native");
var _element = require("@wordpress/element");
var _i18n = require("@wordpress/i18n");
var _components = require("@wordpress/components");
var _compose = require("@wordpress/compose");
var _backgroundView = _interopRequireDefault(require("./background-view"));
var _getDefaultUseItems = _interopRequireDefault(require("./get-default-use-items"));
var _style = _interopRequireDefault(require("./style.scss"));
/**
* External dependencies
*/
/**
* WordPress dependencies
*/
/**
* Internal dependencies
*/
const {
compose: stylesCompose
} = _reactNative.StyleSheet;
function getAutoCompleterUI(autocompleter) {
const useItems = autocompleter.useItems ? autocompleter.useItems : (0, _getDefaultUseItems.default)(autocompleter);
function AutocompleterUI({
filterValue,
selectedIndex,
onChangeOptions,
onSelect,
value,
reset
}) {
const [items] = useItems(filterValue);
const filteredItems = items.filter(item => !item.isDisabled);
const scrollViewRef = (0, _element.useRef)();
const animationValue = (0, _element.useRef)(new _reactNative.Animated.Value(0)).current;
const [isVisible, setIsVisible] = (0, _element.useState)(false);
const {
text
} = value;
(0, _element.useEffect)(() => {
if (!isVisible && text.length > 0) {
setIsVisible(true);
}
}, [isVisible, text]);
(0, _element.useLayoutEffect)(() => {
onChangeOptions(items);
scrollViewRef.current?.scrollTo({
x: 0,
animated: false
});
if (isVisible && text.length > 0) {
startAnimation(true);
} else if (isVisible && text.length === 0) {
startAnimation(false);
}
// Temporarily disabling exhaustive-deps to avoid introducing unexpected side effecst.
// See https://github.com/WordPress/gutenberg/pull/41820
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [items, isVisible, text]);
const activeItemStyles = (0, _compose.usePreferredColorSchemeStyle)(_style.default['components-autocomplete__item-active'], _style.default['components-autocomplete__item-active-dark']);
const iconStyles = (0, _compose.usePreferredColorSchemeStyle)(_style.default['components-autocomplete__icon'], _style.default['components-autocomplete__icon-active-dark']);
const activeIconStyles = (0, _compose.usePreferredColorSchemeStyle)(_style.default['components-autocomplete__icon-active '], _style.default['components-autocomplete__icon-active-dark']);
const textStyles = (0, _compose.usePreferredColorSchemeStyle)(_style.default['components-autocomplete__text'], _style.default['components-autocomplete__text-dark']);
const activeTextStyles = (0, _compose.usePreferredColorSchemeStyle)(_style.default['components-autocomplete__text-active'], _style.default['components-autocomplete__text-active-dark']);
const startAnimation = (0, _element.useCallback)(show => {
_reactNative.Animated.timing(animationValue, {
toValue: show ? 1 : 0,
duration: show ? 200 : 100,
useNativeDriver: true
}).start(({
finished
}) => {
if (finished && !show && isVisible) {
setIsVisible(false);
reset();
}
});
},
// Temporarily disabling exhaustive-deps to avoid introducing unexpected side effecst.
// See https://github.com/WordPress/gutenberg/pull/41820
// eslint-disable-next-line react-hooks/exhaustive-deps
[isVisible]);
const contentStyles = {
transform: [{
translateY: animationValue.interpolate({
inputRange: [0, 1],
outputRange: [_style.default['components-autocomplete'].height, 0]
})
}]
};
if (!filteredItems.length > 0 || !isVisible) {
return null;
}
return (0, _react.createElement)(_components.__unstableAutocompletionItemsFill, null, (0, _react.createElement)(_reactNative.View, {
style: _style.default['components-autocomplete']
}, (0, _react.createElement)(_reactNative.Animated.View, {
style: contentStyles
}, (0, _react.createElement)(_backgroundView.default, null, (0, _react.createElement)(_reactNative.ScrollView, {
testID: "autocompleter",
ref: scrollViewRef,
horizontal: true,
contentContainerStyle: _style.default['components-autocomplete__content'],
showsHorizontalScrollIndicator: false,
keyboardShouldPersistTaps: "always",
accessibilityLabel:
// translators: Slash inserter autocomplete results
(0, _i18n.__)('Slash inserter results')
}, filteredItems.map((option, index) => {
const isActive = index === selectedIndex;
const itemStyle = stylesCompose(_style.default['components-autocomplete__item'], isActive && activeItemStyles);
const textStyle = stylesCompose(textStyles, isActive && activeTextStyles);
const iconStyle = stylesCompose(iconStyles, isActive && activeIconStyles);
const iconSource = option?.value?.icon?.src || option?.value?.icon;
return (0, _react.createElement)(_reactNative.TouchableOpacity, {
activeOpacity: 0.5,
style: itemStyle,
key: index,
onPress: () => onSelect(option),
accessibilityLabel: (0, _i18n.sprintf)(
// translators: %s: Block name e.g. "Image block"
(0, _i18n.__)('%s block'), option?.value?.title)
}, (0, _react.createElement)(_reactNative.View, {
style: _style.default['components-autocomplete__icon']
}, (0, _react.createElement)(_components.Icon, {
icon: iconSource,
size: 24,
style: iconStyle
})), (0, _react.createElement)(_reactNative.Text, {
style: textStyle
}, option?.value?.title));
}))))));
}
return AutocompleterUI;
}
var _default = getAutoCompleterUI;
exports.default = _default;
//# sourceMappingURL=autocompleter-ui.native.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,34 @@
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _react = require("react");
var _reactNative = require("react-native");
var _compose = require("@wordpress/compose");
var _style = _interopRequireDefault(require("./style.scss"));
/**
* External dependencies
*/
/**
* WordPress dependencies
*/
/**
* Internal dependencies
*/
const BackgroundView = ({
children
}) => {
const backgroundStyles = (0, _compose.usePreferredColorSchemeStyle)(_style.default['components-autocomplete__background'], _style.default['components-autocomplete__background-dark']);
return (0, _react.createElement)(_reactNative.View, {
style: backgroundStyles
}, children);
};
var _default = BackgroundView;
exports.default = _default;
//# sourceMappingURL=background-view.android.js.map

View File

@@ -0,0 +1 @@
{"version":3,"names":["_reactNative","require","_compose","_style","_interopRequireDefault","BackgroundView","children","backgroundStyles","usePreferredColorSchemeStyle","styles","_react","createElement","View","style","_default","exports","default"],"sources":["@wordpress/components/src/autocomplete/background-view.android.js"],"sourcesContent":["/**\n * External dependencies\n */\nimport { View } from 'react-native';\n\n/**\n * WordPress dependencies\n */\nimport { usePreferredColorSchemeStyle } from '@wordpress/compose';\n\n/**\n * Internal dependencies\n */\nimport styles from './style.scss';\n\nconst BackgroundView = ( { children } ) => {\n\tconst backgroundStyles = usePreferredColorSchemeStyle(\n\t\tstyles[ 'components-autocomplete__background' ],\n\t\tstyles[ 'components-autocomplete__background-dark' ]\n\t);\n\n\treturn <View style={ backgroundStyles }>{ children }</View>;\n};\n\nexport default BackgroundView;\n"],"mappings":";;;;;;;;AAGA,IAAAA,YAAA,GAAAC,OAAA;AAKA,IAAAC,QAAA,GAAAD,OAAA;AAKA,IAAAE,MAAA,GAAAC,sBAAA,CAAAH,OAAA;AAbA;AACA;AACA;;AAGA;AACA;AACA;;AAGA;AACA;AACA;;AAGA,MAAMI,cAAc,GAAGA,CAAE;EAAEC;AAAS,CAAC,KAAM;EAC1C,MAAMC,gBAAgB,GAAG,IAAAC,qCAA4B,EACpDC,cAAM,CAAE,qCAAqC,CAAE,EAC/CA,cAAM,CAAE,0CAA0C,CACnD,CAAC;EAED,OAAO,IAAAC,MAAA,CAAAC,aAAA,EAACX,YAAA,CAAAY,IAAI;IAACC,KAAK,EAAGN;EAAkB,GAAGD,QAAgB,CAAC;AAC5D,CAAC;AAAC,IAAAQ,QAAA,GAEaT,cAAc;AAAAU,OAAA,CAAAC,OAAA,GAAAF,QAAA"}

View File

@@ -0,0 +1,30 @@
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _react = require("react");
var _blur = require("@react-native-community/blur");
var _style = _interopRequireDefault(require("./style.scss"));
/**
* External dependencies
*/
/**
* Internal dependencies
*/
const BackgroundView = ({
children
}) => {
return (0, _react.createElement)(_blur.BlurView, {
style: _style.default['components-autocomplete__background-blur'],
blurType: "prominent",
blurAmount: 10
}, children);
};
var _default = BackgroundView;
exports.default = _default;
//# sourceMappingURL=background-view.ios.js.map

View File

@@ -0,0 +1 @@
{"version":3,"names":["_blur","require","_style","_interopRequireDefault","BackgroundView","children","_react","createElement","BlurView","style","styles","blurType","blurAmount","_default","exports","default"],"sources":["@wordpress/components/src/autocomplete/background-view.ios.js"],"sourcesContent":["/**\n * External dependencies\n */\nimport { BlurView } from '@react-native-community/blur';\n\n/**\n * Internal dependencies\n */\nimport styles from './style.scss';\n\nconst BackgroundView = ( { children } ) => {\n\treturn (\n\t\t<BlurView\n\t\t\tstyle={ styles[ 'components-autocomplete__background-blur' ] }\n\t\t\tblurType=\"prominent\"\n\t\t\tblurAmount={ 10 }\n\t\t>\n\t\t\t{ children }\n\t\t</BlurView>\n\t);\n};\n\nexport default BackgroundView;\n"],"mappings":";;;;;;;;AAGA,IAAAA,KAAA,GAAAC,OAAA;AAKA,IAAAC,MAAA,GAAAC,sBAAA,CAAAF,OAAA;AARA;AACA;AACA;;AAGA;AACA;AACA;;AAGA,MAAMG,cAAc,GAAGA,CAAE;EAAEC;AAAS,CAAC,KAAM;EAC1C,OACC,IAAAC,MAAA,CAAAC,aAAA,EAACP,KAAA,CAAAQ,QAAQ;IACRC,KAAK,EAAGC,cAAM,CAAE,0CAA0C,CAAI;IAC9DC,QAAQ,EAAC,WAAW;IACpBC,UAAU,EAAG;EAAI,GAEfP,QACO,CAAC;AAEb,CAAC;AAAC,IAAAQ,QAAA,GAEaT,cAAc;AAAAU,OAAA,CAAAC,OAAA,GAAAF,QAAA"}

View File

@@ -0,0 +1,98 @@
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = getDefaultUseItems;
var _removeAccents = _interopRequireDefault(require("remove-accents"));
var _compose = require("@wordpress/compose");
var _element = require("@wordpress/element");
var _strings = require("../utils/strings");
/**
* External dependencies
*/
/**
* WordPress dependencies
*/
/**
* Internal dependencies
*/
function filterOptions(search, options = [], maxResults = 10) {
const filtered = [];
for (let i = 0; i < options.length; i++) {
const option = options[i];
// Merge label into keywords.
let {
keywords = []
} = option;
if ('string' === typeof option.label) {
keywords = [...keywords, option.label];
}
const isMatch = keywords.some(keyword => search.test((0, _removeAccents.default)(keyword)));
if (!isMatch) {
continue;
}
filtered.push(option);
// Abort early if max reached.
if (filtered.length === maxResults) {
break;
}
}
return filtered;
}
function getDefaultUseItems(autocompleter) {
return filterValue => {
const [items, setItems] = (0, _element.useState)([]);
/*
* We support both synchronous and asynchronous retrieval of completer options
* but internally treat all as async so we maintain a single, consistent code path.
*
* Because networks can be slow, and the internet is wonderfully unpredictable,
* we don't want two promises updating the state at once. This ensures that only
* the most recent promise will act on `optionsData`. This doesn't use the state
* because `setState` is batched, and so there's no guarantee that setting
* `activePromise` in the state would result in it actually being in `this.state`
* before the promise resolves and we check to see if this is the active promise or not.
*/
(0, _element.useLayoutEffect)(() => {
const {
options,
isDebounced
} = autocompleter;
const loadOptions = (0, _compose.debounce)(() => {
const promise = Promise.resolve(typeof options === 'function' ? options(filterValue) : options).then(optionsData => {
if (promise.canceled) {
return;
}
const keyedOptions = optionsData.map((optionData, optionIndex) => ({
key: `${autocompleter.name}-${optionIndex}`,
value: optionData,
label: autocompleter.getOptionLabel(optionData),
keywords: autocompleter.getOptionKeywords ? autocompleter.getOptionKeywords(optionData) : [],
isDisabled: autocompleter.isOptionDisabled ? autocompleter.isOptionDisabled(optionData) : false
}));
// Create a regular expression to filter the options.
const search = new RegExp('(?:\\b|\\s|^)' + (0, _strings.escapeRegExp)(filterValue), 'i');
setItems(filterOptions(search, keyedOptions));
});
return promise;
}, isDebounced ? 250 : 0);
const promise = loadOptions();
return () => {
loadOptions.cancel();
if (promise) {
promise.canceled = true;
}
};
}, [filterValue]);
return [items];
};
}
//# sourceMappingURL=get-default-use-items.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,367 @@
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = Autocomplete;
exports.useAutocomplete = useAutocomplete;
exports.useAutocompleteProps = useAutocompleteProps;
var _react = require("react");
var _removeAccents = _interopRequireDefault(require("remove-accents"));
var _element = require("@wordpress/element");
var _compose = require("@wordpress/compose");
var _richText = require("@wordpress/rich-text");
var _a11y = require("@wordpress/a11y");
var _keycodes = require("@wordpress/keycodes");
var _autocompleterUi = require("./autocompleter-ui");
var _strings = require("../utils/strings");
/**
* External dependencies
*/
/**
* WordPress dependencies
*/
/**
* Internal dependencies
*/
const getNodeText = node => {
if (node === null) {
return '';
}
switch (typeof node) {
case 'string':
case 'number':
return node.toString();
break;
case 'boolean':
return '';
break;
case 'object':
{
if (node instanceof Array) {
return node.map(getNodeText).join('');
}
if ('props' in node) {
return getNodeText(node.props.children);
}
break;
}
default:
return '';
}
return '';
};
const EMPTY_FILTERED_OPTIONS = [];
function useAutocomplete({
record,
onChange,
onReplace,
completers,
contentRef
}) {
const instanceId = (0, _compose.useInstanceId)(useAutocomplete);
const [selectedIndex, setSelectedIndex] = (0, _element.useState)(0);
const [filteredOptions, setFilteredOptions] = (0, _element.useState)(EMPTY_FILTERED_OPTIONS);
const [filterValue, setFilterValue] = (0, _element.useState)('');
const [autocompleter, setAutocompleter] = (0, _element.useState)(null);
const [AutocompleterUI, setAutocompleterUI] = (0, _element.useState)(null);
const backspacing = (0, _element.useRef)(false);
function insertCompletion(replacement) {
if (autocompleter === null) {
return;
}
const end = record.start;
const start = end - autocompleter.triggerPrefix.length - filterValue.length;
const toInsert = (0, _richText.create)({
html: (0, _element.renderToString)(replacement)
});
onChange((0, _richText.insert)(record, toInsert, start, end));
}
function select(option) {
const {
getOptionCompletion
} = autocompleter || {};
if (option.isDisabled) {
return;
}
if (getOptionCompletion) {
const completion = getOptionCompletion(option.value, filterValue);
const isCompletionObject = obj => {
return obj !== null && typeof obj === 'object' && 'action' in obj && obj.action !== undefined && 'value' in obj && obj.value !== undefined;
};
const completionObject = isCompletionObject(completion) ? completion : {
action: 'insert-at-caret',
value: completion
};
if ('replace' === completionObject.action) {
onReplace([completionObject.value]);
// When replacing, the component will unmount, so don't reset
// state (below) on an unmounted component.
return;
} else if ('insert-at-caret' === completionObject.action) {
insertCompletion(completionObject.value);
}
}
// Reset autocomplete state after insertion rather than before
// so insertion events don't cause the completion menu to redisplay.
reset();
}
function reset() {
setSelectedIndex(0);
setFilteredOptions(EMPTY_FILTERED_OPTIONS);
setFilterValue('');
setAutocompleter(null);
setAutocompleterUI(null);
}
/**
* Load options for an autocompleter.
*
* @param {Array} options
*/
function onChangeOptions(options) {
setSelectedIndex(options.length === filteredOptions.length ? selectedIndex : 0);
setFilteredOptions(options);
}
function handleKeyDown(event) {
backspacing.current = event.key === 'Backspace';
if (!autocompleter) {
return;
}
if (filteredOptions.length === 0) {
return;
}
if (event.defaultPrevented ||
// Ignore keydowns from IMEs
event.isComposing ||
// Workaround for Mac Safari where the final Enter/Backspace of an IME composition
// is `isComposing=false`, even though it's technically still part of the composition.
// These can only be detected by keyCode.
event.keyCode === 229) {
return;
}
switch (event.key) {
case 'ArrowUp':
{
const newIndex = (selectedIndex === 0 ? filteredOptions.length : selectedIndex) - 1;
setSelectedIndex(newIndex);
// See the related PR as to why this is necessary: https://github.com/WordPress/gutenberg/pull/54902.
if ((0, _keycodes.isAppleOS)()) {
(0, _a11y.speak)(getNodeText(filteredOptions[newIndex].label), 'assertive');
}
break;
}
case 'ArrowDown':
{
const newIndex = (selectedIndex + 1) % filteredOptions.length;
setSelectedIndex(newIndex);
if ((0, _keycodes.isAppleOS)()) {
(0, _a11y.speak)(getNodeText(filteredOptions[newIndex].label), 'assertive');
}
break;
}
case 'Escape':
setAutocompleter(null);
setAutocompleterUI(null);
event.preventDefault();
break;
case 'Enter':
select(filteredOptions[selectedIndex]);
break;
case 'ArrowLeft':
case 'ArrowRight':
reset();
return;
default:
return;
}
// Any handled key should prevent original behavior. This relies on
// the early return in the default case.
event.preventDefault();
}
// textContent is a primitive (string), memoizing is not strictly necessary
// but this is a preemptive performance improvement, since the autocompleter
// is a potential bottleneck for the editor type metric.
const textContent = (0, _element.useMemo)(() => {
if ((0, _richText.isCollapsed)(record)) {
return (0, _richText.getTextContent)((0, _richText.slice)(record, 0));
}
return '';
}, [record]);
(0, _element.useEffect)(() => {
if (!textContent) {
if (autocompleter) reset();
return;
}
// Find the completer with the highest triggerPrefix index in the
// textContent.
const completer = completers.reduce((lastTrigger, currentCompleter) => {
const triggerIndex = textContent.lastIndexOf(currentCompleter.triggerPrefix);
const lastTriggerIndex = lastTrigger !== null ? textContent.lastIndexOf(lastTrigger.triggerPrefix) : -1;
return triggerIndex > lastTriggerIndex ? currentCompleter : lastTrigger;
}, null);
if (!completer) {
if (autocompleter) reset();
return;
}
const {
allowContext,
triggerPrefix
} = completer;
const triggerIndex = textContent.lastIndexOf(triggerPrefix);
const textWithoutTrigger = textContent.slice(triggerIndex + triggerPrefix.length);
const tooDistantFromTrigger = textWithoutTrigger.length > 50; // 50 chars seems to be a good limit.
// This is a final barrier to prevent the effect from completing with
// an extremely long string, which causes the editor to slow-down
// significantly. This could happen, for example, if `matchingWhileBackspacing`
// is true and one of the "words" end up being too long. If that's the case,
// it will be caught by this guard.
if (tooDistantFromTrigger) return;
const mismatch = filteredOptions.length === 0;
const wordsFromTrigger = textWithoutTrigger.split(/\s/);
// We need to allow the effect to run when not backspacing and if there
// was a mismatch. i.e when typing a trigger + the match string or when
// clicking in an existing trigger word on the page. We do that if we
// detect that we have one word from trigger in the current textual context.
//
// Ex.: "Some text @a" <-- "@a" will be detected as the trigger word and
// allow the effect to run. It will run until there's a mismatch.
const hasOneTriggerWord = wordsFromTrigger.length === 1;
// This is used to allow the effect to run when backspacing and if
// "touching" a word that "belongs" to a trigger. We consider a "trigger
// word" any word up to the limit of 3 from the trigger character.
// Anything beyond that is ignored if there's a mismatch. This allows
// us to "escape" a mismatch when backspacing, but still imposing some
// sane limits.
//
// Ex: "Some text @marcelo sekkkk" <--- "kkkk" caused a mismatch, but
// if the user presses backspace here, it will show the completion popup again.
const matchingWhileBackspacing = backspacing.current && wordsFromTrigger.length <= 3;
if (mismatch && !(matchingWhileBackspacing || hasOneTriggerWord)) {
if (autocompleter) reset();
return;
}
const textAfterSelection = (0, _richText.getTextContent)((0, _richText.slice)(record, undefined, (0, _richText.getTextContent)(record).length));
if (allowContext && !allowContext(textContent.slice(0, triggerIndex), textAfterSelection)) {
if (autocompleter) reset();
return;
}
if (/^\s/.test(textWithoutTrigger) || /\s\s+$/.test(textWithoutTrigger)) {
if (autocompleter) reset();
return;
}
if (!/[\u0000-\uFFFF]*$/.test(textWithoutTrigger)) {
if (autocompleter) reset();
return;
}
const safeTrigger = (0, _strings.escapeRegExp)(completer.triggerPrefix);
const text = (0, _removeAccents.default)(textContent);
const match = text.slice(text.lastIndexOf(completer.triggerPrefix)).match(new RegExp(`${safeTrigger}([\u0000-\uFFFF]*)$`));
const query = match && match[1];
setAutocompleter(completer);
setAutocompleterUI(() => completer !== autocompleter ? (0, _autocompleterUi.getAutoCompleterUI)(completer) : AutocompleterUI);
setFilterValue(query === null ? '' : query);
// Temporarily disabling exhaustive-deps to avoid introducing unexpected side effecst.
// See https://github.com/WordPress/gutenberg/pull/41820
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [textContent]);
const {
key: selectedKey = ''
} = filteredOptions[selectedIndex] || {};
const {
className
} = autocompleter || {};
const isExpanded = !!autocompleter && filteredOptions.length > 0;
const listBoxId = isExpanded ? `components-autocomplete-listbox-${instanceId}` : undefined;
const activeId = isExpanded ? `components-autocomplete-item-${instanceId}-${selectedKey}` : null;
const hasSelection = record.start !== undefined;
return {
listBoxId,
activeId,
onKeyDown: handleKeyDown,
popover: hasSelection && AutocompleterUI && (0, _react.createElement)(AutocompleterUI, {
className: className,
filterValue: filterValue,
instanceId: instanceId,
listBoxId: listBoxId,
selectedIndex: selectedIndex,
onChangeOptions: onChangeOptions,
onSelect: select,
value: record,
contentRef: contentRef,
reset: reset
})
};
}
function useLastDifferentValue(value) {
const history = (0, _element.useRef)(new Set());
history.current.add(value);
// Keep the history size to 2.
if (history.current.size > 2) {
history.current.delete(Array.from(history.current)[0]);
}
return Array.from(history.current)[0];
}
function useAutocompleteProps(options) {
const ref = (0, _element.useRef)(null);
const onKeyDownRef = (0, _element.useRef)();
const {
record
} = options;
const previousRecord = useLastDifferentValue(record);
const {
popover,
listBoxId,
activeId,
onKeyDown
} = useAutocomplete({
...options,
contentRef: ref
});
onKeyDownRef.current = onKeyDown;
const mergedRefs = (0, _compose.useMergeRefs)([ref, (0, _compose.useRefEffect)(element => {
function _onKeyDown(event) {
onKeyDownRef.current?.(event);
}
element.addEventListener('keydown', _onKeyDown);
return () => {
element.removeEventListener('keydown', _onKeyDown);
};
}, [])]);
// We only want to show the popover if the user has typed something.
const didUserInput = record.text !== previousRecord?.text;
if (!didUserInput) {
return {
ref: mergedRefs
};
}
return {
ref: mergedRefs,
children: popover,
'aria-autocomplete': listBoxId ? 'list' : undefined,
'aria-owns': listBoxId,
'aria-activedescendant': activeId
};
}
function Autocomplete({
children,
isSelected,
...options
}) {
const {
popover,
...props
} = useAutocomplete(options);
return (0, _react.createElement)(_react.Fragment, null, children(props), isSelected && popover);
}
//# sourceMappingURL=index.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
//# sourceMappingURL=types.js.map

File diff suppressed because one or more lines are too long