UNPKG

@wordpress/block-editor

Version:
421 lines (402 loc) 17.9 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = exports.DeprecatedExperimentalLinkControl = void 0; var _clsx = _interopRequireDefault(require("clsx")); var _components = require("@wordpress/components"); var _i18n = require("@wordpress/i18n"); var _element = require("@wordpress/element"); var _dom = require("@wordpress/dom"); var _keycodes = require("@wordpress/keycodes"); var _isShallowEqual = require("@wordpress/is-shallow-equal"); var _data = require("@wordpress/data"); var _preferences = require("@wordpress/preferences"); var _icons = require("@wordpress/icons"); var _settingsDrawer = _interopRequireDefault(require("./settings-drawer")); var _searchInput = _interopRequireDefault(require("./search-input")); var _linkPreview = _interopRequireDefault(require("./link-preview")); var _settings = _interopRequireDefault(require("./settings")); var _useCreatePage = _interopRequireDefault(require("./use-create-page")); var _useInternalValue = _interopRequireDefault(require("./use-internal-value")); var _viewerSlot = require("./viewer-slot"); var _constants = require("./constants"); var _deprecated = _interopRequireDefault(require("@wordpress/deprecated")); var _jsxRuntime = require("react/jsx-runtime"); /** * External dependencies */ /** * WordPress dependencies */ /** * Internal dependencies */ /** * Default properties associated with a link control value. * * @typedef WPLinkControlDefaultValue * * @property {string} url Link URL. * @property {string=} title Link title. * @property {boolean=} opensInNewTab Whether link should open in a new browser * tab. This value is only assigned if not * providing a custom `settings` prop. */ /* eslint-disable jsdoc/valid-types */ /** * Custom settings values associated with a link. * * @typedef {{[setting:string]:any}} WPLinkControlSettingsValue */ /* eslint-enable */ /** * Custom settings values associated with a link. * * @typedef WPLinkControlSetting * * @property {string} id Identifier to use as property for setting value. * @property {string} title Human-readable label to show in user interface. */ /** * Properties associated with a link control value, composed as a union of the * default properties and any custom settings values. * * @typedef {WPLinkControlDefaultValue&WPLinkControlSettingsValue} WPLinkControlValue */ /** @typedef {(nextValue:WPLinkControlValue)=>void} WPLinkControlOnChangeProp */ /** * Properties associated with a search suggestion used within the LinkControl. * * @typedef WPLinkControlSuggestion * * @property {string} id Identifier to use to uniquely identify the suggestion. * @property {string} type Identifies the type of the suggestion (eg: `post`, * `page`, `url`...etc) * @property {string} title Human-readable label to show in user interface. * @property {string} url A URL for the suggestion. */ /** @typedef {(title:string)=>WPLinkControlSuggestion} WPLinkControlCreateSuggestionProp */ /** * @typedef WPLinkControlProps * * @property {(WPLinkControlSetting[])=} settings An array of settings objects. Each object will used to * render a `ToggleControl` for that setting. * @property {boolean=} forceIsEditingLink If passed as either `true` or `false`, controls the * internal editing state of the component to respective * show or not show the URL input field. * @property {WPLinkControlValue=} value Current link value. * @property {WPLinkControlOnChangeProp=} onChange Value change handler, called with the updated value if * the user selects a new link or updates settings. * @property {boolean=} noDirectEntry Whether to allow turning a URL-like search query directly into a link. * @property {boolean=} showSuggestions Whether to present suggestions when typing the URL. * @property {boolean=} showInitialSuggestions Whether to present initial suggestions immediately. * @property {boolean=} withCreateSuggestion Whether to allow creation of link value from suggestion. * @property {Object=} suggestionsQuery Query parameters to pass along to wp.blockEditor.__experimentalFetchLinkSuggestions. * @property {boolean=} noURLSuggestion Whether to add a fallback suggestion which treats the search query as a URL. * @property {boolean=} hasTextControl Whether to add a text field to the UI to update the value.title. * @property {string|Function|undefined} createSuggestionButtonText The text to use in the button that calls createSuggestion. * @property {Function} renderControlBottom Optional controls to be rendered at the bottom of the component. */const noop = () => {}; const PREFERENCE_SCOPE = 'core/block-editor'; const PREFERENCE_KEY = 'linkControlSettingsDrawer'; /** * Renders a link control. A link control is a controlled input which maintains * a value associated with a link (HTML anchor element) and relevant settings * for how that link is expected to behave. * * @param {WPLinkControlProps} props Component props. */ function LinkControl({ searchInputPlaceholder, value, settings = _constants.DEFAULT_LINK_SETTINGS, onChange = noop, onRemove, onCancel, noDirectEntry = false, showSuggestions = true, showInitialSuggestions, forceIsEditingLink, createSuggestion, withCreateSuggestion, inputValue: propInputValue = '', suggestionsQuery = {}, noURLSuggestion = false, createSuggestionButtonText, hasRichPreviews = false, hasTextControl = false, renderControlBottom = null }) { if (withCreateSuggestion === undefined && createSuggestion) { withCreateSuggestion = true; } const [settingsOpen, setSettingsOpen] = (0, _element.useState)(false); const { advancedSettingsPreference } = (0, _data.useSelect)(select => { var _prefsStore$get; const prefsStore = select(_preferences.store); return { advancedSettingsPreference: (_prefsStore$get = prefsStore.get(PREFERENCE_SCOPE, PREFERENCE_KEY)) !== null && _prefsStore$get !== void 0 ? _prefsStore$get : false }; }, []); const { set: setPreference } = (0, _data.useDispatch)(_preferences.store); /** * Sets the open/closed state of the Advanced Settings Drawer, * optionlly persisting the state to the user's preferences. * * Note that Block Editor components can be consumed by non-WordPress * environments which may not have preferences setup. * Therefore a local state is also used as a fallback. * * @param {boolean} prefVal the open/closed state of the Advanced Settings Drawer. */ const setSettingsOpenWithPreference = prefVal => { if (setPreference) { setPreference(PREFERENCE_SCOPE, PREFERENCE_KEY, prefVal); } setSettingsOpen(prefVal); }; // Block Editor components can be consumed by non-WordPress environments // which may not have these preferences setup. // Therefore a local state is used as a fallback. const isSettingsOpen = advancedSettingsPreference || settingsOpen; const isMountingRef = (0, _element.useRef)(true); const wrapperNode = (0, _element.useRef)(); const textInputRef = (0, _element.useRef)(); const isEndingEditWithFocusRef = (0, _element.useRef)(false); const settingsKeys = settings.map(({ id }) => id); const [internalControlValue, setInternalControlValue, setInternalURLInputValue, setInternalTextInputValue, createSetInternalSettingValueHandler] = (0, _useInternalValue.default)(value); const valueHasChanges = value && !(0, _isShallowEqual.isShallowEqualObjects)(internalControlValue, value); const [isEditingLink, setIsEditingLink] = (0, _element.useState)(forceIsEditingLink !== undefined ? forceIsEditingLink : !value || !value.url); const { createPage, isCreatingPage, errorMessage } = (0, _useCreatePage.default)(createSuggestion); (0, _element.useEffect)(() => { if (forceIsEditingLink === undefined) { return; } setIsEditingLink(forceIsEditingLink); }, [forceIsEditingLink]); (0, _element.useEffect)(() => { // We don't auto focus into the Link UI on mount // because otherwise using the keyboard to select text // *within* the link format is not possible. if (isMountingRef.current) { return; } // Scenario - when: // - switching between editable and non editable LinkControl // - clicking on a link // ...then move focus to the *first* element to avoid focus loss // and to ensure focus is *within* the Link UI. const nextFocusTarget = _dom.focus.focusable.find(wrapperNode.current)[0] || wrapperNode.current; nextFocusTarget.focus(); isEndingEditWithFocusRef.current = false; }, [isEditingLink, isCreatingPage]); // The component mounting reference is maintained separately // to correctly reset values in `StrictMode`. (0, _element.useEffect)(() => { isMountingRef.current = false; return () => { isMountingRef.current = true; }; }, []); const hasLinkValue = value?.url?.trim()?.length > 0; /** * Cancels editing state and marks that focus may need to be restored after * the next render, if focus was within the wrapper when editing finished. */ const stopEditing = () => { isEndingEditWithFocusRef.current = !!wrapperNode.current?.contains(wrapperNode.current.ownerDocument.activeElement); setIsEditingLink(false); }; const handleSelectSuggestion = updatedValue => { // Suggestions may contains "settings" values (e.g. `opensInNewTab`) // which should not override any existing settings values set by the // user. This filters out any settings values from the suggestion. const nonSettingsChanges = Object.keys(updatedValue).reduce((acc, key) => { if (!settingsKeys.includes(key)) { acc[key] = updatedValue[key]; } return acc; }, {}); onChange({ ...internalControlValue, ...nonSettingsChanges, // As title is not a setting, it must be manually applied // in such a way as to preserve the users changes over // any "title" value provided by the "suggestion". title: internalControlValue?.title || updatedValue?.title }); stopEditing(); }; const handleSubmit = () => { if (valueHasChanges) { // Submit the original value with new stored values applied // on top. URL is a special case as it may also be a prop. onChange({ ...value, ...internalControlValue, url: currentUrlInputValue }); } stopEditing(); }; const handleSubmitWithEnter = event => { const { keyCode } = event; if (keyCode === _keycodes.ENTER && !currentInputIsEmpty // Disallow submitting empty values. ) { event.preventDefault(); handleSubmit(); } }; const resetInternalValues = () => { setInternalControlValue(value); }; const handleCancel = event => { event.preventDefault(); event.stopPropagation(); // Ensure that any unsubmitted input changes are reset. resetInternalValues(); if (hasLinkValue) { // If there is a link then exist editing mode and show preview. stopEditing(); } else { // If there is no link value, then remove the link entirely. onRemove?.(); } onCancel?.(); }; const currentUrlInputValue = propInputValue || internalControlValue?.url || ''; const currentInputIsEmpty = !currentUrlInputValue?.trim()?.length; const shownUnlinkControl = onRemove && value && !isEditingLink && !isCreatingPage; const showActions = isEditingLink && hasLinkValue; // Only show text control once a URL value has been committed // and it isn't just empty whitespace. // See https://github.com/WordPress/gutenberg/pull/33849/#issuecomment-932194927. const showTextControl = hasLinkValue && hasTextControl; const isEditing = (isEditingLink || !value) && !isCreatingPage; const isDisabled = !valueHasChanges || currentInputIsEmpty; const showSettings = !!settings?.length && isEditingLink && hasLinkValue; return /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", { tabIndex: -1, ref: wrapperNode, className: "block-editor-link-control", children: [isCreatingPage && /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", { className: "block-editor-link-control__loading", children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_components.Spinner, {}), " ", (0, _i18n.__)('Creating'), "\u2026"] }), isEditing && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, { children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)("div", { className: (0, _clsx.default)({ 'block-editor-link-control__search-input-wrapper': true, 'has-text-control': showTextControl, 'has-actions': showActions }), children: [showTextControl && /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.TextControl, { __nextHasNoMarginBottom: true, ref: textInputRef, className: "block-editor-link-control__field block-editor-link-control__text-content", label: (0, _i18n.__)('Text'), value: internalControlValue?.title, onChange: setInternalTextInputValue, onKeyDown: handleSubmitWithEnter, __next40pxDefaultSize: true }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_searchInput.default, { currentLink: value, className: "block-editor-link-control__field block-editor-link-control__search-input", placeholder: searchInputPlaceholder, value: currentUrlInputValue, withCreateSuggestion: withCreateSuggestion, onCreateSuggestion: createPage, onChange: setInternalURLInputValue, onSelect: handleSelectSuggestion, showInitialSuggestions: showInitialSuggestions, allowDirectEntry: !noDirectEntry, showSuggestions: showSuggestions, suggestionsQuery: suggestionsQuery, withURLSuggestion: !noURLSuggestion, createSuggestionButtonText: createSuggestionButtonText, hideLabelFromVision: !showTextControl, suffix: showActions ? undefined : /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.__experimentalInputControlSuffixWrapper, { variant: "control", children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.Button, { onClick: isDisabled ? noop : handleSubmit, label: (0, _i18n.__)('Submit'), icon: _icons.keyboardReturn, className: "block-editor-link-control__search-submit", "aria-disabled": isDisabled, size: "small" }) }) })] }), errorMessage && /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.Notice, { className: "block-editor-link-control__search-error", status: "error", isDismissible: false, children: errorMessage })] }), value && !isEditingLink && !isCreatingPage && /*#__PURE__*/(0, _jsxRuntime.jsx)(_linkPreview.default, { // force remount when URL changes to avoid race conditions for rich previews value: value, onEditClick: () => setIsEditingLink(true), hasRichPreviews: hasRichPreviews, hasUnlinkControl: shownUnlinkControl, onRemove: () => { onRemove(); setIsEditingLink(true); } }, value?.url), showSettings && /*#__PURE__*/(0, _jsxRuntime.jsx)("div", { className: "block-editor-link-control__tools", children: !currentInputIsEmpty && /*#__PURE__*/(0, _jsxRuntime.jsx)(_settingsDrawer.default, { settingsOpen: isSettingsOpen, setSettingsOpen: setSettingsOpenWithPreference, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_settings.default, { value: internalControlValue, settings: settings, onChange: createSetInternalSettingValueHandler(settingsKeys) }) }) }), showActions && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_components.__experimentalHStack, { justify: "right", className: "block-editor-link-control__search-actions", children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_components.Button, { __next40pxDefaultSize: true, variant: "tertiary", onClick: handleCancel, children: (0, _i18n.__)('Cancel') }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.Button, { __next40pxDefaultSize: true, variant: "primary", onClick: isDisabled ? noop : handleSubmit, className: "block-editor-link-control__search-submit", "aria-disabled": isDisabled, children: (0, _i18n.__)('Save') })] }), !isCreatingPage && renderControlBottom && renderControlBottom()] }); } LinkControl.ViewerFill = _viewerSlot.ViewerFill; LinkControl.DEFAULT_LINK_SETTINGS = _constants.DEFAULT_LINK_SETTINGS; const DeprecatedExperimentalLinkControl = props => { (0, _deprecated.default)('wp.blockEditor.__experimentalLinkControl', { since: '6.8', alternative: 'wp.blockEditor.LinkControl' }); return /*#__PURE__*/(0, _jsxRuntime.jsx)(LinkControl, { ...props }); }; exports.DeprecatedExperimentalLinkControl = DeprecatedExperimentalLinkControl; var _default = exports.default = LinkControl; //# sourceMappingURL=index.js.map