@wordpress/format-library
Version:
Format library for the WordPress editor.
253 lines (237 loc) • 8.86 kB
JavaScript
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _element = require("@wordpress/element");
var _i18n = require("@wordpress/i18n");
var _a11y = require("@wordpress/a11y");
var _components = require("@wordpress/components");
var _url = require("@wordpress/url");
var _richText = require("@wordpress/rich-text");
var _blockEditor = require("@wordpress/block-editor");
var _data = require("@wordpress/data");
var _utils = require("./utils");
var _index = require("./index");
var _jsxRuntime = require("react/jsx-runtime");
/**
* WordPress dependencies
*/
/**
* Internal dependencies
*/
const LINK_SETTINGS = [..._blockEditor.LinkControl.DEFAULT_LINK_SETTINGS, {
id: 'nofollow',
title: (0, _i18n.__)('Mark as nofollow')
}];
function InlineLinkUI({
isActive,
activeAttributes,
value,
onChange,
onFocusOutside,
stopAddingLink,
contentRef,
focusOnMount
}) {
const richLinkTextValue = getRichTextValueFromSelection(value, isActive);
// Get the text content minus any HTML tags.
const richTextText = richLinkTextValue.text;
const {
selectionChange
} = (0, _data.useDispatch)(_blockEditor.store);
const {
createPageEntity,
userCanCreatePages,
selectionStart
} = (0, _data.useSelect)(select => {
const {
getSettings,
getSelectionStart
} = select(_blockEditor.store);
const _settings = getSettings();
return {
createPageEntity: _settings.__experimentalCreatePageEntity,
userCanCreatePages: _settings.__experimentalUserCanCreatePages,
selectionStart: getSelectionStart()
};
}, []);
const linkValue = (0, _element.useMemo)(() => ({
url: activeAttributes.url,
type: activeAttributes.type,
id: activeAttributes.id,
opensInNewTab: activeAttributes.target === '_blank',
nofollow: activeAttributes.rel?.includes('nofollow'),
title: richTextText
}), [activeAttributes.id, activeAttributes.rel, activeAttributes.target, activeAttributes.type, activeAttributes.url, richTextText]);
function removeLink() {
const newValue = (0, _richText.removeFormat)(value, 'core/link');
onChange(newValue);
stopAddingLink();
(0, _a11y.speak)((0, _i18n.__)('Link removed.'), 'assertive');
}
function onChangeLink(nextValue) {
const hasLink = linkValue?.url;
const isNewLink = !hasLink;
// Merge the next value with the current link value.
nextValue = {
...linkValue,
...nextValue
};
const newUrl = (0, _url.prependHTTP)(nextValue.url);
const linkFormat = (0, _utils.createLinkFormat)({
url: newUrl,
type: nextValue.type,
id: nextValue.id !== undefined && nextValue.id !== null ? String(nextValue.id) : undefined,
opensInNewWindow: nextValue.opensInNewTab,
nofollow: nextValue.nofollow
});
const newText = nextValue.title || newUrl;
// Scenario: we have any active text selection or an active format.
let newValue;
if ((0, _richText.isCollapsed)(value) && !isActive) {
// Scenario: we don't have any actively selected text or formats.
const inserted = (0, _richText.insert)(value, newText);
newValue = (0, _richText.applyFormat)(inserted, linkFormat, value.start, value.start + newText.length);
onChange(newValue);
// Close the Link UI.
stopAddingLink();
// Move the selection to the end of the inserted link outside of the format boundary
// so the user can continue typing after the link.
selectionChange({
clientId: selectionStart.clientId,
identifier: selectionStart.attributeKey,
start: value.start + newText.length + 1
});
return;
} else if (newText === richTextText) {
newValue = (0, _richText.applyFormat)(value, linkFormat);
} else {
// Scenario: Editing an existing link.
// Create new RichText value for the new text in order that we
// can apply formats to it.
newValue = (0, _richText.create)({
text: newText
});
// Apply the new Link format to this new text value.
newValue = (0, _richText.applyFormat)(newValue, linkFormat, 0, newText.length);
// Get the boundaries of the active link format.
const boundary = (0, _utils.getFormatBoundary)(value, {
type: 'core/link'
});
// Split the value at the start of the active link format.
// Passing "start" as the 3rd parameter is required to ensure
// the second half of the split value is split at the format's
// start boundary and avoids relying on the value's "end" property
// which may not correspond correctly.
const [valBefore, valAfter] = (0, _richText.split)(value, boundary.start, boundary.start);
// Update the original (full) RichTextValue replacing the
// target text with the *new* RichTextValue containing:
// 1. The new text content.
// 2. The new link format.
// As "replace" will operate on the first match only, it is
// run only against the second half of the value which was
// split at the active format's boundary. This avoids a bug
// with incorrectly targeted replacements.
// See: https://github.com/WordPress/gutenberg/issues/41771.
// Note original formats will be lost when applying this change.
// That is expected behaviour.
// See: https://github.com/WordPress/gutenberg/pull/33849#issuecomment-936134179.
const newValAfter = (0, _richText.replace)(valAfter, richTextText, newValue);
newValue = (0, _richText.concat)(valBefore, newValAfter);
}
onChange(newValue);
// Focus should only be returned to the rich text on submit if this link is not
// being created for the first time. If it is then focus should remain within the
// Link UI because it should remain open for the user to modify the link they have
// just created.
if (!isNewLink) {
stopAddingLink();
}
if (!(0, _utils.isValidHref)(newUrl)) {
(0, _a11y.speak)((0, _i18n.__)('Warning: the link has been inserted but may have errors. Please test it.'), 'assertive');
} else if (isActive) {
(0, _a11y.speak)((0, _i18n.__)('Link edited.'), 'assertive');
} else {
(0, _a11y.speak)((0, _i18n.__)('Link inserted.'), 'assertive');
}
}
const popoverAnchor = (0, _richText.useAnchor)({
editableContentElement: contentRef.current,
settings: {
..._index.link,
isActive
}
});
async function handleCreate(pageTitle) {
const page = await createPageEntity({
title: pageTitle,
status: 'draft'
});
return {
id: page.id,
type: page.type,
title: page.title.rendered,
url: page.link,
kind: 'post-type'
};
}
function createButtonText(searchTerm) {
return (0, _element.createInterpolateElement)((0, _i18n.sprintf)(/* translators: %s: search term. */
(0, _i18n.__)('Create page: <mark>%s</mark>'), searchTerm), {
mark: /*#__PURE__*/(0, _jsxRuntime.jsx)("mark", {})
});
}
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.Popover, {
anchor: popoverAnchor,
animate: false,
onClose: stopAddingLink,
onFocusOutside: onFocusOutside,
placement: "bottom",
offset: 8,
shift: true,
focusOnMount: focusOnMount,
constrainTabbing: true,
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_blockEditor.LinkControl, {
value: linkValue,
onChange: onChangeLink,
onRemove: removeLink,
hasRichPreviews: true,
createSuggestion: createPageEntity && handleCreate,
withCreateSuggestion: userCanCreatePages,
createSuggestionButtonText: createButtonText,
hasTextControl: true,
settings: LINK_SETTINGS,
showInitialSuggestions: true,
suggestionsQuery: {
// always show Pages as initial suggestions
initialSuggestionsSearchOptions: {
type: 'post',
subtype: 'page',
perPage: 20
}
}
})
});
}
function getRichTextValueFromSelection(value, isActive) {
// Default to the selection ranges on the RichTextValue object.
let textStart = value.start;
let textEnd = value.end;
// If the format is currently active then the rich text value
// should always be taken from the bounds of the active format
// and not the selected text.
if (isActive) {
const boundary = (0, _utils.getFormatBoundary)(value, {
type: 'core/link'
});
textStart = boundary.start;
// Text *selection* always extends +1 beyond the edge of the format.
// We account for that here.
textEnd = boundary.end + 1;
}
// Get a RichTextValue containing the selected text content.
return (0, _richText.slice)(value, textStart, textEnd);
}
var _default = exports.default = InlineLinkUI;
//# sourceMappingURL=inline.js.map
;