@wordpress/block-editor
Version:
438 lines (437 loc) • 13.4 kB
JavaScript
// packages/block-editor/src/components/url-input/index.js
import clsx from "clsx";
import { __, sprintf, _n } from "@wordpress/i18n";
import { Component, createRef } from "@wordpress/element";
import { UP, DOWN, ENTER, TAB } from "@wordpress/keycodes";
import {
BaseControl,
Button,
__experimentalInputControl as InputControl,
Spinner,
withSpokenMessages,
Popover
} from "@wordpress/components";
import {
compose,
debounce,
withInstanceId,
withSafeTimeout
} from "@wordpress/compose";
import { withSelect } from "@wordpress/data";
import { isURL } from "@wordpress/url";
import { store as blockEditorStore } from "../../store";
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
import { createElement } from "react";
function isFunction(maybeFunc) {
return typeof maybeFunc === "function";
}
var URLInput = class extends Component {
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this);
this.onFocus = this.onFocus.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
this.selectLink = this.selectLink.bind(this);
this.handleOnClick = this.handleOnClick.bind(this);
this.bindSuggestionNode = this.bindSuggestionNode.bind(this);
this.autocompleteRef = props.autocompleteRef || createRef();
this.inputRef = props.inputRef || createRef();
this.updateSuggestions = debounce(
this.updateSuggestions.bind(this),
200
);
this.suggestionNodes = [];
this.suggestionsRequest = null;
this.state = {
suggestions: [],
showSuggestions: false,
suggestionsValue: null,
selectedSuggestion: null,
suggestionsListboxId: "",
suggestionOptionIdPrefix: ""
};
}
componentDidUpdate(prevProps) {
const { showSuggestions, selectedSuggestion } = this.state;
const { value, __experimentalShowInitialSuggestions = false } = this.props;
if (showSuggestions && selectedSuggestion !== null && this.suggestionNodes[selectedSuggestion]) {
this.suggestionNodes[selectedSuggestion].scrollIntoView({
behavior: "instant",
block: "nearest",
inline: "nearest"
});
}
if (prevProps.value !== value && !this.props.disableSuggestions) {
if (value?.length) {
this.updateSuggestions(value);
} else if (__experimentalShowInitialSuggestions) {
this.updateSuggestions();
}
}
}
componentDidMount() {
if (this.shouldShowInitialSuggestions()) {
this.updateSuggestions();
}
}
componentWillUnmount() {
this.suggestionsRequest?.cancel?.();
this.suggestionsRequest = null;
}
bindSuggestionNode(index) {
return (ref) => {
this.suggestionNodes[index] = ref;
};
}
shouldShowInitialSuggestions() {
const { __experimentalShowInitialSuggestions = false, value } = this.props;
return __experimentalShowInitialSuggestions && !(value && value.length);
}
updateSuggestions(value = "") {
const {
__experimentalFetchLinkSuggestions: fetchLinkSuggestions,
__experimentalHandleURLSuggestions: handleURLSuggestions
} = this.props;
if (!fetchLinkSuggestions) {
return;
}
const isInitialSuggestions = !value?.length;
value = value.trim();
if (!isInitialSuggestions && (value.length < 2 || !handleURLSuggestions && isURL(value))) {
this.suggestionsRequest?.cancel?.();
this.suggestionsRequest = null;
this.setState({
suggestions: [],
showSuggestions: false,
suggestionsValue: value,
selectedSuggestion: null,
loading: false
});
return;
}
this.setState({
selectedSuggestion: null,
loading: true
});
const request = fetchLinkSuggestions(value, {
isInitialSuggestions
});
request.then((suggestions) => {
if (this.suggestionsRequest !== request) {
return;
}
this.setState({
suggestions,
suggestionsValue: value,
loading: false,
showSuggestions: !!suggestions.length
});
if (!!suggestions.length) {
this.props.debouncedSpeak(
sprintf(
/* translators: %d: number of results. */
_n(
"%d result found, use up and down arrow keys to navigate.",
"%d results found, use up and down arrow keys to navigate.",
suggestions.length
),
suggestions.length
),
"assertive"
);
} else {
this.props.debouncedSpeak(
__("No results."),
"assertive"
);
}
}).catch(() => {
if (this.suggestionsRequest !== request) {
return;
}
this.setState({
loading: false
});
}).finally(() => {
if (this.suggestionsRequest === request) {
this.suggestionsRequest = null;
}
});
this.suggestionsRequest = request;
}
onChange(newValue) {
this.props.onChange(newValue);
}
onFocus() {
const { suggestions } = this.state;
const { disableSuggestions, value } = this.props;
if (value && !disableSuggestions && !(suggestions && suggestions.length) && this.suggestionsRequest === null) {
this.updateSuggestions(value);
}
}
onKeyDown(event) {
this.props.onKeyDown?.(event);
const { showSuggestions, selectedSuggestion, suggestions, loading } = this.state;
if (!showSuggestions || !suggestions.length || loading) {
switch (event.keyCode) {
// When UP is pressed, if the caret is at the start of the text, move it to the 0
// position.
case UP: {
if (0 !== event.target.selectionStart) {
event.preventDefault();
event.target.setSelectionRange(0, 0);
}
break;
}
// When DOWN is pressed, if the caret is not at the end of the text, move it to the
// last position.
case DOWN: {
if (this.props.value.length !== event.target.selectionStart) {
event.preventDefault();
event.target.setSelectionRange(
this.props.value.length,
this.props.value.length
);
}
break;
}
// Submitting while loading should trigger onSubmit.
case ENTER: {
if (this.props.onSubmit) {
event.preventDefault();
this.props.onSubmit(null, event);
}
break;
}
}
return;
}
const suggestion = this.state.suggestions[this.state.selectedSuggestion];
switch (event.keyCode) {
case UP: {
event.preventDefault();
const previousIndex = !selectedSuggestion ? suggestions.length - 1 : selectedSuggestion - 1;
this.setState({
selectedSuggestion: previousIndex
});
break;
}
case DOWN: {
event.preventDefault();
const nextIndex = selectedSuggestion === null || selectedSuggestion === suggestions.length - 1 ? 0 : selectedSuggestion + 1;
this.setState({
selectedSuggestion: nextIndex
});
break;
}
case TAB: {
if (this.state.selectedSuggestion !== null) {
this.selectLink(suggestion);
this.props.speak(__("Link selected."));
}
break;
}
case ENTER: {
event.preventDefault();
if (this.state.selectedSuggestion !== null) {
this.selectLink(suggestion);
if (this.props.onSubmit) {
this.props.onSubmit(suggestion, event);
}
} else if (this.props.onSubmit) {
this.props.onSubmit(null, event);
}
break;
}
}
}
selectLink(suggestion) {
this.props.onChange(suggestion.url, suggestion);
this.setState({
selectedSuggestion: null,
showSuggestions: false
});
}
handleOnClick(suggestion) {
this.selectLink(suggestion);
this.inputRef.current.focus();
}
static getDerivedStateFromProps({
value,
instanceId,
disableSuggestions,
__experimentalShowInitialSuggestions = false
}, { showSuggestions }) {
let shouldShowSuggestions = showSuggestions;
const hasValue = value && value.length;
if (!__experimentalShowInitialSuggestions && !hasValue) {
shouldShowSuggestions = false;
}
if (disableSuggestions === true) {
shouldShowSuggestions = false;
}
return {
showSuggestions: shouldShowSuggestions,
suggestionsListboxId: `block-editor-url-input-suggestions-${instanceId}`,
suggestionOptionIdPrefix: `block-editor-url-input-suggestion-${instanceId}`
};
}
render() {
return /* @__PURE__ */ jsxs(Fragment, { children: [
this.renderControl(),
this.renderSuggestions()
] });
}
renderControl() {
const {
label = null,
className,
isFullWidth,
instanceId,
placeholder = __("Paste URL or type to search"),
__experimentalRenderControl: renderControl,
value = "",
hideLabelFromVision = false,
help = null,
disabled = false
} = this.props;
const {
loading,
showSuggestions,
selectedSuggestion,
suggestionsListboxId,
suggestionOptionIdPrefix
} = this.state;
const inputId = `url-input-control-${instanceId}`;
const controlProps = {
id: inputId,
// Passes attribute to label for the for attribute
label,
className: clsx("block-editor-url-input", className, {
"is-full-width": isFullWidth
}),
hideLabelFromVision
};
const inputProps = {
id: inputId,
value,
required: true,
type: "text",
onChange: disabled ? () => {
} : this.onChange,
// Disable onChange when disabled
onFocus: disabled ? () => {
} : this.onFocus,
// Disable onFocus when disabled
placeholder,
onKeyDown: disabled ? () => {
} : this.onKeyDown,
// Disable onKeyDown when disabled
role: "combobox",
"aria-label": label ? void 0 : __("URL"),
// Ensure input always has an accessible label
"aria-expanded": showSuggestions,
"aria-autocomplete": "list",
"aria-owns": suggestionsListboxId,
"aria-activedescendant": selectedSuggestion !== null ? `${suggestionOptionIdPrefix}-${selectedSuggestion}` : void 0,
ref: this.inputRef,
disabled,
suffix: this.props.suffix,
help
};
if (renderControl) {
return renderControl(controlProps, inputProps, loading);
}
return /* @__PURE__ */ jsxs(BaseControl, { __nextHasNoMarginBottom: true, ...controlProps, children: [
/* @__PURE__ */ jsx(InputControl, { ...inputProps, __next40pxDefaultSize: true }),
loading && /* @__PURE__ */ jsx(Spinner, {})
] });
}
renderSuggestions() {
const {
className,
__experimentalRenderSuggestions: renderSuggestions
} = this.props;
const {
showSuggestions,
suggestions,
suggestionsValue,
selectedSuggestion,
suggestionsListboxId,
suggestionOptionIdPrefix,
loading
} = this.state;
if (!showSuggestions || suggestions.length === 0) {
return null;
}
const suggestionsListProps = {
id: suggestionsListboxId,
ref: this.autocompleteRef,
role: "listbox"
};
const buildSuggestionItemProps = (suggestion, index) => {
return {
role: "option",
tabIndex: "-1",
id: `${suggestionOptionIdPrefix}-${index}`,
ref: this.bindSuggestionNode(index),
"aria-selected": index === selectedSuggestion ? true : void 0
};
};
if (isFunction(renderSuggestions)) {
return renderSuggestions({
suggestions,
selectedSuggestion,
suggestionsListProps,
buildSuggestionItemProps,
isLoading: loading,
handleSuggestionClick: this.handleOnClick,
isInitialSuggestions: !suggestionsValue?.length,
currentInputValue: suggestionsValue
});
}
return /* @__PURE__ */ jsx(Popover, { placement: "bottom", focusOnMount: false, children: /* @__PURE__ */ jsx(
"div",
{
...suggestionsListProps,
className: clsx("block-editor-url-input__suggestions", {
[`${className}__suggestions`]: className
}),
children: suggestions.map((suggestion, index) => /* @__PURE__ */ createElement(
Button,
{
__next40pxDefaultSize: true,
...buildSuggestionItemProps(suggestion, index),
key: suggestion.id,
className: clsx(
"block-editor-url-input__suggestion",
{
"is-selected": index === selectedSuggestion
}
),
onClick: () => this.handleOnClick(suggestion)
},
suggestion.title
))
}
) });
}
};
var url_input_default = compose(
withSafeTimeout,
withSpokenMessages,
withInstanceId,
withSelect((select, props) => {
if (isFunction(props.__experimentalFetchLinkSuggestions)) {
return;
}
const { getSettings } = select(blockEditorStore);
return {
__experimentalFetchLinkSuggestions: getSettings().__experimentalFetchLinkSuggestions
};
})
)(URLInput);
export {
url_input_default as default
};
//# sourceMappingURL=index.js.map