@wordpress/block-library
Version:
Block library for the WordPress editor.
471 lines (455 loc) • 17.9 kB
JavaScript
"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