@wordpress/block-library
Version:
Block library for the WordPress editor.
848 lines (773 loc) • 27.6 kB
JavaScript
import { createElement } from "@wordpress/element";
/**
* External dependencies
*/
import classnames from 'classnames';
import { escape, unescape } from 'lodash';
/**
* WordPress dependencies
*/
import { createBlock, switchToBlockType } from '@wordpress/blocks';
import { useSelect, useDispatch } from '@wordpress/data';
import { Button, PanelBody, Popover, TextControl, TextareaControl, ToolbarButton, Tooltip, ToolbarGroup, KeyboardShortcuts } from '@wordpress/components';
import { displayShortcut, isKeyboardEvent, ENTER } from '@wordpress/keycodes';
import { __, sprintf } from '@wordpress/i18n';
import { BlockControls, BlockIcon, InspectorControls, RichText, __experimentalLinkControl as LinkControl, useBlockProps, store as blockEditorStore, getColorClassName } from '@wordpress/block-editor';
import { isURL, prependHTTP, safeDecodeURI } from '@wordpress/url';
import { Fragment, useState, useEffect, useRef, createInterpolateElement } from '@wordpress/element';
import { placeCaretAtHorizontalEdge } from '@wordpress/dom';
import { link as linkIcon, addSubmenu } from '@wordpress/icons';
import { store as coreStore, useResourcePermissions } from '@wordpress/core-data';
import { decodeEntities } from '@wordpress/html-entities';
import { useMergeRefs } from '@wordpress/compose';
/**
* Internal dependencies
*/
const {
name: name
} = {
$schema: "https://schemas.wp.org/trunk/block.json",
apiVersion: 2,
name: "core/navigation-link",
title: "Custom Link",
category: "design",
parent: ["core/navigation"],
description: "Add a page, link, or another item 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"
},
isTopLevelLink: {
type: "boolean"
}
},
usesContext: ["textColor", "customTextColor", "backgroundColor", "customBackgroundColor", "overlayTextColor", "customOverlayTextColor", "overlayBackgroundColor", "customOverlayBackgroundColor", "fontSize", "customFontSize", "showSubmenuIcon", "maxNestingLevel", "style"],
supports: {
reusable: false,
html: false,
__experimentalSlashInserter: true,
typography: {
fontSize: true,
lineHeight: true,
__experimentalFontFamily: true,
__experimentalFontWeight: true,
__experimentalFontStyle: true,
__experimentalTextTransform: true,
__experimentalTextDecoration: true,
__experimentalLetterSpacing: true,
__experimentalDefaultControls: {
fontSize: true
}
}
},
editorStyle: "wp-block-navigation-link-editor",
style: "wp-block-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] = useState(false);
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.
*
*/
export 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: newLabel = '',
// the title of any provided Post.
url: newUrl = '',
opensInNewTab,
id,
kind: newKind = originalKind,
type: newType = originalType
} = updatedValue;
const newLabelWithoutHttp = newLabel.replace(/http(s?):\/\//gi, '');
const newUrlWithoutHttp = newUrl.replace(/http(s?):\/\//gi, '');
const useNewLabel = newLabel && newLabel !== originalLabel && // LinkControl without the title field relies
// on the check below. Specifically, it assumes that
// the URL is the same as a title.
// This logic a) looks suspicious and b) should really
// live in the LinkControl and not here. It's a great
// candidate for future refactoring.
newLabelWithoutHttp !== newUrlWithoutHttp; // Unfortunately this causes the escaping model to be inverted.
// The escaped content is stored in the block attributes (and ultimately in the database),
// and then the raw data is "recovered" when outputting into the DOM.
// It would be preferable to store the **raw** data in the block attributes and escape it in JS.
// Why? Because there isn't one way to escape data. Depending on the context, you need to do
// different transforms. It doesn't make sense to me to choose one of them for the purposes of storage.
// See also:
// - https://github.com/WordPress/gutenberg/pull/41063
// - https://github.com/WordPress/gutenberg/pull/18617.
const label = useNewLabel ? escape(newLabel) : originalLabel || escape(newUrlWithoutHttp); // 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.
...(newUrl && {
url: encodeURI(safeDecodeURI(newUrl))
}),
...(label && {
label
}),
...(undefined !== opensInNewTab && {
opensInNewTab
}),
...(id && Number.isInteger(id) && {
id
}),
...(kind && {
kind
}),
...(type && type !== 'URL' && {
type
})
});
};
const useIsInvalidLink = (kind, type, id) => {
const isPostType = kind === 'post-type' || type === 'post' || type === 'page';
const hasId = Number.isInteger(id);
const postStatus = useSelect(select => {
var _getEntityRecord;
if (!isPostType) {
return null;
}
const {
getEntityRecord
} = select(coreStore);
return (_getEntityRecord = getEntityRecord('postType', type, id)) === null || _getEntityRecord === void 0 ? void 0 : _getEntityRecord.status;
}, [isPostType, 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 = __('Select post');
break;
case 'page':
/* translators: label for missing page in navigation link block */
missingText = __('Select page');
break;
case 'category':
/* translators: label for missing category in navigation link block */
missingText = __('Select category');
break;
case 'tag':
/* translators: label for missing tag in navigation link block */
missingText = __('Select tag');
break;
default:
/* translators: label for missing values in navigation link block */
missingText = __('Add link');
}
return missingText;
}
/**
* Removes HTML from a given string.
* Note the does not provide XSS protection or otherwise attempt
* to filter strings with malicious intent.
*
* See also: https://github.com/WordPress/gutenberg/pull/35539
*
* @param {string} html the string from which HTML should be removed.
* @return {string} the "cleaned" string.
*/
function navStripHTML(html) {
const doc = document.implementation.createHTMLDocument('');
doc.body.innerHTML = html;
return doc.body.textContent || '';
}
/**
* Add transforms to Link Control
*/
function LinkControlTransforms(_ref) {
let {
clientId,
replace
} = _ref;
const {
getBlock,
blockTransforms
} = useSelect(select => {
const {
getBlock: _getBlock,
getBlockRootClientId,
getBlockTransformItems
} = select(blockEditorStore);
return {
getBlock: _getBlock,
blockTransforms: getBlockTransformItems(_getBlock(clientId), getBlockRootClientId(clientId))
};
}, [clientId]);
const featuredBlocks = ['core/site-logo', 'core/social-links', 'core/search'];
const transforms = blockTransforms.filter(item => {
return featuredBlocks.includes(item.name);
});
if (!(transforms !== null && transforms !== void 0 && transforms.length)) {
return null;
}
return createElement("div", {
className: "link-control-transform"
}, createElement("h3", {
className: "link-control-transform__subheading"
}, __('Transform')), createElement("div", {
className: "link-control-transform__items"
}, transforms.map((item, index) => {
return createElement(Button, {
key: `transform-${index}`,
onClick: () => replace(clientId, switchToBlockType(getBlock(clientId), item.name)),
className: "link-control-transform__item"
}, createElement(BlockIcon, {
icon: item.icon
}), item.title);
})));
}
export default function NavigationLinkEdit(_ref2) {
let {
attributes,
isSelected,
setAttributes,
insertBlocksAfter,
mergeBlocks,
onReplace,
context,
clientId
} = _ref2;
const {
id,
label,
type,
opensInNewTab,
url,
description,
rel,
title,
kind
} = attributes;
const [isInvalid, isDraft] = useIsInvalidLink(kind, type, id);
const {
maxNestingLevel
} = context;
const link = {
url,
opensInNewTab,
title: label && navStripHTML(label) // don't allow HTML to display inside the <LinkControl>
};
const {
saveEntityRecord
} = useDispatch(coreStore);
const {
replaceBlock,
__unstableMarkNextChangeAsNotPersistent
} = useDispatch(blockEditorStore);
const [isLinkOpen, setIsLinkOpen] = 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] = useState(null);
const listItemRef = useRef(null);
const isDraggingWithin = useIsDraggingWithin(listItemRef);
const itemLabelPlaceholder = __('Add link…');
const ref = useRef();
const pagesPermissions = useResourcePermissions('pages');
const postsPermissions = useResourcePermissions('posts');
const {
innerBlocks,
isAtMaxNesting,
isTopLevelLink,
isParentOfSelectedBlock,
hasChildren
} = useSelect(select => {
const {
getBlocks,
getBlockCount,
getBlockName,
getBlockRootClientId,
hasSelectedInnerBlock,
getBlockParentsByBlockName
} = select(blockEditorStore);
return {
innerBlocks: getBlocks(clientId),
isAtMaxNesting: getBlockParentsByBlockName(clientId, [name, 'core/navigation-submenu']).length >= maxNestingLevel,
isTopLevelLink: getBlockName(getBlockRootClientId(clientId)) === 'core/navigation',
isParentOfSelectedBlock: hasSelectedInnerBlock(clientId, true),
hasChildren: !!getBlockCount(clientId)
};
}, [clientId]);
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({
isTopLevelLink
});
}, [isTopLevelLink]);
/**
* Transform to submenu block.
*/
function transformToSubmenu() {
const newSubmenu = createBlock('core/navigation-submenu', attributes, innerBlocks);
replaceBlock(clientId, newSubmenu);
}
useEffect(() => {
// 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.
if (!url) {
setIsLinkOpen(true);
} // If block has inner blocks, transform to Submenu.
if (hasChildren) {
transformToSubmenu();
}
}, []);
/**
* 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.
*/
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.
useEffect(() => {
if (isLinkOpen && url) {
// Does this look like a URL and have something TLD-ish?
if (isURL(prependHTTP(label)) && /^.+\.[a-z]+/.test(label)) {
// Focus and select the label text.
selectLabelText();
} else {
// Focus it (but do not select).
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);
}
/**
* Removes the current link if set.
*/
function removeLink() {
// Reset all attributes that comprise the link.
setAttributes({
url: '',
label: '',
id: '',
kind: '',
type: ''
}); // Close the link editing UI.
setIsLinkOpen(false);
}
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,
// Make `title` property consistent with that in `fetchLinkSuggestions` where the `rendered` title (containing HTML entities)
// is also being decoded. By being consistent in both locations we avoid having to branch in the rendering output code.
// Ideally in the future we will update both APIs to utilise the "raw" form of the title which is better suited to edit contexts.
// e.g.
// - title.raw = "Yes & No"
// - title.rendered = "Yes & No"
// - decodeEntities( title.rendered ) = "Yes & No"
// See:
// - https://github.com/WordPress/gutenberg/pull/41063
// - https://github.com/WordPress/gutenberg/blob/a1e1fdc0e6278457e9f4fc0b31ac6d2095f5450b/packages/core-data/src/fetch/__experimental-fetch-link-suggestions.js#L212-L218
title: decodeEntities(page.title.rendered),
url: page.link,
kind: 'post-type'
};
}
const {
textColor,
customTextColor,
backgroundColor,
customBackgroundColor
} = getColors(context, !isTopLevelLink);
function onKeyDown(event) {
if (isKeyboardEvent.primary(event, 'k') || !url && event.keyCode === ENTER) {
setIsLinkOpen(true);
}
}
const blockProps = useBlockProps({
ref: useMergeRefs([setPopoverAnchor, listItemRef]),
className: classnames('wp-block-navigation-item', {
'is-editing': isSelected || isParentOfSelectedBlock,
'is-dragging-within': isDraggingWithin,
'has-link': !!url,
'has-child': hasChildren,
'has-text-color': !!textColor || !!customTextColor,
[getColorClassName('color', textColor)]: !!textColor,
'has-background': !!backgroundColor || customBackgroundColor,
[getColorClassName('background-color', backgroundColor)]: !!backgroundColor
}),
style: {
color: !textColor && customTextColor,
backgroundColor: !backgroundColor && customBackgroundColor
},
onKeyDown
});
if (!url || isInvalid || isDraft) {
blockProps.onClick = () => setIsLinkOpen(true);
}
const classes = classnames('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 ? __('Invalid') : __('Draft')})`;
const tooltipText = isInvalid || isDraft ? __('This item has been deleted, or is a draft') : __('This item is missing a link');
return createElement(Fragment, null, createElement(BlockControls, null, createElement(ToolbarGroup, null, createElement(ToolbarButton, {
name: "link",
icon: linkIcon,
title: __('Link'),
shortcut: displayShortcut.primary('k'),
onClick: () => setIsLinkOpen(true)
}), !isAtMaxNesting && createElement(ToolbarButton, {
name: "submenu",
icon: addSubmenu,
title: __('Add submenu'),
onClick: transformToSubmenu
}))), createElement(InspectorControls, null, createElement(PanelBody, {
title: __('Link settings')
}, createElement(TextareaControl, {
value: description || '',
onChange: descriptionValue => {
setAttributes({
description: descriptionValue
});
},
label: __('Description'),
help: __('The description will be displayed in the menu if the current theme supports it.')
}), createElement(TextControl, {
value: title || '',
onChange: titleValue => {
setAttributes({
title: titleValue
});
},
label: __('Link title'),
autoComplete: "off"
}), createElement(TextControl, {
value: rel || '',
onChange: relValue => {
setAttributes({
rel: relValue
});
},
label: __('Link rel'),
autoComplete: "off"
}))), createElement("div", blockProps, createElement("a", {
className: classes
}, !url ? createElement("div", {
className: "wp-block-navigation-link__placeholder-text"
}, createElement(Tooltip, {
position: "top center",
text: tooltipText
}, createElement(Fragment, null, createElement("span", null, missingText), createElement("span", {
className: "wp-block-navigation-link__missing_text-tooltip"
}, tooltipText)))) : createElement(Fragment, null, !isInvalid && !isDraft && createElement(Fragment, null, createElement(RichText, {
ref: ref,
identifier: "label",
className: "wp-block-navigation-item__label",
value: label,
onChange: labelValue => setAttributes({
label: labelValue
}),
onMerge: mergeBlocks,
onReplace: onReplace,
__unstableOnSplitAtEnd: () => insertBlocksAfter(createBlock('core/navigation-link')),
"aria-label": __('Navigation link text'),
placeholder: itemLabelPlaceholder,
withoutInteractiveFormatting: true,
allowedFormats: ['core/bold', 'core/italic', 'core/image', 'core/strikethrough'],
onClick: () => {
if (!url) {
setIsLinkOpen(true);
}
}
}), description && createElement("span", {
className: "wp-block-navigation-item__description"
}, description)), (isInvalid || isDraft) && createElement("div", {
className: "wp-block-navigation-link__placeholder-text wp-block-navigation-link__label"
}, createElement(KeyboardShortcuts, {
shortcuts: {
enter: () => isSelected && setIsLinkOpen(true)
}
}), createElement(Tooltip, {
position: "top center",
text: tooltipText
}, createElement(Fragment, null, createElement("span", {
"aria-label": __('Navigation link text')
}, // 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 `updateNavigationLinkBlockAttributes` for more details.
`${unescape(label)} ${placeholderText}`.trim()), createElement("span", {
className: "wp-block-navigation-link__missing_text-tooltip"
}, tooltipText))))), isLinkOpen && createElement(Popover, {
position: "bottom center",
onClose: () => setIsLinkOpen(false),
anchor: popoverAnchor,
shift: true
}, createElement(LinkControl, {
hasTextControl: true,
hasRichPreviews: true,
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 = __('Create draft post: <mark>%s</mark>');
} else {
/* translators: %s: search term. */
format = __('Create draft page: <mark>%s</mark>');
}
return createInterpolateElement(sprintf(format, searchTerm), {
mark: createElement("mark", null)
});
},
noDirectEntry: !!type,
noURLSuggestion: !!type,
suggestionsQuery: getSuggestionsQuery(type, kind),
onChange: updatedValue => updateNavigationLinkBlockAttributes(updatedValue, setAttributes, attributes),
onRemove: removeLink,
renderControlBottom: !url ? () => createElement(LinkControlTransforms, {
clientId: clientId,
replace: replaceBlock
}) : null
})))));
}
//# sourceMappingURL=edit.js.map