@gechiui/block-editor
Version:
337 lines (287 loc) • 13.7 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _element = require("@gechiui/element");
var _lodash = require("lodash");
var _classnames = _interopRequireDefault(require("classnames"));
var _components = require("@gechiui/components");
var _icons = require("@gechiui/icons");
var _i18n = require("@gechiui/i18n");
var _dom = require("@gechiui/dom");
var _keycodes = require("@gechiui/keycodes");
var _settingsDrawer = _interopRequireDefault(require("./settings-drawer"));
var _searchInput = _interopRequireDefault(require("./search-input"));
var _linkPreview = _interopRequireDefault(require("./link-preview"));
var _useCreatePage = _interopRequireDefault(require("./use-create-page"));
var _viewerSlot = require("./viewer-slot");
var _constants = require("./constants");
/**
* External dependencies
*/
/**
* GeChiUI dependencies
*/
/**
* Internal dependencies
*/
/**
* Default properties associated with a link control value.
*
* @typedef GCLinkControlDefaultValue
*
* @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}} GCLinkControlSettingsValue
*/
/* eslint-enable */
/**
* Custom settings values associated with a link.
*
* @typedef GCLinkControlSetting
*
* @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 {GCLinkControlDefaultValue&GCLinkControlSettingsValue} GCLinkControlValue
*/
/** @typedef {(nextValue:GCLinkControlValue)=>void} GCLinkControlOnChangeProp */
/**
* Properties associated with a search suggestion used within the LinkControl.
*
* @typedef GCLinkControlSuggestion
*
* @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)=>GCLinkControlSuggestion} GCLinkControlCreateSuggestionProp */
/**
* @typedef GCLinkControlProps
*
* @property {(GCLinkControlSetting[])=} 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 {GCLinkControlValue=} value Current link value.
* @property {GCLinkControlOnChangeProp=} 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 gc.blockEditor.__experimentalFetchLinkSuggestions.
* @property {boolean=} noURLSuggestion Whether to add a fallback suggestion which treats the search query as a URL.
* @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.
*/
/**
* 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 {GCLinkControlProps} props Component props.
*/
function LinkControl(_ref) {
var _currentInputValue$tr, _value$url, _value$url$trim;
let {
searchInputPlaceholder,
value,
settings = _constants.DEFAULT_LINK_SETTINGS,
onChange = _lodash.noop,
onRemove,
noDirectEntry = false,
showSuggestions = true,
showInitialSuggestions,
forceIsEditingLink,
createSuggestion,
withCreateSuggestion,
inputValue: propInputValue = '',
suggestionsQuery = {},
noURLSuggestion = false,
createSuggestionButtonText,
hasRichPreviews = false,
hasTextControl = false,
renderControlBottom = null
} = _ref;
if (withCreateSuggestion === undefined && createSuggestion) {
withCreateSuggestion = true;
}
const isMounting = (0, _element.useRef)(true);
const wrapperNode = (0, _element.useRef)();
const textInputRef = (0, _element.useRef)();
const [internalInputValue, setInternalInputValue] = (0, _element.useState)((value === null || value === void 0 ? void 0 : value.url) || '');
const [internalTextValue, setInternalTextValue] = (0, _element.useState)((value === null || value === void 0 ? void 0 : value.title) || '');
const currentInputValue = propInputValue || internalInputValue;
const [isEditingLink, setIsEditingLink] = (0, _element.useState)(forceIsEditingLink !== undefined ? forceIsEditingLink : !value || !value.url);
const isEndingEditWithFocus = (0, _element.useRef)(false);
const currentInputIsEmpty = !(currentInputValue !== null && currentInputValue !== void 0 && (_currentInputValue$tr = currentInputValue.trim()) !== null && _currentInputValue$tr !== void 0 && _currentInputValue$tr.length);
(0, _element.useEffect)(() => {
if (forceIsEditingLink !== undefined && forceIsEditingLink !== isEditingLink) {
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 (isMounting.current) {
isMounting.current = false;
return;
} // Unless we are mounting, we always want to focus either:
// - the URL input
// - the first focusable element in the Link UI.
// But in editing mode if there is a text input present then
// the URL input is at index 1. If not then it is at index 0.
const whichFocusTargetIndex = textInputRef !== null && textInputRef !== void 0 && textInputRef.current ? 1 : 0; // 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)[whichFocusTargetIndex] || wrapperNode.current;
nextFocusTarget.focus();
isEndingEditWithFocus.current = false;
}, [isEditingLink]);
(0, _element.useEffect)(() => {
/**
* If the value's `text` property changes then sync this
* back up with state.
*/
if (value !== null && value !== void 0 && value.title && value.title !== internalTextValue) {
setInternalTextValue(value.title);
}
/**
* Update the state value internalInputValue if the url value changes
* for example when clicking on another anchor
*/
if (value !== null && value !== void 0 && value.url) {
setInternalInputValue(value.url);
}
}, [value]);
/**
* 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.
*/
function stopEditing() {
var _wrapperNode$current;
isEndingEditWithFocus.current = !!((_wrapperNode$current = wrapperNode.current) !== null && _wrapperNode$current !== void 0 && _wrapperNode$current.contains(wrapperNode.current.ownerDocument.activeElement));
setIsEditingLink(false);
}
const {
createPage,
isCreatingPage,
errorMessage
} = (0, _useCreatePage.default)(createSuggestion);
const handleSelectSuggestion = updatedValue => {
onChange({ ...updatedValue,
title: internalTextValue || (updatedValue === null || updatedValue === void 0 ? void 0 : updatedValue.title)
});
stopEditing();
};
const handleSubmit = () => {
if (currentInputValue !== (value === null || value === void 0 ? void 0 : value.url) || internalTextValue !== (value === null || value === void 0 ? void 0 : value.title)) {
onChange({
url: currentInputValue,
title: internalTextValue
});
}
stopEditing();
};
const handleSubmitWithEnter = event => {
const {
keyCode
} = event;
if (keyCode === _keycodes.ENTER && !currentInputIsEmpty // disallow submitting empty values.
) {
event.preventDefault();
handleSubmit();
}
};
const shownUnlinkControl = onRemove && value && !isEditingLink && !isCreatingPage;
const showSettingsDrawer = !!(settings !== null && settings !== void 0 && settings.length); // Only show text control once a URL value has been committed
// and it isn't just empty whitespace.
// See https://github.com/GeChiUI/gutenberg/pull/33849/#issuecomment-932194927.
const showTextControl = (value === null || value === void 0 ? void 0 : (_value$url = value.url) === null || _value$url === void 0 ? void 0 : (_value$url$trim = _value$url.trim()) === null || _value$url$trim === void 0 ? void 0 : _value$url$trim.length) > 0 && hasTextControl;
return (0, _element.createElement)("div", {
tabIndex: -1,
ref: wrapperNode,
className: "block-editor-link-control"
}, isCreatingPage && (0, _element.createElement)("div", {
className: "block-editor-link-control__loading"
}, (0, _element.createElement)(_components.Spinner, null), " ", (0, _i18n.__)('正在建立…'), "\u2026"), (isEditingLink || !value) && !isCreatingPage && (0, _element.createElement)(_element.Fragment, null, (0, _element.createElement)("div", {
className: (0, _classnames.default)({
'block-editor-link-control__search-input-wrapper': true,
'has-text-control': showTextControl
})
}, showTextControl && (0, _element.createElement)(_components.TextControl, {
ref: textInputRef,
className: "block-editor-link-control__field block-editor-link-control__text-content",
label: "Text",
value: internalTextValue,
onChange: setInternalTextValue,
onKeyDown: handleSubmitWithEnter
}), (0, _element.createElement)(_searchInput.default, {
currentLink: value,
className: "block-editor-link-control__field block-editor-link-control__search-input",
placeholder: searchInputPlaceholder,
value: currentInputValue,
withCreateSuggestion: withCreateSuggestion,
onCreateSuggestion: createPage,
onChange: setInternalInputValue,
onSelect: handleSelectSuggestion,
showInitialSuggestions: showInitialSuggestions,
allowDirectEntry: !noDirectEntry,
showSuggestions: showSuggestions,
suggestionsQuery: suggestionsQuery,
withURLSuggestion: !noURLSuggestion,
createSuggestionButtonText: createSuggestionButtonText,
useLabel: showTextControl
}, (0, _element.createElement)("div", {
className: "block-editor-link-control__search-actions"
}, (0, _element.createElement)(_components.Button, {
onClick: handleSubmit,
label: (0, _i18n.__)('提交'),
icon: _icons.keyboardReturn,
className: "block-editor-link-control__search-submit",
disabled: currentInputIsEmpty // disallow submitting empty values.
})))), errorMessage && (0, _element.createElement)(_components.Notice, {
className: "block-editor-link-control__search-error",
status: "error",
isDismissible: false
}, errorMessage)), value && !isEditingLink && !isCreatingPage && (0, _element.createElement)(_linkPreview.default, {
key: value === null || value === void 0 ? void 0 : value.url // force remount when URL changes to avoid race conditions for rich previews
,
value: value,
onEditClick: () => setIsEditingLink(true),
hasRichPreviews: hasRichPreviews,
hasUnlinkControl: shownUnlinkControl,
onRemove: onRemove
}), showSettingsDrawer && (0, _element.createElement)("div", {
className: "block-editor-link-control__tools"
}, (0, _element.createElement)(_settingsDrawer.default, {
value: value,
settings: settings,
onChange: onChange
})), renderControlBottom && renderControlBottom());
}
LinkControl.ViewerFill = _viewerSlot.ViewerFill;
var _default = LinkControl;
exports.default = _default;
//# sourceMappingURL=index.js.map