@wordpress/block-editor
Version:
421 lines (402 loc) • 17.9 kB
JavaScript
"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