UNPKG

@wordpress/format-library

Version:
253 lines (237 loc) 8.86 kB
"use strict"; 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