diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-17 07:57:26 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-17 07:57:26 +0000 |
commit | 30883c26bdceb9eaf32c8d4a1b0c1bce223b5226 (patch) | |
tree | 39a02e2aeb21ab5b7923c6f5757d66d55b708912 /wp-includes/js/dist/rich-text.js | |
parent | Adding upstream version 6.4.3+dfsg1. (diff) | |
download | wordpress-30883c26bdceb9eaf32c8d4a1b0c1bce223b5226.tar.xz wordpress-30883c26bdceb9eaf32c8d4a1b0c1bce223b5226.zip |
Adding upstream version 6.5+dfsg1.upstream/6.5+dfsg1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'wp-includes/js/dist/rich-text.js')
-rw-r--r-- | wp-includes/js/dist/rich-text.js | 1597 |
1 files changed, 818 insertions, 779 deletions
diff --git a/wp-includes/js/dist/rich-text.js b/wp-includes/js/dist/rich-text.js index 966b5f0..e0aa83e 100644 --- a/wp-includes/js/dist/rich-text.js +++ b/wp-includes/js/dist/rich-text.js @@ -1,48 +1,48 @@ -/******/ (function() { // webpackBootstrap +/******/ (() => { // webpackBootstrap /******/ "use strict"; /******/ // The require scope /******/ var __webpack_require__ = {}; /******/ /************************************************************************/ /******/ /* webpack/runtime/compat get default export */ -/******/ !function() { +/******/ (() => { /******/ // getDefaultExport function for compatibility with non-harmony modules -/******/ __webpack_require__.n = function(module) { +/******/ __webpack_require__.n = (module) => { /******/ var getter = module && module.__esModule ? -/******/ function() { return module['default']; } : -/******/ function() { return module; }; +/******/ () => (module['default']) : +/******/ () => (module); /******/ __webpack_require__.d(getter, { a: getter }); /******/ return getter; /******/ }; -/******/ }(); +/******/ })(); /******/ /******/ /* webpack/runtime/define property getters */ -/******/ !function() { +/******/ (() => { /******/ // define getter functions for harmony exports -/******/ __webpack_require__.d = function(exports, definition) { +/******/ __webpack_require__.d = (exports, definition) => { /******/ for(var key in definition) { /******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { /******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); /******/ } /******/ } /******/ }; -/******/ }(); +/******/ })(); /******/ /******/ /* webpack/runtime/hasOwnProperty shorthand */ -/******/ !function() { -/******/ __webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); } -/******/ }(); +/******/ (() => { +/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) +/******/ })(); /******/ /******/ /* webpack/runtime/make namespace object */ -/******/ !function() { +/******/ (() => { /******/ // define __esModule on exports -/******/ __webpack_require__.r = function(exports) { +/******/ __webpack_require__.r = (exports) => { /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); /******/ } /******/ Object.defineProperty(exports, '__esModule', { value: true }); /******/ }; -/******/ }(); +/******/ })(); /******/ /************************************************************************/ var __webpack_exports__ = {}; @@ -51,57 +51,57 @@ __webpack_require__.r(__webpack_exports__); // EXPORTS __webpack_require__.d(__webpack_exports__, { - __experimentalRichText: function() { return /* reexport */ __experimentalRichText; }, - __unstableCreateElement: function() { return /* reexport */ createElement; }, - __unstableFormatEdit: function() { return /* reexport */ FormatEdit; }, - __unstableToDom: function() { return /* reexport */ toDom; }, - __unstableUseRichText: function() { return /* reexport */ useRichText; }, - applyFormat: function() { return /* reexport */ applyFormat; }, - concat: function() { return /* reexport */ concat; }, - create: function() { return /* reexport */ create; }, - getActiveFormat: function() { return /* reexport */ getActiveFormat; }, - getActiveFormats: function() { return /* reexport */ getActiveFormats; }, - getActiveObject: function() { return /* reexport */ getActiveObject; }, - getTextContent: function() { return /* reexport */ getTextContent; }, - insert: function() { return /* reexport */ insert; }, - insertObject: function() { return /* reexport */ insertObject; }, - isCollapsed: function() { return /* reexport */ isCollapsed; }, - isEmpty: function() { return /* reexport */ isEmpty; }, - join: function() { return /* reexport */ join; }, - registerFormatType: function() { return /* reexport */ registerFormatType; }, - remove: function() { return /* reexport */ remove; }, - removeFormat: function() { return /* reexport */ removeFormat; }, - replace: function() { return /* reexport */ replace_replace; }, - slice: function() { return /* reexport */ slice; }, - split: function() { return /* reexport */ split; }, - store: function() { return /* reexport */ store; }, - toHTMLString: function() { return /* reexport */ toHTMLString; }, - toggleFormat: function() { return /* reexport */ toggleFormat; }, - unregisterFormatType: function() { return /* reexport */ unregisterFormatType; }, - useAnchor: function() { return /* reexport */ useAnchor; }, - useAnchorRef: function() { return /* reexport */ useAnchorRef; } + RichTextData: () => (/* reexport */ RichTextData), + __experimentalRichText: () => (/* reexport */ __experimentalRichText), + __unstableCreateElement: () => (/* reexport */ createElement), + __unstableToDom: () => (/* reexport */ toDom), + __unstableUseRichText: () => (/* reexport */ useRichText), + applyFormat: () => (/* reexport */ applyFormat), + concat: () => (/* reexport */ concat), + create: () => (/* reexport */ create), + getActiveFormat: () => (/* reexport */ getActiveFormat), + getActiveFormats: () => (/* reexport */ getActiveFormats), + getActiveObject: () => (/* reexport */ getActiveObject), + getTextContent: () => (/* reexport */ getTextContent), + insert: () => (/* reexport */ insert), + insertObject: () => (/* reexport */ insertObject), + isCollapsed: () => (/* reexport */ isCollapsed), + isEmpty: () => (/* reexport */ isEmpty), + join: () => (/* reexport */ join), + registerFormatType: () => (/* reexport */ registerFormatType), + remove: () => (/* reexport */ remove_remove), + removeFormat: () => (/* reexport */ removeFormat), + replace: () => (/* reexport */ replace_replace), + slice: () => (/* reexport */ slice), + split: () => (/* reexport */ split), + store: () => (/* reexport */ store), + toHTMLString: () => (/* reexport */ toHTMLString), + toggleFormat: () => (/* reexport */ toggleFormat), + unregisterFormatType: () => (/* reexport */ unregisterFormatType), + useAnchor: () => (/* reexport */ useAnchor), + useAnchorRef: () => (/* reexport */ useAnchorRef) }); // NAMESPACE OBJECT: ./node_modules/@wordpress/rich-text/build-module/store/selectors.js var selectors_namespaceObject = {}; __webpack_require__.r(selectors_namespaceObject); __webpack_require__.d(selectors_namespaceObject, { - getFormatType: function() { return getFormatType; }, - getFormatTypeForBareElement: function() { return getFormatTypeForBareElement; }, - getFormatTypeForClassName: function() { return getFormatTypeForClassName; }, - getFormatTypes: function() { return getFormatTypes; } + getFormatType: () => (getFormatType), + getFormatTypeForBareElement: () => (getFormatTypeForBareElement), + getFormatTypeForClassName: () => (getFormatTypeForClassName), + getFormatTypes: () => (getFormatTypes) }); // NAMESPACE OBJECT: ./node_modules/@wordpress/rich-text/build-module/store/actions.js var actions_namespaceObject = {}; __webpack_require__.r(actions_namespaceObject); __webpack_require__.d(actions_namespaceObject, { - addFormatTypes: function() { return addFormatTypes; }, - removeFormatTypes: function() { return removeFormatTypes; } + addFormatTypes: () => (addFormatTypes), + removeFormatTypes: () => (removeFormatTypes) }); ;// CONCATENATED MODULE: external ["wp","data"] -var external_wp_data_namespaceObject = window["wp"]["data"]; +const external_wp_data_namespaceObject = window["wp"]["data"]; ;// CONCATENATED MODULE: ./node_modules/@wordpress/rich-text/build-module/store/reducer.js /** * WordPress dependencies @@ -132,7 +132,7 @@ function formatTypes(state = {}, action) { } return state; } -/* harmony default export */ var reducer = ((0,external_wp_data_namespaceObject.combineReducers)({ +/* harmony default export */ const reducer = ((0,external_wp_data_namespaceObject.combineReducers)({ formatTypes })); @@ -879,6 +879,524 @@ const OBJECT_REPLACEMENT_CHARACTER = '\ufffc'; */ const ZWNBSP = '\ufeff'; +;// CONCATENATED MODULE: external ["wp","escapeHtml"] +const external_wp_escapeHtml_namespaceObject = window["wp"]["escapeHtml"]; +;// CONCATENATED MODULE: ./node_modules/@wordpress/rich-text/build-module/get-active-formats.js +/** @typedef {import('./types').RichTextValue} RichTextValue */ +/** @typedef {import('./types').RichTextFormatList} RichTextFormatList */ + +/** + * Internal dependencies + */ + + +/** + * Gets the all format objects at the start of the selection. + * + * @param {RichTextValue} value Value to inspect. + * @param {Array} EMPTY_ACTIVE_FORMATS Array to return if there are no + * active formats. + * + * @return {RichTextFormatList} Active format objects. + */ +function getActiveFormats(value, EMPTY_ACTIVE_FORMATS = []) { + const { + formats, + start, + end, + activeFormats + } = value; + if (start === undefined) { + return EMPTY_ACTIVE_FORMATS; + } + if (start === end) { + // For a collapsed caret, it is possible to override the active formats. + if (activeFormats) { + return activeFormats; + } + const formatsBefore = formats[start - 1] || EMPTY_ACTIVE_FORMATS; + const formatsAfter = formats[start] || EMPTY_ACTIVE_FORMATS; + + // By default, select the lowest amount of formats possible (which means + // the caret is positioned outside the format boundary). The user can + // then use arrow keys to define `activeFormats`. + if (formatsBefore.length < formatsAfter.length) { + return formatsBefore; + } + return formatsAfter; + } + + // If there's no formats at the start index, there are not active formats. + if (!formats[start]) { + return EMPTY_ACTIVE_FORMATS; + } + const selectedFormats = formats.slice(start, end); + + // Clone the formats so we're not mutating the live value. + const _activeFormats = [...selectedFormats[0]]; + let i = selectedFormats.length; + + // For performance reasons, start from the end where it's much quicker to + // realise that there are no active formats. + while (i--) { + const formatsAtIndex = selectedFormats[i]; + + // If we run into any index without formats, we're sure that there's no + // active formats. + if (!formatsAtIndex) { + return EMPTY_ACTIVE_FORMATS; + } + let ii = _activeFormats.length; + + // Loop over the active formats and remove any that are not present at + // the current index. + while (ii--) { + const format = _activeFormats[ii]; + if (!formatsAtIndex.find(_format => isFormatEqual(format, _format))) { + _activeFormats.splice(ii, 1); + } + } + + // If there are no active formats, we can stop. + if (_activeFormats.length === 0) { + return EMPTY_ACTIVE_FORMATS; + } + } + return _activeFormats || EMPTY_ACTIVE_FORMATS; +} + +;// CONCATENATED MODULE: ./node_modules/@wordpress/rich-text/build-module/get-format-type.js +/** + * WordPress dependencies + */ + +/** + * Internal dependencies + */ + + +/** @typedef {import('./register-format-type').RichTextFormatType} RichTextFormatType */ + +/** + * Returns a registered format type. + * + * @param {string} name Format name. + * + * @return {RichTextFormatType|undefined} Format type. + */ +function get_format_type_getFormatType(name) { + return (0,external_wp_data_namespaceObject.select)(store).getFormatType(name); +} + +;// CONCATENATED MODULE: ./node_modules/@wordpress/rich-text/build-module/to-tree.js +/** + * Internal dependencies + */ + + + + +function restoreOnAttributes(attributes, isEditableTree) { + if (isEditableTree) { + return attributes; + } + const newAttributes = {}; + for (const key in attributes) { + let newKey = key; + if (key.startsWith('data-disable-rich-text-')) { + newKey = key.slice('data-disable-rich-text-'.length); + } + newAttributes[newKey] = attributes[key]; + } + return newAttributes; +} + +/** + * Converts a format object to information that can be used to create an element + * from (type, attributes and object). + * + * @param {Object} $1 Named parameters. + * @param {string} $1.type The format type. + * @param {string} $1.tagName The tag name. + * @param {Object} $1.attributes The format attributes. + * @param {Object} $1.unregisteredAttributes The unregistered format + * attributes. + * @param {boolean} $1.object Whether or not it is an object + * format. + * @param {boolean} $1.boundaryClass Whether or not to apply a boundary + * class. + * @param {boolean} $1.isEditableTree + * + * @return {Object} Information to be used for element creation. + */ +function fromFormat({ + type, + tagName, + attributes, + unregisteredAttributes, + object, + boundaryClass, + isEditableTree +}) { + const formatType = get_format_type_getFormatType(type); + let elementAttributes = {}; + if (boundaryClass && isEditableTree) { + elementAttributes['data-rich-text-format-boundary'] = 'true'; + } + if (!formatType) { + if (attributes) { + elementAttributes = { + ...attributes, + ...elementAttributes + }; + } + return { + type, + attributes: restoreOnAttributes(elementAttributes, isEditableTree), + object + }; + } + elementAttributes = { + ...unregisteredAttributes, + ...elementAttributes + }; + for (const name in attributes) { + const key = formatType.attributes ? formatType.attributes[name] : false; + if (key) { + elementAttributes[key] = attributes[name]; + } else { + elementAttributes[name] = attributes[name]; + } + } + if (formatType.className) { + if (elementAttributes.class) { + elementAttributes.class = `${formatType.className} ${elementAttributes.class}`; + } else { + elementAttributes.class = formatType.className; + } + } + + // When a format is declared as non editable, make it non editable in the + // editor. + if (isEditableTree && formatType.contentEditable === false) { + elementAttributes.contenteditable = 'false'; + } + return { + type: tagName || formatType.tagName, + object: formatType.object, + attributes: restoreOnAttributes(elementAttributes, isEditableTree) + }; +} + +/** + * Checks if both arrays of formats up until a certain index are equal. + * + * @param {Array} a Array of formats to compare. + * @param {Array} b Array of formats to compare. + * @param {number} index Index to check until. + */ +function isEqualUntil(a, b, index) { + do { + if (a[index] !== b[index]) { + return false; + } + } while (index--); + return true; +} +function toTree({ + value, + preserveWhiteSpace, + createEmpty, + append, + getLastChild, + getParent, + isText, + getText, + remove, + appendText, + onStartIndex, + onEndIndex, + isEditableTree, + placeholder +}) { + const { + formats, + replacements, + text, + start, + end + } = value; + const formatsLength = formats.length + 1; + const tree = createEmpty(); + const activeFormats = getActiveFormats(value); + const deepestActiveFormat = activeFormats[activeFormats.length - 1]; + let lastCharacterFormats; + let lastCharacter; + append(tree, ''); + for (let i = 0; i < formatsLength; i++) { + const character = text.charAt(i); + const shouldInsertPadding = isEditableTree && ( + // Pad the line if the line is empty. + !lastCharacter || + // Pad the line if the previous character is a line break, otherwise + // the line break won't be visible. + lastCharacter === '\n'); + const characterFormats = formats[i]; + let pointer = getLastChild(tree); + if (characterFormats) { + characterFormats.forEach((format, formatIndex) => { + if (pointer && lastCharacterFormats && + // Reuse the last element if all formats remain the same. + isEqualUntil(characterFormats, lastCharacterFormats, formatIndex)) { + pointer = getLastChild(pointer); + return; + } + const { + type, + tagName, + attributes, + unregisteredAttributes + } = format; + const boundaryClass = isEditableTree && format === deepestActiveFormat; + const parent = getParent(pointer); + const newNode = append(parent, fromFormat({ + type, + tagName, + attributes, + unregisteredAttributes, + boundaryClass, + isEditableTree + })); + if (isText(pointer) && getText(pointer).length === 0) { + remove(pointer); + } + pointer = append(newNode, ''); + }); + } + + // If there is selection at 0, handle it before characters are inserted. + if (i === 0) { + if (onStartIndex && start === 0) { + onStartIndex(tree, pointer); + } + if (onEndIndex && end === 0) { + onEndIndex(tree, pointer); + } + } + if (character === OBJECT_REPLACEMENT_CHARACTER) { + const replacement = replacements[i]; + if (!replacement) continue; + const { + type, + attributes, + innerHTML + } = replacement; + const formatType = get_format_type_getFormatType(type); + if (!isEditableTree && type === 'script') { + pointer = append(getParent(pointer), fromFormat({ + type: 'script', + isEditableTree + })); + append(pointer, { + html: decodeURIComponent(attributes['data-rich-text-script']) + }); + } else if (formatType?.contentEditable === false) { + // For non editable formats, render the stored inner HTML. + pointer = append(getParent(pointer), fromFormat({ + ...replacement, + isEditableTree, + boundaryClass: start === i && end === i + 1 + })); + if (innerHTML) { + append(pointer, { + html: innerHTML + }); + } + } else { + pointer = append(getParent(pointer), fromFormat({ + ...replacement, + object: true, + isEditableTree + })); + } + // Ensure pointer is text node. + pointer = append(getParent(pointer), ''); + } else if (!preserveWhiteSpace && character === '\n') { + pointer = append(getParent(pointer), { + type: 'br', + attributes: isEditableTree ? { + 'data-rich-text-line-break': 'true' + } : undefined, + object: true + }); + // Ensure pointer is text node. + pointer = append(getParent(pointer), ''); + } else if (!isText(pointer)) { + pointer = append(getParent(pointer), character); + } else { + appendText(pointer, character); + } + if (onStartIndex && start === i + 1) { + onStartIndex(tree, pointer); + } + if (onEndIndex && end === i + 1) { + onEndIndex(tree, pointer); + } + if (shouldInsertPadding && i === text.length) { + append(getParent(pointer), ZWNBSP); + if (placeholder && text.length === 0) { + append(getParent(pointer), { + type: 'span', + attributes: { + 'data-rich-text-placeholder': placeholder, + // Necessary to prevent the placeholder from catching + // selection and being editable. + style: 'pointer-events:none;user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;' + } + }); + } + } + lastCharacterFormats = characterFormats; + lastCharacter = character; + } + return tree; +} + +;// CONCATENATED MODULE: ./node_modules/@wordpress/rich-text/build-module/to-html-string.js +/** + * WordPress dependencies + */ + + + +/** + * Internal dependencies + */ + + + +/** @typedef {import('./types').RichTextValue} RichTextValue */ + +/** + * Create an HTML string from a Rich Text value. + * + * @param {Object} $1 Named argements. + * @param {RichTextValue} $1.value Rich text value. + * @param {boolean} [$1.preserveWhiteSpace] Preserves newlines if true. + * + * @return {string} HTML string. + */ +function toHTMLString({ + value, + preserveWhiteSpace +}) { + const tree = toTree({ + value, + preserveWhiteSpace, + createEmpty, + append, + getLastChild, + getParent, + isText, + getText, + remove, + appendText + }); + return createChildrenHTML(tree.children); +} +function createEmpty() { + return {}; +} +function getLastChild({ + children +}) { + return children && children[children.length - 1]; +} +function append(parent, object) { + if (typeof object === 'string') { + object = { + text: object + }; + } + object.parent = parent; + parent.children = parent.children || []; + parent.children.push(object); + return object; +} +function appendText(object, text) { + object.text += text; +} +function getParent({ + parent +}) { + return parent; +} +function isText({ + text +}) { + return typeof text === 'string'; +} +function getText({ + text +}) { + return text; +} +function remove(object) { + const index = object.parent.children.indexOf(object); + if (index !== -1) { + object.parent.children.splice(index, 1); + } + return object; +} +function createElementHTML({ + type, + attributes, + object, + children +}) { + let attributeString = ''; + for (const key in attributes) { + if (!(0,external_wp_escapeHtml_namespaceObject.isValidAttributeName)(key)) { + continue; + } + attributeString += ` ${key}="${(0,external_wp_escapeHtml_namespaceObject.escapeAttribute)(attributes[key])}"`; + } + if (object) { + return `<${type}${attributeString}>`; + } + return `<${type}${attributeString}>${createChildrenHTML(children)}</${type}>`; +} +function createChildrenHTML(children = []) { + return children.map(child => { + if (child.html !== undefined) { + return child.html; + } + return child.text === undefined ? createElementHTML(child) : (0,external_wp_escapeHtml_namespaceObject.escapeEditableHTML)(child.text); + }).join(''); +} + +;// CONCATENATED MODULE: ./node_modules/@wordpress/rich-text/build-module/get-text-content.js +/** + * Internal dependencies + */ + + +/** @typedef {import('./types').RichTextValue} RichTextValue */ + +/** + * Get the textual content of a Rich Text value. This is similar to + * `Element.textContent`. + * + * @param {RichTextValue} value Value to use. + * + * @return {string} The text content. + */ +function getTextContent({ + text +}) { + return text.replace(OBJECT_REPLACEMENT_CHARACTER, ''); +} + ;// CONCATENATED MODULE: ./node_modules/@wordpress/rich-text/build-module/create.js /** * WordPress dependencies @@ -893,6 +1411,8 @@ const ZWNBSP = '\ufeff'; + + /** @typedef {import('./types').RichTextValue} RichTextValue */ function createEmptyValue() { @@ -946,9 +1466,6 @@ function toFormat({ for (const key in formatType.attributes) { const name = formatType.attributes[key]; registeredAttributes[key] = _attributes[name]; - if (formatType.__unstableFilterAttributeValue) { - registeredAttributes[key] = formatType.__unstableFilterAttributeValue(key, registeredAttributes[key]); - } // delete the attribute and what's left is considered // to be unregistered. @@ -973,6 +1490,99 @@ function toFormat({ } /** + * The RichTextData class is used to instantiate a wrapper around rich text + * values, with methods that can be used to transform or manipulate the data. + * + * - Create an empty instance: `new RichTextData()`. + * - Create one from an HTML string: `RichTextData.fromHTMLString( + * '<em>hello</em>' )`. + * - Create one from a wrapper HTMLElement: `RichTextData.fromHTMLElement( + * document.querySelector( 'p' ) )`. + * - Create one from plain text: `RichTextData.fromPlainText( '1\n2' )`. + * - Create one from a rich text value: `new RichTextData( { text: '...', + * formats: [ ... ] } )`. + * + * @todo Add methods to manipulate the data, such as applyFormat, slice etc. + */ +class RichTextData { + #value; + static empty() { + return new RichTextData(); + } + static fromPlainText(text) { + return new RichTextData(create({ + text + })); + } + static fromHTMLString(html) { + return new RichTextData(create({ + html + })); + } + static fromHTMLElement(htmlElement, options = {}) { + const { + preserveWhiteSpace = false + } = options; + const element = preserveWhiteSpace ? htmlElement : collapseWhiteSpace(htmlElement); + const richTextData = new RichTextData(create({ + element + })); + Object.defineProperty(richTextData, 'originalHTML', { + value: htmlElement.innerHTML + }); + return richTextData; + } + constructor(init = createEmptyValue()) { + this.#value = init; + } + toPlainText() { + return getTextContent(this.#value); + } + // We could expose `toHTMLElement` at some point as well, but we'd only use + // it internally. + toHTMLString({ + preserveWhiteSpace + } = {}) { + return this.originalHTML || toHTMLString({ + value: this.#value, + preserveWhiteSpace + }); + } + valueOf() { + return this.toHTMLString(); + } + toString() { + return this.toHTMLString(); + } + toJSON() { + return this.toHTMLString(); + } + get length() { + return this.text.length; + } + get formats() { + return this.#value.formats; + } + get replacements() { + return this.#value.replacements; + } + get text() { + return this.#value.text; + } +} +for (const name of Object.getOwnPropertyNames(String.prototype)) { + if (RichTextData.prototype.hasOwnProperty(name)) { + continue; + } + Object.defineProperty(RichTextData.prototype, name, { + value(...args) { + // Should we convert back to RichTextData? + return this.toHTMLString()[name](...args); + } + }); +} + +/** * Create a RichText value from an `Element` tree (DOM), an HTML string or a * plain text string, with optionally a `Range` object to set the selection. If * called without any input, an empty value will be created. The optional @@ -1003,10 +1613,7 @@ function toFormat({ * @param {string} [$1.text] Text to create value from. * @param {string} [$1.html] HTML to create value from. * @param {Range} [$1.range] Range to create value from. - * @param {boolean} [$1.preserveWhiteSpace] Whether or not to collapse - * white space characters. * @param {boolean} [$1.__unstableIsEditableTree] - * * @return {RichTextValue} A rich text value. */ function create({ @@ -1014,9 +1621,15 @@ function create({ text, html, range, - __unstableIsEditableTree: isEditableTree, - preserveWhiteSpace + __unstableIsEditableTree: isEditableTree } = {}) { + if (html instanceof RichTextData) { + return { + text: html.text, + formats: html.formats, + replacements: html.replacements + }; + } if (typeof text === 'string' && text.length > 0) { return { formats: Array(text.length), @@ -1035,8 +1648,7 @@ function create({ return createFromElement({ element, range, - isEditableTree, - preserveWhiteSpace + isEditableTree }); } @@ -1138,30 +1750,69 @@ function filterRange(node, range, filter) { * Collapse any whitespace used for HTML formatting to one space character, * because it will also be displayed as such by the browser. * - * @param {string} string + * We need to strip it from the content because we use white-space: pre-wrap for + * displaying editable rich text. Without using white-space: pre-wrap, the + * browser will litter the content with non breaking spaces, among other issues. + * See packages/rich-text/src/component/use-default-style.js. + * + * @see + * https://developer.mozilla.org/en-US/docs/Web/CSS/white-space-collapse#collapsing_of_white_space + * + * @param {HTMLElement} element + * @param {boolean} isRoot + * + * @return {HTMLElement} New element with collapsed whitespace. */ -function collapseWhiteSpace(string) { - return string.replace(/[\n\r\t]+/g, ' '); +function collapseWhiteSpace(element, isRoot = true) { + const clone = element.cloneNode(true); + clone.normalize(); + Array.from(clone.childNodes).forEach((node, i, nodes) => { + if (node.nodeType === node.TEXT_NODE) { + let newNodeValue = node.nodeValue; + if (/[\n\t\r\f]/.test(newNodeValue)) { + newNodeValue = newNodeValue.replace(/[\n\t\r\f]+/g, ' '); + } + if (newNodeValue.indexOf(' ') !== -1) { + newNodeValue = newNodeValue.replace(/ {2,}/g, ' '); + } + if (i === 0 && newNodeValue.startsWith(' ')) { + newNodeValue = newNodeValue.slice(1); + } else if (isRoot && i === nodes.length - 1 && newNodeValue.endsWith(' ')) { + newNodeValue = newNodeValue.slice(0, -1); + } + node.nodeValue = newNodeValue; + } else if (node.nodeType === node.ELEMENT_NODE) { + collapseWhiteSpace(node, false); + } + }); + return clone; } /** - * Removes reserved characters used by rich-text (zero width non breaking spaces added by `toTree` and object replacement characters). + * We need to normalise line breaks to `\n` so they are consistent across + * platforms and serialised properly. Not removing \r would cause it to + * linger and result in double line breaks when whitespace is preserved. + */ +const CARRIAGE_RETURN = '\r'; + +/** + * Removes reserved characters used by rich-text (zero width non breaking spaces + * added by `toTree` and object replacement characters). * * @param {string} string */ function removeReservedCharacters(string) { - // with the global flag, note that we should create a new regex each time OR reset lastIndex state. - return string.replace(new RegExp(`[${ZWNBSP}${OBJECT_REPLACEMENT_CHARACTER}]`, 'gu'), ''); + // with the global flag, note that we should create a new regex each time OR + // reset lastIndex state. + return string.replace(new RegExp(`[${ZWNBSP}${OBJECT_REPLACEMENT_CHARACTER}${CARRIAGE_RETURN}]`, 'gu'), ''); } /** * Creates a Rich Text value from a DOM element and range. * - * @param {Object} $1 Named argements. - * @param {Element} [$1.element] Element to create value from. - * @param {Range} [$1.range] Range to create value from. - * @param {boolean} [$1.preserveWhiteSpace] Whether or not to collapse white - * space characters. + * @param {Object} $1 Named argements. + * @param {Element} [$1.element] Element to create value from. + * @param {Range} [$1.range] Range to create value from. * @param {boolean} [$1.isEditableTree] * * @return {RichTextValue} A rich text value. @@ -1169,8 +1820,7 @@ function removeReservedCharacters(string) { function createFromElement({ element, range, - isEditableTree, - preserveWhiteSpace + isEditableTree }) { const accumulator = createEmptyValue(); if (!element) { @@ -1187,12 +1837,8 @@ function createFromElement({ const node = element.childNodes[index]; const tagName = node.nodeName.toLowerCase(); if (node.nodeType === node.TEXT_NODE) { - let filter = removeReservedCharacters; - if (!preserveWhiteSpace) { - filter = string => removeReservedCharacters(collapseWhiteSpace(string)); - } - const text = filter(node.nodeValue); - range = filterRange(node, range, filter); + const text = removeReservedCharacters(node.nodeValue); + range = filterRange(node, range, removeReservedCharacters); accumulateSelection(accumulator, node, range, { text }); @@ -1206,11 +1852,9 @@ function createFromElement({ if (node.nodeType !== node.ELEMENT_NODE) { continue; } - if (isEditableTree && ( - // Ignore any placeholders. - node.getAttribute('data-rich-text-placeholder') || + if (isEditableTree && // Ignore any line breaks that are not inserted by us. - tagName === 'br' && !node.getAttribute('data-rich-text-line-break'))) { + tagName === 'br' && !node.getAttribute('data-rich-text-line-break')) { accumulateSelection(accumulator, node, range, createEmptyValue()); continue; } @@ -1262,11 +1906,13 @@ function createFromElement({ const value = createFromElement({ element: node, range, - isEditableTree, - preserveWhiteSpace + isEditableTree }); accumulateSelection(accumulator, node, range, value); - if (!format) { + + // Ignore any placeholders, but keep their content since the browser + // might insert text inside them when the editable element is flex. + if (!format || node.getAttribute('data-rich-text-placeholder')) { mergePair(accumulator, value); } else if (value.text.length === 0) { if (format.attributes) { @@ -1373,90 +2019,6 @@ function concat(...values) { return normaliseFormats(values.reduce(mergePair, create())); } -;// CONCATENATED MODULE: ./node_modules/@wordpress/rich-text/build-module/get-active-formats.js -/** @typedef {import('./types').RichTextValue} RichTextValue */ -/** @typedef {import('./types').RichTextFormatList} RichTextFormatList */ - -/** - * Internal dependencies - */ - - -/** - * Gets the all format objects at the start of the selection. - * - * @param {RichTextValue} value Value to inspect. - * @param {Array} EMPTY_ACTIVE_FORMATS Array to return if there are no - * active formats. - * - * @return {RichTextFormatList} Active format objects. - */ -function getActiveFormats(value, EMPTY_ACTIVE_FORMATS = []) { - const { - formats, - start, - end, - activeFormats - } = value; - if (start === undefined) { - return EMPTY_ACTIVE_FORMATS; - } - if (start === end) { - // For a collapsed caret, it is possible to override the active formats. - if (activeFormats) { - return activeFormats; - } - const formatsBefore = formats[start - 1] || EMPTY_ACTIVE_FORMATS; - const formatsAfter = formats[start] || EMPTY_ACTIVE_FORMATS; - - // By default, select the lowest amount of formats possible (which means - // the caret is positioned outside the format boundary). The user can - // then use arrow keys to define `activeFormats`. - if (formatsBefore.length < formatsAfter.length) { - return formatsBefore; - } - return formatsAfter; - } - - // If there's no formats at the start index, there are not active formats. - if (!formats[start]) { - return EMPTY_ACTIVE_FORMATS; - } - const selectedFormats = formats.slice(start, end); - - // Clone the formats so we're not mutating the live value. - const _activeFormats = [...selectedFormats[0]]; - let i = selectedFormats.length; - - // For performance reasons, start from the end where it's much quicker to - // realise that there are no active formats. - while (i--) { - const formatsAtIndex = selectedFormats[i]; - - // If we run into any index without formats, we're sure that there's no - // active formats. - if (!formatsAtIndex) { - return EMPTY_ACTIVE_FORMATS; - } - let ii = _activeFormats.length; - - // Loop over the active formats and remove any that are not present at - // the current index. - while (ii--) { - const format = _activeFormats[ii]; - if (!formatsAtIndex.find(_format => isFormatEqual(format, _format))) { - _activeFormats.splice(ii, 1); - } - } - - // If there are no active formats, we can stop. - if (_activeFormats.length === 0) { - return EMPTY_ACTIVE_FORMATS; - } - } - return _activeFormats || EMPTY_ACTIVE_FORMATS; -} - ;// CONCATENATED MODULE: ./node_modules/@wordpress/rich-text/build-module/get-active-format.js /** * Internal dependencies @@ -1513,28 +2075,6 @@ function getActiveObject({ return replacements[start]; } -;// CONCATENATED MODULE: ./node_modules/@wordpress/rich-text/build-module/get-text-content.js -/** - * Internal dependencies - */ - - -/** @typedef {import('./types').RichTextValue} RichTextValue */ - -/** - * Get the textual content of a Rich Text value. This is similar to - * `Element.textContent`. - * - * @param {RichTextValue} value Value to use. - * - * @return {string} The text content. - */ -function getTextContent({ - text -}) { - return text.replace(OBJECT_REPLACEMENT_CHARACTER, ''); -} - ;// CONCATENATED MODULE: ./node_modules/@wordpress/rich-text/build-module/is-collapsed.js /** * Internal dependencies @@ -1673,8 +2213,8 @@ function registerFormatType(name, settings) { window.console.error('Format class names must be a string, or null to handle bare elements.'); return; } - if (!/^[_a-zA-Z]+[a-zA-Z0-9-]*$/.test(settings.className)) { - window.console.error('A class name must begin with a letter, followed by any number of hyphens, letters, or numbers.'); + if (!/^[_a-zA-Z]+[a-zA-Z0-9_-]*$/.test(settings.className)) { + window.console.error('A class name must begin with a letter, followed by any number of hyphens, underscores, letters, or numbers.'); return; } if (settings.className === null) { @@ -1841,7 +2381,7 @@ function insert(value, valueToInsert, startIndex = value.start, endIndex = value * * @return {RichTextValue} A new value with the content removed. */ -function remove(value, startIndex, endIndex) { +function remove_remove(value, startIndex, endIndex) { return insert(value, create(), startIndex, endIndex); } @@ -2052,303 +2592,6 @@ function splitAtSelection({ return [before, after]; } -;// CONCATENATED MODULE: ./node_modules/@wordpress/rich-text/build-module/get-format-type.js -/** - * WordPress dependencies - */ - -/** - * Internal dependencies - */ - - -/** @typedef {import('./register-format-type').RichTextFormatType} RichTextFormatType */ - -/** - * Returns a registered format type. - * - * @param {string} name Format name. - * - * @return {RichTextFormatType|undefined} Format type. - */ -function get_format_type_getFormatType(name) { - return (0,external_wp_data_namespaceObject.select)(store).getFormatType(name); -} - -;// CONCATENATED MODULE: ./node_modules/@wordpress/rich-text/build-module/to-tree.js -/** - * Internal dependencies - */ - - - - -function restoreOnAttributes(attributes, isEditableTree) { - if (isEditableTree) { - return attributes; - } - const newAttributes = {}; - for (const key in attributes) { - let newKey = key; - if (key.startsWith('data-disable-rich-text-')) { - newKey = key.slice('data-disable-rich-text-'.length); - } - newAttributes[newKey] = attributes[key]; - } - return newAttributes; -} - -/** - * Converts a format object to information that can be used to create an element - * from (type, attributes and object). - * - * @param {Object} $1 Named parameters. - * @param {string} $1.type The format type. - * @param {string} $1.tagName The tag name. - * @param {Object} $1.attributes The format attributes. - * @param {Object} $1.unregisteredAttributes The unregistered format - * attributes. - * @param {boolean} $1.object Whether or not it is an object - * format. - * @param {boolean} $1.boundaryClass Whether or not to apply a boundary - * class. - * @param {boolean} $1.isEditableTree - * - * @return {Object} Information to be used for element creation. - */ -function fromFormat({ - type, - tagName, - attributes, - unregisteredAttributes, - object, - boundaryClass, - isEditableTree -}) { - const formatType = get_format_type_getFormatType(type); - let elementAttributes = {}; - if (boundaryClass && isEditableTree) { - elementAttributes['data-rich-text-format-boundary'] = 'true'; - } - if (!formatType) { - if (attributes) { - elementAttributes = { - ...attributes, - ...elementAttributes - }; - } - return { - type, - attributes: restoreOnAttributes(elementAttributes, isEditableTree), - object - }; - } - elementAttributes = { - ...unregisteredAttributes, - ...elementAttributes - }; - for (const name in attributes) { - const key = formatType.attributes ? formatType.attributes[name] : false; - if (key) { - elementAttributes[key] = attributes[name]; - } else { - elementAttributes[name] = attributes[name]; - } - } - if (formatType.className) { - if (elementAttributes.class) { - elementAttributes.class = `${formatType.className} ${elementAttributes.class}`; - } else { - elementAttributes.class = formatType.className; - } - } - - // When a format is declared as non editable, make it non editable in the - // editor. - if (isEditableTree && formatType.contentEditable === false) { - elementAttributes.contenteditable = 'false'; - } - return { - type: tagName || formatType.tagName, - object: formatType.object, - attributes: restoreOnAttributes(elementAttributes, isEditableTree) - }; -} - -/** - * Checks if both arrays of formats up until a certain index are equal. - * - * @param {Array} a Array of formats to compare. - * @param {Array} b Array of formats to compare. - * @param {number} index Index to check until. - */ -function isEqualUntil(a, b, index) { - do { - if (a[index] !== b[index]) { - return false; - } - } while (index--); - return true; -} -function toTree({ - value, - preserveWhiteSpace, - createEmpty, - append, - getLastChild, - getParent, - isText, - getText, - remove, - appendText, - onStartIndex, - onEndIndex, - isEditableTree, - placeholder -}) { - const { - formats, - replacements, - text, - start, - end - } = value; - const formatsLength = formats.length + 1; - const tree = createEmpty(); - const activeFormats = getActiveFormats(value); - const deepestActiveFormat = activeFormats[activeFormats.length - 1]; - let lastCharacterFormats; - let lastCharacter; - append(tree, ''); - for (let i = 0; i < formatsLength; i++) { - const character = text.charAt(i); - const shouldInsertPadding = isEditableTree && ( - // Pad the line if the line is empty. - !lastCharacter || - // Pad the line if the previous character is a line break, otherwise - // the line break won't be visible. - lastCharacter === '\n'); - const characterFormats = formats[i]; - let pointer = getLastChild(tree); - if (characterFormats) { - characterFormats.forEach((format, formatIndex) => { - if (pointer && lastCharacterFormats && - // Reuse the last element if all formats remain the same. - isEqualUntil(characterFormats, lastCharacterFormats, formatIndex)) { - pointer = getLastChild(pointer); - return; - } - const { - type, - tagName, - attributes, - unregisteredAttributes - } = format; - const boundaryClass = isEditableTree && format === deepestActiveFormat; - const parent = getParent(pointer); - const newNode = append(parent, fromFormat({ - type, - tagName, - attributes, - unregisteredAttributes, - boundaryClass, - isEditableTree - })); - if (isText(pointer) && getText(pointer).length === 0) { - remove(pointer); - } - pointer = append(newNode, ''); - }); - } - - // If there is selection at 0, handle it before characters are inserted. - if (i === 0) { - if (onStartIndex && start === 0) { - onStartIndex(tree, pointer); - } - if (onEndIndex && end === 0) { - onEndIndex(tree, pointer); - } - } - if (character === OBJECT_REPLACEMENT_CHARACTER) { - const replacement = replacements[i]; - if (!replacement) continue; - const { - type, - attributes, - innerHTML - } = replacement; - const formatType = get_format_type_getFormatType(type); - if (!isEditableTree && type === 'script') { - pointer = append(getParent(pointer), fromFormat({ - type: 'script', - isEditableTree - })); - append(pointer, { - html: decodeURIComponent(attributes['data-rich-text-script']) - }); - } else if (formatType?.contentEditable === false) { - // For non editable formats, render the stored inner HTML. - pointer = append(getParent(pointer), fromFormat({ - ...replacement, - isEditableTree, - boundaryClass: start === i && end === i + 1 - })); - if (innerHTML) { - append(pointer, { - html: innerHTML - }); - } - } else { - pointer = append(getParent(pointer), fromFormat({ - ...replacement, - object: true, - isEditableTree - })); - } - // Ensure pointer is text node. - pointer = append(getParent(pointer), ''); - } else if (!preserveWhiteSpace && character === '\n') { - pointer = append(getParent(pointer), { - type: 'br', - attributes: isEditableTree ? { - 'data-rich-text-line-break': 'true' - } : undefined, - object: true - }); - // Ensure pointer is text node. - pointer = append(getParent(pointer), ''); - } else if (!isText(pointer)) { - pointer = append(getParent(pointer), character); - } else { - appendText(pointer, character); - } - if (onStartIndex && start === i + 1) { - onStartIndex(tree, pointer); - } - if (onEndIndex && end === i + 1) { - onEndIndex(tree, pointer); - } - if (shouldInsertPadding && i === text.length) { - append(getParent(pointer), ZWNBSP); - if (placeholder && text.length === 0) { - append(getParent(pointer), { - type: 'span', - attributes: { - 'data-rich-text-placeholder': placeholder, - // Necessary to prevent the placeholder from catching - // selection and being editable. - style: 'pointer-events:none;user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;' - } - }); - } - } - lastCharacterFormats = characterFormats; - lastCharacter = character; - } - return tree; -} - ;// CONCATENATED MODULE: ./node_modules/@wordpress/rich-text/build-module/is-range-equal.js /** * Returns true if two ranges are equal, or false otherwise. Ranges are @@ -2416,7 +2659,7 @@ function getNodeByPath(node, path) { offset: path[0] }; } -function append(element, child) { +function to_dom_append(element, child) { if (child.html !== undefined) { return element.innerHTML += child.html; } @@ -2435,23 +2678,23 @@ function append(element, child) { } return element.appendChild(child); } -function appendText(node, text) { +function to_dom_appendText(node, text) { node.appendData(text); } -function getLastChild({ +function to_dom_getLastChild({ lastChild }) { return lastChild; } -function getParent({ +function to_dom_getParent({ parentNode }) { return parentNode; } -function isText(node) { +function to_dom_isText(node) { return node.nodeType === node.TEXT_NODE; } -function getText({ +function to_dom_getText({ nodeValue }) { return nodeValue; @@ -2489,13 +2732,13 @@ function toDom({ const tree = toTree({ value, createEmpty, - append, - getLastChild, - getParent, - isText, - getText, + append: to_dom_append, + getLastChild: to_dom_getLastChild, + getParent: to_dom_getParent, + isText: to_dom_isText, + getText: to_dom_getText, remove: to_dom_remove, - appendText, + appendText: to_dom_appendText, onStartIndex(body, pointer) { startPath = createPathToNode(pointer, body, [pointer.nodeValue.length]); }, @@ -2647,126 +2890,10 @@ function applySelection({ } } -;// CONCATENATED MODULE: external ["wp","escapeHtml"] -var external_wp_escapeHtml_namespaceObject = window["wp"]["escapeHtml"]; -;// CONCATENATED MODULE: ./node_modules/@wordpress/rich-text/build-module/to-html-string.js -/** - * WordPress dependencies - */ - - - -/** - * Internal dependencies - */ - - - -/** @typedef {import('./types').RichTextValue} RichTextValue */ - -/** - * Create an HTML string from a Rich Text value. - * - * @param {Object} $1 Named argements. - * @param {RichTextValue} $1.value Rich text value. - * @param {boolean} [$1.preserveWhiteSpace] Whether or not to use newline - * characters for line breaks. - * - * @return {string} HTML string. - */ -function toHTMLString({ - value, - preserveWhiteSpace -}) { - const tree = toTree({ - value, - preserveWhiteSpace, - createEmpty, - append: to_html_string_append, - getLastChild: to_html_string_getLastChild, - getParent: to_html_string_getParent, - isText: to_html_string_isText, - getText: to_html_string_getText, - remove: to_html_string_remove, - appendText: to_html_string_appendText - }); - return createChildrenHTML(tree.children); -} -function createEmpty() { - return {}; -} -function to_html_string_getLastChild({ - children -}) { - return children && children[children.length - 1]; -} -function to_html_string_append(parent, object) { - if (typeof object === 'string') { - object = { - text: object - }; - } - object.parent = parent; - parent.children = parent.children || []; - parent.children.push(object); - return object; -} -function to_html_string_appendText(object, text) { - object.text += text; -} -function to_html_string_getParent({ - parent -}) { - return parent; -} -function to_html_string_isText({ - text -}) { - return typeof text === 'string'; -} -function to_html_string_getText({ - text -}) { - return text; -} -function to_html_string_remove(object) { - const index = object.parent.children.indexOf(object); - if (index !== -1) { - object.parent.children.splice(index, 1); - } - return object; -} -function createElementHTML({ - type, - attributes, - object, - children -}) { - let attributeString = ''; - for (const key in attributes) { - if (!(0,external_wp_escapeHtml_namespaceObject.isValidAttributeName)(key)) { - continue; - } - attributeString += ` ${key}="${(0,external_wp_escapeHtml_namespaceObject.escapeAttribute)(attributes[key])}"`; - } - if (object) { - return `<${type}${attributeString}>`; - } - return `<${type}${attributeString}>${createChildrenHTML(children)}</${type}>`; -} -function createChildrenHTML(children = []) { - return children.map(child => { - if (child.html !== undefined) { - return child.html; - } - return child.text === undefined ? createElementHTML(child) : (0,external_wp_escapeHtml_namespaceObject.escapeEditableHTML)(child.text); - }).join(''); -} - ;// CONCATENATED MODULE: external ["wp","a11y"] -var external_wp_a11y_namespaceObject = window["wp"]["a11y"]; +const external_wp_a11y_namespaceObject = window["wp"]["a11y"]; ;// CONCATENATED MODULE: external ["wp","i18n"] -var external_wp_i18n_namespaceObject = window["wp"]["i18n"]; +const external_wp_i18n_namespaceObject = window["wp"]["i18n"]; ;// CONCATENATED MODULE: ./node_modules/@wordpress/rich-text/build-module/toggle-format.js /** * WordPress dependencies @@ -2844,9 +2971,9 @@ function unregisterFormatType(name) { } ;// CONCATENATED MODULE: external ["wp","element"] -var external_wp_element_namespaceObject = window["wp"]["element"]; +const external_wp_element_namespaceObject = window["wp"]["element"]; ;// CONCATENATED MODULE: external ["wp","deprecated"] -var external_wp_deprecated_namespaceObject = window["wp"]["deprecated"]; +const external_wp_deprecated_namespaceObject = window["wp"]["deprecated"]; var external_wp_deprecated_default = /*#__PURE__*/__webpack_require__.n(external_wp_deprecated_namespaceObject); ;// CONCATENATED MODULE: ./node_modules/@wordpress/rich-text/build-module/component/use-anchor-ref.js /** @@ -2922,12 +3049,15 @@ function useAnchorRef({ }, [activeFormat, value.start, value.end, tagName, className]); } +;// CONCATENATED MODULE: external ["wp","compose"] +const external_wp_compose_namespaceObject = window["wp"]["compose"]; ;// CONCATENATED MODULE: ./node_modules/@wordpress/rich-text/build-module/component/use-anchor.js /** * WordPress dependencies */ + /** @typedef {import('../register-format-type').WPFormat} WPFormat */ /** @typedef {import('../types').RichTextValue} RichTextValue */ @@ -3048,14 +3178,13 @@ function useAnchor({ }) { const { tagName, - className + className, + isActive } = settings; const [anchor, setAnchor] = (0,external_wp_element_namespaceObject.useState)(() => getAnchor(editableContentElement, tagName, className)); + const wasActive = (0,external_wp_compose_namespaceObject.usePrevious)(isActive); (0,external_wp_element_namespaceObject.useLayoutEffect)(() => { if (!editableContentElement) return; - const { - ownerDocument - } = editableContentElement; function callback() { setAnchor(getAnchor(editableContentElement, tagName, className)); } @@ -3065,18 +3194,30 @@ function useAnchor({ function detach() { ownerDocument.removeEventListener('selectionchange', callback); } - if (editableContentElement === ownerDocument.activeElement) { + const { + ownerDocument + } = editableContentElement; + if (editableContentElement === ownerDocument.activeElement || + // When a link is created, we need to attach the popover to the newly created anchor. + !wasActive && isActive || + // Sometimes we're _removing_ an active anchor, such as the inline color popover. + // When we add the color, it switches from a virtual anchor to a `<mark>` element. + // When we _remove_ the color, it switches from a `<mark>` element to a virtual anchor. + wasActive && !isActive) { + setAnchor(getAnchor(editableContentElement, tagName, className)); attach(); } editableContentElement.addEventListener('focusin', attach); editableContentElement.addEventListener('focusout', detach); - return detach; - }, [editableContentElement, tagName, className]); + return () => { + detach(); + editableContentElement.removeEventListener('focusin', attach); + editableContentElement.removeEventListener('focusout', detach); + }; + }, [editableContentElement, tagName, className, isActive, wasActive]); return anchor; } -;// CONCATENATED MODULE: external ["wp","compose"] -var external_wp_compose_namespaceObject = window["wp"]["compose"]; ;// CONCATENATED MODULE: ./node_modules/@wordpress/rich-text/build-module/component/use-default-style.js /** * WordPress dependencies @@ -3194,8 +3335,7 @@ function useCopyHandler(props) { return (0,external_wp_compose_namespaceObject.useRefEffect)(element => { function onCopy(event) { const { - record, - preserveWhiteSpace + record } = propsRef.current; const { ownerDocument @@ -3206,8 +3346,7 @@ function useCopyHandler(props) { const selectedRecord = slice(record.current); const plainText = getTextContent(selectedRecord); const html = toHTMLString({ - value: selectedRecord, - preserveWhiteSpace + value: selectedRecord }); event.clipboardData.setData('text/plain', plainText); event.clipboardData.setData('text/html', html); @@ -3227,7 +3366,7 @@ function useCopyHandler(props) { } ;// CONCATENATED MODULE: external ["wp","keycodes"] -var external_wp_keycodes_namespaceObject = window["wp"]["keycodes"]; +const external_wp_keycodes_namespaceObject = window["wp"]["keycodes"]; ;// CONCATENATED MODULE: ./node_modules/@wordpress/rich-text/build-module/component/use-format-boundaries.js /** * WordPress dependencies @@ -3562,49 +3701,11 @@ function useInputAndSelection(props) { return; } - // If the selection changes where the active element is a parent of - // the rich text instance (writing flow), call `onSelectionChange` - // for the rich text instance that contains the start or end of the - // selection. + // Ensure the active element is the rich text element. if (ownerDocument.activeElement !== element) { - // Only process if the active elment is contentEditable, either - // this rich text instance or the writing flow parent. Fixes a - // bug in Firefox where it strangely selects the closest - // contentEditable element, even though the click was outside - // any contentEditable element. - if (ownerDocument.activeElement.contentEditable !== 'true') { - return; - } - if (!ownerDocument.activeElement.contains(element)) { - return; - } - const selection = defaultView.getSelection(); - const { - anchorNode, - focusNode - } = selection; - if (element.contains(anchorNode) && element !== anchorNode && element.contains(focusNode) && element !== focusNode) { - const { - start, - end - } = createRecord(); - record.current.activeFormats = use_input_and_selection_EMPTY_ACTIVE_FORMATS; - onSelectionChange(start, end); - } else if (element.contains(anchorNode) && element !== anchorNode) { - const { - start, - end: offset = start - } = createRecord(); - record.current.activeFormats = use_input_and_selection_EMPTY_ACTIVE_FORMATS; - onSelectionChange(offset); - } else if (element.contains(focusNode)) { - const { - start, - end: offset = start - } = createRecord(); - record.current.activeFormats = use_input_and_selection_EMPTY_ACTIVE_FORMATS; - onSelectionChange(undefined, offset); - } + // If it is not, we can stop listening for selection changes. + // We resume listening when the element is focused. + ownerDocument.removeEventListener('selectionchange', handleSelectionChange); return; } @@ -3705,21 +3806,22 @@ function useInputAndSelection(props) { activeFormats: use_input_and_selection_EMPTY_ACTIVE_FORMATS }; } else { - applyRecord(record.current); - onSelectionChange(record.current.start, record.current.end); + applyRecord(record.current, { + domOnly: true + }); } + onSelectionChange(record.current.start, record.current.end); + ownerDocument.addEventListener('selectionchange', handleSelectionChange); } element.addEventListener('input', onInput); element.addEventListener('compositionstart', onCompositionStart); element.addEventListener('compositionend', onCompositionEnd); element.addEventListener('focus', onFocus); - ownerDocument.addEventListener('selectionchange', handleSelectionChange); return () => { element.removeEventListener('input', onInput); element.removeEventListener('compositionstart', onCompositionStart); element.removeEventListener('compositionend', onCompositionEnd); element.removeEventListener('focus', onFocus); - ownerDocument.removeEventListener('selectionchange', handleSelectionChange); }; }, []); } @@ -3822,7 +3924,7 @@ function useDelete(props) { // Always handle full content deletion ourselves. if (start === 0 && end !== 0 && end === text.length) { - handleChange(remove(currentValue)); + handleChange(remove_remove(currentValue)); event.preventDefault(); } } @@ -3860,8 +3962,8 @@ function useRichText({ selectionStart, selectionEnd, placeholder, - preserveWhiteSpace, onSelectionChange, + preserveWhiteSpace, onChange, __unstableDisableFormats: disableFormats, __unstableIsSelected: isSelected, @@ -3884,8 +3986,7 @@ function useRichText({ return create({ element: ref.current, range, - __unstableIsEditableTree: true, - preserveWhiteSpace + __unstableIsEditableTree: true }); } function applyRecord(newRecord, { @@ -3905,10 +4006,18 @@ function useRichText({ const record = (0,external_wp_element_namespaceObject.useRef)(); function setRecordFromProps() { _value.current = value; - record.current = create({ - html: value, - preserveWhiteSpace - }); + record.current = value; + if (!(value instanceof RichTextData)) { + record.current = value ? RichTextData.fromHTMLString(value, { + preserveWhiteSpace + }) : RichTextData.empty(); + } + // To do: make rich text internally work with RichTextData. + record.current = { + text: record.current.text, + formats: record.current.formats, + replacements: record.current.replacements + }; if (disableFormats) { record.current.formats = Array(value.length); record.current.replacements = Array(value.length); @@ -3923,19 +4032,6 @@ function useRichText({ if (!record.current) { hadSelectionUpdate.current = isSelected; setRecordFromProps(); - // Sometimes formats are added programmatically and we need to make - // sure it's persisted to the block store / markup. If these formats - // are not applied, they could cause inconsistencies between the data - // in the visual editor and the frontend. Right now, it's only relevant - // to the `core/text-color` format, which is applied at runtime in - // certain circunstances. See the `__unstableFilterAttributeValue` - // function in `packages/format-library/src/text-color/index.js`. - // @todo find a less-hacky way of solving this. - - const hasRelevantInitFormat = record.current?.formats[0]?.[0]?.type === 'core/text-color'; - if (hasRelevantInitFormat) { - handleChangesUponInit(record.current); - } } else if (selectionStart !== record.current.start || selectionEnd !== record.current.end) { hadSelectionUpdate.current = isSelected; record.current = { @@ -3958,20 +4054,26 @@ function useRichText({ if (disableFormats) { _value.current = newRecord.text; } else { - _value.current = toHTMLString({ - value: __unstableBeforeSerialize ? { - ...newRecord, - formats: __unstableBeforeSerialize(newRecord) - } : newRecord, - preserveWhiteSpace - }); + const newFormats = __unstableBeforeSerialize ? __unstableBeforeSerialize(newRecord) : newRecord.formats; + newRecord = { + ...newRecord, + formats: newFormats + }; + if (typeof value === 'string') { + _value.current = toHTMLString({ + value: newRecord, + preserveWhiteSpace + }); + } else { + _value.current = new RichTextData(newRecord); + } } const { start, end, formats, text - } = newRecord; + } = record.current; // Selection must be updated first, so it is recorded in history when // the content change happens. @@ -3985,27 +4087,6 @@ function useRichText({ }); forceRender(); } - function handleChangesUponInit(newRecord) { - record.current = newRecord; - _value.current = toHTMLString({ - value: __unstableBeforeSerialize ? { - ...newRecord, - formats: __unstableBeforeSerialize(newRecord) - } : newRecord, - preserveWhiteSpace - }); - const { - formats, - text - } = newRecord; - registry.batch(() => { - onChange(_value.current, { - __unstableFormats: formats, - __unstableText: text - }); - }); - forceRender(); - } function applyFromProps() { setRecordFromProps(); applyRecord(record.current); @@ -4034,8 +4115,7 @@ function useRichText({ const mergedRefs = (0,external_wp_compose_namespaceObject.useMergeRefs)([ref, useDefaultStyle(), useBoundaryStyle({ record }), useCopyHandler({ - record, - preserveWhiteSpace + record }), useSelectObject(), useFormatBoundaries({ record, applyRecord @@ -4067,46 +4147,6 @@ function useRichText({ } function __experimentalRichText() {} -;// CONCATENATED MODULE: ./node_modules/@wordpress/rich-text/build-module/component/format-edit.js - -/** - * Internal dependencies - */ - - -function FormatEdit({ - formatTypes, - onChange, - onFocus, - value, - forwardedRef -}) { - return formatTypes.map(settings => { - const { - name, - edit: Edit - } = settings; - if (!Edit) { - return null; - } - const activeFormat = getActiveFormat(value, name); - const isActive = activeFormat !== undefined; - const activeObject = getActiveObject(value); - const isObjectActive = activeObject !== undefined && activeObject.type === name; - return (0,external_wp_element_namespaceObject.createElement)(Edit, { - key: name, - isActive: isActive, - activeAttributes: isActive ? activeFormat.attributes || {} : {}, - isObjectActive: isObjectActive, - activeObjectAttributes: isObjectActive ? activeObject.attributes || {} : {}, - value: value, - onChange: onChange, - onFocus: onFocus, - contentRef: forwardedRef - }); - }); -} - ;// CONCATENATED MODULE: ./node_modules/@wordpress/rich-text/build-module/index.js @@ -4136,7 +4176,6 @@ function FormatEdit({ - /** * An object which represents a formatted string. See main `@wordpress/rich-text` * documentation for more information. |