diff --git a/core-blocks/heading/index.js b/core-blocks/heading/index.js
index 1d440482bba2c..cbad44d4f0ffe 100644
--- a/core-blocks/heading/index.js
+++ b/core-blocks/heading/index.js
@@ -7,12 +7,12 @@ import { omit } from 'lodash';
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
-import { concatChildren } from '@wordpress/element';
import {
createBlock,
getPhrasingContentSchema,
getBlockAttributes,
getBlockType,
+ children,
} from '@wordpress/blocks';
import { RichText } from '@wordpress/editor';
@@ -167,7 +167,10 @@ export const settings = {
merge( attributes, attributesToMerge ) {
return {
- content: concatChildren( attributes.content, attributesToMerge.content ),
+ content: children.concat(
+ attributes.content,
+ attributesToMerge.content
+ ),
};
},
diff --git a/core-blocks/paragraph/index.js b/core-blocks/paragraph/index.js
index acdc498c55faf..5ebc8f7fff8a2 100644
--- a/core-blocks/paragraph/index.js
+++ b/core-blocks/paragraph/index.js
@@ -9,7 +9,6 @@ import { isFinite, find, omit } from 'lodash';
*/
import { __ } from '@wordpress/i18n';
import {
- concatChildren,
Component,
Fragment,
RawHTML,
@@ -30,7 +29,11 @@ import {
PanelColor,
RichText,
} from '@wordpress/editor';
-import { createBlock, getPhrasingContentSchema } from '@wordpress/blocks';
+import {
+ createBlock,
+ getPhrasingContentSchema,
+ children,
+} from '@wordpress/blocks';
import { compose } from '@wordpress/compose';
/**
@@ -470,7 +473,10 @@ export const settings = {
merge( attributes, attributesToMerge ) {
return {
- content: concatChildren( attributes.content, attributesToMerge.content ),
+ content: children.concat(
+ attributes.content,
+ attributesToMerge.content
+ ),
};
},
diff --git a/core-blocks/test/fixtures/core__heading__h2-em.json b/core-blocks/test/fixtures/core__heading__h2-em.json
index ea753522fc29b..1635da5f8efbb 100644
--- a/core-blocks/test/fixtures/core__heading__h2-em.json
+++ b/core-blocks/test/fixtures/core__heading__h2-em.json
@@ -8,7 +8,9 @@
"The ",
{
"type": "em",
- "children": "Inserter"
+ "children": [
+ "Inserter"
+ ]
},
" Tool"
],
diff --git a/core-blocks/test/fixtures/core__list__ul.json b/core-blocks/test/fixtures/core__list__ul.json
index ff66a5426b967..6d2ba10a4cb33 100644
--- a/core-blocks/test/fixtures/core__list__ul.json
+++ b/core-blocks/test/fixtures/core__list__ul.json
@@ -8,23 +8,33 @@
"values": [
{
"type": "li",
- "children": "Text & Headings"
+ "children": [
+ "Text & Headings"
+ ]
},
{
"type": "li",
- "children": "Images & Videos"
+ "children": [
+ "Images & Videos"
+ ]
},
{
"type": "li",
- "children": "Galleries"
+ "children": [
+ "Galleries"
+ ]
},
{
"type": "li",
- "children": "Embeds, like YouTube, Tweets, or other WordPress posts."
+ "children": [
+ "Embeds, like YouTube, Tweets, or other WordPress posts."
+ ]
},
{
"type": "li",
- "children": "Layout blocks, like Buttons, Hero Images, Separators, etc."
+ "children": [
+ "Layout blocks, like Buttons, Hero Images, Separators, etc."
+ ]
},
{
"type": "li",
@@ -32,7 +42,9 @@
"And ",
{
"type": "em",
- "children": "Lists"
+ "children": [
+ "Lists"
+ ]
},
" like this one of course :)"
]
diff --git a/core-blocks/test/fixtures/core__preformatted.json b/core-blocks/test/fixtures/core__preformatted.json
index 842557c90b428..6bf8d032b3e40 100644
--- a/core-blocks/test/fixtures/core__preformatted.json
+++ b/core-blocks/test/fixtures/core__preformatted.json
@@ -8,11 +8,14 @@
"Some ",
{
"type": "em",
- "children": "preformatted"
+ "children": [
+ "preformatted"
+ ]
},
" text...",
{
- "type": "br"
+ "type": "br",
+ "children": []
},
"And more!"
]
diff --git a/core-blocks/test/fixtures/core__pullquote.json b/core-blocks/test/fixtures/core__pullquote.json
index cc8642543873d..754ba9d007cc1 100644
--- a/core-blocks/test/fixtures/core__pullquote.json
+++ b/core-blocks/test/fixtures/core__pullquote.json
@@ -8,13 +8,11 @@
{
"children": {
"type": "p",
- "key": null,
- "ref": null,
"props": {
- "children": "Testing pullquote block..."
- },
- "_owner": null,
- "_store": {}
+ "children": [
+ "Testing pullquote block..."
+ ]
+ }
}
}
],
diff --git a/core-blocks/test/fixtures/core__pullquote__multi-paragraph.json b/core-blocks/test/fixtures/core__pullquote__multi-paragraph.json
index dfb28dafea49c..efd2a82c1d116 100644
--- a/core-blocks/test/fixtures/core__pullquote__multi-paragraph.json
+++ b/core-blocks/test/fixtures/core__pullquote__multi-paragraph.json
@@ -8,37 +8,29 @@
{
"children": {
"type": "p",
- "key": null,
- "ref": null,
"props": {
"children": [
"Paragraph ",
{
"type": "strong",
- "key": "_domReact73",
- "ref": null,
"props": {
- "children": "one"
- },
- "_owner": null,
- "_store": {}
+ "children": [
+ "one"
+ ]
+ }
}
]
- },
- "_owner": null,
- "_store": {}
+ }
}
},
{
"children": {
"type": "p",
- "key": null,
- "ref": null,
"props": {
- "children": "Paragraph two"
- },
- "_owner": null,
- "_store": {}
+ "children": [
+ "Paragraph two"
+ ]
+ }
}
}
],
diff --git a/core-blocks/test/fixtures/core__quote__style-1.json b/core-blocks/test/fixtures/core__quote__style-1.json
index f152d155dac51..67722f404cd0a 100644
--- a/core-blocks/test/fixtures/core__quote__style-1.json
+++ b/core-blocks/test/fixtures/core__quote__style-1.json
@@ -8,13 +8,11 @@
{
"children": {
"type": "p",
- "key": null,
- "ref": null,
"props": {
- "children": "The editor will endeavour to create a new page and post building experience that makes writing rich posts effortless, and has “blocks” to make it easy what today might take shortcodes, custom HTML, or “mystery meat” embed discovery."
- },
- "_owner": null,
- "_store": {}
+ "children": [
+ "The editor will endeavour to create a new page and post building experience that makes writing rich posts effortless, and has “blocks” to make it easy what today might take shortcodes, custom HTML, or “mystery meat” embed discovery."
+ ]
+ }
}
}
],
diff --git a/core-blocks/test/fixtures/core__quote__style-2.json b/core-blocks/test/fixtures/core__quote__style-2.json
index 2b910b66eeb1e..ebf72ece86181 100644
--- a/core-blocks/test/fixtures/core__quote__style-2.json
+++ b/core-blocks/test/fixtures/core__quote__style-2.json
@@ -8,13 +8,11 @@
{
"children": {
"type": "p",
- "key": null,
- "ref": null,
"props": {
- "children": "There is no greater agony than bearing an untold story inside you."
- },
- "_owner": null,
- "_store": {}
+ "children": [
+ "There is no greater agony than bearing an untold story inside you."
+ ]
+ }
}
}
],
diff --git a/core-blocks/test/fixtures/core__subhead.json b/core-blocks/test/fixtures/core__subhead.json
index 6bee5aa70dc8c..64d8bb2f030c7 100644
--- a/core-blocks/test/fixtures/core__subhead.json
+++ b/core-blocks/test/fixtures/core__subhead.json
@@ -8,7 +8,9 @@
"This is a ",
{
"type": "em",
- "children": "subhead"
+ "children": [
+ "subhead"
+ ]
},
"."
]
diff --git a/core-blocks/test/fixtures/core__table.json b/core-blocks/test/fixtures/core__table.json
index b0ae80fa903d1..f6a6c42c9c545 100644
--- a/core-blocks/test/fixtures/core__table.json
+++ b/core-blocks/test/fixtures/core__table.json
@@ -7,23 +7,31 @@
"content": [
{
"type": "thead",
- "children": {
- "type": "tr",
- "children": [
- {
- "type": "th",
- "children": "Version"
- },
- {
- "type": "th",
- "children": "Musician"
- },
- {
- "type": "th",
- "children": "Date"
- }
- ]
- }
+ "children": [
+ {
+ "type": "tr",
+ "children": [
+ {
+ "type": "th",
+ "children": [
+ "Version"
+ ]
+ },
+ {
+ "type": "th",
+ "children": [
+ "Musician"
+ ]
+ },
+ {
+ "type": "th",
+ "children": [
+ "Date"
+ ]
+ }
+ ]
+ }
+ ]
},
{
"type": "tbody",
@@ -33,21 +41,29 @@
"children": [
{
"type": "td",
- "children": {
- "type": "a",
- "attributes": {
- "href": "https://wordpress.org/news/2003/05/wordpress-now-available/"
- },
- "children": ".70"
- }
+ "children": [
+ {
+ "type": "a",
+ "attributes": {
+ "href": "https://wordpress.org/news/2003/05/wordpress-now-available/"
+ },
+ "children": [
+ ".70"
+ ]
+ }
+ ]
},
{
"type": "td",
- "children": "No musician chosen."
+ "children": [
+ "No musician chosen."
+ ]
},
{
"type": "td",
- "children": "May 27, 2003"
+ "children": [
+ "May 27, 2003"
+ ]
}
]
},
@@ -56,21 +72,29 @@
"children": [
{
"type": "td",
- "children": {
- "type": "a",
- "attributes": {
- "href": "https://wordpress.org/news/2004/01/wordpress-10/"
- },
- "children": "1.0"
- }
+ "children": [
+ {
+ "type": "a",
+ "attributes": {
+ "href": "https://wordpress.org/news/2004/01/wordpress-10/"
+ },
+ "children": [
+ "1.0"
+ ]
+ }
+ ]
},
{
"type": "td",
- "children": "Miles Davis"
+ "children": [
+ "Miles Davis"
+ ]
},
{
"type": "td",
- "children": "January 3, 2004"
+ "children": [
+ "January 3, 2004"
+ ]
}
]
},
@@ -86,17 +110,23 @@
"attributes": {
"href": "https://codex.wordpress.org/WordPress_Versions"
},
- "children": "the full list"
+ "children": [
+ "the full list"
+ ]
}
]
},
{
"type": "td",
- "children": "…"
+ "children": [
+ "…"
+ ]
},
{
"type": "td",
- "children": "…"
+ "children": [
+ "…"
+ ]
}
]
},
@@ -105,21 +135,29 @@
"children": [
{
"type": "td",
- "children": {
- "type": "a",
- "attributes": {
- "href": "https://wordpress.org/news/2015/12/clifford/"
- },
- "children": "4.4"
- }
+ "children": [
+ {
+ "type": "a",
+ "attributes": {
+ "href": "https://wordpress.org/news/2015/12/clifford/"
+ },
+ "children": [
+ "4.4"
+ ]
+ }
+ ]
},
{
"type": "td",
- "children": "Clifford Brown"
+ "children": [
+ "Clifford Brown"
+ ]
},
{
"type": "td",
- "children": "December 8, 2015"
+ "children": [
+ "December 8, 2015"
+ ]
}
]
},
@@ -128,21 +166,29 @@
"children": [
{
"type": "td",
- "children": {
- "type": "a",
- "attributes": {
- "href": "https://wordpress.org/news/2016/04/coleman/"
- },
- "children": "4.5"
- }
+ "children": [
+ {
+ "type": "a",
+ "attributes": {
+ "href": "https://wordpress.org/news/2016/04/coleman/"
+ },
+ "children": [
+ "4.5"
+ ]
+ }
+ ]
},
{
"type": "td",
- "children": "Coleman Hawkins"
+ "children": [
+ "Coleman Hawkins"
+ ]
},
{
"type": "td",
- "children": "April 12, 2016"
+ "children": [
+ "April 12, 2016"
+ ]
}
]
},
@@ -151,21 +197,29 @@
"children": [
{
"type": "td",
- "children": {
- "type": "a",
- "attributes": {
- "href": "https://wordpress.org/news/2016/08/pepper/"
- },
- "children": "4.6"
- }
+ "children": [
+ {
+ "type": "a",
+ "attributes": {
+ "href": "https://wordpress.org/news/2016/08/pepper/"
+ },
+ "children": [
+ "4.6"
+ ]
+ }
+ ]
},
{
"type": "td",
- "children": "Pepper Adams"
+ "children": [
+ "Pepper Adams"
+ ]
},
{
"type": "td",
- "children": "August 16, 2016"
+ "children": [
+ "August 16, 2016"
+ ]
}
]
},
@@ -174,21 +228,29 @@
"children": [
{
"type": "td",
- "children": {
- "type": "a",
- "attributes": {
- "href": "https://wordpress.org/news/2016/12/vaughan/"
- },
- "children": "4.7"
- }
+ "children": [
+ {
+ "type": "a",
+ "attributes": {
+ "href": "https://wordpress.org/news/2016/12/vaughan/"
+ },
+ "children": [
+ "4.7"
+ ]
+ }
+ ]
},
{
"type": "td",
- "children": "Sarah Vaughan"
+ "children": [
+ "Sarah Vaughan"
+ ]
},
{
"type": "td",
- "children": "December 6, 2016"
+ "children": [
+ "December 6, 2016"
+ ]
}
]
}
diff --git a/core-blocks/test/fixtures/core__text__converts-to-paragraph.json b/core-blocks/test/fixtures/core__text__converts-to-paragraph.json
index b2e12e5ce758f..10066958f0b3b 100644
--- a/core-blocks/test/fixtures/core__text__converts-to-paragraph.json
+++ b/core-blocks/test/fixtures/core__text__converts-to-paragraph.json
@@ -8,7 +8,9 @@
"This is an old-style text block. Changed to ",
{
"type": "code",
- "children": "paragraph"
+ "children": [
+ "paragraph"
+ ]
},
" in #2135."
],
diff --git a/core-blocks/test/fixtures/core__verse.json b/core-blocks/test/fixtures/core__verse.json
index b002134e73471..4412cb039d849 100644
--- a/core-blocks/test/fixtures/core__verse.json
+++ b/core-blocks/test/fixtures/core__verse.json
@@ -8,11 +8,14 @@
"A ",
{
"type": "em",
- "children": "verse"
+ "children": [
+ "verse"
+ ]
},
"…",
{
- "type": "br"
+ "type": "br",
+ "children": []
},
"And more!"
]
diff --git a/docs/reference/deprecated.md b/docs/reference/deprecated.md
index 1656ee5f5bbab..57075082b4e68 100644
--- a/docs/reference/deprecated.md
+++ b/docs/reference/deprecated.md
@@ -17,6 +17,7 @@ Gutenberg's deprecation policy is intended to support backwards-compatibility fo
- The `wp.editor.UrlInput` component has been renamed to `wp.editor.URLInput`.
- The Text Columns block has been removed. Please use the Columns block instead.
- `InnerBlocks` grouped layout is removed. Use intermediary nested inner blocks instead. See Columns / Column block for reference implementation.
+ - `RichText` explicit `element` format removed. Please use the compatible `children` format instead.
## 3.4.0
diff --git a/editor/components/rich-text/README.md b/editor/components/rich-text/README.md
index 306bb28cd7b85..b50833379d378 100644
--- a/editor/components/rich-text/README.md
+++ b/editor/components/rich-text/README.md
@@ -6,9 +6,9 @@ Render a rich [`contenteditable` input](https://developer.mozilla.org/en-US/docs
### `format: String`
-*Optional.* Format of the RichText provided value prop. It can be `element` or `string`.
+*Optional.* Format of the RichText provided value prop. It can be `children` or `string`.
-*Default: `element`*.
+*Default: `children`*.
### `value: Array|String`
diff --git a/editor/components/rich-text/format.js b/editor/components/rich-text/format.js
index 221966b81fa7e..c6da19e4190a4 100644
--- a/editor/components/rich-text/format.js
+++ b/editor/components/rich-text/format.js
@@ -8,6 +8,81 @@ import { nodeListToReact } from 'dom-react';
* WordPress dependencies
*/
import { createElement, renderToString } from '@wordpress/element';
+import { children } from '@wordpress/blocks';
+
+const { TEXT_NODE, ELEMENT_NODE } = window.Node;
+
+/**
+ * Zero-width space character used by TinyMCE as a caret landing point for
+ * inline boundary nodes.
+ *
+ * @see tinymce/src/core/main/ts/text/Zwsp.ts
+ *
+ * @type {string}
+ */
+const TINYMCE_ZWSP = '\uFEFF';
+
+/**
+ * Regular expression matching TinyMCE zero-width space character globally.
+ *
+ * @type {RegExp}
+ */
+const REGEXP_TINYMCE_ZWSP = new RegExp( TINYMCE_ZWSP, 'g' );
+
+/**
+ * Returns true if the given attribute name is a TinyMCE internal temporary
+ * attribute which should not be included in the serialized output, or false
+ * otherwise. Would use the given editor's serializer as basis for determining
+ * temporary attributes, but fails to include attributes like `data-mce-src`.
+ *
+ * @param {string} attributeName Attribute name to test.
+ *
+ * @return {boolean} Whether attribute is an internal temporary attribute.
+ */
+export function isTinyMCEInternalAttribute( attributeName ) {
+ return attributeName.indexOf( 'data-mce-' ) === 0;
+}
+
+/**
+ * Returns true if the given HTMLElement is a TinyMCE bogus element. During
+ * serialization, a bogus element should be skipped.
+ *
+ * @param {HTMLElement} element Element to test.
+ *
+ * @return {boolean} Whether element is a TinyMCE bogus element.
+ */
+export function isTinyMCEBogusElement( element ) {
+ return element.getAttribute( 'data-mce-bogus' ) === 'all';
+}
+
+/**
+ * Returns true if the given HTMLElement is a TinyMCE bogus wrapper. During
+ * serialization, a bogus wrapper should be substituted with its childrens'
+ * content.
+ *
+ * @param {HTMLElement} element Element to test.
+ *
+ * @return {boolean} Whether element is a TinyMCE bogus wrapper.
+ */
+export function isTinyMCEBogusWrapperElement( element ) {
+ return (
+ element.hasAttribute( 'data-mce-bogus' ) &&
+ ! isTinyMCEBogusElement( element )
+ );
+}
+
+/**
+ * Given a text node, returns its node value with any TinyMCE internal zero-
+ * width space characters omitted.
+ *
+ * @param {Text} node Text node from which to derive value.
+ *
+ * @return {string} Cleaned text node value.
+ */
+export function getCleanTextNodeValue( node ) {
+ const { nodeValue } = node;
+ return nodeValue.replace( REGEXP_TINYMCE_ZWSP, '' );
+}
/**
* Transforms a WP Element to its corresponding HTML string.
@@ -29,11 +104,19 @@ export function elementToString( value ) {
* @return {string} HTML output as string.
*/
export function valueToString( value, format ) {
+ if ( ! value ) {
+ return '';
+ }
+
switch ( format ) {
case 'string':
- return value || '';
- default:
+ return value;
+
+ case 'element':
return elementToString( value );
+
+ case 'children':
+ return children.toHTML( value );
}
}
@@ -46,22 +129,50 @@ export function valueToString( value, format ) {
*
* @return {Element} WPElement.
*/
-export function createTinyMCEElement( type, props, ...children ) {
+export function createTinyMCEElement( type, props, ...elementChildren ) {
if ( props[ 'data-mce-bogus' ] === 'all' ) {
return null;
}
if ( props.hasOwnProperty( 'data-mce-bogus' ) ) {
- return children;
+ return elementChildren;
}
return createElement(
type,
omitBy( props, ( _, key ) => key.indexOf( 'data-mce-' ) === 0 ),
- ...children
+ ...elementChildren
);
}
+/**
+ * Given an HTMLElement from a TinyMCE editor body element, returns equivalent
+ * WPBlockChildren value. The element may undergo some preprocessing to remove
+ * temporary or internal elements and attributes.
+ *
+ * @param {HTMLElement} element TinyMCE DOM element.
+ *
+ * @return {WPBlockChildren} WPBlockChildren equivalent value to element.
+ */
+export function createBlockChildrenFromTinyMCEElement( element ) {
+ const attributes = {};
+ for ( let i = 0; i < element.attributes.length; i++ ) {
+ const { name, value } = element.attributes[ i ];
+
+ if ( ! isTinyMCEInternalAttribute( name ) ) {
+ attributes[ name ] = value;
+ }
+ }
+
+ return {
+ type: element.nodeName.toLowerCase(),
+ props: {
+ ...attributes,
+ children: domToBlockChildren( element.childNodes ),
+ },
+ };
+}
+
/**
* Transforms an array of DOM Elements to their corresponding WP element.
*
@@ -74,21 +185,53 @@ export function domToElement( value ) {
}
/**
- * Transforms an array of DOM Elements to their corresponding HTML string output.
+ * Given an array of HTMLElement from a TinyMCE editor body element, returns an
+ * equivalent WPBlockChildren value. The element may undergo some preprocessing
+ * to remove temporary or internal elements and attributes.
*
- * @param {Array} value DOM Elements.
- * @param {Editor} editor TinyMCE editor instance.
+ * @param {Array} value TinyMCE DOM elements.
*
- * @return {string} HTML.
+ * @return {WPBlockChildren} WPBlockChildren equivalent value to element.
*/
-export function domToString( value, editor ) {
- const doc = document.implementation.createHTMLDocument( '' );
+export function domToBlockChildren( value ) {
+ const result = [];
+
+ for ( let i = 0; i < value.length; i++ ) {
+ let node = value[ i ];
+ switch ( node.nodeType ) {
+ case TEXT_NODE:
+ node = getCleanTextNodeValue( node );
+ if ( node.length ) {
+ result.push( node );
+ }
+ break;
+
+ case ELEMENT_NODE:
+ if ( isTinyMCEBogusElement( node ) ) {
+ break;
+ }
- Array.from( value ).forEach( ( child ) => {
- doc.body.appendChild( child );
- } );
+ if ( ! isTinyMCEBogusWrapperElement( node ) ) {
+ result.push( createBlockChildrenFromTinyMCEElement( node ) );
+ } else if ( node.hasChildNodes() ) {
+ result.push( ...domToBlockChildren( node.childNodes ) );
+ }
+ break;
+ }
+ }
+
+ return result;
+}
- return editor ? editor.serializer.serialize( doc.body ) : doc.body.innerHTML;
+/**
+ * Transforms an array of DOM Elements to their corresponding HTML string output.
+ *
+ * @param {Array} value DOM Elements.
+ *
+ * @return {string} HTML.
+ */
+export function domToString( value ) {
+ return children.toHTML( domToBlockChildren( value ) );
}
/**
@@ -96,15 +239,18 @@ export function domToString( value, editor ) {
*
* @param {Array} value DOM Elements.
* @param {string} format Output format (string or element)
- * @param {Editor} editor TinyMCE editor instance.
*
* @return {*} Output.
*/
-export function domToFormat( value, format, editor ) {
+export function domToFormat( value, format ) {
switch ( format ) {
case 'string':
- return domToString( value, editor );
- default:
+ return domToString( value );
+
+ case 'element':
return domToElement( value );
+
+ case 'children':
+ return domToBlockChildren( value );
}
}
diff --git a/editor/components/rich-text/index.js b/editor/components/rich-text/index.js
index 2134d9c3fe6f2..ce14e3b91f64a 100644
--- a/editor/components/rich-text/index.js
+++ b/editor/components/rich-text/index.js
@@ -10,7 +10,6 @@ import {
find,
defer,
noop,
- reject,
} from 'lodash';
import 'element-closest';
@@ -27,7 +26,8 @@ import { createBlobURL } from '@wordpress/blob';
import { BACKSPACE, DELETE, ENTER, LEFT, RIGHT, rawShortcut } from '@wordpress/keycodes';
import { Slot } from '@wordpress/components';
import { withSelect } from '@wordpress/data';
-import { rawHandler } from '@wordpress/blocks';
+import { rawHandler, children } from '@wordpress/blocks';
+import deprecated from '@wordpress/deprecated';
import { withInstanceId, withSafeTimeout, compose } from '@wordpress/compose';
/**
@@ -60,50 +60,6 @@ const { Node } = window;
*/
const TINYMCE_ZWSP = '\uFEFF';
-/**
- * Returns true if the node is the inline node boundary. This is used in node
- * filtering prevent the inline boundary from being included in the split which
- * occurs while within but at the end of an inline node, since TinyMCE includes
- * a placeholder caret character at the end.
- *
- * @see https://github.com/tinymce/tinymce/blob/master/src/plugins/link/main/ts/core/Utils.ts
- *
- * @param {Node} node Node to test.
- *
- * @return {boolean} Whether node is inline boundary.
- */
-export function isEmptyInlineBoundary( node ) {
- const text = node.nodeName === 'A' ? node.innerText : node.textContent;
- return text === TINYMCE_ZWSP;
-}
-
-/**
- * Returns true if the node is empty, meaning it contains only the placeholder
- * caret character or is an empty text node.
- *
- * @param {Node} node Node to test.
- *
- * @return {boolean} Whether node is empty.
- */
-export function isEmptyNode( node ) {
- return (
- '' === node.nodeValue ||
- isEmptyInlineBoundary( node )
- );
-}
-
-/**
- * Given a set of Nodes, filters to set to exclude any empty nodes: those with
- * either empty text nodes or only including the inline boundary caret.
- *
- * @param {Node[]} childNodes Nodes to filter.
- *
- * @return {Node[]} Non-empty nodes.
- */
-export function filterEmptyNodes( childNodes ) {
- return reject( childNodes, isEmptyNode );
-}
-
export function getFormatProperties( formatName, parents ) {
switch ( formatName ) {
case 'link' : {
@@ -539,8 +495,8 @@ export class RichText extends Component {
const afterNodes = childNodes.slice( index + 1 );
const { format } = this.props;
- const before = domToFormat( beforeNodes, format, this.editor );
- const after = domToFormat( afterNodes, format, this.editor );
+ const before = domToFormat( beforeNodes, format );
+ const after = domToFormat( afterNodes, format );
this.props.onSplit( before, after );
} else {
@@ -637,8 +593,8 @@ export class RichText extends Component {
const afterFragment = afterRange.cloneContents();
const { format } = this.props;
- before = domToFormat( filterEmptyNodes( beforeFragment.childNodes ), format, this.editor );
- after = domToFormat( filterEmptyNodes( afterFragment.childNodes ), format, this.editor );
+ before = domToFormat( beforeFragment.childNodes, format );
+ after = domToFormat( afterFragment.childNodes, format );
} else {
before = [];
after = [];
@@ -721,12 +677,7 @@ export class RichText extends Component {
getContent() {
const { format } = this.props;
- switch ( format ) {
- case 'string':
- return this.editor.getContent();
- default:
- return domToFormat( this.editor.getBody().childNodes || [], 'element', this.editor );
- }
+ return domToFormat( this.editor.getBody().childNodes, format );
}
componentDidUpdate( prevProps ) {
@@ -928,7 +879,7 @@ RichText.contextTypes = {
RichText.defaultProps = {
formattingControls: DEFAULT_FORMATS,
formatters: [],
- format: 'element',
+ format: 'children',
};
const RichTextContainer = compose( [
@@ -963,22 +914,39 @@ const RichTextContainer = compose( [
withSafeTimeout,
] )( RichText );
-RichTextContainer.Content = ( { value, format = 'element', tagName: Tag, ...props } ) => {
- let children;
+RichTextContainer.Content = ( { value, format, tagName: Tag, ...props } ) => {
+ let content;
switch ( format ) {
case 'string':
- children =
Child
; @@ -78,12 +164,46 @@ describe( 'domToElement', () => { } ); } ); +describe( 'createBlockChildrenFromTinyMCEElement', () => { + it( 'returns recusrively cleaned tinymce element as block children', () => { + const element = document.createElement( 'div' ); + element.setAttribute( 'style', 'color: red' ); + const text = document.createTextNode( 'Aaaargh\uFEFF' ); + element.appendChild( text ); + const br = document.createElement( 'br' ); + br.setAttribute( 'data-mce-bogus', 'all' ); + element.appendChild( br ); + + const result = createBlockChildrenFromTinyMCEElement( element ); + + expect( result ).toEqual( { + type: 'div', + props: { + style: 'color: red', + children: [ 'Aaaargh' ], + }, + } ); + } ); +} ); + +describe( 'domToBlockChildren', () => { + test( 'should return an empty array', () => { + expect( domToBlockChildren( [] ) ).toEqual( [] ); + } ); + + test( 'should return the corresponding element ', () => { + const domElement = document.createElement( 'div' ); + domElement.innerHTML = '+" +`; + +exports[`adding blocks should clean TinyMCE content 2`] = ` +" +
Inside
+" +`; + exports[`adding blocks should navigate around inline boundaries 1`] = ` "FirstAfter
diff --git a/test/e2e/specs/adding-inline-tokens.test.js b/test/e2e/specs/adding-inline-tokens.test.js index 8e611b300f71e..3d0457ef80d63 100644 --- a/test/e2e/specs/adding-inline-tokens.test.js +++ b/test/e2e/specs/adding-inline-tokens.test.js @@ -41,7 +41,7 @@ describe( 'adding inline tokens', () => { await page.click( '.media-modal button.media-button-select' ); // Check the content. - const regex = new RegExp( '\\s*a\\u00A0<\\/p>\\s*' ); + const regex = new RegExp( '\\s*
a\\u00A0<\\/p>\\s*' ); expect( await getEditedPostContent() ).toMatch( regex ); } ); } ); diff --git a/test/e2e/specs/writing-flow.test.js b/test/e2e/specs/writing-flow.test.js index 854a209c9dca5..6753e7aba078d 100644 --- a/test/e2e/specs/writing-flow.test.js +++ b/test/e2e/specs/writing-flow.test.js @@ -142,4 +142,22 @@ describe( 'adding blocks', () => { expect( await getEditedPostContent() ).toMatchSnapshot(); } ); + + it( 'should clean TinyMCE content', async () => { + // Ensure no zero-width space character. Notably, this can occur when + // save occurs while at an inline boundary edge. + await page.click( '.editor-default-block-appender__content' ); + await pressWithModifier( 'mod', 'b' ); + expect( await getEditedPostContent() ).toMatchSnapshot(); + + // When returning to Visual mode, backspace in selected block should + // reset to the provisional block. + await page.keyboard.press( 'Backspace' ); + + // Ensure no data-mce-selected. Notably, this can occur when content + // is saved while typing within an inline boundary. + await pressWithModifier( 'mod', 'b' ); + await page.keyboard.type( 'Inside' ); + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); } );