UNPKG

@wordpress/block-library

Version:
471 lines (455 loc) 17.9 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = NavigationSubmenuEdit; var _clsx = _interopRequireDefault(require("clsx")); 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 _icons = require("@wordpress/icons"); var _a11y = require("@wordpress/a11y"); var _blocks = require("@wordpress/blocks"); var _compose = require("@wordpress/compose"); var _icons2 = require("./icons"); var _linkUi = require("../navigation-link/link-ui"); var _updateAttributes = require("../navigation-link/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 ALLOWED_BLOCKS = ['core/navigation-link', 'core/navigation-submenu', 'core/page-list']; 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; }; /** * @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. */ function NavigationSubmenuEdit({ attributes, isSelected, setAttributes, mergeBlocks, onReplace, context, clientId }) { const { label, url, description, rel } = attributes; const { showSubmenuIcon, maxNestingLevel, openSubmenusOnClick } = context; const { __unstableMarkNextChangeAsNotPersistent, replaceBlock, selectBlock } = (0, _data.useDispatch)(_blockEditor.store); const [isLinkOpen, setIsLinkOpen] = (0, _element.useState)(false); // 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 text…'); const ref = (0, _element.useRef)(); const dropdownMenuProps = (0, _hooks.useToolsPanelDropdownMenuProps)(); const { parentCount, 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?.length === 1) { const singleBlock = getBlock(selectedBlockChildren[0]); _onlyDescendantIsEmptyLink = singleBlock?.name === 'core/navigation-link' && !singleBlock?.attributes?.label; } return { parentCount: getBlockParentsByBlockName(clientId, 'core/navigation-submenu').length, isParentOfSelectedBlock: hasSelectedInnerBlock(clientId, true), isImmediateParentOfSelectedBlock: hasSelectedInnerBlock(clientId, false), hasChildren: !!getBlockCount(clientId), selectedBlockHasChildren: !!selectedBlockChildren?.length, onlyDescendantIsEmptyLink: _onlyDescendantIsEmptyLink }; }, [clientId]); const prevHasChildren = (0, _compose.usePrevious)(hasChildren); // 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); } }, []); /** * 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(); } } }, [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); } const { textColor, customTextColor, backgroundColor, customBackgroundColor } = (0, _utils.getColors)(context, parentCount > 0); 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 we don't stop propagation, this event bubbles up to the parent submenu item 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, 'open-on-click': openSubmenusOnClick }), style: { color: !textColor && customTextColor, backgroundColor: !backgroundColor && customBackgroundColor }, onKeyDown }); // Always use overlay colors for submenus. const innerBlocksColors = (0, _utils.getColors)(context, true); const allowedBlocks = parentCount >= maxNestingLevel ? ALLOWED_BLOCKS.filter(blockName => blockName !== 'core/navigation-submenu') : ALLOWED_BLOCKS; const navigationChildBlockProps = (0, _utils.getNavigationChildBlockProps)(innerBlocksColors); const innerBlocksProps = (0, _blockEditor.useInnerBlocksProps)(navigationChildBlockProps, { allowedBlocks, defaultBlock: DEFAULT_BLOCK, directInsert: 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); } (0, _element.useEffect)(() => { // If block becomes empty, transform to Navigation Link. if (!hasChildren && prevHasChildren) { // This side-effect should not create an undo level as those should // only be created via user interactions. __unstableMarkNextChangeAsNotPersistent(); transformToLink(); } }, [hasChildren, prevHasChildren]); const canConvertToLink = !selectedBlockHasChildren || onlyDescendantIsEmptyLink; return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, { children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_blockEditor.BlockControls, { children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_components.ToolbarGroup, { children: [!openSubmenusOnClick && /*#__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); } }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.ToolbarButton, { name: "revert", icon: _icons.removeSubmenu, title: (0, _i18n.__)('Convert to Link'), onClick: transformToLink, className: "wp-block-navigation__submenu__revert", disabled: !canConvertToLink })] }) }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_blockEditor.InspectorControls, { children: /*#__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, { label: (0, _i18n.__)('Text'), isShownByDefault: true, hasValue: () => !!label, onDeselect: () => setAttributes({ label: '' }), children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.TextControl, { __nextHasNoMarginBottom: true, __next40pxDefaultSize: true, value: label || '', onChange: labelValue => { setAttributes({ label: labelValue }); }, label: (0, _i18n.__)('Text'), autoComplete: "off" }) }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.__experimentalToolsPanelItem, { label: (0, _i18n.__)('Link'), isShownByDefault: true, hasValue: () => !!url, onDeselect: () => setAttributes({ url: '' }), children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.TextControl, { __nextHasNoMarginBottom: true, __next40pxDefaultSize: true, value: url || '', onChange: urlValue => { setAttributes({ url: urlValue }); }, label: (0, _i18n.__)('Link'), autoComplete: "off", type: "url" }) }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.__experimentalToolsPanelItem, { label: (0, _i18n.__)('Description'), isShownByDefault: true, hasValue: () => !!description, onDeselect: () => setAttributes({ description: '' }), children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.TextareaControl, { __nextHasNoMarginBottom: true, 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.') }) }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.__experimentalToolsPanelItem, { label: (0, _i18n.__)('Rel attribute'), isShownByDefault: true, hasValue: () => !!rel, onDeselect: () => setAttributes({ rel: '' }), children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.TextControl, { __nextHasNoMarginBottom: true, __next40pxDefaultSize: true, value: rel || '', onChange: relValue => { setAttributes({ rel: relValue }); }, label: (0, _i18n.__)('Rel attribute'), autoComplete: "off", help: (0, _i18n.__)('The relationship of the linked URL as space-separated link types.') }) })] }) }), /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", { ...blockProps, children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(ParentElement, { className: "wp-block-navigation-item__content", 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, "aria-label": (0, _i18n.__)('Navigation link text'), placeholder: itemLabelPlaceholder, withoutInteractiveFormatting: true, onClick: () => { if (!openSubmenusOnClick && !url) { setIsLinkOpen(true); setOpenedBy(ref.current); } } }), description && /*#__PURE__*/(0, _jsxRuntime.jsx)("span", { className: "wp-block-navigation-item__description", children: description }), !openSubmenusOnClick && isLinkOpen && /*#__PURE__*/(0, _jsxRuntime.jsx)(_linkUi.LinkUI, { clientId: clientId, link: attributes, onClose: () => { setIsLinkOpen(false); if (openedBy) { openedBy.focus(); setOpenedBy(null); } else { selectBlock(clientId); } }, anchor: popoverAnchor, onRemove: () => { setAttributes({ url: '' }); (0, _a11y.speak)((0, _i18n.__)('Link removed.'), 'assertive'); }, onChange: updatedValue => { (0, _updateAttributes.updateAttributes)(updatedValue, setAttributes, attributes); } })] }), (showSubmenuIcon || openSubmenusOnClick) && /*#__PURE__*/(0, _jsxRuntime.jsx)("span", { className: "wp-block-navigation__submenu-icon", children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_icons2.ItemSubmenuIcon, {}) }), /*#__PURE__*/(0, _jsxRuntime.jsx)("div", { ...innerBlocksProps })] })] }); } //# sourceMappingURL=edit.js.map