UNPKG

@wordpress/block-library

Version:
581 lines (563 loc) 21.9 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = NavigationLinkEdit; var _clsx = _interopRequireDefault(require("clsx")); var _blocks = require("@wordpress/blocks"); 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 _element = require("@wordpress/element"); var _dom = require("@wordpress/dom"); var _htmlEntities = require("@wordpress/html-entities"); var _icons = require("@wordpress/icons"); var _coreData = require("@wordpress/core-data"); var _compose = require("@wordpress/compose"); var _linkUi = require("./link-ui"); var _updateAttributes = require("./update-attributes"); var _utils = require("../navigation/edit/utils"); var _hooks = require("../utils/hooks"); var _jsxRuntime = require("react/jsx-runtime"); /** * External dependencies */ /** * WordPress dependencies */ /** * Internal dependencies */ const DEFAULT_BLOCK = { name: 'core/navigation-link' }; const NESTING_BLOCK_NAMES = ['core/navigation-link', 'core/navigation-submenu']; /** * 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); }; }, [elementRef]); return isDraggingWithin; }; const useIsInvalidLink = (kind, type, id, enabled) => { const isPostType = kind === 'post-type' || type === 'post' || type === 'page'; const hasId = Number.isInteger(id); const blockEditingMode = (0, _blockEditor.useBlockEditingMode)(); const postStatus = (0, _data.useSelect)(select => { if (!isPostType) { return null; } // Fetching the posts status is an "expensive" operation. Especially for sites with large navigations. // When the block is rendered in a template or other disabled contexts we can skip this check in order // to avoid all these additional requests that don't really add any value in that mode. if (blockEditingMode === 'disabled' || !enabled) { return null; } const { getEntityRecord } = select(_coreData.store); return getEntityRecord('postType', type, id)?.status; }, [isPostType, blockEditingMode, enabled, type, id]); // Check Navigation Link validity if: // 1. Link is 'post-type'. // 2. It has an id. // 3. It's neither null, nor undefined, as valid items might be either of those while loading. // If those conditions are met, check if // 1. The post status is published. // 2. The Navigation Link item has no label. // If either of those is true, invalidate. const isInvalid = isPostType && hasId && postStatus && 'trash' === postStatus; const isDraft = 'draft' === postStatus; return [isInvalid, isDraft]; }; function getMissingText(type) { let missingText = ''; switch (type) { case 'post': /* translators: label for missing post in navigation link block */ missingText = (0, _i18n.__)('Select post'); break; case 'page': /* translators: label for missing page in navigation link block */ missingText = (0, _i18n.__)('Select page'); break; case 'category': /* translators: label for missing category in navigation link block */ missingText = (0, _i18n.__)('Select category'); break; case 'tag': /* translators: label for missing tag in navigation link block */ missingText = (0, _i18n.__)('Select tag'); break; default: /* translators: label for missing values in navigation link block */ missingText = (0, _i18n.__)('Add link'); } return missingText; } /* * Warning, this duplicated in * packages/block-library/src/navigation-submenu/edit.js * Consider reusing this components for both blocks. */ function Controls({ attributes, setAttributes, setIsLabelFieldFocused }) { const { label, url, description, rel } = attributes; const dropdownMenuProps = (0, _hooks.useToolsPanelDropdownMenuProps)(); return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_components.__experimentalToolsPanel, { label: (0, _i18n.__)('Settings'), resetAll: () => { setAttributes({ label: '', url: '', description: '', rel: '' }); }, dropdownMenuProps: dropdownMenuProps, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_components.__experimentalToolsPanelItem, { hasValue: () => !!label, label: (0, _i18n.__)('Text'), onDeselect: () => setAttributes({ label: '' }), isShownByDefault: true, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.TextControl, { __nextHasNoMarginBottom: true, __next40pxDefaultSize: true, label: (0, _i18n.__)('Text'), value: label ? (0, _dom.__unstableStripHTML)(label) : '', onChange: labelValue => { setAttributes({ label: labelValue }); }, autoComplete: "off", onFocus: () => setIsLabelFieldFocused(true), onBlur: () => setIsLabelFieldFocused(false) }) }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.__experimentalToolsPanelItem, { hasValue: () => !!url, label: (0, _i18n.__)('Link'), onDeselect: () => setAttributes({ url: '' }), isShownByDefault: true, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.TextControl, { __nextHasNoMarginBottom: true, __next40pxDefaultSize: true, label: (0, _i18n.__)('Link'), value: url ? (0, _url.safeDecodeURI)(url) : '', onChange: urlValue => { (0, _updateAttributes.updateAttributes)({ url: urlValue }, setAttributes, attributes); }, autoComplete: "off", type: "url" }) }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.__experimentalToolsPanelItem, { hasValue: () => !!description, label: (0, _i18n.__)('Description'), onDeselect: () => setAttributes({ description: '' }), isShownByDefault: true, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.TextareaControl, { __nextHasNoMarginBottom: true, label: (0, _i18n.__)('Description'), value: description || '', onChange: descriptionValue => { setAttributes({ description: descriptionValue }); }, help: (0, _i18n.__)('The description will be displayed in the menu if the current theme supports it.') }) }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.__experimentalToolsPanelItem, { hasValue: () => !!rel, label: (0, _i18n.__)('Rel attribute'), onDeselect: () => setAttributes({ rel: '' }), isShownByDefault: true, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.TextControl, { __nextHasNoMarginBottom: true, __next40pxDefaultSize: true, label: (0, _i18n.__)('Rel attribute'), value: rel || '', onChange: relValue => { setAttributes({ rel: relValue }); }, autoComplete: "off", help: (0, _i18n.__)('The relationship of the linked URL as space-separated link types.') }) })] }); } function NavigationLinkEdit({ attributes, isSelected, setAttributes, insertBlocksAfter, mergeBlocks, onReplace, context, clientId }) { const { id, label, type, url, description, kind } = attributes; const { maxNestingLevel } = context; const { replaceBlock, __unstableMarkNextChangeAsNotPersistent, selectBlock, selectPreviousBlock } = (0, _data.useDispatch)(_blockEditor.store); // Have the link editing ui open on mount when lacking a url and selected. const [isLinkOpen, setIsLinkOpen] = (0, _element.useState)(isSelected && !url); // Store what element opened the popover, so we know where to return focus to (toolbar button vs navigation link text) const [openedBy, setOpenedBy] = (0, _element.useState)(null); // 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 label…'); const ref = (0, _element.useRef)(); const linkUIref = (0, _element.useRef)(); const prevUrl = (0, _compose.usePrevious)(url); // Change the label using inspector causes rich text to change focus on firefox. // This is a workaround to keep the focus on the label field when label filed is focused we don't render the rich text. const [isLabelFieldFocused, setIsLabelFieldFocused] = (0, _element.useState)(false); const { isAtMaxNesting, isTopLevelLink, isParentOfSelectedBlock, hasChildren, validateLinkStatus } = (0, _data.useSelect)(select => { const { getBlockCount, getBlockName, getBlockRootClientId, hasSelectedInnerBlock, getBlockParentsByBlockName, getSelectedBlockClientId } = select(_blockEditor.store); const rootClientId = getBlockRootClientId(clientId); const isTopLevel = getBlockName(rootClientId) === 'core/navigation'; const selectedBlockClientId = getSelectedBlockClientId(); const rootNavigationClientId = isTopLevel ? rootClientId : getBlockParentsByBlockName(clientId, 'core/navigation')[0]; // Enable when the root Navigation block is selected or any of its inner blocks. const enableLinkStatusValidation = selectedBlockClientId === rootNavigationClientId || hasSelectedInnerBlock(rootNavigationClientId, true); return { isAtMaxNesting: getBlockParentsByBlockName(clientId, NESTING_BLOCK_NAMES).length >= maxNestingLevel, isTopLevelLink: isTopLevel, isParentOfSelectedBlock: hasSelectedInnerBlock(clientId, true), hasChildren: !!getBlockCount(clientId), validateLinkStatus: enableLinkStatusValidation }; }, [clientId, maxNestingLevel]); const { getBlocks } = (0, _data.useSelect)(_blockEditor.store); const [isInvalid, isDraft] = useIsInvalidLink(kind, type, id, validateLinkStatus); /** * Transform to submenu block. */ const transformToSubmenu = () => { let innerBlocks = getBlocks(clientId); if (innerBlocks.length === 0) { innerBlocks = [(0, _blocks.createBlock)('core/navigation-link')]; selectBlock(innerBlocks[0].clientId); } const newSubmenu = (0, _blocks.createBlock)('core/navigation-submenu', attributes, innerBlocks); replaceBlock(clientId, newSubmenu); }; (0, _element.useEffect)(() => { // If block has inner blocks, transform to Submenu. if (hasChildren) { // This side-effect should not create an undo level as those should // only be created via user interactions. __unstableMarkNextChangeAsNotPersistent(); transformToSubmenu(); } }, [hasChildren]); // If the LinkControl popover is open and the URL has changed, close the LinkControl and focus the label text. (0, _element.useEffect)(() => { // We only want to do this when the URL has gone from nothing to a new URL AND the label looks like a URL if (!prevUrl && url && isLinkOpen && (0, _url.isURL)((0, _url.prependHTTP)(label)) && /^.+\.[a-z]+/.test(label)) { // Focus and select the label text. selectLabelText(); } }, [prevUrl, url, isLinkOpen, label]); /** * 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); } /** * Removes the current link if set. */ function removeLink() { // Reset all attributes that comprise the link. // It is critical that all attributes are reset // to their default values otherwise this may // in advertently trigger side effects because // the values will have "changed". setAttributes({ url: undefined, label: undefined, id: undefined, kind: undefined, type: undefined, opensInNewTab: false }); // Close the link editing UI. setIsLinkOpen(false); } const { textColor, customTextColor, backgroundColor, customBackgroundColor } = (0, _utils.getColors)(context, !isTopLevelLink); function onKeyDown(event) { if (_keycodes.isKeyboardEvent.primary(event, 'k')) { // Required to prevent the command center from opening, // as it shares the CMD+K shortcut. // See https://github.com/WordPress/gutenberg/pull/59845. event.preventDefault(); // If this link is a child of a parent submenu item, the parent submenu item event will also open, closing this popover event.stopPropagation(); setIsLinkOpen(true); setOpenedBy(ref.current); } } const blockProps = (0, _blockEditor.useBlockProps)({ ref: (0, _compose.useMergeRefs)([setPopoverAnchor, listItemRef]), className: (0, _clsx.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 }), style: { color: !textColor && customTextColor, backgroundColor: !backgroundColor && customBackgroundColor }, onKeyDown }); const innerBlocksProps = (0, _blockEditor.useInnerBlocksProps)({ ...blockProps, className: 'remove-outline' // Remove the outline from the inner blocks container. }, { defaultBlock: DEFAULT_BLOCK, directInsert: true, renderAppender: false }); if (!url || isInvalid || isDraft) { blockProps.onClick = () => { setIsLinkOpen(true); setOpenedBy(ref.current); }; } const classes = (0, _clsx.default)('wp-block-navigation-item__content', { 'wp-block-navigation-link__placeholder': !url || isInvalid || isDraft }); const missingText = getMissingText(type); /* translators: Whether the navigation link is Invalid or a Draft. */ const placeholderText = `(${isInvalid ? (0, _i18n.__)('Invalid') : (0, _i18n.__)('Draft')})`; return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, { children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_blockEditor.BlockControls, { children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_components.ToolbarGroup, { children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_components.ToolbarButton, { name: "link", icon: _icons.link, title: (0, _i18n.__)('Link'), shortcut: _keycodes.displayShortcut.primary('k'), onClick: event => { setIsLinkOpen(true); setOpenedBy(event.currentTarget); } }), !isAtMaxNesting && /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.ToolbarButton, { name: "submenu", icon: _icons.addSubmenu, title: (0, _i18n.__)('Add submenu'), onClick: transformToSubmenu })] }) }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_blockEditor.InspectorControls, { children: /*#__PURE__*/(0, _jsxRuntime.jsx)(Controls, { attributes: attributes, setAttributes: setAttributes, setIsLabelFieldFocused: setIsLabelFieldFocused }) }), /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", { ...blockProps, children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)("a", { className: classes, children: [!url ? /*#__PURE__*/(0, _jsxRuntime.jsx)("div", { className: "wp-block-navigation-link__placeholder-text", children: /*#__PURE__*/(0, _jsxRuntime.jsx)("span", { children: missingText }) }) : /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, { children: [!isInvalid && !isDraft && !isLabelFieldFocused && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, { children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_blockEditor.RichText, { ref: ref, identifier: "label", className: "wp-block-navigation-item__label", value: label, onChange: labelValue => setAttributes({ label: labelValue }), onMerge: mergeBlocks, onReplace: onReplace, __unstableOnSplitAtEnd: () => insertBlocksAfter((0, _blocks.createBlock)('core/navigation-link')), "aria-label": (0, _i18n.__)('Navigation link text'), placeholder: itemLabelPlaceholder, withoutInteractiveFormatting: true }), description && /*#__PURE__*/(0, _jsxRuntime.jsx)("span", { className: "wp-block-navigation-item__description", children: description })] }), (isInvalid || isDraft || isLabelFieldFocused) && /*#__PURE__*/(0, _jsxRuntime.jsx)("div", { className: (0, _clsx.default)('wp-block-navigation-link__placeholder-text', 'wp-block-navigation-link__label', { 'is-invalid': isInvalid, 'is-draft': isDraft }), children: /*#__PURE__*/(0, _jsxRuntime.jsx)("span", { children: // Some attributes are stored in an escaped form. It's a legacy issue. // Ideally they would be stored in a raw, unescaped form. // Unescape is used here to "recover" the escaped characters // so they display without encoding. // See `updateAttributes` for more details. `${(0, _htmlEntities.decodeEntities)(label)} ${isInvalid || isDraft ? placeholderText : ''}`.trim() }) })] }), isLinkOpen && /*#__PURE__*/(0, _jsxRuntime.jsx)(_linkUi.LinkUI, { ref: linkUIref, clientId: clientId, link: attributes, onClose: () => { // If there is no link then remove the auto-inserted block. // This avoids empty blocks which can provided a poor UX. if (!url) { // Fixes https://github.com/WordPress/gutenberg/issues/61361 // There's a chance we're closing due to the user selecting the browse all button. // Only move focus if the focus is still within the popover ui. If it's not within // the popover, it's because something has taken the focus from the popover, and // we don't want to steal it back. if (linkUIref.current.contains(window.document.activeElement)) { // Select the previous block to keep focus nearby selectPreviousBlock(clientId, true); } // Remove the link. onReplace([]); return; } setIsLinkOpen(false); if (openedBy) { openedBy.focus(); setOpenedBy(null); } else if (ref.current) { // select the ref when adding a new link ref.current.focus(); } else { // Fallback selectPreviousBlock(clientId, true); } }, anchor: popoverAnchor, onRemove: removeLink, onChange: updatedValue => { (0, _updateAttributes.updateAttributes)(updatedValue, setAttributes, attributes); } })] }), /*#__PURE__*/(0, _jsxRuntime.jsx)("div", { ...innerBlocksProps })] })] }); } //# sourceMappingURL=edit.js.map