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