UNPKG

@wordpress/block-library

Version:
694 lines (621 loc) 23 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = NavigationSubmenuEdit; exports.updateNavigationLinkBlockAttributes = void 0; var _element = require("@wordpress/element"); var _classnames = _interopRequireDefault(require("classnames")); var _lodash = require("lodash"); var _data = require("@wordpress/data"); var _components = require("@wordpress/components"); var _keycodes = require("@wordpress/keycodes"); var _i18n = require("@wordpress/i18n"); var _blockEditor = require("@wordpress/block-editor"); var _url = require("@wordpress/url"); var _dom = require("@wordpress/dom"); var _icons = require("@wordpress/icons"); var _coreData = require("@wordpress/core-data"); var _a11y = require("@wordpress/a11y"); var _blocks = require("@wordpress/blocks"); var _compose = require("@wordpress/compose"); var _icons2 = require("./icons"); /** * External dependencies */ /** * WordPress dependencies */ /** * Internal dependencies */ const { name: name } = { $schema: "https://schemas.wp.org/trunk/block.json", apiVersion: 2, name: "core/navigation-submenu", title: "Submenu", category: "design", parent: ["core/navigation"], description: "Add a submenu to your navigation.", textdomain: "default", attributes: { label: { type: "string" }, type: { type: "string" }, description: { type: "string" }, rel: { type: "string" }, id: { type: "number" }, opensInNewTab: { type: "boolean", "default": false }, url: { type: "string" }, title: { type: "string" }, kind: { type: "string" }, isTopLevelItem: { type: "boolean" } }, usesContext: ["textColor", "customTextColor", "backgroundColor", "customBackgroundColor", "overlayTextColor", "customOverlayTextColor", "overlayBackgroundColor", "customOverlayBackgroundColor", "fontSize", "customFontSize", "showSubmenuIcon", "maxNestingLevel", "openSubmenusOnClick", "style"], supports: { reusable: false, html: false }, editorStyle: "wp-block-navigation-submenu-editor", style: "wp-block-navigation-submenu" }; const ALLOWED_BLOCKS = ['core/navigation-link', 'core/navigation-submenu']; const DEFAULT_BLOCK = { name: 'core/navigation-link' }; /** * A React hook to determine if it's dragging within the target element. * * @typedef {import('@wordpress/element').RefObject} RefObject * * @param {RefObject<HTMLElement>} elementRef The target elementRef object. * * @return {boolean} Is dragging within the target element. */ const useIsDraggingWithin = elementRef => { const [isDraggingWithin, setIsDraggingWithin] = (0, _element.useState)(false); (0, _element.useEffect)(() => { const { ownerDocument } = elementRef.current; function handleDragStart(event) { // Check the first time when the dragging starts. handleDragEnter(event); } // Set to false whenever the user cancel the drag event by either releasing the mouse or press Escape. function handleDragEnd() { setIsDraggingWithin(false); } function handleDragEnter(event) { // Check if the current target is inside the item element. if (elementRef.current.contains(event.target)) { setIsDraggingWithin(true); } else { setIsDraggingWithin(false); } } // Bind these events to the document to catch all drag events. // Ideally, we can also use `event.relatedTarget`, but sadly that // doesn't work in Safari. ownerDocument.addEventListener('dragstart', handleDragStart); ownerDocument.addEventListener('dragend', handleDragEnd); ownerDocument.addEventListener('dragenter', handleDragEnter); return () => { ownerDocument.removeEventListener('dragstart', handleDragStart); ownerDocument.removeEventListener('dragend', handleDragEnd); ownerDocument.removeEventListener('dragenter', handleDragEnter); }; }, []); return isDraggingWithin; }; /** * Given the Link block's type attribute, return the query params to give to * /wp/v2/search. * * @param {string} type Link block's type attribute. * @param {string} kind Link block's entity of kind (post-type|taxonomy) * @return {{ type?: string, subtype?: string }} Search query params. */ function getSuggestionsQuery(type, kind) { switch (type) { case 'post': case 'page': return { type: 'post', subtype: type }; case 'category': return { type: 'term', subtype: 'category' }; case 'tag': return { type: 'term', subtype: 'post_tag' }; case 'post_format': return { type: 'post-format' }; default: if (kind === 'taxonomy') { return { type: 'term', subtype: type }; } if (kind === 'post-type') { return { type: 'post', subtype: type }; } return {}; } } /** * Determine the colors for a menu. * * Order of priority is: * 1: Overlay custom colors (if submenu) * 2: Overlay theme colors (if submenu) * 3: Custom colors * 4: Theme colors * 5: Global styles * * @param {Object} context * @param {boolean} isSubMenu */ function getColors(context, isSubMenu) { var _style$color, _style$color2; const { textColor, customTextColor, backgroundColor, customBackgroundColor, overlayTextColor, customOverlayTextColor, overlayBackgroundColor, customOverlayBackgroundColor, style } = context; const colors = {}; if (isSubMenu && !!customOverlayTextColor) { colors.customTextColor = customOverlayTextColor; } else if (isSubMenu && !!overlayTextColor) { colors.textColor = overlayTextColor; } else if (!!customTextColor) { colors.customTextColor = customTextColor; } else if (!!textColor) { colors.textColor = textColor; } else if (!!(style !== null && style !== void 0 && (_style$color = style.color) !== null && _style$color !== void 0 && _style$color.text)) { colors.customTextColor = style.color.text; } if (isSubMenu && !!customOverlayBackgroundColor) { colors.customBackgroundColor = customOverlayBackgroundColor; } else if (isSubMenu && !!overlayBackgroundColor) { colors.backgroundColor = overlayBackgroundColor; } else if (!!customBackgroundColor) { colors.customBackgroundColor = customBackgroundColor; } else if (!!backgroundColor) { colors.backgroundColor = backgroundColor; } else if (!!(style !== null && style !== void 0 && (_style$color2 = style.color) !== null && _style$color2 !== void 0 && _style$color2.background)) { colors.customTextColor = style.color.background; } return colors; } /** * @typedef {'post-type'|'custom'|'taxonomy'|'post-type-archive'} WPNavigationLinkKind */ /** * Navigation Link Block Attributes * * @typedef {Object} WPNavigationLinkBlockAttributes * * @property {string} [label] Link text. * @property {WPNavigationLinkKind} [kind] Kind is used to differentiate between term and post ids to check post draft status. * @property {string} [type] The type such as post, page, tag, category and other custom types. * @property {string} [rel] The relationship of the linked URL. * @property {number} [id] A post or term id. * @property {boolean} [opensInNewTab] Sets link target to _blank when true. * @property {string} [url] Link href. * @property {string} [title] Link title attribute. */ /** * Link Control onChange handler that updates block attributes when a setting is changed. * * @param {Object} updatedValue New block attributes to update. * @param {Function} setAttributes Block attribute update function. * @param {WPNavigationLinkBlockAttributes} blockAttributes Current block attributes. * */ const updateNavigationLinkBlockAttributes = function () { let updatedValue = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; let setAttributes = arguments.length > 1 ? arguments[1] : undefined; let blockAttributes = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; const { label: originalLabel = '', kind: originalKind = '', type: originalType = '' } = blockAttributes; const { title = '', url = '', opensInNewTab, id, kind: newKind = originalKind, type: newType = originalType } = updatedValue; const normalizedTitle = title.replace(/http(s?):\/\//gi, ''); const normalizedURL = url.replace(/http(s?):\/\//gi, ''); const escapeTitle = title !== '' && normalizedTitle !== normalizedURL && originalLabel !== title; const label = escapeTitle ? (0, _lodash.escape)(title) : originalLabel || (0, _lodash.escape)(normalizedURL); // In https://github.com/WordPress/gutenberg/pull/24670 we decided to use "tag" in favor of "post_tag" const type = newType === 'post_tag' ? 'tag' : newType.replace('-', '_'); const isBuiltInType = ['post', 'page', 'tag', 'category'].indexOf(type) > -1; const isCustomLink = !newKind && !isBuiltInType || newKind === 'custom'; const kind = isCustomLink ? 'custom' : newKind; setAttributes({ // Passed `url` may already be encoded. To prevent double encoding, decodeURI is executed to revert to the original string. ...(url && { url: encodeURI((0, _url.safeDecodeURI)(url)) }), ...(label && { label }), ...(undefined !== opensInNewTab && { opensInNewTab }), ...(id && Number.isInteger(id) && { id }), ...(kind && { kind }), ...(type && type !== 'URL' && { type }) }); }; exports.updateNavigationLinkBlockAttributes = updateNavigationLinkBlockAttributes; function NavigationSubmenuEdit(_ref) { let { attributes, isSelected, setAttributes, mergeBlocks, onReplace, context, clientId } = _ref; const { label, type, opensInNewTab, url, description, rel, title, kind } = attributes; const link = { url, opensInNewTab }; const { showSubmenuIcon, maxNestingLevel, openSubmenusOnClick } = context; const { saveEntityRecord } = (0, _data.useDispatch)(_coreData.store); const { __unstableMarkNextChangeAsNotPersistent, replaceBlock } = (0, _data.useDispatch)(_blockEditor.store); const [isLinkOpen, setIsLinkOpen] = (0, _element.useState)(false); // Use internal state instead of a ref to make sure that the component // re-renders when the popover's anchor updates. const [popoverAnchor, setPopoverAnchor] = (0, _element.useState)(null); const listItemRef = (0, _element.useRef)(null); const isDraggingWithin = useIsDraggingWithin(listItemRef); const itemLabelPlaceholder = (0, _i18n.__)('Add text…'); const ref = (0, _element.useRef)(); const pagesPermissions = (0, _coreData.useResourcePermissions)('pages'); const postsPermissions = (0, _coreData.useResourcePermissions)('posts'); const { isAtMaxNesting, isTopLevelItem, isParentOfSelectedBlock, isImmediateParentOfSelectedBlock, hasChildren, selectedBlockHasChildren, onlyDescendantIsEmptyLink } = (0, _data.useSelect)(select => { const { hasSelectedInnerBlock, getSelectedBlockClientId, getBlockParentsByBlockName, getBlock, getBlockCount, getBlockOrder } = select(_blockEditor.store); let _onlyDescendantIsEmptyLink; const selectedBlockId = getSelectedBlockClientId(); const selectedBlockChildren = getBlockOrder(selectedBlockId); // Check for a single descendant in the submenu. If that block // is a link block in a "placeholder" state with no label then // we can consider as an "empty" link. if ((selectedBlockChildren === null || selectedBlockChildren === void 0 ? void 0 : selectedBlockChildren.length) === 1) { var _singleBlock$attribut; const singleBlock = getBlock(selectedBlockChildren[0]); _onlyDescendantIsEmptyLink = (singleBlock === null || singleBlock === void 0 ? void 0 : singleBlock.name) === 'core/navigation-link' && !(singleBlock !== null && singleBlock !== void 0 && (_singleBlock$attribut = singleBlock.attributes) !== null && _singleBlock$attribut !== void 0 && _singleBlock$attribut.label); } return { isAtMaxNesting: getBlockParentsByBlockName(clientId, name).length >= maxNestingLevel, isTopLevelItem: getBlockParentsByBlockName(clientId, name).length === 0, isParentOfSelectedBlock: hasSelectedInnerBlock(clientId, true), isImmediateParentOfSelectedBlock: hasSelectedInnerBlock(clientId, false), hasChildren: !!getBlockCount(clientId), selectedBlockHasChildren: !!(selectedBlockChildren !== null && selectedBlockChildren !== void 0 && selectedBlockChildren.length), onlyDescendantIsEmptyLink: _onlyDescendantIsEmptyLink }; }, [clientId]); // Show the LinkControl on mount if the URL is empty // ( When adding a new menu item) // This can't be done in the useState call because it conflicts // with the autofocus behavior of the BlockListBlock component. (0, _element.useEffect)(() => { if (!openSubmenusOnClick && !url) { setIsLinkOpen(true); } }, []); // Store the colors from context as attributes for rendering. (0, _element.useEffect)(() => { // This side-effect should not create an undo level as those should // only be created via user interactions. Mark this change as // not persistent to avoid undo level creation. // See https://github.com/WordPress/gutenberg/issues/34564. __unstableMarkNextChangeAsNotPersistent(); setAttributes({ isTopLevelItem }); }, [isTopLevelItem]); /** * The hook shouldn't be necessary but due to a focus loss happening * when selecting a suggestion in the link popover, we force close on block unselection. */ (0, _element.useEffect)(() => { if (!isSelected) { setIsLinkOpen(false); } }, [isSelected]); // If the LinkControl popover is open and the URL has changed, close the LinkControl and focus the label text. (0, _element.useEffect)(() => { if (isLinkOpen && url) { // Does this look like a URL and have something TLD-ish? if ((0, _url.isURL)((0, _url.prependHTTP)(label)) && /^.+\.[a-z]+/.test(label)) { // Focus and select the label text. selectLabelText(); } else { // Focus it (but do not select). (0, _dom.placeCaretAtHorizontalEdge)(ref.current, true); } } }, [url]); /** * Focus the Link label text and select it. */ function selectLabelText() { ref.current.focus(); const { ownerDocument } = ref.current; const { defaultView } = ownerDocument; const selection = defaultView.getSelection(); const range = ownerDocument.createRange(); // Get the range of the current ref contents so we can add this range to the selection. range.selectNodeContents(ref.current); selection.removeAllRanges(); selection.addRange(range); } let userCanCreate = false; if (!type || type === 'page') { userCanCreate = pagesPermissions.canCreate; } else if (type === 'post') { userCanCreate = postsPermissions.canCreate; } async function handleCreate(pageTitle) { const postType = type || 'page'; const page = await saveEntityRecord('postType', postType, { title: pageTitle, status: 'draft' }); return { id: page.id, type: postType, title: page.title.rendered, url: page.link, kind: 'post-type' }; } const { textColor, customTextColor, backgroundColor, customBackgroundColor } = getColors(context, !isTopLevelItem); function onKeyDown(event) { if (_keycodes.isKeyboardEvent.primary(event, 'k')) { setIsLinkOpen(true); } } const blockProps = (0, _blockEditor.useBlockProps)({ ref: (0, _compose.useMergeRefs)([setPopoverAnchor, listItemRef]), className: (0, _classnames.default)('wp-block-navigation-item', { 'is-editing': isSelected || isParentOfSelectedBlock, 'is-dragging-within': isDraggingWithin, 'has-link': !!url, 'has-child': hasChildren, 'has-text-color': !!textColor || !!customTextColor, [(0, _blockEditor.getColorClassName)('color', textColor)]: !!textColor, 'has-background': !!backgroundColor || customBackgroundColor, [(0, _blockEditor.getColorClassName)('background-color', backgroundColor)]: !!backgroundColor, 'open-on-click': openSubmenusOnClick }), style: { color: !textColor && customTextColor, backgroundColor: !backgroundColor && customBackgroundColor }, onKeyDown }); // Always use overlay colors for submenus. const innerBlocksColors = getColors(context, true); const allowedBlocks = isAtMaxNesting ? (0, _lodash.without)(ALLOWED_BLOCKS, 'core/navigation-submenu') : ALLOWED_BLOCKS; const innerBlocksProps = (0, _blockEditor.useInnerBlocksProps)({ className: (0, _classnames.default)('wp-block-navigation__submenu-container', { 'is-parent-of-selected-block': isParentOfSelectedBlock, 'has-text-color': !!(innerBlocksColors.textColor || innerBlocksColors.customTextColor), [`has-${innerBlocksColors.textColor}-color`]: !!innerBlocksColors.textColor, 'has-background': !!(innerBlocksColors.backgroundColor || innerBlocksColors.customBackgroundColor), [`has-${innerBlocksColors.backgroundColor}-background-color`]: !!innerBlocksColors.backgroundColor }), style: { color: innerBlocksColors.customTextColor, backgroundColor: innerBlocksColors.customBackgroundColor } }, { allowedBlocks, __experimentalDefaultBlock: DEFAULT_BLOCK, __experimentalDirectInsert: true, // Ensure block toolbar is not too far removed from item // being edited. // see: https://github.com/WordPress/gutenberg/pull/34615. __experimentalCaptureToolbars: true, renderAppender: isSelected || isImmediateParentOfSelectedBlock && !selectedBlockHasChildren || // Show the appender while dragging to allow inserting element between item and the appender. hasChildren ? _blockEditor.InnerBlocks.ButtonBlockAppender : false }); const ParentElement = openSubmenusOnClick ? 'button' : 'a'; function transformToLink() { const newLinkBlock = (0, _blocks.createBlock)('core/navigation-link', attributes); replaceBlock(clientId, newLinkBlock); } const canConvertToLink = !selectedBlockHasChildren || onlyDescendantIsEmptyLink; return (0, _element.createElement)(_element.Fragment, null, (0, _element.createElement)(_blockEditor.BlockControls, null, (0, _element.createElement)(_components.ToolbarGroup, null, !openSubmenusOnClick && (0, _element.createElement)(_components.ToolbarButton, { name: "link", icon: _icons.link, title: (0, _i18n.__)('Link'), shortcut: _keycodes.displayShortcut.primary('k'), onClick: () => setIsLinkOpen(true) }), (0, _element.createElement)(_components.ToolbarButton, { name: "revert", icon: _icons.removeSubmenu, title: (0, _i18n.__)('Convert to Link'), onClick: transformToLink, className: "wp-block-navigation__submenu__revert", isDisabled: !canConvertToLink }))), (0, _element.createElement)(_blockEditor.InspectorControls, null, (0, _element.createElement)(_components.PanelBody, { title: (0, _i18n.__)('Link settings') }, (0, _element.createElement)(_components.TextareaControl, { value: description || '', onChange: descriptionValue => { setAttributes({ description: descriptionValue }); }, label: (0, _i18n.__)('Description'), help: (0, _i18n.__)('The description will be displayed in the menu if the current theme supports it.') }), (0, _element.createElement)(_components.TextControl, { value: title || '', onChange: titleValue => { setAttributes({ title: titleValue }); }, label: (0, _i18n.__)('Link title'), autoComplete: "off" }), (0, _element.createElement)(_components.TextControl, { value: rel || '', onChange: relValue => { setAttributes({ rel: relValue }); }, label: (0, _i18n.__)('Link rel'), autoComplete: "off" }))), (0, _element.createElement)("div", blockProps, (0, _element.createElement)(ParentElement, { className: "wp-block-navigation-item__content" }, (0, _element.createElement)(_blockEditor.RichText, { ref: ref, identifier: "label", className: "wp-block-navigation-item__label", value: label, onChange: labelValue => setAttributes({ label: labelValue }), onMerge: mergeBlocks, onReplace: onReplace, "aria-label": (0, _i18n.__)('Navigation link text'), placeholder: itemLabelPlaceholder, withoutInteractiveFormatting: true, allowedFormats: ['core/bold', 'core/italic', 'core/image', 'core/strikethrough'], onClick: () => { if (!openSubmenusOnClick && !url) { setIsLinkOpen(true); } } }), !openSubmenusOnClick && isLinkOpen && (0, _element.createElement)(_components.Popover, { position: "bottom center", onClose: () => setIsLinkOpen(false), anchor: popoverAnchor, shift: true }, (0, _element.createElement)(_blockEditor.__experimentalLinkControl, { className: "wp-block-navigation-link__inline-link-input", value: link, showInitialSuggestions: true, withCreateSuggestion: userCanCreate, createSuggestion: handleCreate, createSuggestionButtonText: searchTerm => { let format; if (type === 'post') { /* translators: %s: search term. */ format = (0, _i18n.__)('Create draft post: <mark>%s</mark>'); } else { /* translators: %s: search term. */ format = (0, _i18n.__)('Create draft page: <mark>%s</mark>'); } return (0, _element.createInterpolateElement)((0, _i18n.sprintf)(format, searchTerm), { mark: (0, _element.createElement)("mark", null) }); }, noDirectEntry: !!type, noURLSuggestion: !!type, suggestionsQuery: getSuggestionsQuery(type, kind), onChange: updatedValue => updateNavigationLinkBlockAttributes(updatedValue, setAttributes, attributes), onRemove: () => { setAttributes({ url: '' }); (0, _a11y.speak)((0, _i18n.__)('Link removed.'), 'assertive'); } }))), (showSubmenuIcon || openSubmenusOnClick) && (0, _element.createElement)("span", { className: "wp-block-navigation__submenu-icon" }, (0, _element.createElement)(_icons2.ItemSubmenuIcon, null)), (0, _element.createElement)("div", innerBlocksProps))); } //# sourceMappingURL=edit.js.map