diff --git a/core-blocks/audio/edit.js b/core-blocks/audio/edit.js
index 3f2591abc70d8d..cd566f1215b20d 100644
--- a/core-blocks/audio/edit.js
+++ b/core-blocks/audio/edit.js
@@ -135,7 +135,7 @@ class AudioEdit extends Component {
- { ( ( caption && caption.length ) || !! isSelected ) && (
+ { ( ! RichText.isEmpty( caption ) || isSelected ) && (
- { caption && caption.length > 0 && }
+ { ! RichText.isEmpty( caption ) && }
);
},
diff --git a/core-blocks/button/index.js b/core-blocks/button/index.js
index c69ad0d7c52d85..bfc2703e5b58f9 100644
--- a/core-blocks/button/index.js
+++ b/core-blocks/button/index.js
@@ -33,8 +33,8 @@ const blockAttributes = {
attribute: 'title',
},
text: {
- type: 'array',
- source: 'children',
+ type: 'object',
+ source: 'rich-text',
selector: 'a',
},
backgroundColor: {
diff --git a/core-blocks/cover-image/index.js b/core-blocks/cover-image/index.js
index 332c3c38143a7f..8daa270d920fb9 100644
--- a/core-blocks/cover-image/index.js
+++ b/core-blocks/cover-image/index.js
@@ -31,8 +31,8 @@ const validAlignments = [ 'left', 'center', 'right', 'wide', 'full' ];
const blockAttributes = {
title: {
- type: 'array',
- source: 'children',
+ type: 'object',
+ source: 'rich-text',
selector: 'p',
},
url: {
@@ -245,7 +245,7 @@ export const settings = {
return (
- { title && title.length > 0 && (
+ { ! RichText.isEmpty( title ) && (
) }
@@ -256,8 +256,8 @@ export const settings = {
attributes: {
...blockAttributes,
title: {
- type: 'array',
- source: 'children',
+ type: 'object',
+ source: 'rich-text',
selector: 'h2',
},
},
diff --git a/core-blocks/cover-image/test/__snapshots__/index.js.snap b/core-blocks/cover-image/test/__snapshots__/index.js.snap
index f6690fdae6fe9f..40be79ecf3f6d2 100644
--- a/core-blocks/cover-image/test/__snapshots__/index.js.snap
+++ b/core-blocks/cover-image/test/__snapshots__/index.js.snap
@@ -7,21 +7,26 @@ exports[`core/cover-image block edit matches snapshot 1`] = `
{ `\n${ url }\n` /* URL needs to be on its own line. */ }
- { caption && caption.length > 0 &&
}
+ { ! RichText.isEmpty( caption ) &&
}
);
},
diff --git a/core-blocks/gallery/index.js b/core-blocks/gallery/index.js
index e02377a8ff3459..51668065e6d79b 100644
--- a/core-blocks/gallery/index.js
+++ b/core-blocks/gallery/index.js
@@ -46,8 +46,8 @@ const blockAttributes = {
attribute: 'data-id',
},
caption: {
- type: 'array',
- source: 'children',
+ type: 'object',
+ source: 'rich-text',
selector: 'figcaption',
},
},
diff --git a/core-blocks/heading/index.js b/core-blocks/heading/index.js
index cbad44d4f0ffe7..a14e0b88b482ec 100644
--- a/core-blocks/heading/index.js
+++ b/core-blocks/heading/index.js
@@ -39,8 +39,8 @@ const supports = {
const schema = {
content: {
- type: 'array',
- source: 'children',
+ type: 'object',
+ source: 'rich-text',
selector: 'h1,h2,h3,h4,h5,h6',
},
level: {
diff --git a/core-blocks/image/edit.js b/core-blocks/image/edit.js
index 8b2c728906983f..f50a3c59bc79dd 100644
--- a/core-blocks/image/edit.js
+++ b/core-blocks/image/edit.js
@@ -467,7 +467,7 @@ class ImageEdit extends Component {
);
} }
- { ( caption && caption.length > 0 ) || isSelected ? (
+ { ! RichText.isEmpty( caption ) || isSelected ? (
{ href ? { image } : image }
- { caption && caption.length > 0 && }
+ { ! RichText.isEmpty( caption ) && }
);
},
@@ -244,7 +244,7 @@ export const settings = {
return (
{ href ? { image } : image }
- { caption && caption.length > 0 && }
+ { ! RichText.isEmpty( caption ) && }
);
},
@@ -267,7 +267,7 @@ export const settings = {
return (
{ href ? { image } : image }
- { caption && caption.length > 0 && }
+ { ! RichText.isEmpty( caption ) && }
);
},
diff --git a/core-blocks/list/index.js b/core-blocks/list/index.js
index f6788f435d1115..cec87c3f4666b5 100644
--- a/core-blocks/list/index.js
+++ b/core-blocks/list/index.js
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
-import { find, compact, get, initial, last, isEmpty, omit } from 'lodash';
+import { find, omit } from 'lodash';
/**
* WordPress dependencies
@@ -52,9 +52,9 @@ const schema = {
},
values: {
type: 'array',
- source: 'children',
+ source: 'rich-text',
selector: 'ol,ul',
- default: [],
+ multiline: 'li',
},
};
@@ -78,10 +78,8 @@ export const settings = {
isMultiBlock: true,
blocks: [ 'core/paragraph' ],
transform: ( blockAttributes ) => {
- const items = blockAttributes.map( ( { content } ) => content );
- const hasItems = ! items.every( isEmpty );
return createBlock( 'core/list', {
- values: hasItems ? items.map( ( content, index ) => { content } ) : [],
+ values: blockAttributes.map( ( { content } ) => content ),
} );
},
},
@@ -89,13 +87,12 @@ export const settings = {
type: 'block',
blocks: [ 'core/quote' ],
transform: ( { value, citation } ) => {
- const items = value.map( ( p ) => get( p, [ 'children', 'props', 'children' ] ) );
- if ( ! isEmpty( citation ) ) {
- items.push( citation );
+ if ( ! RichText.isEmpty( citation ) ) {
+ value.push( citation );
}
- const hasItems = ! items.every( isEmpty );
+
return createBlock( 'core/list', {
- values: hasItems ? items.map( ( content, index ) => { content } ) : [],
+ values: value,
} );
},
},
@@ -121,7 +118,7 @@ export const settings = {
regExp: /^[*-]\s/,
transform: ( { content } ) => {
return createBlock( 'core/list', {
- values: [ { content } ],
+ values: [ content ],
} );
},
},
@@ -131,7 +128,7 @@ export const settings = {
transform: ( { content } ) => {
return createBlock( 'core/list', {
ordered: true,
- values: [ { content } ],
+ values: [ content ],
} );
},
},
@@ -141,20 +138,14 @@ export const settings = {
type: 'block',
blocks: [ 'core/paragraph' ],
transform: ( { values } ) =>
- compact( values.map( ( value ) => get( value, [ 'props', 'children' ], null ) ) )
- .map( ( content ) => createBlock( 'core/paragraph', {
- content: [ content ],
- } ) ),
+ values.map( ( content ) => createBlock( 'core/paragraph', { content } ) ),
},
{
type: 'block',
blocks: [ 'core/quote' ],
transform: ( { values } ) => {
return createBlock( 'core/quote', {
- value: compact( ( values.length === 1 ? values : initial( values ) )
- .map( ( value ) => get( value, [ 'props', 'children' ], null ) ) )
- .map( ( children ) => ( { children: { children }
} ) ),
- citation: ( values.length === 1 ? undefined : [ get( last( values ), [ 'props', 'children' ] ) ] ),
+ value: values,
} );
},
},
@@ -370,7 +361,7 @@ export const settings = {
const tagName = ordered ? 'ol' : 'ul';
return (
-
+
);
},
};
diff --git a/core-blocks/paragraph/index.js b/core-blocks/paragraph/index.js
index f1ebe001709305..39e9e7053ca9b3 100644
--- a/core-blocks/paragraph/index.js
+++ b/core-blocks/paragraph/index.js
@@ -34,7 +34,6 @@ import {
import {
createBlock,
getPhrasingContentSchema,
- children,
} from '@wordpress/blocks';
import { compose } from '@wordpress/compose';
@@ -246,10 +245,9 @@ const supports = {
const schema = {
content: {
- type: 'array',
- source: 'children',
+ type: 'object',
+ source: 'rich-text',
selector: 'p',
- default: [],
},
align: {
type: 'string',
@@ -425,10 +423,7 @@ export const settings = {
merge( attributes, attributesToMerge ) {
return {
- content: children.concat(
- attributes.content,
- attributesToMerge.content
- ),
+ content: RichText.concat( attributes.content, attributesToMerge.content ),
};
},
diff --git a/core-blocks/preformatted/index.js b/core-blocks/preformatted/index.js
index 1b7e1255256e54..71dc64ad53487e 100644
--- a/core-blocks/preformatted/index.js
+++ b/core-blocks/preformatted/index.js
@@ -24,8 +24,8 @@ export const settings = {
attributes: {
content: {
- type: 'array',
- source: 'children',
+ type: 'object',
+ source: 'rich-text',
selector: 'pre',
},
},
diff --git a/core-blocks/pullquote/index.js b/core-blocks/pullquote/index.js
index 02faeed99d0d80..e93bcb81f6537d 100644
--- a/core-blocks/pullquote/index.js
+++ b/core-blocks/pullquote/index.js
@@ -1,8 +1,3 @@
-/**
- * External dependencies
- */
-import { map } from 'lodash';
-
/**
* WordPress dependencies
*/
@@ -18,24 +13,16 @@ import './editor.scss';
import './style.scss';
import './theme.scss';
-const toRichTextValue = ( value ) => map( value, ( ( subValue ) => subValue.children ) );
-const fromRichTextValue = ( value ) => map( value, ( subValue ) => ( {
- children: subValue,
-} ) );
const blockAttributes = {
value: {
type: 'array',
- source: 'query',
- selector: 'blockquote > p',
- query: {
- children: {
- source: 'node',
- },
- },
+ source: 'rich-text',
+ selector: 'blockquote',
+ multiline: 'p',
},
citation: {
- type: 'array',
- source: 'children',
+ type: 'object',
+ source: 'rich-text',
selector: 'cite',
},
};
@@ -65,17 +52,17 @@ export const settings = {
setAttributes( {
- value: fromRichTextValue( nextValue ),
+ value: nextValue,
} )
}
/* translators: the text of the quotation */
placeholder={ __( 'Write quote…' ) }
wrapperClassName="core-blocks-pullquote__content"
/>
- { ( citation || isSelected ) && (
+ { ( ! RichText.isEmpty( citation ) || isSelected ) && (
-
- { citation && citation.length > 0 && }
+
+ { ! RichText.isEmpty( citation ) && }
);
},
@@ -122,8 +109,8 @@ export const settings = {
return (
-
- { citation && citation.length > 0 && }
+
+ { ! RichText.isEmpty( citation ) && }
);
},
diff --git a/core-blocks/quote/index.js b/core-blocks/quote/index.js
index 34903efa574c62..2c472908136c54 100644
--- a/core-blocks/quote/index.js
+++ b/core-blocks/quote/index.js
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
-import { castArray, get, isString, isEmpty, omit } from 'lodash';
+import { omit } from 'lodash';
/**
* WordPress dependencies
@@ -22,26 +22,16 @@ import './style.scss';
import './editor.scss';
import './theme.scss';
-const toRichTextValue = ( value ) => value.map( ( ( subValue ) => subValue.children ) );
-const fromRichTextValue = ( value ) => value.map( ( subValue ) => ( {
- children: subValue,
-} ) );
-
const blockAttributes = {
value: {
type: 'array',
- source: 'query',
- selector: 'blockquote > p',
- query: {
- children: {
- source: 'node',
- },
- },
- default: [],
+ source: 'rich-text',
+ selector: 'blockquote',
+ multiline: 'p',
},
citation: {
- type: 'array',
- source: 'children',
+ type: 'object',
+ source: 'rich-text',
selector: 'cite',
},
align: {
@@ -71,12 +61,8 @@ export const settings = {
isMultiBlock: true,
blocks: [ 'core/paragraph' ],
transform: ( attributes ) => {
- const items = attributes.map( ( { content } ) => content );
- const hasItems = ! items.every( isEmpty );
return createBlock( 'core/quote', {
- value: hasItems ?
- items.map( ( content, index ) => ( { children: { content }
} ) ) :
- [],
+ value: attributes.map( ( { content } ) => content ),
} );
},
},
@@ -85,9 +71,7 @@ export const settings = {
blocks: [ 'core/heading' ],
transform: ( { content } ) => {
return createBlock( 'core/quote', {
- value: [
- { children: { content }
},
- ],
+ value: [ content ],
} );
},
},
@@ -96,9 +80,7 @@ export const settings = {
regExp: /^>\s/,
transform: ( { content } ) => {
return createBlock( 'core/quote', {
- value: [
- { children: { content }
},
- ],
+ value: [ content ],
} );
},
},
@@ -121,16 +103,11 @@ export const settings = {
type: 'block',
blocks: [ 'core/paragraph' ],
transform: ( { value, citation } ) => {
- // transforming an empty quote
- if ( ( ! value || ! value.length ) && ! citation ) {
- return createBlock( 'core/paragraph' );
+ if ( ! RichText.isEmpty( citation ) ) {
+ value.push( citation );
}
- // transforming a quote with content
- return ( value || [] ).map( ( item ) => createBlock( 'core/paragraph', {
- content: [ get( item, [ 'children', 'props', 'children' ], '' ) ],
- } ) ).concat( citation ? createBlock( 'core/paragraph', {
- content: citation,
- } ) : [] );
+
+ return value.map( ( content ) => createBlock( 'core/paragraph', { content } ) );
},
},
{
@@ -139,39 +116,22 @@ export const settings = {
transform: ( { value, citation, ...attrs } ) => {
// if no text content exist just transform the quote into an heading block
// using citation as the content, it may be empty creating an empty heading block.
- if ( ( ! value || ! value.length ) ) {
+ if ( RichText.isEmpty( value ) ) {
return createBlock( 'core/heading', {
content: citation,
} );
}
- const firstValue = get( value, [ 0, 'children' ] );
- const headingContent = castArray( isString( firstValue ) ?
- firstValue :
- get( firstValue, [ 'props', 'children' ], '' )
- );
-
- // if the quote content just contains a paragraph and no citation exist
- // convert the quote content into and heading block.
- if ( ! citation && value.length === 1 ) {
- return createBlock( 'core/heading', {
- content: headingContent,
- } );
- }
-
- // In the normal case convert the first paragraph of quote into an heading
- // and create a new quote block equal tl what we had excluding the first paragraph
- const heading = createBlock( 'core/heading', {
- content: headingContent,
- } );
-
- const quote = createBlock( 'core/quote', {
- ...attrs,
- citation,
- value: value.slice( 1 ),
- } );
-
- return [ heading, quote ];
+ return [
+ createBlock( 'core/heading', {
+ content: value[ 0 ],
+ } ),
+ createBlock( 'core/quote', {
+ ...attrs,
+ citation,
+ value: value.slice( 1 ),
+ } ),
+ ];
},
},
],
@@ -193,10 +153,10 @@ export const settings = {
setAttributes( {
- value: fromRichTextValue( nextValue ),
+ value: nextValue,
} )
}
onMerge={ mergeBlocks }
@@ -209,7 +169,7 @@ export const settings = {
/* translators: the text of the quotation */
placeholder={ __( 'Write quote…' ) }
/>
- { ( ( citation && citation.length > 0 ) || isSelected ) && (
+ { ( ! RichText.isEmpty( citation ) || isSelected ) && (
-
- { citation && citation.length > 0 && }
+
+ { ! RichText.isEmpty( citation ) && }
);
},
@@ -267,8 +227,8 @@ export const settings = {
className={ style === 2 ? 'is-large' : '' }
style={ { textAlign: align ? align : null } }
>
-
- { citation && citation.length > 0 && }
+
+ { ! RichText.isEmpty( citation ) && }
);
},
@@ -295,8 +255,8 @@ export const settings = {
className={ `blocks-quote-style-${ style }` }
style={ { textAlign: align ? align : null } }
>
-
- { citation && citation.length > 0 && }
+
+ { ! RichText.isEmpty( citation ) && }
);
},
diff --git a/core-blocks/subhead/index.js b/core-blocks/subhead/index.js
index 552d91ce47c73e..12f6d6e7c987e9 100644
--- a/core-blocks/subhead/index.js
+++ b/core-blocks/subhead/index.js
@@ -33,8 +33,8 @@ export const settings = {
attributes: {
content: {
- type: 'array',
- source: 'children',
+ type: 'object',
+ source: 'rich-text',
selector: 'p',
},
align: {
diff --git a/core-blocks/table/index.js b/core-blocks/table/index.js
index 41c33d78699d82..cc69c87266aa1f 100644
--- a/core-blocks/table/index.js
+++ b/core-blocks/table/index.js
@@ -65,16 +65,25 @@ export const settings = {
category: 'formatting',
attributes: {
- content: {
+ rows: {
type: 'array',
- source: 'children',
- selector: 'table',
- default: [
-
-
-
- ,
- ],
+ default: [],
+ source: 'query',
+ selector: 'tr',
+ query: {
+ cells: {
+ type: 'array',
+ default: [],
+ source: 'query',
+ selector: 'th,tr',
+ query: {
+ cell: {
+ type: 'object',
+ source: 'rich-text',
+ },
+ },
+ },
+ },
},
hasFixedLayout: {
type: 'boolean',
diff --git a/core-blocks/test/fixtures/core-embed__animoto.json b/core-blocks/test/fixtures/core-embed__animoto.json
index 136fce645d4eeb..f4969f0201363e 100644
--- a/core-blocks/test/fixtures/core-embed__animoto.json
+++ b/core-blocks/test/fixtures/core-embed__animoto.json
@@ -5,9 +5,40 @@
"isValid": true,
"attributes": {
"url": "https://animoto.com/",
- "caption": [
- "Embedded content from animoto"
- ]
+ "caption": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Embedded content from animoto"
+ }
},
"innerBlocks": [],
"originalContent": "\n https://animoto.com/\n Embedded content from animoto \n "
diff --git a/core-blocks/test/fixtures/core-embed__cloudup.json b/core-blocks/test/fixtures/core-embed__cloudup.json
index 88e4a195e1ea49..ec534db4c790d7 100644
--- a/core-blocks/test/fixtures/core-embed__cloudup.json
+++ b/core-blocks/test/fixtures/core-embed__cloudup.json
@@ -5,9 +5,40 @@
"isValid": true,
"attributes": {
"url": "https://cloudup.com/",
- "caption": [
- "Embedded content from cloudup"
- ]
+ "caption": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Embedded content from cloudup"
+ }
},
"innerBlocks": [],
"originalContent": "\n https://cloudup.com/\n Embedded content from cloudup \n "
diff --git a/core-blocks/test/fixtures/core-embed__collegehumor.json b/core-blocks/test/fixtures/core-embed__collegehumor.json
index 86722305b5ab00..59ef71bf35d67e 100644
--- a/core-blocks/test/fixtures/core-embed__collegehumor.json
+++ b/core-blocks/test/fixtures/core-embed__collegehumor.json
@@ -5,9 +5,45 @@
"isValid": true,
"attributes": {
"url": "https://collegehumor.com/",
- "caption": [
- "Embedded content from collegehumor"
- ]
+ "caption": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Embedded content from collegehumor"
+ }
},
"innerBlocks": [],
"originalContent": "\n https://collegehumor.com/\n Embedded content from collegehumor \n "
diff --git a/core-blocks/test/fixtures/core-embed__dailymotion.json b/core-blocks/test/fixtures/core-embed__dailymotion.json
index 317a615c5c78ad..d2272321f8e54f 100644
--- a/core-blocks/test/fixtures/core-embed__dailymotion.json
+++ b/core-blocks/test/fixtures/core-embed__dailymotion.json
@@ -5,9 +5,44 @@
"isValid": true,
"attributes": {
"url": "https://dailymotion.com/",
- "caption": [
- "Embedded content from dailymotion"
- ]
+ "caption": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Embedded content from dailymotion"
+ }
},
"innerBlocks": [],
"originalContent": "\n https://dailymotion.com/\n Embedded content from dailymotion \n "
diff --git a/core-blocks/test/fixtures/core-embed__facebook.json b/core-blocks/test/fixtures/core-embed__facebook.json
index df9b5d5cb6e822..e7df0249668459 100644
--- a/core-blocks/test/fixtures/core-embed__facebook.json
+++ b/core-blocks/test/fixtures/core-embed__facebook.json
@@ -5,9 +5,41 @@
"isValid": true,
"attributes": {
"url": "https://facebook.com/",
- "caption": [
- "Embedded content from facebook"
- ]
+ "caption": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Embedded content from facebook"
+ }
},
"innerBlocks": [],
"originalContent": "\n https://facebook.com/\n Embedded content from facebook \n "
diff --git a/core-blocks/test/fixtures/core-embed__flickr.json b/core-blocks/test/fixtures/core-embed__flickr.json
index 103a3912a7ab4f..47ca065d97ca13 100644
--- a/core-blocks/test/fixtures/core-embed__flickr.json
+++ b/core-blocks/test/fixtures/core-embed__flickr.json
@@ -5,9 +5,39 @@
"isValid": true,
"attributes": {
"url": "https://flickr.com/",
- "caption": [
- "Embedded content from flickr"
- ]
+ "caption": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Embedded content from flickr"
+ }
},
"innerBlocks": [],
"originalContent": "\n https://flickr.com/\n Embedded content from flickr \n "
diff --git a/core-blocks/test/fixtures/core-embed__funnyordie.json b/core-blocks/test/fixtures/core-embed__funnyordie.json
index c8e313cd80e439..39b454c8d1915b 100644
--- a/core-blocks/test/fixtures/core-embed__funnyordie.json
+++ b/core-blocks/test/fixtures/core-embed__funnyordie.json
@@ -5,9 +5,43 @@
"isValid": true,
"attributes": {
"url": "https://funnyordie.com/",
- "caption": [
- "Embedded content from funnyordie"
- ]
+ "caption": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Embedded content from funnyordie"
+ }
},
"innerBlocks": [],
"originalContent": "\n https://funnyordie.com/\n Embedded content from funnyordie \n "
diff --git a/core-blocks/test/fixtures/core-embed__hulu.json b/core-blocks/test/fixtures/core-embed__hulu.json
index b9185cd0f68fb0..43ac673306c7b3 100644
--- a/core-blocks/test/fixtures/core-embed__hulu.json
+++ b/core-blocks/test/fixtures/core-embed__hulu.json
@@ -5,9 +5,37 @@
"isValid": true,
"attributes": {
"url": "https://hulu.com/",
- "caption": [
- "Embedded content from hulu"
- ]
+ "caption": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Embedded content from hulu"
+ }
},
"innerBlocks": [],
"originalContent": "\n https://hulu.com/\n Embedded content from hulu \n "
diff --git a/core-blocks/test/fixtures/core-embed__imgur.json b/core-blocks/test/fixtures/core-embed__imgur.json
index a80705b7011274..be3a53014b0c60 100644
--- a/core-blocks/test/fixtures/core-embed__imgur.json
+++ b/core-blocks/test/fixtures/core-embed__imgur.json
@@ -5,9 +5,38 @@
"isValid": true,
"attributes": {
"url": "https://imgur.com/",
- "caption": [
- "Embedded content from imgur"
- ]
+ "caption": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Embedded content from imgur"
+ }
},
"innerBlocks": [],
"originalContent": "\n https://imgur.com/\n Embedded content from imgur \n "
diff --git a/core-blocks/test/fixtures/core-embed__instagram.json b/core-blocks/test/fixtures/core-embed__instagram.json
index fb19a254a10408..dc94fd3b5a1e8f 100644
--- a/core-blocks/test/fixtures/core-embed__instagram.json
+++ b/core-blocks/test/fixtures/core-embed__instagram.json
@@ -5,9 +5,42 @@
"isValid": true,
"attributes": {
"url": "https://instagram.com/",
- "caption": [
- "Embedded content from instagram"
- ]
+ "caption": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Embedded content from instagram"
+ }
},
"innerBlocks": [],
"originalContent": "\n https://instagram.com/\n Embedded content from instagram \n "
diff --git a/core-blocks/test/fixtures/core-embed__issuu.json b/core-blocks/test/fixtures/core-embed__issuu.json
index 91fc678f576d4d..9955b8ae1e5551 100644
--- a/core-blocks/test/fixtures/core-embed__issuu.json
+++ b/core-blocks/test/fixtures/core-embed__issuu.json
@@ -5,9 +5,38 @@
"isValid": true,
"attributes": {
"url": "https://issuu.com/",
- "caption": [
- "Embedded content from issuu"
- ]
+ "caption": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Embedded content from issuu"
+ }
},
"innerBlocks": [],
"originalContent": "\n https://issuu.com/\n Embedded content from issuu \n "
diff --git a/core-blocks/test/fixtures/core-embed__kickstarter.json b/core-blocks/test/fixtures/core-embed__kickstarter.json
index a0c4f3d932bbd7..964f8eac58e13d 100644
--- a/core-blocks/test/fixtures/core-embed__kickstarter.json
+++ b/core-blocks/test/fixtures/core-embed__kickstarter.json
@@ -5,9 +5,44 @@
"isValid": true,
"attributes": {
"url": "https://kickstarter.com/",
- "caption": [
- "Embedded content from kickstarter"
- ]
+ "caption": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Embedded content from kickstarter"
+ }
},
"innerBlocks": [],
"originalContent": "\n https://kickstarter.com/\n Embedded content from kickstarter \n "
diff --git a/core-blocks/test/fixtures/core-embed__meetup-com.json b/core-blocks/test/fixtures/core-embed__meetup-com.json
index e6e4e10f91e7d5..90aef74884e9f1 100644
--- a/core-blocks/test/fixtures/core-embed__meetup-com.json
+++ b/core-blocks/test/fixtures/core-embed__meetup-com.json
@@ -5,9 +5,43 @@
"isValid": true,
"attributes": {
"url": "https://meetup.com/",
- "caption": [
- "Embedded content from meetup-com"
- ]
+ "caption": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Embedded content from meetup-com"
+ }
},
"innerBlocks": [],
"originalContent": "\n https://meetup.com/\n Embedded content from meetup-com \n "
diff --git a/core-blocks/test/fixtures/core-embed__mixcloud.json b/core-blocks/test/fixtures/core-embed__mixcloud.json
index 6155fa124229f6..c137b0523701c6 100644
--- a/core-blocks/test/fixtures/core-embed__mixcloud.json
+++ b/core-blocks/test/fixtures/core-embed__mixcloud.json
@@ -5,9 +5,41 @@
"isValid": true,
"attributes": {
"url": "https://mixcloud.com/",
- "caption": [
- "Embedded content from mixcloud"
- ]
+ "caption": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Embedded content from mixcloud"
+ }
},
"innerBlocks": [],
"originalContent": "\n https://mixcloud.com/\n Embedded content from mixcloud \n "
diff --git a/core-blocks/test/fixtures/core-embed__photobucket.json b/core-blocks/test/fixtures/core-embed__photobucket.json
index d4a5a6031dab52..4da6c592395292 100644
--- a/core-blocks/test/fixtures/core-embed__photobucket.json
+++ b/core-blocks/test/fixtures/core-embed__photobucket.json
@@ -5,9 +5,44 @@
"isValid": true,
"attributes": {
"url": "https://photobucket.com/",
- "caption": [
- "Embedded content from photobucket"
- ]
+ "caption": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Embedded content from photobucket"
+ }
},
"innerBlocks": [],
"originalContent": "\n https://photobucket.com/\n Embedded content from photobucket \n "
diff --git a/core-blocks/test/fixtures/core-embed__polldaddy.json b/core-blocks/test/fixtures/core-embed__polldaddy.json
index 1b4fd5fe9c9f8a..6daa433d5f62ad 100644
--- a/core-blocks/test/fixtures/core-embed__polldaddy.json
+++ b/core-blocks/test/fixtures/core-embed__polldaddy.json
@@ -5,9 +5,42 @@
"isValid": true,
"attributes": {
"url": "https://polldaddy.com/",
- "caption": [
- "Embedded content from polldaddy"
- ]
+ "caption": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Embedded content from polldaddy"
+ }
},
"innerBlocks": [],
"originalContent": "\n https://polldaddy.com/\n Embedded content from polldaddy \n "
diff --git a/core-blocks/test/fixtures/core-embed__reddit.json b/core-blocks/test/fixtures/core-embed__reddit.json
index d3c0a401484bed..e9c3bacd7bc514 100644
--- a/core-blocks/test/fixtures/core-embed__reddit.json
+++ b/core-blocks/test/fixtures/core-embed__reddit.json
@@ -5,9 +5,39 @@
"isValid": true,
"attributes": {
"url": "https://reddit.com/",
- "caption": [
- "Embedded content from reddit"
- ]
+ "caption": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Embedded content from reddit"
+ }
},
"innerBlocks": [],
"originalContent": "\n https://reddit.com/\n Embedded content from reddit \n "
diff --git a/core-blocks/test/fixtures/core-embed__reverbnation.json b/core-blocks/test/fixtures/core-embed__reverbnation.json
index 4114e247dda08f..070d416c0ea471 100644
--- a/core-blocks/test/fixtures/core-embed__reverbnation.json
+++ b/core-blocks/test/fixtures/core-embed__reverbnation.json
@@ -5,9 +5,45 @@
"isValid": true,
"attributes": {
"url": "https://reverbnation.com/",
- "caption": [
- "Embedded content from reverbnation"
- ]
+ "caption": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Embedded content from reverbnation"
+ }
},
"innerBlocks": [],
"originalContent": "\n https://reverbnation.com/\n Embedded content from reverbnation \n "
diff --git a/core-blocks/test/fixtures/core-embed__screencast.json b/core-blocks/test/fixtures/core-embed__screencast.json
index 9439ecc21cbc19..f09f584e812944 100644
--- a/core-blocks/test/fixtures/core-embed__screencast.json
+++ b/core-blocks/test/fixtures/core-embed__screencast.json
@@ -5,9 +5,43 @@
"isValid": true,
"attributes": {
"url": "https://screencast.com/",
- "caption": [
- "Embedded content from screencast"
- ]
+ "caption": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Embedded content from screencast"
+ }
},
"innerBlocks": [],
"originalContent": "\n https://screencast.com/\n Embedded content from screencast \n "
diff --git a/core-blocks/test/fixtures/core-embed__scribd.json b/core-blocks/test/fixtures/core-embed__scribd.json
index 151487f1d9386c..9f8c4e43f2fa5e 100644
--- a/core-blocks/test/fixtures/core-embed__scribd.json
+++ b/core-blocks/test/fixtures/core-embed__scribd.json
@@ -5,9 +5,39 @@
"isValid": true,
"attributes": {
"url": "https://scribd.com/",
- "caption": [
- "Embedded content from scribd"
- ]
+ "caption": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Embedded content from scribd"
+ }
},
"innerBlocks": [],
"originalContent": "\n https://scribd.com/\n Embedded content from scribd \n "
diff --git a/core-blocks/test/fixtures/core-embed__slideshare.json b/core-blocks/test/fixtures/core-embed__slideshare.json
index 860a12582c5805..0e8a16352aec98 100644
--- a/core-blocks/test/fixtures/core-embed__slideshare.json
+++ b/core-blocks/test/fixtures/core-embed__slideshare.json
@@ -5,9 +5,43 @@
"isValid": true,
"attributes": {
"url": "https://slideshare.com/",
- "caption": [
- "Embedded content from slideshare"
- ]
+ "caption": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Embedded content from slideshare"
+ }
},
"innerBlocks": [],
"originalContent": "\n https://slideshare.com/\n Embedded content from slideshare \n "
diff --git a/core-blocks/test/fixtures/core-embed__smugmug.json b/core-blocks/test/fixtures/core-embed__smugmug.json
index 9b9c253816f2d9..851302abfc2b2c 100644
--- a/core-blocks/test/fixtures/core-embed__smugmug.json
+++ b/core-blocks/test/fixtures/core-embed__smugmug.json
@@ -5,9 +5,40 @@
"isValid": true,
"attributes": {
"url": "https://smugmug.com/",
- "caption": [
- "Embedded content from smugmug"
- ]
+ "caption": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Embedded content from smugmug"
+ }
},
"innerBlocks": [],
"originalContent": "\n https://smugmug.com/\n Embedded content from smugmug \n "
diff --git a/core-blocks/test/fixtures/core-embed__soundcloud.json b/core-blocks/test/fixtures/core-embed__soundcloud.json
index 71917b33b0fcb0..f8e50276da7e99 100644
--- a/core-blocks/test/fixtures/core-embed__soundcloud.json
+++ b/core-blocks/test/fixtures/core-embed__soundcloud.json
@@ -5,9 +5,43 @@
"isValid": true,
"attributes": {
"url": "https://soundcloud.com/",
- "caption": [
- "Embedded content from soundcloud"
- ]
+ "caption": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Embedded content from soundcloud"
+ }
},
"innerBlocks": [],
"originalContent": "\n https://soundcloud.com/\n Embedded content from soundcloud \n "
diff --git a/core-blocks/test/fixtures/core-embed__speakerdeck.json b/core-blocks/test/fixtures/core-embed__speakerdeck.json
new file mode 100644
index 00000000000000..bb7dac08226851
--- /dev/null
+++ b/core-blocks/test/fixtures/core-embed__speakerdeck.json
@@ -0,0 +1,50 @@
+[
+ {
+ "clientId": "_clientId_0",
+ "name": "core-embed/speakerdeck",
+ "isValid": true,
+ "attributes": {
+ "url": "https://speakerdeck.com/",
+ "caption": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Embedded content from speakerdeck"
+ }
+ },
+ "innerBlocks": [],
+ "originalContent": "\n\thttps://speakerdeck.com/\n\tEmbedded content from speakerdeck \n "
+ }
+]
diff --git a/core-blocks/test/fixtures/core-embed__spotify.json b/core-blocks/test/fixtures/core-embed__spotify.json
index 34ae81f8800c7c..11b4e7bec11611 100644
--- a/core-blocks/test/fixtures/core-embed__spotify.json
+++ b/core-blocks/test/fixtures/core-embed__spotify.json
@@ -5,9 +5,40 @@
"isValid": true,
"attributes": {
"url": "https://spotify.com/",
- "caption": [
- "Embedded content from spotify"
- ]
+ "caption": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Embedded content from spotify"
+ }
},
"innerBlocks": [],
"originalContent": "\n https://spotify.com/\n Embedded content from spotify \n "
diff --git a/core-blocks/test/fixtures/core-embed__ted.json b/core-blocks/test/fixtures/core-embed__ted.json
index 71cf962f2299d8..e657d11633eaec 100644
--- a/core-blocks/test/fixtures/core-embed__ted.json
+++ b/core-blocks/test/fixtures/core-embed__ted.json
@@ -5,9 +5,36 @@
"isValid": true,
"attributes": {
"url": "https://ted.com/",
- "caption": [
- "Embedded content from ted"
- ]
+ "caption": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Embedded content from ted"
+ }
},
"innerBlocks": [],
"originalContent": "\n https://ted.com/\n Embedded content from ted \n "
diff --git a/core-blocks/test/fixtures/core-embed__tumblr.json b/core-blocks/test/fixtures/core-embed__tumblr.json
index d9d8ec226b6d7c..c76724b3a59fa4 100644
--- a/core-blocks/test/fixtures/core-embed__tumblr.json
+++ b/core-blocks/test/fixtures/core-embed__tumblr.json
@@ -5,9 +5,39 @@
"isValid": true,
"attributes": {
"url": "https://tumblr.com/",
- "caption": [
- "Embedded content from tumblr"
- ]
+ "caption": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Embedded content from tumblr"
+ }
},
"innerBlocks": [],
"originalContent": "\n https://tumblr.com/\n Embedded content from tumblr \n "
diff --git a/core-blocks/test/fixtures/core-embed__twitter.json b/core-blocks/test/fixtures/core-embed__twitter.json
index 14f254a84f7d16..6ffede453daefb 100644
--- a/core-blocks/test/fixtures/core-embed__twitter.json
+++ b/core-blocks/test/fixtures/core-embed__twitter.json
@@ -5,9 +5,28 @@
"isValid": true,
"attributes": {
"url": "https://twitter.com/automattic",
- "caption": [
- "We are Automattic"
- ]
+ "caption": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "We are Automattic"
+ }
},
"innerBlocks": [],
"originalContent": ""
diff --git a/core-blocks/test/fixtures/core-embed__videopress.json b/core-blocks/test/fixtures/core-embed__videopress.json
index 30a559d9c2057d..6249d066870923 100644
--- a/core-blocks/test/fixtures/core-embed__videopress.json
+++ b/core-blocks/test/fixtures/core-embed__videopress.json
@@ -5,9 +5,43 @@
"isValid": true,
"attributes": {
"url": "https://videopress.com/",
- "caption": [
- "Embedded content from videopress"
- ]
+ "caption": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Embedded content from videopress"
+ }
},
"innerBlocks": [],
"originalContent": "\n https://videopress.com/\n Embedded content from videopress \n "
diff --git a/core-blocks/test/fixtures/core-embed__vimeo.json b/core-blocks/test/fixtures/core-embed__vimeo.json
index a0f2b798e75a3a..d719eb76af9c99 100644
--- a/core-blocks/test/fixtures/core-embed__vimeo.json
+++ b/core-blocks/test/fixtures/core-embed__vimeo.json
@@ -5,9 +5,38 @@
"isValid": true,
"attributes": {
"url": "https://vimeo.com/",
- "caption": [
- "Embedded content from vimeo"
- ]
+ "caption": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Embedded content from vimeo"
+ }
},
"innerBlocks": [],
"originalContent": "\n https://vimeo.com/\n Embedded content from vimeo \n "
diff --git a/core-blocks/test/fixtures/core-embed__wordpress-tv.json b/core-blocks/test/fixtures/core-embed__wordpress-tv.json
index 7c1f6cf966c823..247467cecbe9df 100644
--- a/core-blocks/test/fixtures/core-embed__wordpress-tv.json
+++ b/core-blocks/test/fixtures/core-embed__wordpress-tv.json
@@ -5,9 +5,45 @@
"isValid": true,
"attributes": {
"url": "https://wordpress.tv/",
- "caption": [
- "Embedded content from wordpress-tv"
- ]
+ "caption": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Embedded content from wordpress-tv"
+ }
},
"innerBlocks": [],
"originalContent": "\n https://wordpress.tv/\n Embedded content from wordpress-tv \n "
diff --git a/core-blocks/test/fixtures/core-embed__wordpress.json b/core-blocks/test/fixtures/core-embed__wordpress.json
index 25980b1db72ba6..1fba166d511d00 100644
--- a/core-blocks/test/fixtures/core-embed__wordpress.json
+++ b/core-blocks/test/fixtures/core-embed__wordpress.json
@@ -5,9 +5,42 @@
"isValid": true,
"attributes": {
"url": "https://wordpress.com/",
- "caption": [
- "Embedded content from WordPress"
- ]
+ "caption": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Embedded content from WordPress"
+ }
},
"innerBlocks": [],
"originalContent": "\n https://wordpress.com/\n Embedded content from WordPress \n "
diff --git a/core-blocks/test/fixtures/core-embed__youtube.json b/core-blocks/test/fixtures/core-embed__youtube.json
index c98fb7d333cf2e..a766e1a29561af 100644
--- a/core-blocks/test/fixtures/core-embed__youtube.json
+++ b/core-blocks/test/fixtures/core-embed__youtube.json
@@ -5,9 +5,40 @@
"isValid": true,
"attributes": {
"url": "https://youtube.com/",
- "caption": [
- "Embedded content from youtube"
- ]
+ "caption": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Embedded content from youtube"
+ }
},
"innerBlocks": [],
"originalContent": "\n https://youtube.com/\n Embedded content from youtube \n "
diff --git a/core-blocks/test/fixtures/core__button__center.json b/core-blocks/test/fixtures/core__button__center.json
index 497b74f6601668..4ccdc68fc2090f 100644
--- a/core-blocks/test/fixtures/core__button__center.json
+++ b/core-blocks/test/fixtures/core__button__center.json
@@ -5,9 +5,31 @@
"isValid": true,
"attributes": {
"url": "https://github.com/WordPress/gutenberg",
- "text": [
- "Help build Gutenberg"
- ],
+ "text": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Help build Gutenberg"
+ },
"align": "center"
},
"innerBlocks": [],
diff --git a/core-blocks/test/fixtures/core__column.json b/core-blocks/test/fixtures/core__column.json
index 427423f08b8357..358a0d4e94a75c 100644
--- a/core-blocks/test/fixtures/core__column.json
+++ b/core-blocks/test/fixtures/core__column.json
@@ -10,9 +10,36 @@
"name": "core/paragraph",
"isValid": true,
"attributes": {
- "content": [
- "Column One, Paragraph One"
- ],
+ "content": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Column One, Paragraph One"
+ },
"dropCap": false
},
"innerBlocks": [],
@@ -23,9 +50,36 @@
"name": "core/paragraph",
"isValid": true,
"attributes": {
- "content": [
- "Column One, Paragraph Two"
- ],
+ "content": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Column One, Paragraph Two"
+ },
"dropCap": false
},
"innerBlocks": [],
diff --git a/core-blocks/test/fixtures/core__columns.json b/core-blocks/test/fixtures/core__columns.json
index 5b234e0980bee8..bbadafd319fb80 100644
--- a/core-blocks/test/fixtures/core__columns.json
+++ b/core-blocks/test/fixtures/core__columns.json
@@ -18,9 +18,36 @@
"name": "core/paragraph",
"isValid": true,
"attributes": {
- "content": [
- "Column One, Paragraph One"
- ],
+ "content": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Column One, Paragraph One"
+ },
"dropCap": false
},
"innerBlocks": [],
@@ -31,9 +58,36 @@
"name": "core/paragraph",
"isValid": true,
"attributes": {
- "content": [
- "Column One, Paragraph Two"
- ],
+ "content": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Column One, Paragraph Two"
+ },
"dropCap": false
},
"innerBlocks": [],
@@ -53,9 +107,36 @@
"name": "core/paragraph",
"isValid": true,
"attributes": {
- "content": [
- "Column Two, Paragraph One"
- ],
+ "content": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Column Two, Paragraph One"
+ },
"dropCap": false
},
"innerBlocks": [],
@@ -66,9 +147,38 @@
"name": "core/paragraph",
"isValid": true,
"attributes": {
- "content": [
- "Column Three, Paragraph One"
- ],
+ "content": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Column Three, Paragraph One"
+ },
"dropCap": false
},
"innerBlocks": [],
diff --git a/core-blocks/test/fixtures/core__cover-image.json b/core-blocks/test/fixtures/core__cover-image.json
index d4e5877c330b78..1dfe4234cc147d 100644
--- a/core-blocks/test/fixtures/core__cover-image.json
+++ b/core-blocks/test/fixtures/core__cover-image.json
@@ -4,9 +4,22 @@
"name": "core/cover-image",
"isValid": true,
"attributes": {
- "title": [
- "Guten Berg!"
- ],
+ "title": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Guten Berg!"
+ },
"url": "https://cldup.com/uuUqE_dXzy.jpg",
"contentAlign": "center",
"hasParallax": false,
diff --git a/core-blocks/test/fixtures/core__embed.json b/core-blocks/test/fixtures/core__embed.json
index ec2cbd732a343f..12d865ffab939d 100644
--- a/core-blocks/test/fixtures/core__embed.json
+++ b/core-blocks/test/fixtures/core__embed.json
@@ -5,9 +5,47 @@
"isValid": true,
"attributes": {
"url": "https://example.com/",
- "caption": [
- "Embedded content from an example URL"
- ]
+ "caption": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Embedded content from an example URL"
+ }
},
"innerBlocks": [],
"originalContent": "\n https://example.com/\n Embedded content from an example URL \n "
diff --git a/core-blocks/test/fixtures/core__gallery.json b/core-blocks/test/fixtures/core__gallery.json
index acc930adeb1cc3..2dcebc80a03af8 100644
--- a/core-blocks/test/fixtures/core__gallery.json
+++ b/core-blocks/test/fixtures/core__gallery.json
@@ -8,12 +8,18 @@
{
"url": "https://cldup.com/uuUqE_dXzy.jpg",
"alt": "title",
- "caption": []
+ "caption": {
+ "formats": [],
+ "text": ""
+ }
},
{
"url": "http://google.com/hi.png",
"alt": "title",
- "caption": []
+ "caption": {
+ "formats": [],
+ "text": ""
+ }
}
],
"imageCrop": true,
diff --git a/core-blocks/test/fixtures/core__gallery__columns.json b/core-blocks/test/fixtures/core__gallery__columns.json
index 09ae7dd3b138c8..a9d27c457cb01d 100644
--- a/core-blocks/test/fixtures/core__gallery__columns.json
+++ b/core-blocks/test/fixtures/core__gallery__columns.json
@@ -8,12 +8,18 @@
{
"url": "https://cldup.com/uuUqE_dXzy.jpg",
"alt": "title",
- "caption": []
+ "caption": {
+ "formats": [],
+ "text": ""
+ }
},
{
"url": "http://google.com/hi.png",
"alt": "title",
- "caption": []
+ "caption": {
+ "formats": [],
+ "text": ""
+ }
}
],
"columns": 1,
diff --git a/core-blocks/test/fixtures/core__heading__h2-em.json b/core-blocks/test/fixtures/core__heading__h2-em.json
index 1635da5f8efbb1..786643107f39ec 100644
--- a/core-blocks/test/fixtures/core__heading__h2-em.json
+++ b/core-blocks/test/fixtures/core__heading__h2-em.json
@@ -4,16 +4,60 @@
"name": "core/heading",
"isValid": true,
"attributes": {
- "content": [
- "The ",
- {
- "type": "em",
- "children": [
- "Inserter"
- ]
- },
- " Tool"
- ],
+ "content": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ [
+ {
+ "type": "em"
+ }
+ ],
+ [
+ {
+ "type": "em"
+ }
+ ],
+ [
+ {
+ "type": "em"
+ }
+ ],
+ [
+ {
+ "type": "em"
+ }
+ ],
+ [
+ {
+ "type": "em"
+ }
+ ],
+ [
+ {
+ "type": "em"
+ }
+ ],
+ [
+ {
+ "type": "em"
+ }
+ ],
+ [
+ {
+ "type": "em"
+ }
+ ],
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "The Inserter Tool"
+ },
"level": 2
},
"innerBlocks": [],
diff --git a/core-blocks/test/fixtures/core__heading__h2.json b/core-blocks/test/fixtures/core__heading__h2.json
index f50eb3b39cd55f..388f98214362da 100644
--- a/core-blocks/test/fixtures/core__heading__h2.json
+++ b/core-blocks/test/fixtures/core__heading__h2.json
@@ -4,9 +4,69 @@
"name": "core/heading",
"isValid": true,
"attributes": {
- "content": [
- "A picture is worth a thousand words, or so the saying goes"
- ],
+ "content": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "A picture is worth a thousand words, or so the saying goes"
+ },
"level": 2
},
"innerBlocks": [],
diff --git a/core-blocks/test/fixtures/core__image.json b/core-blocks/test/fixtures/core__image.json
index 159ec45e1a4ca6..dd89c78069c8eb 100644
--- a/core-blocks/test/fixtures/core__image.json
+++ b/core-blocks/test/fixtures/core__image.json
@@ -6,7 +6,10 @@
"attributes": {
"url": "https://cldup.com/uuUqE_dXzy.jpg",
"alt": "",
- "caption": [],
+ "caption": {
+ "formats": [],
+ "text": ""
+ },
"linkDestination": "none"
},
"innerBlocks": [],
diff --git a/core-blocks/test/fixtures/core__image__attachment-link.json b/core-blocks/test/fixtures/core__image__attachment-link.json
index 927d2e6699428f..3aaa94408659ad 100644
--- a/core-blocks/test/fixtures/core__image__attachment-link.json
+++ b/core-blocks/test/fixtures/core__image__attachment-link.json
@@ -6,7 +6,10 @@
"attributes": {
"url": "https://cldup.com/uuUqE_dXzy.jpg",
"alt": "",
- "caption": [],
+ "caption": {
+ "formats": [],
+ "text": ""
+ },
"href": "http://localhost:8888/?attachment_id=7",
"linkDestination": "attachment"
},
diff --git a/core-blocks/test/fixtures/core__image__center-caption.json b/core-blocks/test/fixtures/core__image__center-caption.json
index c9b19750b1868f..b1eabe566a8af3 100644
--- a/core-blocks/test/fixtures/core__image__center-caption.json
+++ b/core-blocks/test/fixtures/core__image__center-caption.json
@@ -6,9 +6,78 @@
"attributes": {
"url": "https://cldup.com/YLYhpou2oq.jpg",
"alt": "",
- "caption": [
- "Give it a try. Press the \"really wide\" button on the image toolbar."
- ],
+ "caption": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Give it a try. Press the \"really wide\" button on the image toolbar."
+ },
"align": "center",
"linkDestination": "none"
},
diff --git a/core-blocks/test/fixtures/core__image__custom-link.json b/core-blocks/test/fixtures/core__image__custom-link.json
index 55e604712ac01e..136ae7357e6803 100644
--- a/core-blocks/test/fixtures/core__image__custom-link.json
+++ b/core-blocks/test/fixtures/core__image__custom-link.json
@@ -6,7 +6,10 @@
"attributes": {
"url": "https://cldup.com/uuUqE_dXzy.jpg",
"alt": "",
- "caption": [],
+ "caption": {
+ "formats": [],
+ "text": ""
+ },
"href": "https://wordpress.org/",
"linkDestination": "custom"
},
diff --git a/core-blocks/test/fixtures/core__image__media-link.json b/core-blocks/test/fixtures/core__image__media-link.json
index 7e10bde887a5f5..6b329e94a48378 100644
--- a/core-blocks/test/fixtures/core__image__media-link.json
+++ b/core-blocks/test/fixtures/core__image__media-link.json
@@ -6,7 +6,10 @@
"attributes": {
"url": "https://cldup.com/uuUqE_dXzy.jpg",
"alt": "",
- "caption": [],
+ "caption": {
+ "formats": [],
+ "text": ""
+ },
"href": "https://cldup.com/uuUqE_dXzy.jpg",
"linkDestination": "media"
},
diff --git a/core-blocks/test/fixtures/core__list__ul.json b/core-blocks/test/fixtures/core__list__ul.json
index 6d2ba10a4cb336..c5d7e526b7f011 100644
--- a/core-blocks/test/fixtures/core__list__ul.json
+++ b/core-blocks/test/fixtures/core__list__ul.json
@@ -7,47 +7,242 @@
"ordered": false,
"values": [
{
- "type": "li",
- "children": [
- "Text & Headings"
- ]
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Text & Headings"
},
{
- "type": "li",
- "children": [
- "Images & Videos"
- ]
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Images & Videos"
},
{
- "type": "li",
- "children": [
- "Galleries"
- ]
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Galleries"
},
{
- "type": "li",
- "children": [
- "Embeds, like YouTube, Tweets, or other WordPress posts."
- ]
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Embeds, like YouTube, Tweets, or other WordPress posts."
},
{
- "type": "li",
- "children": [
- "Layout blocks, like Buttons, Hero Images, Separators, etc."
- ]
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Layout blocks, like Buttons, Hero Images, Separators, etc."
},
{
- "type": "li",
- "children": [
- "And ",
- {
- "type": "em",
- "children": [
- "Lists"
- ]
- },
- " like this one of course :)"
- ]
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ [
+ {
+ "type": "em"
+ }
+ ],
+ [
+ {
+ "type": "em"
+ }
+ ],
+ [
+ {
+ "type": "em"
+ }
+ ],
+ [
+ {
+ "type": "em"
+ }
+ ],
+ [
+ {
+ "type": "em"
+ }
+ ],
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "And Lists like this one of course :)"
}
]
},
diff --git a/core-blocks/test/fixtures/core__paragraph__align-right.json b/core-blocks/test/fixtures/core__paragraph__align-right.json
index 9d33128ef3af0e..cc4e7274557bde 100644
--- a/core-blocks/test/fixtures/core__paragraph__align-right.json
+++ b/core-blocks/test/fixtures/core__paragraph__align-right.json
@@ -4,9 +4,81 @@
"name": "core/paragraph",
"isValid": true,
"attributes": {
- "content": [
- "... like this one, which is separate from the above and right aligned."
- ],
+ "content": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "... like this one, which is separate from the above and right aligned."
+ },
"align": "right",
"dropCap": false
},
diff --git a/core-blocks/test/fixtures/core__preformatted.json b/core-blocks/test/fixtures/core__preformatted.json
index 6bf8d032b3e40f..5c7db59e09b97c 100644
--- a/core-blocks/test/fixtures/core__preformatted.json
+++ b/core-blocks/test/fixtures/core__preformatted.json
@@ -4,21 +4,94 @@
"name": "core/preformatted",
"isValid": true,
"attributes": {
- "content": [
- "Some ",
- {
- "type": "em",
- "children": [
- "preformatted"
- ]
- },
- " text...",
- {
- "type": "br",
- "children": []
- },
- "And more!"
- ]
+ "content": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ [
+ {
+ "type": "em"
+ }
+ ],
+ [
+ {
+ "type": "em"
+ }
+ ],
+ [
+ {
+ "type": "em"
+ }
+ ],
+ [
+ {
+ "type": "em"
+ }
+ ],
+ [
+ {
+ "type": "em"
+ }
+ ],
+ [
+ {
+ "type": "em"
+ }
+ ],
+ [
+ {
+ "type": "em"
+ }
+ ],
+ [
+ {
+ "type": "em"
+ }
+ ],
+ [
+ {
+ "type": "em"
+ }
+ ],
+ [
+ {
+ "type": "em"
+ }
+ ],
+ [
+ {
+ "type": "em"
+ }
+ ],
+ [
+ {
+ "type": "em"
+ }
+ ],
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Some preformatted text...\nAnd more!"
+ }
},
"innerBlocks": [],
"originalContent": "Some preformatted text... And more! "
diff --git a/core-blocks/test/fixtures/core__preformatted.serialized.html b/core-blocks/test/fixtures/core__preformatted.serialized.html
index 86bf1df537b252..8c7cf539e4ea2e 100644
--- a/core-blocks/test/fixtures/core__preformatted.serialized.html
+++ b/core-blocks/test/fixtures/core__preformatted.serialized.html
@@ -1,3 +1,3 @@
-Some preformatted text... And more!
+Some preformatted text... And more!
diff --git a/core-blocks/test/fixtures/core__pullquote.json b/core-blocks/test/fixtures/core__pullquote.json
index 11d576614228cd..54317c84e64744 100644
--- a/core-blocks/test/fixtures/core__pullquote.json
+++ b/core-blocks/test/fixtures/core__pullquote.json
@@ -6,19 +6,59 @@
"attributes": {
"value": [
{
- "children": {
- "type": "p",
- "props": {
- "children": [
- "Testing pullquote block..."
- ]
- }
- }
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Testing pullquote block..."
}
],
- "citation": [
- "...with a caption"
- ]
+ "citation": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "...with a caption"
+ }
},
"innerBlocks": [],
"originalContent": "\nTesting pullquote block...
...with a caption \n "
diff --git a/core-blocks/test/fixtures/core__pullquote__multi-paragraph.json b/core-blocks/test/fixtures/core__pullquote__multi-paragraph.json
index 266568fa51bc00..03d7d8bd9bd442 100644
--- a/core-blocks/test/fixtures/core__pullquote__multi-paragraph.json
+++ b/core-blocks/test/fixtures/core__pullquote__multi-paragraph.json
@@ -6,37 +6,70 @@
"attributes": {
"value": [
{
- "children": {
- "type": "p",
- "props": {
- "children": [
- "Paragraph ",
- {
- "type": "strong",
- "props": {
- "children": [
- "one"
- ]
- }
- }
- ]
- }
- }
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ [
+ {
+ "type": "strong"
+ }
+ ],
+ [
+ {
+ "type": "strong"
+ }
+ ],
+ [
+ {
+ "type": "strong"
+ }
+ ]
+ ],
+ "text": "Paragraph one"
},
{
- "children": {
- "type": "p",
- "props": {
- "children": [
- "Paragraph two"
- ]
- }
- }
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Paragraph two"
}
],
- "citation": [
- "by whomever"
- ]
+ "citation": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "by whomever"
+ }
},
"innerBlocks": [],
"originalContent": "\n Paragraph one
\n Paragraph two
\n by whomever \n "
diff --git a/core-blocks/test/fixtures/core__quote__style-1.json b/core-blocks/test/fixtures/core__quote__style-1.json
index 67722f404cd0ae..71e500dae91bcc 100644
--- a/core-blocks/test/fixtures/core__quote__style-1.json
+++ b/core-blocks/test/fixtures/core__quote__style-1.json
@@ -6,19 +6,268 @@
"attributes": {
"value": [
{
- "children": {
- "type": "p",
- "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."
- ]
- }
- }
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "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."
}
],
- "citation": [
- "Matt Mullenweg, 2017"
- ]
+ "citation": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Matt Mullenweg, 2017"
+ }
},
"innerBlocks": [],
"originalContent": "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.
Matt Mullenweg, 2017 "
diff --git a/core-blocks/test/fixtures/core__quote__style-2.json b/core-blocks/test/fixtures/core__quote__style-2.json
index ebf72ece861815..017e4d20ae88b1 100644
--- a/core-blocks/test/fixtures/core__quote__style-2.json
+++ b/core-blocks/test/fixtures/core__quote__style-2.json
@@ -6,19 +6,94 @@
"attributes": {
"value": [
{
- "children": {
- "type": "p",
- "props": {
- "children": [
- "There is no greater agony than bearing an untold story inside you."
- ]
- }
- }
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "There is no greater agony than bearing an untold story inside you."
}
],
- "citation": [
- "Maya Angelou"
- ],
+ "citation": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "Maya Angelou"
+ },
"className": "is-style-large"
},
"innerBlocks": [],
diff --git a/core-blocks/test/fixtures/core__subhead.json b/core-blocks/test/fixtures/core__subhead.json
index 64d8bb2f030c7e..291374ad1a6c1d 100644
--- a/core-blocks/test/fixtures/core__subhead.json
+++ b/core-blocks/test/fixtures/core__subhead.json
@@ -4,16 +4,57 @@
"name": "core/subhead",
"isValid": true,
"attributes": {
- "content": [
- "This is a ",
- {
- "type": "em",
- "children": [
- "subhead"
- ]
- },
- "."
- ]
+ "content": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ [
+ {
+ "type": "em"
+ }
+ ],
+ [
+ {
+ "type": "em"
+ }
+ ],
+ [
+ {
+ "type": "em"
+ }
+ ],
+ [
+ {
+ "type": "em"
+ }
+ ],
+ [
+ {
+ "type": "em"
+ }
+ ],
+ [
+ {
+ "type": "em"
+ }
+ ],
+ [
+ {
+ "type": "em"
+ }
+ ],
+ null
+ ],
+ "text": "This is a subhead."
+ }
},
"innerBlocks": [],
"originalContent": "This is a subhead .
"
diff --git a/core-blocks/test/fixtures/core__table.html b/core-blocks/test/fixtures/core__table.html
index 632c38d213f740..b9b41659e835aa 100644
--- a/core-blocks/test/fixtures/core__table.html
+++ b/core-blocks/test/fixtures/core__table.html
@@ -1,4 +1,3 @@
Version Musician Date .70 No musician chosen. May 27, 2003 1.0 Miles Davis January 3, 2004 Lots of versions skipped, see the full list … … 4.4 Clifford Brown December 8, 2015 4.5 Coleman Hawkins April 12, 2016 4.6 Pepper Adams August 16, 2016 4.7 Sarah Vaughan December 6, 2016
-
diff --git a/core-blocks/test/fixtures/core__table.json b/core-blocks/test/fixtures/core__table.json
deleted file mode 100644
index f6a6c42c9c5453..00000000000000
--- a/core-blocks/test/fixtures/core__table.json
+++ /dev/null
@@ -1,265 +0,0 @@
-[
- {
- "clientId": "_clientId_0",
- "name": "core/table",
- "isValid": true,
- "attributes": {
- "content": [
- {
- "type": "thead",
- "children": [
- {
- "type": "tr",
- "children": [
- {
- "type": "th",
- "children": [
- "Version"
- ]
- },
- {
- "type": "th",
- "children": [
- "Musician"
- ]
- },
- {
- "type": "th",
- "children": [
- "Date"
- ]
- }
- ]
- }
- ]
- },
- {
- "type": "tbody",
- "children": [
- {
- "type": "tr",
- "children": [
- {
- "type": "td",
- "children": [
- {
- "type": "a",
- "attributes": {
- "href": "https://wordpress.org/news/2003/05/wordpress-now-available/"
- },
- "children": [
- ".70"
- ]
- }
- ]
- },
- {
- "type": "td",
- "children": [
- "No musician chosen."
- ]
- },
- {
- "type": "td",
- "children": [
- "May 27, 2003"
- ]
- }
- ]
- },
- {
- "type": "tr",
- "children": [
- {
- "type": "td",
- "children": [
- {
- "type": "a",
- "attributes": {
- "href": "https://wordpress.org/news/2004/01/wordpress-10/"
- },
- "children": [
- "1.0"
- ]
- }
- ]
- },
- {
- "type": "td",
- "children": [
- "Miles Davis"
- ]
- },
- {
- "type": "td",
- "children": [
- "January 3, 2004"
- ]
- }
- ]
- },
- {
- "type": "tr",
- "children": [
- {
- "type": "td",
- "children": [
- "Lots of versions skipped, see ",
- {
- "type": "a",
- "attributes": {
- "href": "https://codex.wordpress.org/WordPress_Versions"
- },
- "children": [
- "the full list"
- ]
- }
- ]
- },
- {
- "type": "td",
- "children": [
- "…"
- ]
- },
- {
- "type": "td",
- "children": [
- "…"
- ]
- }
- ]
- },
- {
- "type": "tr",
- "children": [
- {
- "type": "td",
- "children": [
- {
- "type": "a",
- "attributes": {
- "href": "https://wordpress.org/news/2015/12/clifford/"
- },
- "children": [
- "4.4"
- ]
- }
- ]
- },
- {
- "type": "td",
- "children": [
- "Clifford Brown"
- ]
- },
- {
- "type": "td",
- "children": [
- "December 8, 2015"
- ]
- }
- ]
- },
- {
- "type": "tr",
- "children": [
- {
- "type": "td",
- "children": [
- {
- "type": "a",
- "attributes": {
- "href": "https://wordpress.org/news/2016/04/coleman/"
- },
- "children": [
- "4.5"
- ]
- }
- ]
- },
- {
- "type": "td",
- "children": [
- "Coleman Hawkins"
- ]
- },
- {
- "type": "td",
- "children": [
- "April 12, 2016"
- ]
- }
- ]
- },
- {
- "type": "tr",
- "children": [
- {
- "type": "td",
- "children": [
- {
- "type": "a",
- "attributes": {
- "href": "https://wordpress.org/news/2016/08/pepper/"
- },
- "children": [
- "4.6"
- ]
- }
- ]
- },
- {
- "type": "td",
- "children": [
- "Pepper Adams"
- ]
- },
- {
- "type": "td",
- "children": [
- "August 16, 2016"
- ]
- }
- ]
- },
- {
- "type": "tr",
- "children": [
- {
- "type": "td",
- "children": [
- {
- "type": "a",
- "attributes": {
- "href": "https://wordpress.org/news/2016/12/vaughan/"
- },
- "children": [
- "4.7"
- ]
- }
- ]
- },
- {
- "type": "td",
- "children": [
- "Sarah Vaughan"
- ]
- },
- {
- "type": "td",
- "children": [
- "December 6, 2016"
- ]
- }
- ]
- }
- ]
- }
- ],
- "hasFixedLayout": false
- },
- "innerBlocks": [],
- "originalContent": "Version Musician Date .70 No musician chosen. May 27, 2003 1.0 Miles Davis January 3, 2004 Lots of versions skipped, see the full list … … 4.4 Clifford Brown December 8, 2015 4.5 Coleman Hawkins April 12, 2016 4.6 Pepper Adams August 16, 2016 4.7 Sarah Vaughan December 6, 2016
"
- }
-]
diff --git a/core-blocks/test/fixtures/core__table.parsed.json b/core-blocks/test/fixtures/core__table.parsed.json
index 0051d503b8d629..737ac5ea49098c 100644
--- a/core-blocks/test/fixtures/core__table.parsed.json
+++ b/core-blocks/test/fixtures/core__table.parsed.json
@@ -7,6 +7,6 @@
},
{
"attrs": {},
- "innerHTML": "\n\n"
+ "innerHTML": "\n"
}
]
diff --git a/core-blocks/test/fixtures/core__table.serialized.html b/core-blocks/test/fixtures/core__table.serialized.html
deleted file mode 100644
index a791eb149f6273..00000000000000
--- a/core-blocks/test/fixtures/core__table.serialized.html
+++ /dev/null
@@ -1,3 +0,0 @@
-
-Version Musician Date .70 No musician chosen. May 27, 2003 1.0 Miles Davis January 3, 2004 Lots of versions skipped, see the full list … … 4.4 Clifford Brown December 8, 2015 4.5 Coleman Hawkins April 12, 2016 4.6 Pepper Adams August 16, 2016 4.7 Sarah Vaughan December 6, 2016
-
diff --git a/core-blocks/test/fixtures/core__text-columns.json b/core-blocks/test/fixtures/core__text-columns.json
index a1f531dd8fe3c4..2a31ae6ff865fa 100644
--- a/core-blocks/test/fixtures/core__text-columns.json
+++ b/core-blocks/test/fixtures/core__text-columns.json
@@ -6,14 +6,24 @@
"attributes": {
"content": [
{
- "children": [
- "One"
- ]
+ "children": {
+ "formats": [
+ null,
+ null,
+ null
+ ],
+ "text": "One"
+ }
},
{
- "children": [
- "Two"
- ]
+ "children": {
+ "formats": [
+ null,
+ null,
+ null
+ ],
+ "text": "Two"
+ }
}
],
"columns": 2,
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 10066958f0b3bb..951cc3900f3c13 100644
--- a/core-blocks/test/fixtures/core__text__converts-to-paragraph.json
+++ b/core-blocks/test/fixtures/core__text__converts-to-paragraph.json
@@ -4,16 +4,111 @@
"name": "core/paragraph",
"isValid": true,
"attributes": {
- "content": [
- "This is an old-style text block. Changed to ",
- {
- "type": "code",
- "children": [
- "paragraph"
- ]
- },
- " in #2135."
- ],
+ "content": {
+ "formats": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ [
+ {
+ "type": "code"
+ }
+ ],
+ [
+ {
+ "type": "code"
+ }
+ ],
+ [
+ {
+ "type": "code"
+ }
+ ],
+ [
+ {
+ "type": "code"
+ }
+ ],
+ [
+ {
+ "type": "code"
+ }
+ ],
+ [
+ {
+ "type": "code"
+ }
+ ],
+ [
+ {
+ "type": "code"
+ }
+ ],
+ [
+ {
+ "type": "code"
+ }
+ ],
+ [
+ {
+ "type": "code"
+ }
+ ],
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "This is an old-style text block. Changed to paragraph in #2135."
+ },
"dropCap": false
},
"innerBlocks": [],
diff --git a/core-blocks/test/fixtures/core__verse.json b/core-blocks/test/fixtures/core__verse.json
index 4412cb039d8490..e7d92ea856bff2 100644
--- a/core-blocks/test/fixtures/core__verse.json
+++ b/core-blocks/test/fixtures/core__verse.json
@@ -4,21 +4,49 @@
"name": "core/verse",
"isValid": true,
"attributes": {
- "content": [
- "A ",
- {
- "type": "em",
- "children": [
- "verse"
- ]
- },
- "…",
- {
- "type": "br",
- "children": []
- },
- "And more!"
- ]
+ "content": {
+ "formats": [
+ null,
+ null,
+ [
+ {
+ "type": "em"
+ }
+ ],
+ [
+ {
+ "type": "em"
+ }
+ ],
+ [
+ {
+ "type": "em"
+ }
+ ],
+ [
+ {
+ "type": "em"
+ }
+ ],
+ [
+ {
+ "type": "em"
+ }
+ ],
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "text": "A verse…\nAnd more!"
+ }
},
"innerBlocks": [],
"originalContent": "A verse … And more! "
diff --git a/core-blocks/test/fixtures/core__verse.serialized.html b/core-blocks/test/fixtures/core__verse.serialized.html
index ff4983491f13db..a601b259912a8e 100644
--- a/core-blocks/test/fixtures/core__verse.serialized.html
+++ b/core-blocks/test/fixtures/core__verse.serialized.html
@@ -1,3 +1,3 @@
-A verse … And more!
+A verse … And more!
diff --git a/core-blocks/test/fixtures/core__video.json b/core-blocks/test/fixtures/core__video.json
index 7992d3b94fb3e2..b86cde85794245 100644
--- a/core-blocks/test/fixtures/core__video.json
+++ b/core-blocks/test/fixtures/core__video.json
@@ -5,7 +5,10 @@
"isValid": true,
"attributes": {
"autoplay": false,
- "caption": [],
+ "caption": {
+ "formats": [],
+ "text": ""
+ },
"controls": true,
"loop": false,
"muted": false,
diff --git a/core-blocks/test/full-content.js b/core-blocks/test/full-content.js
index f1b79b1007c528..73e49bfd126dd3 100644
--- a/core-blocks/test/full-content.js
+++ b/core-blocks/test/full-content.js
@@ -3,7 +3,7 @@
*/
import fs from 'fs';
import path from 'path';
-import { uniq, isObject, omit, startsWith, get } from 'lodash';
+import { uniq, startsWith, get } from 'lodash';
import { format } from 'util';
/**
@@ -54,31 +54,6 @@ function writeFixtureFile( filename, content ) {
);
}
-function normalizeReactTree( element ) {
- if ( Array.isArray( element ) ) {
- return element.map( ( child ) => normalizeReactTree( child ) );
- }
-
- // Check if we got an object first, then if it actually has a `type` like a
- // React component. Sometimes we get other stuff here, which probably
- // indicates a bug.
- if ( isObject( element ) && element.type && element.props ) {
- const toReturn = {
- type: element.type,
- };
- const attributes = omit( element.props, 'children' );
- if ( Object.keys( attributes ).length ) {
- toReturn.attributes = attributes;
- }
- if ( element.props.children ) {
- toReturn.children = normalizeReactTree( element.props.children );
- }
- return toReturn;
- }
-
- return element;
-}
-
function normalizeParsedBlocks( blocks ) {
return blocks.map( ( block, index ) => {
// Clone and remove React-instance-specific stuff; also, attribute
@@ -90,12 +65,6 @@ function normalizeParsedBlocks( blocks ) {
// TODO: Remove in 3.5 "UID" deprecation.
delete block.uid;
- // Walk each attribute and get a more concise representation of any
- // React elements
- for ( const k in block.attributes ) {
- block.attributes[ k ] = normalizeReactTree( block.attributes[ k ] );
- }
-
// Recurse to normalize inner blocks
block.innerBlocks = normalizeParsedBlocks( block.innerBlocks );
diff --git a/core-blocks/text-columns/index.js b/core-blocks/text-columns/index.js
index fc8245daa798e7..103d7f51121be5 100644
--- a/core-blocks/text-columns/index.js
+++ b/core-blocks/text-columns/index.js
@@ -46,10 +46,10 @@ export const settings = {
selector: 'p',
query: {
children: {
- source: 'children',
+ source: 'rich-text',
},
},
- default: [ [], [] ],
+ default: [ {}, {} ],
},
columns: {
type: 'number',
diff --git a/core-blocks/verse/index.js b/core-blocks/verse/index.js
index 810dae1d1249c2..974fd4b1eecec5 100644
--- a/core-blocks/verse/index.js
+++ b/core-blocks/verse/index.js
@@ -30,8 +30,8 @@ export const settings = {
attributes: {
content: {
- type: 'array',
- source: 'children',
+ type: 'object',
+ source: 'rich-text',
selector: 'pre',
},
textAlign: {
diff --git a/core-blocks/video/edit.js b/core-blocks/video/edit.js
index 19dd60c226fa31..4419570b50a7d1 100644
--- a/core-blocks/video/edit.js
+++ b/core-blocks/video/edit.js
@@ -157,7 +157,7 @@ class VideoEdit extends Component {
- { ( ( caption && caption.length ) || !! isSelected ) && (
+ { ( ! RichText.isEmpty( caption ) || isSelected ) && (
) }
- { caption && caption.length > 0 && (
+ { ! RichText.isEmpty( caption ) && (
) }
diff --git a/packages/blocks/src/api/children.js b/packages/blocks/src/api/children.js
deleted file mode 100644
index 3fde2804fb4ed7..00000000000000
--- a/packages/blocks/src/api/children.js
+++ /dev/null
@@ -1,132 +0,0 @@
-/**
- * External dependencies
- */
-import { castArray } from 'lodash';
-
-/**
- * WordPress dependencies
- */
-import { renderToString } from '@wordpress/element';
-
-/**
- * Internal dependencies
- */
-import * as node from './node';
-
-/**
- * A representation of a block's rich text value.
- *
- * @typedef {WPBlockChild[]} WPBlockChildren
- */
-
-/**
- * Given a block node, returns a serialize-capable WordPress element.
- *
- * @param {WPBlockChildren} children Block node to convert.
- *
- * @return {WPElement} A serialize-capable element.
- */
-export function getSerializeCapableElement( children ) {
- // The fact that a block node is compatible with the element serializer is
- // merely an implementation detail that currently serves to be true, but
- // should not be mistaken as being a guarantee on the external API. The
- // public API only offers guarantees to work with strings (toHTML) and DOM
- // elements (fromDOM), and should provide utilities to manipulate the value
- // rather than expect consumers to inspect or construct its shape (concat).
- return children;
-}
-
-/**
- * Given two or more block nodes, returns a new block node representing a
- * concatenation of its values.
- *
- * @param {...WPBlockChildren} blockNodes Block nodes to concatenate.
- *
- * @return {WPBlockChildren} Concatenated block node.
- */
-export function concat( ...blockNodes ) {
- const result = [];
- for ( let i = 0; i < blockNodes.length; i++ ) {
- const blockNode = castArray( blockNodes[ i ] );
- for ( let j = 0; j < blockNode.length; j++ ) {
- const child = blockNode[ j ];
- const canConcatToPreviousString = (
- typeof child === 'string' &&
- typeof result[ result.length - 1 ] === 'string'
- );
-
- if ( canConcatToPreviousString ) {
- result[ result.length - 1 ] += child;
- } else {
- result.push( child );
- }
- }
- }
-
- return result;
-}
-
-/**
- * Given an iterable set of DOM nodes, returns equivalent block children.
- * Ignores any non-element/text nodes included in set.
- *
- * @param {Iterable.} domNodes Iterable set of DOM nodes to convert.
- *
- * @return {WPBlockChildren} Block children equivalent to DOM nodes.
- */
-export function fromDOM( domNodes ) {
- const result = [];
- for ( let i = 0; i < domNodes.length; i++ ) {
- try {
- result.push( node.fromDOM( domNodes[ i ] ) );
- } catch ( error ) {
- // Simply ignore if DOM node could not be converted.
- }
- }
-
- return result;
-}
-
-/**
- * Given a block node, returns its HTML string representation.
- *
- * @param {WPBlockChildren} children Block node(s) to convert to string.
- *
- * @return {string} String HTML representation of block node.
- */
-export function toHTML( children ) {
- const element = getSerializeCapableElement( children );
-
- return renderToString( element );
-}
-
-/**
- * Given a selector, returns an hpq matcher generating a WPBlockChildren value
- * matching the selector result.
- *
- * @param {string} selector DOM selector.
- *
- * @return {Function} hpq matcher.
- */
-export function matcher( selector ) {
- return ( domNode ) => {
- let match = domNode;
-
- if ( selector ) {
- match = domNode.querySelector( selector );
- }
-
- if ( match ) {
- return fromDOM( match.childNodes );
- }
-
- return [];
- };
-}
-
-export default {
- concat,
- fromDOM,
- toHTML,
- matcher,
-};
diff --git a/packages/blocks/src/api/factory.js b/packages/blocks/src/api/factory.js
index ef9de61959bd58..2abc5293feced6 100644
--- a/packages/blocks/src/api/factory.js
+++ b/packages/blocks/src/api/factory.js
@@ -25,6 +25,7 @@ import { createHooks, applyFilters } from '@wordpress/hooks';
* Internal dependencies
*/
import { getBlockType, getBlockTypes } from './registration';
+import { create } from './rich-text-structure';
/**
* Returns a block object given its type and attributes.
@@ -41,12 +42,14 @@ export function createBlock( name, blockAttributes = {}, innerBlocks = [] ) {
// Ensure attributes contains only values defined by block type, and merge
// default values for missing attributes.
- const attributes = reduce( blockType.attributes, ( result, source, key ) => {
+ const attributes = reduce( blockType.attributes, ( result, schema, key ) => {
const value = blockAttributes[ key ];
if ( undefined !== value ) {
result[ key ] = value;
- } else if ( source.hasOwnProperty( 'default' ) ) {
- result[ key ] = source.default;
+ } else if ( schema.source === 'rich-text' ) {
+ result[ key ] = create( null, schema.multiline );
+ } else if ( schema.hasOwnProperty( 'default' ) ) {
+ result[ key ] = schema.default;
}
return result;
diff --git a/packages/blocks/src/api/index.js b/packages/blocks/src/api/index.js
index 4b77d4a00b7883..95bb39f7f5b1d7 100644
--- a/packages/blocks/src/api/index.js
+++ b/packages/blocks/src/api/index.js
@@ -1,3 +1,5 @@
+import * as richTextStructure from './rich-text-structure';
+
export {
createBlock,
cloneBlock,
@@ -53,5 +55,4 @@ export {
doBlocksMatchTemplate,
synchronizeBlocksWithTemplate,
} from './templates';
-export { default as children } from './children';
-export { default as node } from './node';
+export { richTextStructure };
diff --git a/packages/blocks/src/api/matchers.js b/packages/blocks/src/api/matchers.js
index 5a86c2c9b85a89..f329fadca32c87 100644
--- a/packages/blocks/src/api/matchers.js
+++ b/packages/blocks/src/api/matchers.js
@@ -1,10 +1,33 @@
/**
- * External dependencies
+ * WordPress dependencies
*/
-export { attr, prop, html, text, query } from 'hpq';
+import { create } from './rich-text-structure';
/**
- * Internal dependencies
+ * External dependencies
*/
-export { matcher as node } from './node';
-export { matcher as children } from './children';
+export { attr, prop, html, text, query } from 'hpq';
+
+export const children = ( selector, multiline ) => {
+ return ( domNode ) => {
+ let match = domNode;
+
+ if ( selector ) {
+ match = domNode.querySelector( selector );
+ }
+
+ return create( match, multiline );
+ };
+};
+
+export const node = ( selector ) => {
+ return ( domNode ) => {
+ let match = domNode;
+
+ if ( selector ) {
+ match = domNode.querySelector( selector );
+ }
+
+ return create( match );
+ };
+};
diff --git a/packages/blocks/src/api/node.js b/packages/blocks/src/api/node.js
deleted file mode 100644
index 34bcfd394429c6..00000000000000
--- a/packages/blocks/src/api/node.js
+++ /dev/null
@@ -1,112 +0,0 @@
-/**
- * Internal dependencies
- */
-import * as children from './children';
-
-/**
- * Browser dependencies
- */
-const { TEXT_NODE, ELEMENT_NODE } = window.Node;
-
-/**
- * A representation of a single node within a block's rich text value. If
- * representing a text node, the value is simply a string of the node value.
- * As representing an element node, it is an object of:
- *
- * 1. `type` (string): Tag name.
- * 2. `props` (object): Attributes and children array of WPBlockNode.
- *
- * @typedef {string|Object} WPBlockNode
- */
-
-/**
- * Given an object implementing the NamedNodeMap interface, returns a plain
- * object equivalent value of name, value key-value pairs.
- *
- * @see https://dom.spec.whatwg.org/#interface-namednodemap
- *
- * @param {NamedNodeMap} nodeMap NamedNodeMap to convert to object.
- *
- * @return {Object} Object equivalent value of NamedNodeMap.
- */
-export function getNamedNodeMapAsObject( nodeMap ) {
- const result = {};
- for ( let i = 0; i < nodeMap.length; i++ ) {
- const { name, value } = nodeMap[ i ];
- result[ name ] = value;
- }
-
- return result;
-}
-
-/**
- * Given a DOM Element or Text node, returns an equivalent block node. Throws
- * if passed any node type other than element or text.
- *
- * @throws {TypeError} If non-element/text node is passed.
- *
- * @param {Node} domNode DOM node to convert.
- *
- * @return {WPBlockNode} Block node equivalent to DOM node.
- */
-export function fromDOM( domNode ) {
- if ( domNode.nodeType === TEXT_NODE ) {
- return domNode.nodeValue;
- }
-
- if ( domNode.nodeType !== ELEMENT_NODE ) {
- throw new TypeError(
- 'A block node can only be created from a node of type text or ' +
- 'element.'
- );
- }
-
- return {
- type: domNode.nodeName.toLowerCase(),
- props: {
- ...getNamedNodeMapAsObject( domNode.attributes ),
- children: children.fromDOM( domNode.childNodes ),
- },
- };
-}
-
-/**
- * Given a block node, returns its HTML string representation.
- *
- * @param {WPBlockNode} node Block node to convert to string.
- *
- * @return {string} String HTML representation of block node.
- */
-export function toHTML( node ) {
- return children.toHTML( [ node ] );
-}
-
-/**
- * Given a selector, returns an hpq matcher generating a WPBlockNode value
- * matching the selector result.
- *
- * @param {string} selector DOM selector.
- *
- * @return {Function} hpq matcher.
- */
-export function matcher( selector ) {
- return ( domNode ) => {
- let match = domNode;
-
- if ( selector ) {
- match = domNode.querySelector( selector );
- }
-
- try {
- return fromDOM( match );
- } catch ( error ) {
- return null;
- }
- };
-}
-
-export default {
- fromDOM,
- toHTML,
- matcher,
-};
diff --git a/packages/blocks/src/api/parser.js b/packages/blocks/src/api/parser.js
index 5f2ebf531f1696..700142a919d040 100644
--- a/packages/blocks/src/api/parser.js
+++ b/packages/blocks/src/api/parser.js
@@ -20,6 +20,7 @@ import { createBlock } from './factory';
import { isValidBlock } from './validation';
import { getCommentDelimitedContent } from './serializer';
import { attr, prop, html, text, query, node, children } from './matchers';
+import { create } from './rich-text-structure';
/**
* Higher-order hpq matcher which enhances an attribute matcher to return true
@@ -118,7 +119,8 @@ export function matcherFromSource( sourceConfig ) {
case 'text':
return text( sourceConfig.selector );
case 'children':
- return children( sourceConfig.selector );
+ case 'rich-text':
+ return children( sourceConfig.selector, sourceConfig.multiline );
case 'node':
return node( sourceConfig.selector );
case 'query':
@@ -157,6 +159,11 @@ export function parseWithAttributeSchema( innerHTML, attributeSchema ) {
*/
export function getBlockAttribute( attributeKey, attributeSchema, innerHTML, commentAttributes ) {
let value;
+
+ if ( attributeSchema.source === 'rich-text' ) {
+ attributeSchema.default = create( null, attributeSchema.multiline );
+ }
+
switch ( attributeSchema.source ) {
// undefined source means that it's an attribute serialized to the block's "comment"
case undefined:
@@ -167,6 +174,7 @@ export function getBlockAttribute( attributeKey, attributeSchema, innerHTML, com
case 'html':
case 'text':
case 'children':
+ case 'rich-text':
case 'node':
case 'query':
value = parseWithAttributeSchema( innerHTML, attributeSchema );
diff --git a/packages/blocks/src/api/rich-text-structure.js b/packages/blocks/src/api/rich-text-structure.js
new file mode 100644
index 00000000000000..28b6db41be1122
--- /dev/null
+++ b/packages/blocks/src/api/rich-text-structure.js
@@ -0,0 +1,653 @@
+import { find } from 'lodash';
+
+/**
+ * Browser dependencies
+ */
+
+const { TEXT_NODE, ELEMENT_NODE } = window.Node;
+
+export function createWithSelection( element, range, multiline, settings ) {
+ if ( ! multiline ) {
+ return createRecord( element, range, settings );
+ }
+
+ if ( ! element || ! element.hasChildNodes() ) {
+ return {
+ value: [],
+ selection: {},
+ };
+ }
+
+ return Array.from( element.childNodes ).reduce( ( acc, child, index ) => {
+ if ( child.nodeName.toLowerCase() === multiline ) {
+ const { selection, value } = createRecord( child, range, settings );
+
+ if ( range ) {
+ if ( selection.start !== undefined ) {
+ acc.selection.start = [ index ].concat( selection.start );
+ } else if ( child === range.startContainer ) {
+ acc.selection.start = [ index ];
+ }
+
+ if ( selection.end !== undefined ) {
+ acc.selection.end = [ index ].concat( selection.end );
+ } else if ( child === range.endContainer ) {
+ acc.selection.end = [ index ];
+ }
+ }
+
+ acc.value.push( value );
+ }
+
+ return acc;
+ }, {
+ value: [],
+ selection: {},
+ } );
+}
+
+export function create( element, multiline, settings ) {
+ return createWithSelection( element, null, multiline, settings ).value;
+}
+
+function createRecord( element, range, settings = {} ) {
+ if ( ! element ) {
+ return {
+ value: {
+ formats: [],
+ text: '',
+ },
+ selection: {},
+ };
+ }
+
+ const {
+ removeNodeMatch = () => false,
+ unwrapNodeMatch = () => false,
+ filterString = ( string ) => string,
+ } = settings;
+
+ if (
+ element.nodeName === 'BR' &&
+ ! removeNodeMatch( element ) &&
+ ! unwrapNodeMatch( element )
+ ) {
+ return {
+ value: {
+ formats: [ undefined ],
+ text: '\n',
+ },
+ selection: {},
+ };
+ }
+
+ if ( ! element.hasChildNodes() ) {
+ return {
+ value: {
+ formats: [],
+ text: '',
+ },
+ selection: {},
+ };
+ }
+
+ return Array.from( element.childNodes ).reduce( ( accumulator, node ) => {
+ const { formats } = accumulator.value;
+
+ if ( node.nodeType === TEXT_NODE ) {
+ if ( range ) {
+ if ( node === range.startContainer ) {
+ accumulator.selection.start = accumulator.value.text.length + filterString( node.nodeValue.slice( 0, range.startOffset ) ).length;
+ }
+
+ if ( node === range.endContainer ) {
+ accumulator.selection.end = accumulator.value.text.length + filterString( node.nodeValue.slice( 0, range.endOffset ) ).length;
+ }
+ }
+
+ const text = filterString( node.nodeValue, accumulator.selection );
+ accumulator.value.text += text;
+ formats.push( ...Array( text.length ) );
+ } else if ( node.nodeType === ELEMENT_NODE ) {
+ if ( removeNodeMatch( node ) ) {
+ return accumulator;
+ }
+
+ if ( range ) {
+ if (
+ node.parentNode === range.startContainer &&
+ node === range.startContainer.childNodes[ range.startOffset ]
+ ) {
+ accumulator.selection.start = accumulator.value.text.length;
+ }
+
+ if (
+ node.parentNode === range.endContainer &&
+ node === range.endContainer.childNodes[ range.endOffset ]
+ ) {
+ accumulator.selection.end = accumulator.value.text.length;
+ }
+ }
+
+ let format;
+
+ if ( ! unwrapNodeMatch( node ) && node.nodeName !== 'BR' ) {
+ const type = node.nodeName.toLowerCase();
+ const attributes = getAttributes( node, settings );
+
+ format = attributes ? { type, attributes } : { type };
+ }
+
+ const { value, selection } = createRecord( node, range, settings );
+ const text = value.text;
+ const start = accumulator.value.text.length;
+
+ if ( format && text.length === 0 ) {
+ format.object = true;
+
+ if ( formats[ start ] ) {
+ formats[ start ].unshift( format );
+ } else {
+ formats[ start ] = [ format ];
+ }
+ } else {
+ accumulator.value.text += text;
+
+ let i = value.formats.length;
+
+ while ( i-- ) {
+ const index = start + i;
+
+ if ( format ) {
+ if ( formats[ index ] ) {
+ formats[ index ].push( format );
+ } else {
+ formats[ index ] = [ format ];
+ }
+ }
+
+ if ( value.formats[ i ] ) {
+ if ( formats[ index ] ) {
+ formats[ index ].push( ...value.formats[ i ] );
+ } else {
+ formats[ index ] = value.formats[ i ];
+ }
+ }
+
+ if ( ! formats[ index ] ) {
+ formats[ index ] = undefined;
+ }
+ }
+ }
+
+ if ( selection.start !== undefined ) {
+ accumulator.selection.start = start + selection.start;
+ }
+
+ if ( selection.end !== undefined ) {
+ accumulator.selection.end = start + selection.end;
+ }
+ }
+
+ return accumulator;
+ }, {
+ value: {
+ formats: [],
+ text: '',
+ },
+ selection: {},
+ } );
+}
+
+export function apply( value, current, multiline ) {
+ const { body: future, selection } = toDOM( value, multiline );
+ let i = 0;
+
+ while ( future.firstChild ) {
+ const currentChild = current.childNodes[ i ];
+ const futureNodeType = future.firstChild.nodeType;
+
+ if ( ! currentChild ) {
+ current.appendChild( future.firstChild );
+ } else if (
+ futureNodeType !== currentChild.nodeType ||
+ futureNodeType !== TEXT_NODE ||
+ future.firstChild.nodeValue !== currentChild.nodeValue
+ ) {
+ current.replaceChild( future.firstChild, currentChild );
+ } else {
+ future.removeChild( future.firstChild );
+ }
+
+ i++;
+ }
+
+ while ( current.childNodes[ i ] ) {
+ current.removeChild( current.childNodes[ i ] );
+ }
+
+ if ( ! selection.startPath.length ) {
+ return;
+ }
+
+ const { node: startContainer, offset: startOffset } = getNodeByPath( current, selection.startPath );
+ const { node: endContainer, offset: endOffset } = getNodeByPath( current, selection.endPath );
+
+ const sel = window.getSelection();
+ const range = current.ownerDocument.createRange();
+ const collapsed = startContainer === endContainer && startOffset === endOffset;
+
+ if (
+ collapsed &&
+ startOffset === 0 &&
+ startContainer.previousSibling &&
+ startContainer.previousSibling.nodeType === ELEMENT_NODE &&
+ startContainer.previousSibling.nodeName !== 'BR'
+ ) {
+ startContainer.insertData( 0, '\uFEFF' );
+ range.setStart( startContainer, 1 );
+ range.setEnd( endContainer, 1 );
+ } else {
+ range.setStart( startContainer, startOffset );
+ range.setEnd( endContainer, endOffset );
+ }
+
+ sel.removeAllRanges();
+ sel.addRange( range );
+}
+
+function getAttributes( element, settings = {} ) {
+ if ( ! element.hasAttributes() ) {
+ return;
+ }
+
+ const {
+ removeAttributeMatch = () => false,
+ } = settings;
+
+ return Array.from( element.attributes ).reduce( ( acc, { name, value } ) => {
+ if ( ! removeAttributeMatch( name ) ) {
+ acc = acc || {};
+ acc[ name ] = value;
+ }
+
+ return acc;
+ }, undefined );
+}
+
+function createPathToNode( node, rootNode, path ) {
+ const parentNode = node.parentNode;
+ let i = 0;
+
+ while ( ( node = node.previousSibling ) ) {
+ i++;
+ }
+
+ path = [ i, ...path ];
+
+ if ( parentNode !== rootNode ) {
+ path = createPathToNode( parentNode, rootNode, path );
+ }
+
+ return path;
+}
+
+function getNodeByPath( node, path ) {
+ path = [ ...path ];
+
+ while ( node && path.length > 1 ) {
+ node = node.childNodes[ path.shift() ];
+ }
+
+ return {
+ node,
+ offset: path[ 0 ],
+ };
+}
+
+export function toDOM( { value, selection = {} }, multiline, _tag ) {
+ const doc = document.implementation.createHTMLDocument( '' );
+ let { body } = doc;
+ let startPath = [];
+ let endPath = [];
+
+ if ( multiline ) {
+ value.forEach( ( piece, index ) => {
+ const start = selection.start && selection.start[ 0 ] === index ? selection.start[ 1 ] : undefined;
+ const end = selection.end && selection.end[ 0 ] === index ? selection.end[ 1 ] : undefined;
+ const dom = toDOM( {
+ value: piece,
+ selection: {
+ start,
+ end,
+ },
+ }, false, multiline );
+
+ body.appendChild( dom.body );
+
+ if ( dom.selection.startPath.length ) {
+ startPath = [ index, ...dom.selection.startPath ];
+ }
+
+ if ( dom.selection.endPath.length ) {
+ endPath = [ index, ...dom.selection.endPath ];
+ }
+ } );
+
+ return {
+ body,
+ selection: { startPath, endPath },
+ };
+ }
+
+ const { formats, text } = value;
+ const { start, end } = selection;
+
+ if ( _tag ) {
+ body = body.appendChild( doc.createElement( _tag ) );
+ }
+
+ for ( let i = 0, max = text.length; i < max; i++ ) {
+ const character = text.charAt( i );
+ const nextFormats = formats[ i ] || [];
+ let pointer = body.lastChild || body.appendChild( doc.createTextNode( '' ) );
+
+ if ( nextFormats ) {
+ nextFormats.forEach( ( { type, attributes, object } ) => {
+ if ( pointer && type === pointer.nodeName.toLowerCase() ) {
+ pointer = pointer.lastChild;
+ return;
+ }
+
+ const newNode = doc.createElement( type );
+ const parentNode = pointer.parentNode;
+
+ for ( const key in attributes ) {
+ newNode.setAttribute( key, attributes[ key ] );
+ }
+
+ parentNode.appendChild( newNode );
+ pointer = ( object ? parentNode : newNode ).appendChild( doc.createTextNode( '' ) );
+ } );
+ }
+
+ if ( character === '\n' ) {
+ pointer = pointer.parentNode.appendChild( doc.createElement( 'br' ) );
+ } else if ( pointer.nodeType === TEXT_NODE ) {
+ pointer.appendData( character );
+ } else {
+ pointer = pointer.parentNode.appendChild( doc.createTextNode( character ) );
+ }
+
+ if ( start === i ) {
+ const initialPath = pointer.nodeValue ? [ pointer.nodeValue.length - 1 ] : [];
+ startPath = createPathToNode( pointer, body, initialPath );
+ }
+
+ if ( end === i ) {
+ const initialPath = pointer.nodeValue ? [ pointer.nodeValue.length - 1 ] : [];
+ endPath = createPathToNode( pointer, body, initialPath );
+ }
+ }
+
+ const last = text.length;
+
+ if ( formats[ last ] ) {
+ formats[ last ].reduce( ( element, { type, attributes } ) => {
+ const newNode = doc.createElement( type );
+
+ for ( const key in attributes ) {
+ newNode.setAttribute( key, attributes[ key ] );
+ }
+
+ return element.appendChild( newNode );
+ }, body );
+ }
+
+ return {
+ body,
+ selection: { startPath, endPath },
+ };
+}
+
+export function toString( record, multiline ) {
+ return toDOM( { value: record }, multiline ).body.innerHTML;
+}
+
+export function concat( record, ...records ) {
+ if ( Array.isArray( record ) ) {
+ return record.concat( ...records );
+ }
+
+ return records.reduce( ( accu, { formats, text } ) => {
+ accu.text += text;
+ accu.formats.push( ...formats );
+ return accu;
+ }, { ...record } );
+}
+
+export function isEmpty( record ) {
+ if ( Array.isArray( record ) ) {
+ return record.length === 0 || ( record.length === 1 && isEmpty( record[ 0 ] ) );
+ }
+
+ const { text, formats } = record;
+
+ return text.length === 0 && formats.length === 0;
+}
+
+export function splice( { formats, text, selection, value }, start, deleteCount, textToInsert = '', formatsToInsert ) {
+ if ( value !== undefined ) {
+ start = start || selection.start;
+ deleteCount = deleteCount || selection.end - selection.start;
+
+ const diff = textToInsert.length - deleteCount;
+
+ return {
+ selection: {
+ start: selection.start + ( selection.start >= start ? diff : 0 ),
+ end: selection.end + ( selection.end >= start ? diff : 0 ),
+ },
+ value: splice( value, start, deleteCount, textToInsert, formatsToInsert ),
+ };
+ }
+
+ if ( ! Array.isArray( formatsToInsert ) ) {
+ const newFormats = formatsToInsert ? [ formatsToInsert ] : formats[ start ];
+ formatsToInsert = Array( textToInsert.length ).fill( newFormats );
+ }
+
+ formats.splice( start, deleteCount, ...formatsToInsert );
+ text = text.slice( 0, start ) + textToInsert + text.slice( start + deleteCount );
+
+ return { formats, text };
+}
+
+export function getTextContent( { text, value, selection } ) {
+ if ( value !== undefined ) {
+ if ( Array.isArray( value ) ) {
+ if ( isCollapsed( { selection } ) ) {
+ const [ index ] = selection.start;
+ return value[ index ].text;
+ }
+
+ return;
+ }
+
+ return value.text;
+ }
+
+ return text;
+}
+
+export function applyFormat( { formats, text, value, selection }, format, start, end ) {
+ if ( value !== undefined ) {
+ start = start || selection.start;
+ end = end || selection.end;
+
+ if ( Array.isArray( value ) ) {
+ return {
+ selection,
+ value: value.map( ( item, index ) => {
+ const [ startRecord, startOffset ] = start;
+ const [ endRecord, endOffset ] = end;
+
+ if ( startRecord === endRecord && startRecord === index ) {
+ return applyFormat( item, format, startOffset, endOffset );
+ }
+
+ if ( startRecord === index ) {
+ return applyFormat( item, format, startOffset, item.text.length );
+ }
+
+ if ( endRecord === index ) {
+ return applyFormat( item, format, 0, endOffset );
+ }
+
+ if ( index > startRecord && index < endRecord ) {
+ return applyFormat( item, format, 0, item.text.length );
+ }
+
+ return item;
+ } ),
+ };
+ }
+
+ return {
+ selection,
+ value: applyFormat( value, format, start, end ),
+ };
+ }
+
+ for ( let i = start; i < end; i++ ) {
+ if ( formats[ i ] ) {
+ const newFormats = formats[ i ].filter( ( { type } ) => type !== format.type );
+ newFormats.push( format );
+ formats[ i ] = newFormats;
+ } else {
+ formats[ i ] = [ format ];
+ }
+ }
+
+ return { formats, text };
+}
+
+export function removeFormat( { formats, text, value, selection }, formatType, start, end ) {
+ if ( value !== undefined ) {
+ start = start || selection.start;
+ end = end || selection.end;
+
+ if ( Array.isArray( value ) ) {
+ return {
+ selection,
+ value: value.map( ( item, index ) => {
+ const [ startRecord, startOffset ] = start;
+ const [ endRecord, endOffset ] = end;
+
+ if ( startRecord === endRecord && startRecord === index ) {
+ return removeFormat( item, formatType, startOffset, endOffset );
+ }
+
+ if ( startRecord === index ) {
+ return removeFormat( item, formatType, startOffset, item.text.length );
+ }
+
+ if ( endRecord === index ) {
+ return removeFormat( item, formatType, 0, endOffset );
+ }
+
+ if ( index > startRecord && index < endRecord ) {
+ return removeFormat( item, formatType, 0, item.text.length );
+ }
+
+ return item;
+ } ),
+ };
+ }
+
+ return {
+ selection,
+ value: removeFormat( value, formatType, start, end ),
+ };
+ }
+
+ for ( let i = start; i < end; i++ ) {
+ if ( formats[ i ] ) {
+ const newFormats = formats[ i ].filter( ( { type } ) => type !== formatType );
+ formats[ i ] = newFormats.length ? newFormats : undefined;
+ }
+ }
+
+ return { formats, text };
+}
+
+export function getActiveFormat( { value, selection }, formatType ) {
+ if ( ! selection || ! selection.start ) {
+ return false;
+ }
+
+ if ( Array.isArray( value ) ) {
+ return getActiveFormat( {
+ value: value[ selection.start[ 0 ] ],
+ selection: {
+ start: selection.start[ 1 ],
+ end: selection.end[ 1 ],
+ },
+ }, formatType );
+ }
+
+ const formats = value.formats[ selection.start ];
+
+ return find( formats, { type: formatType } );
+}
+
+export function split( { text, formats, selection, value }, start, end ) {
+ if ( value !== undefined ) {
+ start = start || selection.start;
+ end = end || selection.end;
+
+ const [ startValue, endValue ] = split( value, start, end );
+
+ return [
+ {
+ selection: {},
+ value: startValue,
+ },
+ {
+ selection: {
+ start: 0,
+ end: 0,
+ },
+ value: endValue,
+ },
+ ];
+ }
+
+ return [
+ {
+ formats: formats.slice( 0, start ),
+ text: text.slice( 0, start ),
+ },
+ {
+ formats: formats.slice( end ),
+ text: text.slice( end ),
+ },
+ ];
+}
+
+export function isCollapsed( { selection } ) {
+ const { start, end } = selection;
+
+ if ( ! start ) {
+ return;
+ }
+
+ if ( typeof start === 'number' ) {
+ return start === end;
+ }
+
+ const [ startRecord, startOffset ] = start;
+ const [ endRecord, endOffset ] = end;
+
+ return startRecord === endRecord && startOffset === endOffset;
+}
diff --git a/packages/blocks/src/api/test/children.js b/packages/blocks/src/api/test/children.js
deleted file mode 100644
index cc3fe5d76d28ad..00000000000000
--- a/packages/blocks/src/api/test/children.js
+++ /dev/null
@@ -1,137 +0,0 @@
-/**
- * WordPress dependencies
- */
-import { renderToString } from '@wordpress/element';
-
-/**
- * Internal dependencies
- */
-import {
- getSerializeCapableElement,
- concat,
- toHTML,
- fromDOM,
-} from '../children';
-
-describe( 'getSerializeCapableElement', () => {
- it( 'returns a serialize capable element', () => {
- const blockNode = [
- 'This ',
- {
- type: 'strong',
- props: {
- class: 'is-extra-strong',
- children: [ 'is' ],
- },
- },
- ' a test',
- ];
-
- const element = getSerializeCapableElement( blockNode );
-
- // Intentionally avoid introspecting the shape of the generated element
- // since all that is cared about is that it can be serialized.
- const html = renderToString( element );
-
- expect( html ).toBe( 'This a test' );
- } );
-} );
-
-describe( 'concat', () => {
- it( 'should combine two or more sets of block nodes', () => {
- const result = concat(
- {
- type: 'strong',
- props: {
- children: [ 'Hello' ],
- },
- },
- ' ',
- {
- type: 'em',
- props: {
- children: [ 'world' ],
- },
- },
- );
-
- expect( result ).toEqual( [
- {
- type: 'strong',
- props: {
- children: [ 'Hello' ],
- },
- },
- ' ',
- {
- type: 'em',
- props: {
- children: [ 'world' ],
- },
- },
- ] );
- } );
-
- it( 'should merge adjacent strings', () => {
- const result = concat(
- 'Hello',
- ' ',
- {
- type: 'strong',
- props: {
- children: [ 'World' ],
- },
- },
- );
-
- expect( result ).toEqual( [
- 'Hello ',
- {
- type: 'strong',
- props: {
- children: [ 'World' ],
- },
- },
- ] );
- } );
-} );
-
-describe( 'toHTML', () => {
- it( 'should convert a children array of block nodes to its equivalent html string', () => {
- const children = [
- 'This is a ',
- {
- type: 'strong',
- props: {
- children: [ 'test' ],
- },
- },
- '!',
- ];
-
- const html = toHTML( children );
-
- expect( html ).toBe( 'This is a test !' );
- } );
-} );
-
-describe( 'fromDOM', () => {
- it( 'should return an equivalent block children', () => {
- const node = document.createElement( 'div' );
- node.innerHTML = 'This a test';
-
- const blockNode = fromDOM( node.childNodes );
-
- expect( blockNode ).toEqual( [
- 'This ',
- {
- type: 'strong',
- props: {
- class: 'is-extra-strong',
- children: [ 'is' ],
- },
- },
- ' a test',
- ] );
- } );
-} );
diff --git a/packages/blocks/src/api/test/matchers.js b/packages/blocks/src/api/test/matchers.js
deleted file mode 100644
index 32d1246a2e9d6e..00000000000000
--- a/packages/blocks/src/api/test/matchers.js
+++ /dev/null
@@ -1,50 +0,0 @@
-/**
- * External dependencies
- */
-import { parse } from 'hpq';
-
-/**
- * WordPress dependencies
- */
-import { renderToString } from '@wordpress/element';
-
-/**
- * Internal dependencies
- */
-import * as sources from '../matchers';
-
-describe( 'matchers', () => {
- describe( 'children()', () => {
- it( 'should return a source function', () => {
- const source = sources.children();
-
- expect( typeof source ).toBe( 'function' );
- } );
-
- it( 'should return HTML equivalent WPElement of matched element', () => {
- // Assumption here is that we can cleanly convert back and forth
- // between a string and WPElement representation
- const html = 'A delicious sundae dessert
';
- const match = parse( html, sources.children() );
-
- expect( renderToString( match ) ).toBe( html );
- } );
- } );
-
- describe( 'node()', () => {
- it( 'should return a source function', () => {
- const source = sources.node();
-
- expect( typeof source ).toBe( 'function' );
- } );
-
- it( 'should return HTML equivalent WPElement of matched element', () => {
- // Assumption here is that we can cleanly convert back and forth
- // between a string and WPElement representation
- const html = 'A delicious sundae dessert
';
- const match = parse( html, sources.node() );
-
- expect( renderToString( match ) ).toBe( `${ html }` );
- } );
- } );
-} );
diff --git a/packages/blocks/src/api/test/node.js b/packages/blocks/src/api/test/node.js
deleted file mode 100644
index 447395a24ed125..00000000000000
--- a/packages/blocks/src/api/test/node.js
+++ /dev/null
@@ -1,77 +0,0 @@
-/**
- * Internal dependencies
- */
-import {
- getNamedNodeMapAsObject,
- toHTML,
- fromDOM,
-} from '../node';
-
-describe( 'getNamedNodeMapAsObject', () => {
- it( 'should return an object of node attributes', () => {
- const node = document.createElement( 'img' );
- node.setAttribute( 'src', 'https://s.w.org/style/images/wporg-logo.svg' );
-
- const object = getNamedNodeMapAsObject( node.attributes );
- expect( object ).toEqual( {
- src: 'https://s.w.org/style/images/wporg-logo.svg',
- } );
- } );
-} );
-
-describe( 'toHTML', () => {
- it( 'should convert a block node to its equivalent html string', () => {
- const blockNode = {
- type: 'strong',
- props: {
- class: 'is-extra-strong',
- children: [ 'This is a test' ],
- },
- };
-
- const html = toHTML( blockNode );
-
- expect( html ).toBe( '' );
- } );
-} );
-
-describe( 'fromDOM', () => {
- it( 'should return a text node as its string node value', () => {
- const node = document.createTextNode( 'Hello world' );
-
- const blockNode = fromDOM( node );
-
- expect( blockNode ).toBe( 'Hello world' );
- } );
-
- it( 'should throw an error on receiving non-element/text node', () => {
- expect( () => {
- fromDOM( document.createDocumentFragment() );
- } ).toThrow( TypeError );
- } );
-
- it( 'should return an equivalent block node, including children', () => {
- const node = document.createElement( 'strong' );
- node.setAttribute( 'class', 'is-extra-strong' );
- node.innerHTML = 'Hello world !';
-
- const blockNode = fromDOM( node );
-
- expect( blockNode ).toEqual( {
- type: 'strong',
- props: {
- class: 'is-extra-strong',
- children: [
- 'Hello ',
- {
- type: 'em',
- props: {
- children: [ 'world' ],
- },
- },
- '!',
- ],
- },
- } );
- } );
-} );
diff --git a/packages/blocks/src/api/test/rich-text-structure.js b/packages/blocks/src/api/test/rich-text-structure.js
new file mode 100644
index 00000000000000..b18647fe5f61e1
--- /dev/null
+++ b/packages/blocks/src/api/test/rich-text-structure.js
@@ -0,0 +1,1107 @@
+import { deepEqual } from 'assert';
+import { JSDOM } from 'jsdom';
+
+const { window } = new JSDOM();
+const { document } = window;
+
+import {
+ create,
+ createWithSelection,
+ toString,
+ concat,
+ isEmpty,
+ splice,
+ applyFormat,
+ removeFormat,
+ split,
+ getActiveFormat,
+} from '../rich-text-structure';
+
+function createNode( HTML ) {
+ const doc = document.implementation.createHTMLDocument( '' );
+ doc.body.innerHTML = HTML;
+ return doc.body.firstChild;
+}
+
+describe( 'create', () => {
+ it( 'should extract text with formats', () => {
+ const element = createNode( 'one two 🍒 three
' );
+ const range = {
+ startOffset: 1,
+ startContainer: element.querySelector( 'em' ).firstChild,
+ endOffset: 0,
+ endContainer: element.querySelector( 'strong' ).firstChild,
+ };
+
+ deepEqual( createWithSelection( element, range ), {
+ value: {
+ formats: [
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ [ { type: 'em' } ],
+ [ { type: 'em' } ],
+ [ { type: 'em' } ],
+ [ { type: 'em' } ],
+ [ { type: 'em' } ],
+ [ { type: 'em' } ],
+ undefined,
+ [ { type: 'a', attributes: { href: '#' } }, { type: 'img', attributes: { src: '' }, object: true }, { type: 'strong' } ],
+ [ { type: 'a', attributes: { href: '#' } }, { type: 'strong' } ],
+ [ { type: 'a', attributes: { href: '#' } }, { type: 'strong' } ],
+ [ { type: 'a', attributes: { href: '#' } }, { type: 'strong' } ],
+ [ { type: 'a', attributes: { href: '#' } }, { type: 'strong' } ],
+ [ { type: 'img', attributes: { src: '' }, object: true } ],
+ ],
+ text: 'one two 🍒 three',
+ },
+ selection: {
+ start: 5,
+ end: 11,
+ },
+ } );
+ } );
+
+ it( 'should extract multiline text', () => {
+ const element = createNode( '' );
+ const range = {
+ startOffset: 1,
+ startContainer: element.querySelector( 'em' ).firstChild,
+ endOffset: 0,
+ endContainer: element.lastChild,
+ };
+
+ deepEqual( createWithSelection( element, range, 'p' ), {
+ value: [
+ {
+ formats: [
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ [ { type: 'em' } ],
+ [ { type: 'em' } ],
+ [ { type: 'em' } ],
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ ],
+ text: 'one two three',
+ },
+ {
+ formats: [
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ ],
+ text: 'test',
+ },
+ ],
+ selection: {
+ start: [ 0, 5 ],
+ end: [ 1 ],
+ },
+ } );
+ } );
+
+ it( 'should extract multiline text list', () => {
+ const element = createNode( '' );
+
+ deepEqual( create( element, 'li' ), [
+ {
+ formats: [
+ undefined,
+ undefined,
+ undefined,
+ [ { type: 'ul' }, { type: 'li' } ],
+ [ { type: 'ul' }, { type: 'li' } ],
+ [ { type: 'ul' }, { type: 'li' } ],
+ ],
+ text: 'onetwo',
+ },
+ {
+ formats: [
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ ],
+ text: 'three',
+ },
+ ] );
+ } );
+
+ it( 'should skip bogus 1', () => {
+ const element = createNode( 'test
' );
+ const range = {
+ startOffset: 1,
+ startContainer: element.querySelector( 'strong' ).firstChild,
+ endOffset: 1,
+ endContainer: element.querySelector( 'strong' ).firstChild,
+ };
+ const settings = {
+ removeNodeMatch: ( node ) => node.getAttribute( 'data-mce-bogus' ) === 'all',
+ unwrapNodeMatch: ( node ) => !! node.getAttribute( 'data-mce-bogus' ),
+ removeAttributeMatch: ( attribute ) => attribute.indexOf( 'data-mce-' ) === 0,
+ filterString: ( string ) => string.replace( '\uFEFF', '' ),
+ };
+
+ deepEqual( createWithSelection( element, range, false, settings ), {
+ value: {
+ formats: [
+ [ { type: 'strong' } ],
+ [ { type: 'strong' } ],
+ [ { type: 'strong' } ],
+ [ { type: 'strong' } ],
+ ],
+ text: 'test',
+ },
+ selection: {
+ start: 0,
+ end: 0,
+ },
+ } );
+ } );
+
+ it( 'should skip bogus 2', () => {
+ const element = createNode( 'testtest test
' );
+ const range = {
+ startOffset: 1,
+ startContainer: element.lastChild,
+ endOffset: 1,
+ endContainer: element.lastChild,
+ };
+ const settings = {
+ removeNodeMatch: ( node ) => node.getAttribute( 'data-mce-bogus' ) === 'all',
+ unwrapNodeMatch: ( node ) => !! node.getAttribute( 'data-mce-bogus' ),
+ removeAttributeMatch: ( attribute ) => attribute.indexOf( 'data-mce-' ) === 0,
+ filterString: ( string ) => string.replace( '\uFEFF', '' ),
+ };
+
+ deepEqual( createWithSelection( element, range, false, settings ), {
+ value: {
+ formats: [
+ [ { type: 'strong' } ],
+ [ { type: 'strong' } ],
+ [ { type: 'strong' } ],
+ [ { type: 'strong' } ],
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ ],
+ text: 'test test',
+ },
+ selection: {
+ start: 5,
+ end: 5,
+ },
+ } );
+ } );
+
+ it( 'should handle br', () => {
+ const element = createNode( 'test test
' );
+ const range1 = {
+ startOffset: 1,
+ startContainer: element,
+ endOffset: 1,
+ endContainer: element,
+ };
+ const range2 = {
+ startOffset: 0,
+ startContainer: element.lastChild,
+ endOffset: 0,
+ endContainer: element.lastChild,
+ };
+
+ deepEqual( createWithSelection( element, range1, false ), {
+ value: {
+ formats: [
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ ],
+ text: 'test\ntest',
+ },
+ selection: {
+ start: 4,
+ end: 4,
+ },
+ } );
+
+ deepEqual( createWithSelection( element, range2, false ), {
+ value: {
+ formats: [
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ ],
+ text: 'test\ntest',
+ },
+ selection: {
+ start: 5,
+ end: 5,
+ },
+ } );
+ } );
+} );
+
+describe( 'toString', () => {
+ it( 'should extract recreate HTML 1', () => {
+ const HTML = 'one two 🍒 three ';
+
+ deepEqual( toString( create( createNode( `${ HTML }
` ) ) ), HTML );
+ } );
+
+ it( 'should extract recreate HTML 2', () => {
+ const HTML = 'one two 🍒 test three ';
+
+ deepEqual( toString( create( createNode( `${ HTML }
` ) ) ), HTML );
+ } );
+
+ it( 'should extract recreate HTML 3', () => {
+ const HTML = ' ';
+
+ deepEqual( toString( create( createNode( `${ HTML }
` ) ) ), HTML );
+ } );
+
+ it( 'should extract recreate HTML 4', () => {
+ const HTML = ' ';
+
+ deepEqual( toString( create( createNode( `${ HTML }
` ) ) ), HTML );
+ } );
+
+ it( 'should extract recreate HTML 5', () => {
+ const HTML = 'two 🍒 ';
+
+ deepEqual( toString( create( createNode( `${ HTML }
` ) ) ), HTML );
+ } );
+
+ it( 'should extract recreate HTML 6', () => {
+ const HTML = 'If you want to learn more about how to build additional blocks, or if you are interested in helping with the project, head over to the GitHub repository . ';
+
+ deepEqual( toString( create( createNode( `${ HTML }
` ) ) ), HTML );
+ } );
+
+ it( 'should extract recreate HTML 7', () => {
+ const HTML = 'one three ';
+
+ deepEqual( toString( create( createNode( `` ), 'li' ), 'li' ), HTML );
+ } );
+} );
+
+describe( 'create with settings', () => {
+ const settings = {
+ removeNodeMatch: ( node ) => node.getAttribute( 'data-mce-bogus' ) === 'all',
+ unwrapNodeMatch: ( node ) => !! node.getAttribute( 'data-mce-bogus' ),
+ removeAttributeMatch: ( attribute ) => attribute.indexOf( 'data-mce-' ) === 0,
+ filterString: ( string ) => string.replace( '\uFEFF', '' ),
+ };
+
+ it( 'should skip bogus 1', () => {
+ const HTML = ' ';
+
+ deepEqual( toString( create( createNode( `${ HTML }
` ), false, settings ) ), '' );
+ } );
+
+ it( 'should skip bogus 2', () => {
+ const HTML = ' ';
+
+ deepEqual( toString( create( createNode( `${ HTML }
` ), false, settings ) ), '' );
+ } );
+
+ it( 'should skip bogus 3', () => {
+ const HTML = 'test test ';
+
+ deepEqual( toString( create( createNode( `${ HTML }
` ), false, settings ) ), 'test test ' );
+ } );
+
+ it( 'should skip bogus 4', () => {
+ const HTML = 'test ';
+
+ deepEqual( toString( create( createNode( `${ HTML }
` ), false, settings ) ), '' );
+ } );
+
+ it( 'should skip bogus 5', () => {
+ const HTML = 'test ';
+
+ deepEqual( toString( create( createNode( `${ HTML }
` ), false, settings ) ), 'test ' );
+ } );
+} );
+
+describe( 'concat', () => {
+ it( 'should merge records', () => {
+ const one = {
+ formats: [
+ undefined,
+ undefined,
+ [ { type: 'em' } ],
+ ],
+ text: 'one',
+ };
+ const two = {
+ formats: [
+ [ { type: 'em' } ],
+ undefined,
+ undefined,
+ ],
+ text: 'two',
+ };
+ const three = {
+ formats: [
+ undefined,
+ undefined,
+ [ { type: 'em' } ],
+ [ { type: 'em' } ],
+ undefined,
+ undefined,
+ ],
+ text: 'onetwo',
+ };
+
+ const merged = concat( one, two );
+
+ expect( merged ).not.toBe( one );
+ expect( merged ).toEqual( three );
+ } );
+
+ it( 'should merge multiline records', () => {
+ const one = [ {
+ formats: [
+ undefined,
+ undefined,
+ [ { type: 'em' } ],
+ ],
+ text: 'one',
+ } ];
+ const two = [ {
+ formats: [
+ undefined,
+ undefined,
+ [ { type: 'em' } ],
+ ],
+ text: 'two',
+ } ];
+
+ const merged = concat( one, two );
+
+ expect( merged ).not.toBe( one );
+ expect( merged ).toEqual( [ ...one, ...two ] );
+ } );
+} );
+
+describe( 'isEmpty', () => {
+ const emptyRecord = {
+ formats: [],
+ text: '',
+ };
+
+ it( 'should return true', () => {
+ const one = emptyRecord;
+ const two = [ emptyRecord ];
+ const three = [];
+
+ expect( isEmpty( one ) ).toBe( true );
+ expect( isEmpty( two ) ).toBe( true );
+ expect( isEmpty( three ) ).toBe( true );
+ } );
+
+ it( 'should return false', () => {
+ const one = {
+ formats: [],
+ text: 'test',
+ };
+ const two = {
+ formats: [
+ [ { type: 'image' } ],
+ ],
+ text: '',
+ };
+ const three = [ emptyRecord, one ];
+ const four = [ one ];
+
+ expect( isEmpty( one ) ).toBe( false );
+ expect( isEmpty( two ) ).toBe( false );
+ expect( isEmpty( three ) ).toBe( false );
+ expect( isEmpty( four ) ).toBe( false );
+ } );
+} );
+
+describe( 'splice', () => {
+ it( 'should delete and insert', () => {
+ const record = {
+ value: {
+ formats: [
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ [ { type: 'em' } ],
+ [ { type: 'em' } ],
+ [ { type: 'em' } ],
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ ],
+ text: 'one two three',
+ },
+ selection: {
+ start: 6,
+ end: 6,
+ },
+ };
+
+ const expected = {
+ value: {
+ formats: [
+ undefined,
+ undefined,
+ [ { type: 'strong' } ],
+ [ { type: 'em' } ],
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ ],
+ text: 'onao three',
+ },
+ selection: {
+ start: 3,
+ end: 3,
+ },
+ };
+
+ expect( splice( record, 2, 4, 'a', [ [ { type: 'strong' } ] ] ) ).toEqual( expected );
+ } );
+
+ it( 'should insert line break with selection', () => {
+ const record = {
+ value: {
+ formats: [
+ undefined,
+ undefined,
+ ],
+ text: 'tt',
+ },
+ selection: {
+ start: 1,
+ end: 1,
+ },
+ };
+
+ const expected = {
+ value: {
+ formats: [
+ undefined,
+ undefined,
+ undefined,
+ ],
+ text: 't\nt',
+ },
+ selection: {
+ start: 2,
+ end: 2,
+ },
+ };
+
+ expect( splice( record, undefined, 0, '\n' ) ).toEqual( expected );
+ } );
+
+ // it( 'should delete and insert multiline', () => {
+ // const record = {
+ // value: [
+ // {
+ // formats: [
+ // undefined,
+ // undefined,
+ // undefined,
+ // undefined,
+ // [ { type: 'em' } ],
+ // [ { type: 'em' } ],
+ // [ { type: 'em' } ],
+ // undefined,
+ // undefined,
+ // undefined,
+ // undefined,
+ // undefined,
+ // undefined,
+ // ],
+ // text: 'one two three',
+ // }
+ // ],
+ // selection: {
+ // start: [ 0, 6 ],
+ // end: [ 0, 6 ],
+ // },
+ // };
+
+ // const expected = {
+ // value: [
+ // {
+ // formats: [
+ // undefined,
+ // undefined,
+ // [ { type: 'strong' } ],
+ // [ { type: 'em' } ],
+ // undefined,
+ // undefined,
+ // undefined,
+ // undefined,
+ // undefined,
+ // undefined,
+ // ],
+ // text: 'onao three',
+ // }
+ // ],
+ // selection: {
+ // start: [ 0, 3 ],
+ // end: [ 0, 3 ],
+ // },
+ // };
+
+ // expect( splice( record, [ 0, 2 ], [ 0, 4 ], 'a', [ [ { type: 'strong' } ] ] ) ).toEqual( expected );
+ // } );
+} );
+
+describe( 'applyFormat', () => {
+ it( 'should apply format', () => {
+ const record = {
+ formats: [
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ [ { type: 'em' } ],
+ [ { type: 'em' } ],
+ [ { type: 'em' } ],
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ ],
+ text: 'one two three',
+ };
+
+ const expected = {
+ formats: [
+ undefined,
+ undefined,
+ undefined,
+ [ { type: 'strong' } ],
+ [ { type: 'em' }, { type: 'strong' } ],
+ [ { type: 'em' }, { type: 'strong' } ],
+ [ { type: 'em' } ],
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ ],
+ text: 'one two three',
+ };
+
+ expect( applyFormat( record, { type: 'strong' }, 3, 6 ) ).toEqual( expected );
+ } );
+
+ it( 'should apply format by selection', () => {
+ const record = {
+ value: {
+ formats: [
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ [ { type: 'em' } ],
+ [ { type: 'em' } ],
+ [ { type: 'em' } ],
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ ],
+ text: 'one two three',
+ },
+ selection: {
+ start: 3,
+ end: 6,
+ },
+ };
+
+ const expected = {
+ value: {
+ formats: [
+ undefined,
+ undefined,
+ undefined,
+ [ { type: 'strong' } ],
+ [ { type: 'em' }, { type: 'strong' } ],
+ [ { type: 'em' }, { type: 'strong' } ],
+ [ { type: 'em' } ],
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ ],
+ text: 'one two three',
+ },
+ selection: {
+ start: 3,
+ end: 6,
+ },
+ };
+
+ expect( applyFormat( record, { type: 'strong' } ) ).toEqual( expected );
+ } );
+
+ it( 'should apply format for multiline', () => {
+ const record = {
+ value: [
+ {
+ formats: [
+ undefined,
+ undefined,
+ undefined,
+ ],
+ text: 'one',
+ },
+ {
+ formats: [
+ undefined,
+ undefined,
+ undefined,
+ ],
+ text: 'two',
+ },
+ {
+ formats: [
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ ],
+ text: 'three',
+ },
+ {
+ formats: [
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ ],
+ text: 'four',
+ },
+ ],
+ selection: {
+ start: [ 0, 2 ],
+ end: [ 2, 1 ],
+ },
+ };
+
+ const expected = {
+ value: [
+ {
+ formats: [
+ undefined,
+ undefined,
+ [ { type: 'em' } ],
+ ],
+ text: 'one',
+ },
+ {
+ formats: [
+ [ { type: 'em' } ],
+ [ { type: 'em' } ],
+ [ { type: 'em' } ],
+ ],
+ text: 'two',
+ },
+ {
+ formats: [
+ [ { type: 'em' } ],
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ ],
+ text: 'three',
+ },
+ {
+ formats: [
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ ],
+ text: 'four',
+ },
+ ],
+ selection: {
+ start: [ 0, 2 ],
+ end: [ 2, 1 ],
+ },
+ };
+
+ expect( applyFormat( record, { type: 'em' } ) ).toEqual( expected );
+ } );
+} );
+
+describe( 'removeFormat', () => {
+ it( 'should remove format', () => {
+ const record = {
+ formats: [
+ undefined,
+ undefined,
+ undefined,
+ [ { type: 'strong' } ],
+ [ { type: 'em' }, { type: 'strong' } ],
+ [ { type: 'em' }, { type: 'strong' } ],
+ [ { type: 'em' } ],
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ ],
+ text: 'one two three',
+ };
+
+ const expected = {
+ formats: [
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ [ { type: 'em' } ],
+ [ { type: 'em' } ],
+ [ { type: 'em' } ],
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ ],
+ text: 'one two three',
+ };
+
+ expect( removeFormat( record, 'strong', 3, 6 ) ).toEqual( expected );
+ } );
+
+ it( 'should remove format for multiline', () => {
+ const record = {
+ value: [
+ {
+ formats: [
+ undefined,
+ undefined,
+ [ { type: 'em' } ],
+ ],
+ text: 'one',
+ },
+ {
+ formats: [
+ [ { type: 'em' } ],
+ [ { type: 'em' } ],
+ [ { type: 'em' } ],
+ ],
+ text: 'two',
+ },
+ {
+ formats: [
+ [ { type: 'em' } ],
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ ],
+ text: 'three',
+ },
+ {
+ formats: [
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ ],
+ text: 'four',
+ },
+ ],
+ selection: {
+ start: [ 0, 2 ],
+ end: [ 2, 1 ],
+ },
+ };
+
+ const expected = {
+ value: [
+ {
+ formats: [
+ undefined,
+ undefined,
+ undefined,
+ ],
+ text: 'one',
+ },
+ {
+ formats: [
+ undefined,
+ undefined,
+ undefined,
+ ],
+ text: 'two',
+ },
+ {
+ formats: [
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ ],
+ text: 'three',
+ },
+ {
+ formats: [
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ ],
+ text: 'four',
+ },
+ ],
+ selection: {
+ start: [ 0, 2 ],
+ end: [ 2, 1 ],
+ },
+ };
+
+ expect( removeFormat( record, 'em' ) ).toEqual( expected );
+ } );
+} );
+
+describe( 'getActiveFormat', () => {
+ it( 'should get format by selection', () => {
+ const record = {
+ value: {
+ formats: [
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ [ { type: 'em' } ],
+ [ { type: 'em' } ],
+ [ { type: 'em' } ],
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ ],
+ text: 'one two three',
+ },
+ selection: {
+ start: 4,
+ end: 4,
+ },
+ };
+
+ const expected = { type: 'em' };
+
+ expect( getActiveFormat( record, 'em' ) ).toEqual( expected );
+ } );
+
+ it( 'should get format by selection for multiline', () => {
+ const record = {
+ value: [ {
+ formats: [
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ [ { type: 'em' } ],
+ [ { type: 'em' } ],
+ [ { type: 'em' } ],
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ ],
+ text: 'one two three',
+ } ],
+ selection: {
+ start: [ 0, 4 ],
+ end: [ 0, 4 ],
+ },
+ };
+
+ const expected = { type: 'em' };
+
+ expect( getActiveFormat( record, 'em' ) ).toEqual( expected );
+ } );
+} );
+
+describe( 'split', () => {
+ it( 'should split', () => {
+ const record = {
+ formats: [
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ [ { type: 'em' } ],
+ [ { type: 'em' } ],
+ [ { type: 'em' } ],
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ ],
+ text: 'one two three',
+ };
+
+ const expected = [
+ {
+ formats: [
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ [ { type: 'em' } ],
+ [ { type: 'em' } ],
+ ],
+ text: 'one tw',
+ },
+ {
+ formats: [
+ [ { type: 'em' } ],
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ ],
+ text: 'o three',
+ },
+ ];
+
+ expect( split( record, 6, 6 ) ).toEqual( expected );
+ } );
+
+ it( 'should split with selection', () => {
+ const record = {
+ value: {
+ formats: [
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ [ { type: 'em' } ],
+ [ { type: 'em' } ],
+ [ { type: 'em' } ],
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ ],
+ text: 'one two three',
+ },
+ selection: {
+ start: 6,
+ end: 6,
+ },
+ };
+
+ const expected = [
+ {
+ value: {
+ formats: [
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ [ { type: 'em' } ],
+ [ { type: 'em' } ],
+ ],
+ text: 'one tw',
+ },
+ selection: {},
+ },
+ {
+ value: {
+ formats: [
+ [ { type: 'em' } ],
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ ],
+ text: 'o three',
+ },
+ selection: {
+ start: 0,
+ end: 0,
+ },
+ },
+ ];
+
+ expect( split( record ) ).toEqual( expected );
+ } );
+
+ it( 'should split empty', () => {
+ const record = {
+ formats: [],
+ text: '',
+ };
+
+ const expected = [
+ record,
+ record,
+ ];
+
+ expect( split( record, 6, 6 ) ).toEqual( expected );
+ } );
+} );
diff --git a/packages/components/src/autocomplete/index.js b/packages/components/src/autocomplete/index.js
index 17a2d29aeab0c1..fcb32d6599b552 100644
--- a/packages/components/src/autocomplete/index.js
+++ b/packages/components/src/autocomplete/index.js
@@ -2,16 +2,18 @@
* External dependencies
*/
import classnames from 'classnames';
-import { escapeRegExp, find, filter, map, debounce } from 'lodash';
+import { escapeRegExp, find, map, debounce } from 'lodash';
import 'element-closest';
/**
* WordPress dependencies
*/
-import { Component, renderToString } from '@wordpress/element';
+import { Component } from '@wordpress/element';
import { ENTER, ESCAPE, UP, DOWN, LEFT, RIGHT, SPACE } from '@wordpress/keycodes';
import { __, _n, sprintf } from '@wordpress/i18n';
import { withInstanceId, compose } from '@wordpress/compose';
+import { richTextStructure } from '@wordpress/blocks';
+import { getRectangleFromRange } from '@wordpress/dom';
/**
* Internal dependencies
@@ -104,74 +106,6 @@ import withSpokenMessages from '../higher-order/with-spoken-messages';
* @property {FnGetOptionCompletion} getOptionCompletion get the completion associated with a given option.
*/
-/**
- * Recursively select the firstChild until hitting a leaf node.
- *
- * @param {Node} node The node to find the recursive first child.
- *
- * @return {Node} The first leaf-node >= node in the ordering.
- */
-function descendFirst( node ) {
- let n = node;
- while ( n.firstChild ) {
- n = n.firstChild;
- }
- return n;
-}
-
-/**
- * Recursively select the lastChild until hitting a leaf node.
- *
- * @param {Node} node The node to find the recursive last child.
- *
- * @return {Node} The first leaf-node <= node in the ordering.
- */
-function descendLast( node ) {
- let n = node;
- while ( n.lastChild ) {
- n = n.lastChild;
- }
- return n;
-}
-
-/**
- * Is the node a text node.
- *
- * @param {?Node} node The node to check.
- *
- * @return {boolean} True if the node is a text node.
- */
-function isTextNode( node ) {
- return node !== null && node.nodeType === 3;
-}
-
-/**
- * Return the node only if it is a text node, otherwise return null.
- *
- * @param {?Node} node The node to filter.
- *
- * @return {?Node} The node or null if it is not a text node.
- */
-function onlyTextNode( node ) {
- return isTextNode( node ) ? node : null;
-}
-
-/**
- * Find the index of the last character in the text that is whitespace.
- *
- * @param {string} text The text to search.
- *
- * @return {number} The last index of a white space character in the text or -1.
- */
-function lastIndexOfSpace( text ) {
- for ( let i = text.length - 1; i >= 0; i-- ) {
- if ( /\s/.test( text.charAt( i ) ) ) {
- return i;
- }
- }
- return -1;
-}
-
function filterOptions( search, options = [], maxResults = 10 ) {
const filtered = [];
for ( let i = 0; i < options.length; i++ ) {
@@ -199,6 +133,14 @@ function filterOptions( search, options = [], maxResults = 10 ) {
return filtered;
}
+function getCaretRect() {
+ const range = window.getSelection().getRangeAt( 0 );
+
+ if ( range ) {
+ return getRectangleFromRange( range );
+ }
+}
+
export class Autocomplete extends Component {
static getInitialState() {
return {
@@ -219,9 +161,7 @@ export class Autocomplete extends Component {
this.select = this.select.bind( this );
this.reset = this.reset.bind( this );
this.resetWhenSuppressed = this.resetWhenSuppressed.bind( this );
- this.search = this.search.bind( this );
this.handleKeyDown = this.handleKeyDown.bind( this );
- this.getWordRect = this.getWordRect.bind( this );
this.debouncedLoadOptions = debounce( this.loadOptions, 250 );
this.state = this.constructor.getInitialState();
@@ -231,31 +171,19 @@ export class Autocomplete extends Component {
this.node = node;
}
- insertCompletion( range, replacement ) {
- const container = document.createElement( 'div' );
- container.innerHTML = renderToString( replacement );
- while ( container.firstChild ) {
- const child = container.firstChild;
- container.removeChild( child );
- range.insertNode( child );
- range.setStartAfter( child );
- }
- range.deleteContents();
+ insertCompletion( replacement ) {
+ const { record, onChange } = this.props;
- let inputEvent;
- if ( undefined !== window.InputEvent ) {
- inputEvent = new window.InputEvent( 'input', { bubbles: true, cancelable: false } );
- } else {
- // IE11 doesn't provide an InputEvent constructor.
- inputEvent = document.createEvent( 'UIEvent' );
- inputEvent.initEvent( 'input', true /* bubbles */, false /* cancelable */ );
- }
- range.commonAncestorContainer.closest( '[contenteditable=true]' ).dispatchEvent( inputEvent );
+ replacement = replacement.slice( 1 + this.state.query.length );
+
+ const newRecord = richTextStructure.splice( record, undefined, 0, replacement );
+
+ onChange( newRecord );
}
select( option ) {
const { onReplace } = this.props;
- const { open, range, query } = this.state;
+ const { open, query } = this.state;
const { getOptionCompletion } = open || {};
if ( option.isDisabled ) {
@@ -263,7 +191,7 @@ export class Autocomplete extends Component {
}
if ( getOptionCompletion ) {
- const completion = getOptionCompletion( option.value, range, query );
+ const completion = getOptionCompletion( option.value, query );
const { action, value } =
( undefined === completion.action || undefined === completion.value ) ?
@@ -273,7 +201,7 @@ export class Autocomplete extends Component {
if ( 'replace' === action ) {
onReplace( [ value ] );
} else if ( 'insert-at-caret' === action ) {
- this.insertCompletion( range, value );
+ this.insertCompletion( value );
}
}
@@ -303,31 +231,6 @@ export class Autocomplete extends Component {
this.reset();
}
- // this method is separate so it can be overridden in tests
- getCursor( container ) {
- const selection = window.getSelection();
- if ( selection.isCollapsed ) {
- if ( 'production' !== process.env.NODE_ENV ) {
- if ( ! container.contains( selection.anchorNode ) ) {
- throw new Error( 'Invalid assumption: expected selection to be within the autocomplete container' );
- }
- }
- return {
- node: selection.anchorNode,
- offset: selection.anchorOffset,
- };
- }
- return null;
- }
-
- // this method is separate so it can be overridden in tests
- createRange( startNode, startOffset, endNode, endOffset ) {
- const range = document.createRange();
- range.setStart( startNode, startOffset );
- range.setEnd( endNode, endOffset );
- return range;
- }
-
announce( filteredOptions ) {
const { debouncedSpeak } = this.props;
if ( ! debouncedSpeak ) {
@@ -391,119 +294,6 @@ export class Autocomplete extends Component {
} );
}
- findMatch( container, cursor, allCompleters, wasOpen ) {
- const allowAnything = () => true;
- let endTextNode;
- let endIndex;
- // search backwards to find the first preceding space or non-text node.
- if ( isTextNode( cursor.node ) ) { // TEXT node
- endTextNode = cursor.node;
- endIndex = cursor.offset;
- } else if ( cursor.offset === 0 ) {
- endTextNode = onlyTextNode( descendFirst( cursor.node ) );
- endIndex = 0;
- } else {
- endTextNode = onlyTextNode( descendLast( cursor.node.childNodes[ cursor.offset - 1 ] ) );
- endIndex = endTextNode ? endTextNode.nodeValue.length : 0;
- }
- if ( endTextNode === null ) {
- return null;
- }
- // store the index of a completer in the object so we can use it to reference the options
- let completers = map( allCompleters, ( completer, idx ) => ( { ...completer, idx } ) );
- if ( wasOpen ) {
- // put the open completer at the start so it has priority
- completers = [
- wasOpen,
- ...filter( completers, ( completer ) => completer.idx !== wasOpen.idx ),
- ];
- }
- // filter the completers to those that could handle this node
- completers = filter( completers,
- ( { allowNode = allowAnything } ) => allowNode( endTextNode, container ) );
- // exit early if nothing can handle it
- if ( completers.length === 0 ) {
- return null;
- }
- let startTextNode = endTextNode;
- let text = endTextNode.nodeValue.substring( 0, endIndex );
- let pos = lastIndexOfSpace( text );
- while ( pos === -1 ) {
- const prev = onlyTextNode( startTextNode.previousSibling );
- if ( prev === null ) {
- break;
- }
- // filter the completers to those that could handle this node
- completers = filter( completers,
- ( { allowNode = allowAnything } ) => allowNode( endTextNode, container ) );
- // exit early if nothing can handle it
- if ( completers.length === 0 ) {
- return null;
- }
- startTextNode = prev;
- text = prev.nodeValue + text;
- pos = lastIndexOfSpace( prev.nodeValue );
- }
- // exit early if nothing can handle it
- if ( text.length <= pos + 1 ) {
- return null;
- }
- // find a completer that matches
- const open = find( completers, ( { triggerPrefix = '', allowContext = allowAnything } ) => {
- if ( text.substr( pos + 1, triggerPrefix.length ) !== triggerPrefix ) {
- return false;
- }
- const before = this.createRange( container, 0, startTextNode, pos + 1 );
- const after = this.createRange( endTextNode, endIndex, container, container.childNodes.length );
- return allowContext( before, after );
- } );
- // exit if no completers match
- if ( ! open ) {
- return null;
- }
- const { triggerPrefix = '' } = open;
- const range = this.createRange( startTextNode, pos + 1, endTextNode, endIndex );
- const query = text.substr( pos + 1 + triggerPrefix.length );
- return { open, range, query };
- }
-
- search( event ) {
- const { completers } = this.props;
- const { open: wasOpen, suppress: wasSuppress, query: wasQuery } = this.state;
- const container = event.target;
-
- // ensure that the cursor location is unambiguous
- const cursor = this.getCursor( container );
- if ( ! cursor ) {
- return;
- }
- // look for the trigger prefix and search query just before the cursor location
- const match = this.findMatch( container, cursor, completers, wasOpen );
- const { open, query, range } = match || {};
- // asynchronously load the options for the open completer
- if ( open && ( ! wasOpen || open.idx !== wasOpen.idx || query !== wasQuery ) ) {
- if ( open.isDebounced ) {
- this.debouncedLoadOptions( open, query );
- } else {
- this.loadOptions( open, query );
- }
- }
- // create a regular expression to filter the options
- const search = open ? new RegExp( '(?:\\b|\\s|^)' + escapeRegExp( query ), 'i' ) : /./;
- // filter the options we already have
- const filteredOptions = open ? filterOptions( search, this.state[ 'options_' + open.idx ] ) : [];
- // check if we should still suppress the popover
- const suppress = ( open && wasSuppress === open.idx ) ? wasSuppress : undefined;
- // update the state
- if ( wasOpen || open ) {
- this.setState( { selectedIndex: 0, filteredOptions, suppress, search, open, query, range } );
- }
- // announce the count of filtered options but only if they have loaded
- if ( open && this.state[ 'options_' + open.idx ] ) {
- this.announce( filteredOptions );
- }
- }
-
handleKeyDown( event ) {
const { open, suppress, selectedIndex, filteredOptions } = this.state;
if ( ! open ) {
@@ -568,15 +358,6 @@ export class Autocomplete extends Component {
event.stopPropagation();
}
- getWordRect() {
- const { range } = this.state;
- if ( ! range ) {
- return;
- }
-
- return range.getBoundingClientRect();
- }
-
toggleKeyEvents( isListening ) {
// This exists because we must capture ENTER key presses before RichText.
// It seems that react fires the simulated capturing events after the
@@ -588,10 +369,53 @@ export class Autocomplete extends Component {
}
componentDidUpdate( prevProps, prevState ) {
- const { open } = this.state;
+ const { record, completers } = this.props;
+ const { record: prevRecord } = prevProps;
const { open: prevOpen } = prevState;
- if ( ( ! open ) !== ( ! prevOpen ) ) {
- this.toggleKeyEvents( ! ! open );
+
+ if ( ( ! this.state.open ) !== ( ! prevOpen ) ) {
+ this.toggleKeyEvents( ! ! this.state.open );
+ }
+
+ if ( richTextStructure.isCollapsed( record ) ) {
+ const text = richTextStructure.getTextContent( record );
+ const prevText = richTextStructure.getTextContent( prevRecord );
+ const textToSearch = text.slice( 0, record.selection.start );
+ const prevTextToSearch = prevText.slice( 0, prevRecord.selection.start );
+
+ if ( textToSearch !== prevTextToSearch ) {
+ const allCompleters = map( completers, ( completer, idx ) => ( { ...completer, idx } ) );
+ const open = find( allCompleters, ( settings ) => settings.test( textToSearch ) );
+
+ if ( ! open ) {
+ return;
+ }
+
+ const query = open.getQuery( textToSearch );
+ const { open: wasOpen, suppress: wasSuppress, query: wasQuery } = this.state;
+
+ if ( open && ( ! wasOpen || open.idx !== wasOpen.idx || query !== wasQuery ) ) {
+ if ( open.isDebounced ) {
+ this.debouncedLoadOptions( open, query );
+ } else {
+ this.loadOptions( open, query );
+ }
+ }
+ // create a regular expression to filter the options
+ const search = open ? new RegExp( '(?:\\b|\\s|^)' + escapeRegExp( query ), 'i' ) : /./;
+ // filter the options we already have
+ const filteredOptions = open ? filterOptions( search, this.state[ 'options_' + open.idx ] ) : [];
+ // check if we should still suppress the popover
+ const suppress = ( open && wasSuppress === open.idx ) ? wasSuppress : undefined;
+ // update the state
+ if ( wasOpen || open ) {
+ this.setState( { selectedIndex: 0, filteredOptions, suppress, search, open, query } );
+ }
+ // announce the count of filtered options but only if they have loaded
+ if ( open && this.state[ 'options_' + open.idx ] ) {
+ this.announce( filteredOptions );
+ }
+ }
}
}
@@ -608,12 +432,12 @@ export class Autocomplete extends Component {
const isExpanded = suppress !== idx && filteredOptions.length > 0;
const listBoxId = isExpanded ? `components-autocomplete-listbox-${ instanceId }` : null;
const activeId = isExpanded ? `components-autocomplete-item-${ instanceId }-${ selectedKey }` : null;
+
// Disable reason: Clicking the editor should reset the autocomplete when the menu is suppressed
/* eslint-disable jsx-a11y/no-static-element-interactions, jsx-a11y/onclick-has-role, jsx-a11y/click-events-have-key-events */
return (
@@ -624,7 +448,7 @@ export class Autocomplete extends Component {
onClose={ this.reset }
position="top right"
className="components-autocomplete__popover"
- getAnchorRect={ this.getWordRect }
+ getAnchorRect={ getCaretRect }
>
- { children }
-
- );
- }
-}
-
-function makeAutocompleter( completers, {
- AutocompleteComponent = Autocomplete,
- mountImplementation = mount,
- onReplace = noop,
-} = {} ) {
- return mountImplementation(
-
- { ( { isExpanded, listBoxId, activeId } ) => (
-
- ) }
-
- );
-}
-
-/**
- * Create a text node.
- *
- * @param {string} text Text of text node.
-
- * @return {Node} A text node.
- */
-function tx( text ) {
- return document.createTextNode( text );
-}
-
-/**
- * Create a paragraph node with the arguments as children.
-
- * @return {Node} A paragraph node.
- */
-function par( /* arguments */ ) {
- const p = document.createElement( 'p' );
- Array.from( arguments ).forEach( ( element ) => p.appendChild( element ) );
- return p;
-}
-
-/**
- * Simulate typing into the fake editor by updating the content and simulating
- * an input event. It also updates the data-cursor attribute which is used to
- * simulate the cursor position in the test mocks.
- *
- * @param {*} wrapper Enzyme wrapper around react node
- * containing a FakeEditor.
- * @param {Array.
} nodeList Array of dom nodes.
- * @param {Array.} cursorPosition Array specifying the child indexes and
- * offset of the cursor.
- */
-function simulateInput( wrapper, nodeList, cursorPosition ) {
- // update the editor content
- const fakeEditor = wrapper.getDOMNode().querySelector( '.fake-editor' );
- fakeEditor.innerHTML = '';
- nodeList.forEach( ( element ) => fakeEditor.appendChild( element ) );
- if ( cursorPosition && cursorPosition.length >= 1 ) {
- fakeEditor.setAttribute( 'data-cursor', cursorPosition.join( ',' ) );
- } else {
- fakeEditor.removeAttribute( 'data-cursor' );
- }
- // simulate input event
- wrapper.find( '.fake-editor' ).simulate( 'input', {
- target: fakeEditor,
- } );
- wrapper.update();
-}
-
-/**
- * Same as simulateInput except configured for use with React.TestUtils
- * @param {*} wrapper Wrapper around react node
- * containing a FakeEditor.
- * @param {Array.} nodeList Array of dom nodes.
- * @param {Array.} cursorPosition Array specifying the child indexes and
- * offset of the cursor.
- */
-function simulateInputForUtils( wrapper, nodeList, cursorPosition ) {
- // update the editor content
- const fakeEditor = TestUtils.findRenderedDOMComponentWithClass(
- wrapper,
- 'fake-editor'
- );
- fakeEditor.innerHTML = '';
- nodeList.forEach( ( element ) => fakeEditor.appendChild( element ) );
- if ( cursorPosition && cursorPosition.length >= 1 ) {
- fakeEditor.setAttribute( 'data-cursor', cursorPosition.join( ',' ) );
- } else {
- fakeEditor.removeAttribute( 'data-cursor' );
- }
- TestUtils.Simulate.input(
- fakeEditor,
- {
- target: fakeEditor,
- }
- );
-}
-
-/**
- * Fire a native keydown event on the fake editor in the wrapper.
- *
- * @param {*} wrapper The wrapper containing the FakeEditor where the event will
- * be dispatched.
- * @param {*} keyCode The keycode of the key event.
- */
-function simulateKeydown( wrapper, keyCode ) {
- const fakeEditor = wrapper.getDOMNode().querySelector( '.fake-editor' );
- const event = new KeyboardEvent( 'keydown', { keyCode } ); // eslint-disable-line
- fakeEditor.dispatchEvent( event );
- wrapper.update();
-}
-
-/**
- * Check that the autocomplete matches the initial state.
- *
- * @param {*} wrapper The enzyme react wrapper.
- */
-function expectInitialState( wrapper ) {
- expect( wrapper.state( 'open' ) ).toBeUndefined();
- expect( wrapper.state( 'selectedIndex' ) ).toBe( 0 );
- expect( wrapper.state( 'query' ) ).toBeUndefined();
- expect( wrapper.state( 'search' ) ).toEqual( /./ );
- expect( wrapper.state( 'filteredOptions' ) ).toEqual( [] );
- expect( wrapper.find( 'Popover' ) ).toHaveLength( 0 );
- expect( wrapper.find( '.components-autocomplete__result' ) ).toHaveLength( 0 );
-}
-
-describe( 'Autocomplete', () => {
- const options = [
- {
- id: 1,
- label: 'Bananas',
- keywords: [ 'fruit' ],
- },
- {
- id: 2,
- label: 'Apple',
- keywords: [ 'fruit' ],
- },
- {
- id: 3,
- label: 'Avocado',
- keywords: [ 'fruit' ],
- },
- ];
-
- const basicCompleter = {
- options,
- getOptionLabel: ( option ) => option.label,
- getOptionKeywords: ( option ) => option.keywords,
- isOptionDisabled: ( option ) => option.isDisabled,
- };
-
- const slashCompleter = {
- triggerPrefix: '/',
- ...basicCompleter,
- };
-
- let realGetCursor, realCreateRange;
-
- beforeAll( () => {
- realGetCursor = Autocomplete.prototype.getCursor;
-
- Autocomplete.prototype.getCursor = jest.fn( ( container ) => {
- if ( container.hasAttribute( 'data-cursor' ) ) {
- // the cursor position is specified by a list of child indexes (relative to the container) and the offset
- const path = container.getAttribute( 'data-cursor' ).split( ',' ).map( ( val ) => parseInt( val, 10 ) );
- const offset = path.pop();
- let node = container;
- for ( let i = 0; i < path.length; i++ ) {
- node = container.childNodes[ path[ i ] ];
- }
- return { node, offset };
- }
- // by default we say the cursor is at the end of the editor
- return {
- node: container,
- offset: container.childNodes.length,
- };
- } );
-
- realCreateRange = Autocomplete.prototype.createRange;
-
- Autocomplete.prototype.createRange = jest.fn( ( startNode, startOffset, endNode, endOffset ) => {
- const fakeBounds = { x: 0, y: 0, width: 1, height: 1, top: 0, right: 1, bottom: 1, left: 0 };
- return {
- startNode,
- startOffset,
- endNode,
- endOffset,
- getClientRects: () => [ fakeBounds ],
- getBoundingClientRect: () => fakeBounds,
- };
- } );
- } );
-
- afterAll( () => {
- Autocomplete.prototype.getCursor = realGetCursor;
- Autocomplete.prototype.createRange = realCreateRange;
- } );
-
- describe( 'render()', () => {
- it( 'renders children', () => {
- const wrapper = makeAutocompleter( [] );
- expect( wrapper.state().open ).toBeUndefined();
- expect( wrapper.childAt( 0 ).hasClass( 'components-autocomplete' ) ).toBe( true );
- expect( wrapper.find( '.fake-editor' ) ).toHaveLength( 1 );
- } );
-
- it( 'opens on absent trigger prefix search', ( done ) => {
- const wrapper = makeAutocompleter( [ basicCompleter ] );
- expectInitialState( wrapper );
- // simulate typing 'b'
- simulateInput( wrapper, [ par( tx( 'b' ) ) ] );
- // wait for async popover display
- process.nextTick( function() {
- wrapper.update();
- expect( wrapper.state( 'open' ) ).toBeDefined();
- expect( wrapper.state( 'selectedIndex' ) ).toBe( 0 );
- expect( wrapper.state( 'query' ) ).toEqual( 'b' );
- expect( wrapper.state( 'search' ) ).toEqual( /(?:\b|\s|^)b/i );
- expect( wrapper.state( 'filteredOptions' ) ).toEqual( [
- { key: '0-0', value: options[ 0 ], label: 'Bananas', keywords: [ 'fruit' ] },
- ] );
- expect( wrapper.find( 'Popover' ) ).toHaveLength( 1 );
- expect( wrapper.find( 'Popover' ).prop( 'focusOnMount' ) ).toBe( false );
- expect( wrapper.find( 'button.components-autocomplete__result' ) ).toHaveLength( 1 );
- done();
- } );
- } );
-
- it( 'does not render popover as open if no results', ( done ) => {
- const wrapper = makeAutocompleter( [ basicCompleter ] );
- expectInitialState( wrapper );
- // simulate typing 'zzz'
- simulateInput( wrapper, [ tx( 'zzz' ) ] );
- // wait for async popover display
- process.nextTick( () => {
- wrapper.update();
- // now check that we've opened the popup and filtered the options to empty
- expect( wrapper.state( 'open' ) ).toBeDefined();
- expect( wrapper.state( 'query' ) ).toEqual( 'zzz' );
- expect( wrapper.state( 'search' ) ).toEqual( /(?:\b|\s|^)zzz/i );
- expect( wrapper.state( 'filteredOptions' ) ).toEqual( [] );
- expect( wrapper.find( 'Popover' ) ).toHaveLength( 0 );
- expect( wrapper.find( 'button.components-autocomplete__result' ) ).toHaveLength( 0 );
- done();
- } );
- } );
-
- it( 'does not open without trigger prefix', ( done ) => {
- const wrapper = makeAutocompleter( [ slashCompleter ] );
- expectInitialState( wrapper );
- // simulate typing 'b'
- simulateInput( wrapper, [ par( tx( 'b' ) ) ] );
- // wait for async popover display
- process.nextTick( () => {
- wrapper.update();
- // now check that the popup is not open
- expectInitialState( wrapper );
- done();
- } );
- } );
-
- it( 'opens on trigger prefix search', ( done ) => {
- const wrapper = makeAutocompleter( [ slashCompleter ] );
- expectInitialState( wrapper );
- // simulate typing '/'
- simulateInput( wrapper, [ par( tx( '/' ) ) ] );
- // wait for async popover display
- process.nextTick( () => {
- wrapper.update();
- // now check that we've opened the popup and filtered the options
- expect( wrapper.state( 'open' ) ).toBeDefined();
- expect( wrapper.state( 'selectedIndex' ) ).toBe( 0 );
- expect( wrapper.state( 'query' ) ).toEqual( '' );
- expect( wrapper.state( 'search' ) ).toEqual( /(?:\b|\s|^)/i );
- expect( wrapper.state( 'filteredOptions' ) ).toEqual( [
- { key: '0-0', value: options[ 0 ], label: 'Bananas', keywords: [ 'fruit' ] },
- { key: '0-1', value: options[ 1 ], label: 'Apple', keywords: [ 'fruit' ] },
- { key: '0-2', value: options[ 2 ], label: 'Avocado', keywords: [ 'fruit' ] },
- ] );
- expect( wrapper.find( 'Popover' ) ).toHaveLength( 1 );
- expect( wrapper.find( 'button.components-autocomplete__result' ) ).toHaveLength( 3 );
- done();
- } );
- } );
-
- it( 'searches by keywords', ( done ) => {
- const wrapper = makeAutocompleter( [ basicCompleter ] );
- expectInitialState( wrapper );
- // simulate typing fruit (split over 2 text nodes because these things happen)
- simulateInput( wrapper, [ par( tx( 'fru' ), tx( 'it' ) ) ] );
- // wait for async popover display
- process.nextTick( () => {
- wrapper.update();
- // now check that we've opened the popup and filtered the options
- expect( wrapper.state( 'open' ) ).toBeDefined();
- expect( wrapper.state( 'selectedIndex' ) ).toBe( 0 );
- expect( wrapper.state( 'query' ) ).toEqual( 'fruit' );
- expect( wrapper.state( 'search' ) ).toEqual( /(?:\b|\s|^)fruit/i );
- expect( wrapper.state( 'filteredOptions' ) ).toEqual( [
- { key: '0-0', value: options[ 0 ], label: 'Bananas', keywords: [ 'fruit' ] },
- { key: '0-1', value: options[ 1 ], label: 'Apple', keywords: [ 'fruit' ] },
- { key: '0-2', value: options[ 2 ], label: 'Avocado', keywords: [ 'fruit' ] },
- ] );
- expect( wrapper.find( 'Popover' ) ).toHaveLength( 1 );
- expect( wrapper.find( 'button.components-autocomplete__result' ) ).toHaveLength( 3 );
- done();
- } );
- } );
-
- it( 'closes when search ends (whitespace)', ( done ) => {
- const wrapper = makeAutocompleter( [ basicCompleter ] );
- expectInitialState( wrapper );
- // simulate typing 'a'
- simulateInput( wrapper, [ tx( 'a' ) ] );
- // wait for async popover display
- process.nextTick( () => {
- wrapper.update();
- // now check that we've opened the popup and all options are displayed
- expect( wrapper.state( 'open' ) ).toBeDefined();
- expect( wrapper.state( 'selectedIndex' ) ).toBe( 0 );
- expect( wrapper.state( 'query' ) ).toEqual( 'a' );
- expect( wrapper.state( 'search' ) ).toEqual( /(?:\b|\s|^)a/i );
- expect( wrapper.state( 'filteredOptions' ) ).toEqual( [
- { key: '0-1', value: options[ 1 ], label: 'Apple', keywords: [ 'fruit' ] },
- { key: '0-2', value: options[ 2 ], label: 'Avocado', keywords: [ 'fruit' ] },
- ] );
- expect( wrapper.find( 'Popover' ) ).toHaveLength( 1 );
- expect( wrapper.find( 'button.components-autocomplete__result' ) ).toHaveLength( 2 );
- // simulate typing 'p'
- simulateInput( wrapper, [ tx( 'ap' ) ] );
- // now check that the popup is still open and we've filtered the options to just the apple
- expect( wrapper.state( 'open' ) ).toBeDefined();
- expect( wrapper.state( 'selectedIndex' ) ).toBe( 0 );
- expect( wrapper.state( 'query' ) ).toEqual( 'ap' );
- expect( wrapper.state( 'search' ) ).toEqual( /(?:\b|\s|^)ap/i );
- expect( wrapper.state( 'filteredOptions' ) ).toEqual( [
- { key: '0-1', value: options[ 1 ], label: 'Apple', keywords: [ 'fruit' ] },
- ] );
- expect( wrapper.find( 'Popover' ) ).toHaveLength( 1 );
- expect( wrapper.find( 'button.components-autocomplete__result' ) ).toHaveLength( 1 );
- // simulate typing ' '
- simulateInput( wrapper, [ tx( 'ap ' ) ] );
- // check the popup closes
- expectInitialState( wrapper );
- done();
- } );
- } );
-
- it( 'renders options provided via array', ( done ) => {
- const wrapper = makeAutocompleter( [
- { ...slashCompleter, options },
- ] );
- expectInitialState( wrapper );
- // simulate typing '/'
- simulateInput( wrapper, [ par( tx( '/' ) ) ] );
- // wait for async popover display
- process.nextTick( () => {
- wrapper.update();
-
- expect( wrapper.find( 'Popover' ) ).toHaveLength( 1 );
-
- const itemWrappers = wrapper.find( 'button.components-autocomplete__result' );
- expect( itemWrappers ).toHaveLength( 3 );
-
- const expectedLabelContent = options.map( ( o ) => o.label );
- const actualLabelContent = itemWrappers.map( ( itemWrapper ) => itemWrapper.text() );
- expect( actualLabelContent ).toEqual( expectedLabelContent );
-
- done();
- } );
- } );
- it( 'renders options provided via function that returns array', ( done ) => {
- const optionsMock = jest.fn( () => options );
-
- const wrapper = makeAutocompleter( [
- { ...slashCompleter, options: optionsMock },
- ] );
- expectInitialState( wrapper );
- // simulate typing '/'
- simulateInput( wrapper, [ par( tx( '/' ) ) ] );
- // wait for async popover display
- process.nextTick( () => {
- wrapper.update();
-
- expect( wrapper.find( 'Popover' ) ).toHaveLength( 1 );
-
- const itemWrappers = wrapper.find( 'button.components-autocomplete__result' );
- expect( itemWrappers ).toHaveLength( 3 );
-
- const expectedLabelContent = options.map( ( o ) => o.label );
- const actualLabelContent = itemWrappers.map( ( itemWrapper ) => itemWrapper.text() );
- expect( actualLabelContent ).toEqual( expectedLabelContent );
-
- done();
- } );
- } );
- it( 'renders options provided via function that returns promise', ( done ) => {
- const optionsMock = jest.fn( () => Promise.resolve( options ) );
-
- const wrapper = makeAutocompleter( [
- { ...slashCompleter, options: optionsMock },
- ] );
- expectInitialState( wrapper );
- // simulate typing '/'
- simulateInput( wrapper, [ par( tx( '/' ) ) ] );
- // wait for async popover display
- process.nextTick( () => {
- wrapper.update();
-
- expect( wrapper.find( 'Popover' ) ).toHaveLength( 1 );
-
- const itemWrappers = wrapper.find( 'button.components-autocomplete__result' );
- expect( itemWrappers ).toHaveLength( 3 );
-
- const expectedLabelContent = options.map( ( o ) => o.label );
- const actualLabelContent = itemWrappers.map( ( itemWrapper ) => itemWrapper.text() );
- expect( actualLabelContent ).toEqual( expectedLabelContent );
-
- done();
- } );
- } );
-
- it( 'set the disabled attribute on results', ( done ) => {
- const wrapper = makeAutocompleter( [
- {
- ...slashCompleter,
- options: [
- {
- id: 1,
- label: 'Bananas',
- keywords: [ 'fruit' ],
- isDisabled: true,
- },
- {
- id: 2,
- label: 'Apple',
- keywords: [ 'fruit' ],
- isDisabled: false,
- },
- ],
- },
- ] );
- expectInitialState( wrapper );
- // simulate typing '/'
- simulateInput( wrapper, [ par( tx( '/' ) ) ] );
- // wait for async popover display
- process.nextTick( () => {
- wrapper.update();
-
- const firstItem = wrapper.find( 'button.components-autocomplete__result' ).at( 0 ).getDOMNode();
- expect( firstItem.hasAttribute( 'disabled' ) ).toBe( true );
-
- const secondItem = wrapper.find( 'button.components-autocomplete__result' ).at( 1 ).getDOMNode();
- expect( secondItem.hasAttribute( 'disabled' ) ).toBe( false );
-
- done();
- } );
- } );
-
- it( 'navigates options by arrow keys', ( done ) => {
- const wrapper = makeAutocompleter( [ slashCompleter ] );
- // listen to keydown events on the editor to see if it gets them
- const editorKeydown = jest.fn();
- const fakeEditor = wrapper.getDOMNode().querySelector( '.fake-editor' );
- fakeEditor.addEventListener( 'keydown', editorKeydown, false );
- expectInitialState( wrapper );
- // the menu is not open so press an arrow and see if the editor gets it
- expect( editorKeydown ).not.toHaveBeenCalled();
- simulateKeydown( wrapper, DOWN );
- expect( editorKeydown ).toHaveBeenCalledTimes( 1 );
- // clear the call count
- editorKeydown.mockClear();
- // simulate typing '/', the menu is open so the editor should not get key down events
- simulateInput( wrapper, [ par( tx( '/' ) ) ] );
- // wait for async popover display
- process.nextTick( () => {
- wrapper.update();
- expect( wrapper.state( 'selectedIndex' ) ).toBe( 0 );
- simulateKeydown( wrapper, DOWN );
- expect( wrapper.state( 'selectedIndex' ) ).toBe( 1 );
- simulateKeydown( wrapper, DOWN );
- expect( wrapper.state( 'selectedIndex' ) ).toBe( 2 );
- simulateKeydown( wrapper, DOWN );
- expect( wrapper.state( 'selectedIndex' ) ).toBe( 0 );
- simulateKeydown( wrapper, UP );
- expect( wrapper.state( 'selectedIndex' ) ).toBe( 2 );
- simulateKeydown( wrapper, UP );
- expect( wrapper.state( 'selectedIndex' ) ).toBe( 1 );
- simulateKeydown( wrapper, UP );
- expect( wrapper.state( 'selectedIndex' ) ).toBe( 0 );
- simulateKeydown( wrapper, UP );
- expect( wrapper.state( 'selectedIndex' ) ).toBe( 2 );
- expect( editorKeydown ).not.toHaveBeenCalled();
- done();
- } );
- } );
-
- it( 'resets selected index on subsequent search', ( done ) => {
- const wrapper = makeAutocompleter( [ slashCompleter ] );
- expectInitialState( wrapper );
- // simulate typing '/'
- simulateInput( wrapper, [ par( tx( '/' ) ) ] );
- // wait for async popover display
- process.nextTick( () => {
- wrapper.update();
- expect( wrapper.state( 'selectedIndex' ) ).toBe( 0 );
- simulateKeydown( wrapper, DOWN );
- expect( wrapper.state( 'selectedIndex' ) ).toBe( 1 );
- // simulate typing 'f
- simulateInput( wrapper, [ par( tx( '/f' ) ) ] );
- expect( wrapper.state( 'selectedIndex' ) ).toBe( 0 );
- done();
- } );
- } );
-
- it( 'closes by escape', ( done ) => {
- const wrapper = makeAutocompleter( [ slashCompleter ] );
- // listen to keydown events on the editor to see if it gets them
- const editorKeydown = jest.fn();
- const fakeEditor = wrapper.getDOMNode().querySelector( '.fake-editor' );
- fakeEditor.addEventListener( 'keydown', editorKeydown, false );
- expectInitialState( wrapper );
- // the menu is not open so press escape and see if the editor gets it
- expect( editorKeydown ).not.toHaveBeenCalled();
- simulateKeydown( wrapper, ESCAPE );
- expect( editorKeydown ).toHaveBeenCalledTimes( 1 );
- // clear the call count
- editorKeydown.mockClear();
- // simulate typing '/'
- simulateInput( wrapper, [ par( tx( '/' ) ) ] );
- // wait for async popover display
- process.nextTick( () => {
- wrapper.update();
- // menu should be open with all options
- expect( wrapper.state( 'open' ) ).toBeDefined();
- expect( wrapper.state( 'suppress' ) ).toBeUndefined();
- expect( wrapper.state( 'selectedIndex' ) ).toBe( 0 );
- expect( wrapper.state( 'query' ) ).toEqual( '' );
- expect( wrapper.state( 'search' ) ).toEqual( /(?:\b|\s|^)/i );
- expect( wrapper.state( 'filteredOptions' ) ).toEqual( [
- { key: '0-0', value: options[ 0 ], label: 'Bananas', keywords: [ 'fruit' ] },
- { key: '0-1', value: options[ 1 ], label: 'Apple', keywords: [ 'fruit' ] },
- { key: '0-2', value: options[ 2 ], label: 'Avocado', keywords: [ 'fruit' ] },
- ] );
- // pressing escape should suppress the dialog but it maintains the state
- simulateKeydown( wrapper, ESCAPE );
- expect( wrapper.state( 'suppress' ) ).toEqual( 0 );
- expect( wrapper.state( 'filteredOptions' ) ).toEqual( [
- { key: '0-0', value: options[ 0 ], label: 'Bananas', keywords: [ 'fruit' ] },
- { key: '0-1', value: options[ 1 ], label: 'Apple', keywords: [ 'fruit' ] },
- { key: '0-2', value: options[ 2 ], label: 'Avocado', keywords: [ 'fruit' ] },
- ] );
- expect( wrapper.find( 'Popover' ) ).toHaveLength( 0 );
- // the editor should not have gotten the event
- expect( editorKeydown ).not.toHaveBeenCalled();
- done();
- } );
- } );
-
- it( 'closes by blur', () => {
- jest.spyOn( Autocomplete.prototype, 'handleFocusOutside' );
- // required because TestUtils doesn't handle stateless components for some
- // reason. Without this, wrapper would end up with the value of null.
- class Enhanced extends Component {
- render() {
- return ;
- }
- }
-
- const wrapper = makeAutocompleter( [], {
- AutocompleteComponent: Enhanced,
- mountImplementation: TestUtils.renderIntoDocument,
- } );
- simulateInputForUtils( wrapper, [ par( tx( '/' ) ) ] );
- TestUtils.Simulate.blur(
- TestUtils.findRenderedDOMComponentWithClass(
- wrapper,
- 'fake-editor'
- )
- );
-
- jest.runAllTimers();
-
- expect( Autocomplete.prototype.handleFocusOutside ).toHaveBeenCalled();
- } );
-
- it( 'selects by enter', ( done ) => {
- const getOptionCompletion = jest.fn().mockReturnValue( {
- action: 'non-existent-action',
- value: 'dummy-value',
- } );
- const wrapper = makeAutocompleter( [ { ...slashCompleter, getOptionCompletion } ] );
- // listen to keydown events on the editor to see if it gets them
- const editorKeydown = jest.fn();
- const fakeEditor = wrapper.getDOMNode().querySelector( '.fake-editor' );
- fakeEditor.addEventListener( 'keydown', editorKeydown, false );
- expectInitialState( wrapper );
- // the menu is not open so press enter and see if the editor gets it
- expect( editorKeydown ).not.toHaveBeenCalled();
- simulateKeydown( wrapper, ENTER );
- expect( editorKeydown ).toHaveBeenCalledTimes( 1 );
- // clear the call count
- editorKeydown.mockClear();
- // simulate typing '/'
- simulateInput( wrapper, [ par( tx( '/' ) ) ] );
- // wait for async popover display
- process.nextTick( () => {
- wrapper.update();
- // menu should be open with all options
- expect( wrapper.state( 'open' ) ).toBeDefined();
- expect( wrapper.state( 'selectedIndex' ) ).toBe( 0 );
- expect( wrapper.state( 'query' ) ).toEqual( '' );
- expect( wrapper.state( 'search' ) ).toEqual( /(?:\b|\s|^)/i );
- expect( wrapper.state( 'filteredOptions' ) ).toEqual( [
- { key: '0-0', value: options[ 0 ], label: 'Bananas', keywords: [ 'fruit' ] },
- { key: '0-1', value: options[ 1 ], label: 'Apple', keywords: [ 'fruit' ] },
- { key: '0-2', value: options[ 2 ], label: 'Avocado', keywords: [ 'fruit' ] },
- ] );
- // pressing enter should reset and call getOptionCompletion
- simulateKeydown( wrapper, ENTER );
- expectInitialState( wrapper );
- expect( getOptionCompletion ).toHaveBeenCalled();
- // the editor should not have gotten the event
- expect( editorKeydown ).not.toHaveBeenCalled();
- done();
- } );
- } );
-
- it( 'does not select when option is disabled', ( done ) => {
- const getOptionCompletion = jest.fn();
- const testOptions = [
- {
- id: 1,
- label: 'Bananas',
- keywords: [ 'fruit' ],
- isDisabled: true,
- },
- {
- id: 2,
- label: 'Apple',
- keywords: [ 'fruit' ],
- isDisabled: false,
- },
- ];
- const wrapper = makeAutocompleter( [ { ...slashCompleter, getOptionCompletion, options: testOptions } ] );
- // listen to keydown events on the editor to see if it gets them
- const editorKeydown = jest.fn();
- const fakeEditor = wrapper.getDOMNode().querySelector( '.fake-editor' );
- fakeEditor.addEventListener( 'keydown', editorKeydown, false );
- expectInitialState( wrapper );
- // the menu is not open so press enter and see if the editor gets it
- expect( editorKeydown ).not.toHaveBeenCalled();
- simulateKeydown( wrapper, ENTER );
- expect( editorKeydown ).toHaveBeenCalledTimes( 1 );
- // clear the call count
- editorKeydown.mockClear();
- // simulate typing '/'
- simulateInput( wrapper, [ par( tx( '/' ) ) ] );
- // wait for async popover display
- process.nextTick( () => {
- wrapper.update();
- // menu should be open with all options
- expect( wrapper.state( 'open' ) ).toBeDefined();
- expect( wrapper.state( 'selectedIndex' ) ).toBe( 0 );
- expect( wrapper.state( 'query' ) ).toEqual( '' );
- expect( wrapper.state( 'search' ) ).toEqual( /(?:\b|\s|^)/i );
- expect( wrapper.state( 'filteredOptions' ) ).toEqual( [
- { key: '0-0', value: testOptions[ 0 ], label: 'Bananas', keywords: [ 'fruit' ], isDisabled: true },
- { key: '0-1', value: testOptions[ 1 ], label: 'Apple', keywords: [ 'fruit' ], isDisabled: false },
- ] );
- // pressing enter should NOT reset and NOT call getOptionCompletion
- simulateKeydown( wrapper, ENTER );
- expect( wrapper.state( 'open' ) ).toBeDefined();
- expect( getOptionCompletion ).not.toHaveBeenCalled();
- // the editor should not have gotten the event
- expect( editorKeydown ).not.toHaveBeenCalled();
- done();
- } );
- } );
-
- it( "doesn't otherwise interfere with keydown behavior", ( done ) => {
- const wrapper = makeAutocompleter( [ slashCompleter ] );
- // listen to keydown events on the editor to see if it gets them
- const editorKeydown = jest.fn();
- const fakeEditor = wrapper.getDOMNode().querySelector( '.fake-editor' );
- fakeEditor.addEventListener( 'keydown', editorKeydown, false );
- expectInitialState( wrapper );
- [ UP, DOWN, ENTER, ESCAPE, SPACE ].forEach( ( keyCode, i ) => {
- simulateKeydown( wrapper, keyCode );
- expect( editorKeydown ).toHaveBeenCalledTimes( i + 1 );
- } );
- expect( editorKeydown ).toHaveBeenCalledTimes( 5 );
- done();
- } );
-
- it( 'selects by click', ( done ) => {
- const getOptionCompletion = jest.fn().mockReturnValue( {
- action: 'non-existent-action',
- value: 'dummy-value',
- } );
- const wrapper = makeAutocompleter( [ { ...slashCompleter, getOptionCompletion } ] );
- expectInitialState( wrapper );
- // simulate typing '/'
- simulateInput( wrapper, [ par( tx( '/' ) ) ] );
- // wait for async popover display
- process.nextTick( () => {
- wrapper.update();
- // menu should be open with all options
- expect( wrapper.state( 'open' ) ).toBeDefined();
- expect( wrapper.state( 'selectedIndex' ) ).toBe( 0 );
- expect( wrapper.state( 'query' ) ).toEqual( '' );
- expect( wrapper.state( 'search' ) ).toEqual( /(?:\b|\s|^)/i );
- expect( wrapper.state( 'filteredOptions' ) ).toEqual( [
- { key: '0-0', value: options[ 0 ], label: 'Bananas', keywords: [ 'fruit' ] },
- { key: '0-1', value: options[ 1 ], label: 'Apple', keywords: [ 'fruit' ] },
- { key: '0-2', value: options[ 2 ], label: 'Avocado', keywords: [ 'fruit' ] },
- ] );
- // clicking should reset and select the item
- wrapper.find( '.components-autocomplete__result Button' ).at( 0 ).simulate( 'click' );
- wrapper.update();
- expectInitialState( wrapper );
- expect( getOptionCompletion ).toHaveBeenCalled();
- done();
- } );
- } );
-
- it( 'calls insertCompletion for a completion with action `insert-at-caret`', ( done ) => {
- const getOptionCompletion = jest.fn()
- .mockReturnValueOnce( {
- action: 'insert-at-caret',
- value: 'expected-value',
- } );
-
- const insertCompletion = jest.fn();
-
- const wrapper = makeAutocompleter(
- [ { ...slashCompleter, getOptionCompletion } ],
- {
- AutocompleteComponent: class extends Autocomplete {
- insertCompletion( ...args ) {
- return insertCompletion( ...args );
- }
- },
- }
- );
- expectInitialState( wrapper );
-
- // simulate typing '/'
- simulateInput( wrapper, [ par( tx( '/' ) ) ] );
-
- // wait for async popover display
- process.nextTick( () => {
- wrapper.update();
- // menu should be open with at least one option
- expect( wrapper.state( 'open' ) ).toBeDefined();
- expect( wrapper.state( 'filteredOptions' ).length ).toBeGreaterThanOrEqual( 1 );
-
- // clicking should reset and select the item
- wrapper.find( '.components-autocomplete__result Button' ).at( 0 ).simulate( 'click' );
- wrapper.update();
-
- expect( insertCompletion ).toHaveBeenCalledTimes( 1 );
- expect( insertCompletion.mock.calls[ 0 ][ 1 ] ).toBe( 'expected-value' );
- done();
- } );
- } );
-
- it( 'calls insertCompletion for a completion without an action property', ( done ) => {
- const getOptionCompletion = jest.fn().mockReturnValueOnce( 'expected-value' );
-
- const insertCompletion = jest.fn();
-
- const wrapper = makeAutocompleter(
- [ { ...slashCompleter, getOptionCompletion } ],
- {
- AutocompleteComponent: class extends Autocomplete {
- insertCompletion( ...args ) {
- return insertCompletion( ...args );
- }
- },
- }
- );
- expectInitialState( wrapper );
-
- // simulate typing '/'
- simulateInput( wrapper, [ par( tx( '/' ) ) ] );
-
- // wait for async popover display
- process.nextTick( () => {
- wrapper.update();
- // menu should be open with at least one option
- expect( wrapper.state( 'open' ) ).toBeDefined();
- expect( wrapper.state( 'filteredOptions' ).length ).toBeGreaterThanOrEqual( 1 );
-
- // clicking should reset and select the item
- wrapper.find( '.components-autocomplete__result Button' ).at( 0 ).simulate( 'click' );
- wrapper.update();
-
- expect( insertCompletion ).toHaveBeenCalledTimes( 1 );
- expect( insertCompletion.mock.calls[ 0 ][ 1 ] ).toBe( 'expected-value' );
- done();
- } );
- } );
-
- it( 'calls onReplace for a completion with action `replace`', ( done ) => {
- const getOptionCompletion = jest.fn()
- .mockReturnValueOnce( {
- action: 'replace',
- value: 'expected-value',
- } );
-
- const onReplace = jest.fn();
-
- const wrapper = makeAutocompleter(
- [ { ...slashCompleter, getOptionCompletion } ],
- { onReplace } );
- expectInitialState( wrapper );
- // simulate typing '/'
- simulateInput( wrapper, [ par( tx( '/' ) ) ] );
-
- // wait for async popover display
- process.nextTick( () => {
- wrapper.update();
- // menu should be open with at least one option
- expect( wrapper.state( 'open' ) ).toBeDefined();
- expect( wrapper.state( 'filteredOptions' ).length ).toBeGreaterThanOrEqual( 1 );
-
- // clicking should reset and select the item
- wrapper.find( '.components-autocomplete__result Button' ).at( 0 ).simulate( 'click' );
- wrapper.update();
-
- expect( onReplace ).toHaveBeenCalledTimes( 1 );
- expect( onReplace ).toHaveBeenLastCalledWith( [ 'expected-value' ] );
- done();
- } );
- } );
- } );
-} );
diff --git a/packages/editor/src/components/autocompleters/block.js b/packages/editor/src/components/autocompleters/block.js
index 01b26951b08cec..dd849106dd13fa 100644
--- a/packages/editor/src/components/autocompleters/block.js
+++ b/packages/editor/src/components/autocompleters/block.js
@@ -58,7 +58,17 @@ export function createBlockCompleter( {
return {
name: 'blocks',
className: 'editor-autocompleters__block',
- triggerPrefix: '/',
+ test( string ) {
+ if ( string.indexOf( '/' ) !== 0 ) {
+ return false;
+ }
+
+ return /^\/\w*$/.test( string );
+ },
+ getQuery( string ) {
+ const match = string.match( /^\/(\w*)$/ );
+ return match && match[ 1 ];
+ },
options() {
const selectedBlockName = getSelectedBlockName();
return getInserterItems( getBlockInsertionParentClientId() ).filter(
@@ -77,9 +87,6 @@ export function createBlockCompleter( {
title,
];
},
- allowContext( before, after ) {
- return ! ( /\S/.test( before.toString() ) || /\S/.test( after.toString() ) );
- },
getOptionCompletion( inserterItem ) {
const { name, initialAttributes } = inserterItem;
return {
diff --git a/packages/editor/src/components/autocompleters/user.js b/packages/editor/src/components/autocompleters/user.js
index 19c7527e015df8..c2f3628971a29b 100644
--- a/packages/editor/src/components/autocompleters/user.js
+++ b/packages/editor/src/components/autocompleters/user.js
@@ -11,7 +11,17 @@ import apiFetch from '@wordpress/api-fetch';
export default {
name: 'users',
className: 'editor-autocompleters__user',
- triggerPrefix: '@',
+ test( string ) {
+ if ( string.indexOf( '@' ) === -1 ) {
+ return false;
+ }
+
+ return /(?:\s|^)@\w*$/.test( string );
+ },
+ getQuery( string ) {
+ const match = string.match( /@(\w*)$/ );
+ return match && match[ 1 ];
+ },
options( search ) {
let payload = '';
if ( search ) {
@@ -30,9 +40,6 @@ export default {
{ user.slug } ,
];
},
- allowNode() {
- return true;
- },
getOptionCompletion( user ) {
return `@${ user.slug }`;
},
diff --git a/packages/editor/src/components/rich-text/format-toolbar/index.js b/packages/editor/src/components/rich-text/format-toolbar/index.js
index f588f18a309194..e1df679b66424e 100644
--- a/packages/editor/src/components/rich-text/format-toolbar/index.js
+++ b/packages/editor/src/components/rich-text/format-toolbar/index.js
@@ -14,6 +14,7 @@ import {
} from '@wordpress/components';
import { ESCAPE, LEFT, RIGHT, UP, DOWN, BACKSPACE, ENTER, displayShortcut } from '@wordpress/keycodes';
import { prependHTTP } from '@wordpress/url';
+import { richTextStructure } from '@wordpress/blocks';
/**
* Internal dependencies
@@ -28,24 +29,28 @@ const FORMATTING_CONTROLS = [
title: __( 'Bold' ),
shortcut: displayShortcut.primary( 'b' ),
format: 'bold',
+ selector: 'strong',
},
{
icon: 'editor-italic',
title: __( 'Italic' ),
shortcut: displayShortcut.primary( 'i' ),
format: 'italic',
+ selector: 'em',
},
{
icon: 'editor-strikethrough',
title: __( 'Strikethrough' ),
shortcut: displayShortcut.access( 'd' ),
format: 'strikethrough',
+ selector: 'del',
},
{
icon: 'admin-links',
title: __( 'Link' ),
shortcut: displayShortcut.primary( 'k' ),
format: 'link',
+ selector: 'a',
},
];
@@ -118,9 +123,16 @@ class FormatToolbar extends Component {
toggleFormat( format ) {
return () => {
- this.props.onChange( {
- [ format ]: ! this.props.formats[ format ],
- } );
+ const { record } = this.props;
+ let newRecord;
+
+ if ( richTextStructure.getActiveFormat( record, format.type ) ) {
+ newRecord = richTextStructure.removeFormat( record, format.type );
+ } else {
+ newRecord = richTextStructure.applyFormat( record, format );
+ }
+
+ this.props.onChange( newRecord );
};
}
@@ -140,65 +152,86 @@ class FormatToolbar extends Component {
}
addLink() {
+ const newRecord = richTextStructure.applyFormat( this.props.record, {
+ type: 'a',
+ attributes: {
+ href: '',
+ },
+ } );
+
+ this.props.onChange( newRecord );
this.setState( { linkValue: '' } );
- this.props.onChange( { link: { isAdding: true } } );
}
dropLink() {
- this.props.onChange( { link: null } );
+ const newRecord = richTextStructure.removeFormat( this.props.record, 'a' );
+
+ this.props.onChange( newRecord );
this.setState( { linkValue: '' } );
}
editLink( event ) {
+ const format = richTextStructure.getActiveFormat( this.props.record, 'a' );
+
+ this.setState( { linkValue: format.attributes.href, isEditing: true } );
event.preventDefault();
- this.props.onChange( { link: { ...this.props.formats.link, isAdding: true } } );
- this.setState( { linkValue: this.props.formats.link.value } );
}
submitLink( event ) {
- event.preventDefault();
- const value = prependHTTP( this.state.linkValue );
- this.props.onChange( { link: {
- isAdding: false,
- target: this.state.opensInNewWindow ? '_blank' : null,
- rel: this.state.opensInNewWindow ? 'noreferrer noopener' : null,
- value,
- } } );
+ const { linkValue, opensInNewWindow } = this.state;
+ const { record } = this.props;
+ const href = prependHTTP( linkValue );
+ const format = {
+ type: 'a',
+ attributes: {
+ href,
+ },
+ };
- this.setState( { linkValue: value } );
- if ( ! this.props.formats.link.value ) {
- this.props.speak( __( 'Link added.' ), 'assertive' );
+ if ( opensInNewWindow ) {
+ format.attributes.target = '_blank';
+ format.attributes.rel = 'noreferrer noopener';
}
+
+ this.props.onChange( richTextStructure.applyFormat( record, format ) );
+
+ this.setState( { linkValue: href } );
+
+ // if ( ! this.props.formats.link.value ) {
+ // this.props.speak( __( 'Link added.' ), 'assertive' );
+ // }
+
+ event.preventDefault();
}
- isFormatActive( format ) {
- return this.props.formats[ format ] && this.props.formats[ format ].isActive;
+ getActiveFormat( formatType ) {
+ return richTextStructure.getActiveFormat( this.props.record, formatType );
}
render() {
- const { formats, enabledControls = DEFAULT_CONTROLS, customControls = [], selectedNodeId } = this.props;
- const { linkValue, settingsVisible, opensInNewWindow } = this.state;
- const isAddingLink = formats.link && formats.link.isAdding;
+ const { enabledControls = DEFAULT_CONTROLS, customControls = [], selectedNodeId } = this.props;
+ const { linkValue, settingsVisible, opensInNewWindow, isEditing } = this.state;
+ const link = this.getActiveFormat( 'a' );
+
+ const isEditingLink = isEditing || ( link && ( ! link.attributes || ! link.attributes.href ) );
const toolbarControls = FORMATTING_CONTROLS.concat( customControls )
.filter( ( control ) => enabledControls.indexOf( control.format ) !== -1 )
.map( ( control ) => {
if ( control.format === 'link' ) {
- const isFormatActive = this.isFormatActive( 'link' );
- const isActive = isFormatActive || isAddingLink;
return {
...control,
- icon: isFormatActive ? 'editor-unlink' : 'admin-links', // TODO: Need proper unlink icon
- title: isFormatActive ? __( 'Unlink' ) : __( 'Link' ),
- onClick: isActive ? this.dropLink : this.addLink,
- isActive,
+ icon: link ? 'editor-unlink' : 'admin-links', // TODO: Need proper unlink icon
+ title: link ? __( 'Unlink' ) : __( 'Link' ),
+ onClick: link ? this.dropLink : this.addLink,
+ isActive: !! link,
};
}
return {
...control,
- onClick: this.toggleFormat( control.format ),
- isActive: this.isFormatActive( control.format ),
+ onClick: this.toggleFormat( { type: control.selector } ),
+ isActive: !! this.getActiveFormat( control.selector ),
};
} );
@@ -215,15 +248,15 @@ class FormatToolbar extends Component {
- { ( isAddingLink || formats.link ) && (
+ { link && (
- { isAddingLink && (
+ { isEditingLink && (
// Disable reason: KeyPress must be suppressed so the block doesn't hide the toolbar
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */