diff --git a/packages/block-editor/src/components/block-controls/test/__snapshots__/index.js.snap b/packages/block-editor/src/components/block-controls/test/__snapshots__/index.js.snap
index 681c33a42d9976..ba44cc9445f656 100644
--- a/packages/block-editor/src/components/block-controls/test/__snapshots__/index.js.snap
+++ b/packages/block-editor/src/components/block-controls/test/__snapshots__/index.js.snap
@@ -8,6 +8,7 @@ exports[`BlockControls should render a dynamic toolbar of controls 1`] = `
"focusedElement": null,
"isSelected": true,
"name": undefined,
+ "onFocus": undefined,
"setFocusedElement": [Function],
}
}
diff --git a/packages/block-editor/src/components/block-edit/index.js b/packages/block-editor/src/components/block-edit/index.js
index c4c78f906aef91..582e3a9f3ed17c 100644
--- a/packages/block-editor/src/components/block-edit/index.js
+++ b/packages/block-editor/src/components/block-edit/index.js
@@ -31,12 +31,13 @@ class BlockEdit extends Component {
}
static getDerivedStateFromProps( props ) {
- const { clientId, name, isSelected } = props;
+ const { clientId, name, isSelected, onFocus } = props;
return {
name,
isSelected,
clientId,
+ onFocus,
};
}
diff --git a/packages/block-editor/src/components/rich-text/index.native.js b/packages/block-editor/src/components/rich-text/index.native.js
index a6d2ef19a86a56..fe7e7a33170352 100644
--- a/packages/block-editor/src/components/rich-text/index.native.js
+++ b/packages/block-editor/src/components/rich-text/index.native.js
@@ -19,6 +19,9 @@ import {
split,
toHTMLString,
insert,
+ insertLineSeparator,
+ insertLineBreak,
+ isEmptyLine,
isCollapsed,
} from '@wordpress/rich-text';
import { decodeEntities } from '@wordpress/html-entities';
@@ -31,6 +34,8 @@ import { isURL } from '@wordpress/url';
*/
import FormatEdit from './format-edit';
import FormatToolbar from './format-toolbar';
+import { withBlockEditContext } from '../block-edit/context';
+import { ListEdit } from './list-edit';
import styles from './style.scss';
@@ -70,14 +75,32 @@ const gutenbergFormatNamesToAztec = {
};
export class RichText extends Component {
- constructor() {
+ constructor( { multiline } ) {
super( ...arguments );
+
+ this.isMultiline = false;
+ if ( multiline === true || multiline === 'p' || multiline === 'li' ) {
+ this.multilineTag = multiline === true ? 'p' : multiline;
+ this.isMultiline = true;
+ }
+
+ if ( this.multilineTag === 'li' ) {
+ this.multilineWrapperTags = [ 'ul', 'ol' ];
+ }
+
+ if ( this.props.onSplit ) {
+ this.onSplit = this.props.onSplit;
+ } else if ( this.props.unstableOnSplit ) {
+ this.onSplit = this.props.unstableOnSplit;
+ }
+
this.isIOS = Platform.OS === 'ios';
this.onChange = this.onChange.bind( this );
this.onEnter = this.onEnter.bind( this );
this.onBackspace = this.onBackspace.bind( this );
this.onPaste = this.onPaste.bind( this );
this.onContentSizeChange = this.onContentSizeChange.bind( this );
+ this.onFormatChangeForceChild = this.onFormatChangeForceChild.bind( this );
this.onFormatChange = this.onFormatChange.bind( this );
// This prevents a bug in Aztec which triggers onSelectionChange twice on format change
this.onSelectionChange = this.onSelectionChange.bind( this );
@@ -120,9 +143,7 @@ export class RichText extends Component {
*
*/
splitContent( currentRecord, blocks = [], isPasted = false ) {
- const { onSplit } = this.props;
-
- if ( ! onSplit ) {
+ if ( ! this.onSplit ) {
return;
}
@@ -161,7 +182,7 @@ export class RichText extends Component {
// always update when provided with new content.
this.lastEventCount = undefined;
- onSplit( before, after, ...blocks );
+ this.onSplit( before, after, ...blocks );
}
valueToFormat( value ) {
@@ -182,7 +203,11 @@ export class RichText extends Component {
} ).map( ( name ) => gutenbergFormatNamesToAztec[ name ] ).filter( Boolean );
}
- onFormatChange( record ) {
+ onFormatChangeForceChild( record ) {
+ this.onFormatChange( record, true );
+ }
+
+ onFormatChange( record, doUpdateChild ) {
let newContent;
// valueToFormat might throw when converting the record to a tree structure
// let's ignore the event for now and force a render update so we're still in sync
@@ -204,9 +229,13 @@ export class RichText extends Component {
needsSelectionUpdate: record.needsSelectionUpdate,
} );
} else {
- // make sure the component rerenders without refreshing the text on gutenberg
- // (this can trigger other events that might update the active formats on aztec)
- this.lastEventCount = 0;
+ if ( doUpdateChild ) {
+ this.lastEventCount = undefined;
+ } else {
+ // make sure the component rerenders without refreshing the text on gutenberg
+ // (this can trigger other events that might update the active formats on aztec)
+ this.lastEventCount = 0;
+ }
this.forceUpdate();
}
}
@@ -255,17 +284,31 @@ export class RichText extends Component {
// eslint-disable-next-line no-unused-vars
onEnter( event ) {
this.lastEventCount = event.nativeEvent.eventCount;
- if ( ! this.props.onSplit ) {
- // TODO: insert the \n char instead?
- return;
- }
const currentRecord = this.createRecord( {
...event.nativeEvent,
currentContent: unescapeSpaces( event.nativeEvent.text ),
} );
- this.splitContent( currentRecord );
+ if ( this.multilineTag ) {
+ if ( event.shiftKey ) {
+ const insertedLineBreak = { needsSelectionUpdate: true, ...insertLineBreak( currentRecord ) };
+ this.onFormatChangeForceChild( insertedLineBreak );
+ } else if ( this.onSplit && isEmptyLine( currentRecord ) ) {
+ this.setState( {
+ needsSelectionUpdate: false,
+ } );
+ this.onSplit( ...split( currentRecord ).map( this.valueToFormat ) );
+ } else {
+ const insertedLineSeparator = { needsSelectionUpdate: true, ...insertLineSeparator( currentRecord ) };
+ this.onFormatChangeForceChild( insertedLineSeparator );
+ }
+ } else if ( event.shiftKey || ! this.onSplit ) {
+ const insertedLineBreak = { needsSelectionUpdate: true, ...insertLineBreak( currentRecord ) };
+ this.onFormatChangeForceChild( insertedLineBreak );
+ } else {
+ this.splitContent( currentRecord );
+ }
}
// eslint-disable-next-line no-unused-vars
@@ -446,7 +489,8 @@ export class RichText extends Component {
...create( {
html: innerContent,
range: null,
- multilineTag: false,
+ multilineTag: this.multilineTag,
+ multilineWrapperTags: this.multilineWrapperTags,
} ),
};
@@ -459,6 +503,7 @@ export class RichText extends Component {
return create( {
html: children.toHTML( value ),
multilineTag: this.multilineTag,
+ multilineWrapperTags: this.multilineWrapperTags,
} );
}
@@ -466,6 +511,7 @@ export class RichText extends Component {
return create( {
html: value,
multilineTag: this.multilineTag,
+ multilineWrapperTags: this.multilineWrapperTags,
} );
}
@@ -525,6 +571,7 @@ export class RichText extends Component {
style,
formattingControls,
isSelected,
+ onTagNameChange,
} = this.props;
const record = this.getRecord();
@@ -546,6 +593,14 @@ export class RichText extends Component {
return (
+ { isSelected && this.multilineTag === 'li' && (
+
+ ) }
{ isSelected && (
@@ -585,6 +640,7 @@ export class RichText extends Component {
fontWeight={ this.props.fontWeight }
fontStyle={ this.props.fontStyle }
disableEditingMenu={ this.props.disableEditingMenu }
+ isMultiline={ this.isMultiline }
/>
{ isSelected && }
@@ -606,13 +662,46 @@ const RichTextContainer = compose( [
formatTypes: getFormatTypes(),
};
} ),
+ withBlockEditContext( ( context, ownProps ) => {
+ // When explicitly set as not selected, do nothing.
+ if ( ownProps.isSelected === false ) {
+ return {
+ clientId: context.clientId,
+ };
+ }
+ // When explicitly set as selected, use the value stored in the context instead.
+ if ( ownProps.isSelected === true ) {
+ return {
+ isSelected: context.isSelected,
+ clientId: context.clientId,
+ };
+ }
+
+ // Ensures that only one RichText component can be focused.
+ return {
+ clientId: context.clientId,
+ isSelected: context.isSelected,
+ onFocus: context.onFocus,
+ };
+ } ),
] )( RichText );
-RichTextContainer.Content = ( { value, format, tagName: Tag, ...props } ) => {
+RichTextContainer.Content = ( { value, format, tagName: Tag, multiline, ...props } ) => {
let content;
+ let html = value;
+ let MultilineTag;
+
+ if ( multiline === true || multiline === 'p' || multiline === 'li' ) {
+ MultilineTag = multiline === true ? 'p' : multiline;
+ }
+
+ if ( ! html && MultilineTag ) {
+ html = `<${ MultilineTag }>${ MultilineTag }>`;
+ }
+
switch ( format ) {
case 'string':
- content = { value };
+ content = { html };
break;
}
diff --git a/packages/block-editor/src/components/rich-text/list-edit.native.js b/packages/block-editor/src/components/rich-text/list-edit.native.js
new file mode 100644
index 00000000000000..27b3d56ac9fdf7
--- /dev/null
+++ b/packages/block-editor/src/components/rich-text/list-edit.native.js
@@ -0,0 +1,55 @@
+/**
+ * WordPress dependencies
+ */
+
+import { Toolbar } from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+import {
+ changeListType,
+ __unstableIsListRootSelected,
+ __unstableIsActiveListType,
+} from '@wordpress/rich-text';
+
+/**
+ * Internal dependencies
+ */
+
+import BlockFormatControls from '../block-format-controls';
+
+export const ListEdit = ( {
+ onTagNameChange,
+ tagName,
+ value,
+ onChange,
+} ) => (
+
+
+
+);
diff --git a/packages/block-library/src/index.native.js b/packages/block-library/src/index.native.js
index 0326be07c92ca4..19f3ee31e231ea 100644
--- a/packages/block-library/src/index.native.js
+++ b/packages/block-library/src/index.native.js
@@ -110,6 +110,7 @@ export const registerCoreBlocks = () => {
more,
image,
nextpage,
+ list,
].forEach( ( { name, settings } ) => {
registerBlockType( name, settings );
} );
diff --git a/packages/rich-text/src/get-line-list-formats.js b/packages/rich-text/src/get-line-list-formats.js
new file mode 100644
index 00000000000000..7eff9df12e13ff
--- /dev/null
+++ b/packages/rich-text/src/get-line-list-formats.js
@@ -0,0 +1,19 @@
+/**
+ * Internal dependencies
+ */
+
+import { getLineIndex } from './get-line-index';
+
+/**
+ * Returns the list format of the line at the selection start position.
+ *
+ * @param {Object} value The rich-text value
+ *
+ * @return {Array} Array of the list formats on the selected line.
+ */
+export function getLineListFormats( value ) {
+ const { replacements, start } = value;
+ const startingLineIndex = getLineIndex( value, start );
+ const startLineFormats = replacements[ startingLineIndex ] || [];
+ return startLineFormats;
+}
diff --git a/packages/rich-text/src/index.js b/packages/rich-text/src/index.js
index 63da636257619d..53acd1179f6f67 100644
--- a/packages/rich-text/src/index.js
+++ b/packages/rich-text/src/index.js
@@ -12,6 +12,8 @@ export { getActiveObject } from './get-active-object';
export { getSelectionEnd } from './get-selection-end';
export { getSelectionStart } from './get-selection-start';
export { getTextContent } from './get-text-content';
+export { isListRootSelected as __unstableIsListRootSelected } from './is-list-root-selected';
+export { isActiveListType as __unstableIsActiveListType } from './is-active-list-type';
export { isCollapsed } from './is-collapsed';
export { isEmpty, isEmptyLine } from './is-empty';
export { join } from './join';
diff --git a/packages/rich-text/src/is-active-list-type.js b/packages/rich-text/src/is-active-list-type.js
new file mode 100644
index 00000000000000..56efa78b3c7e72
--- /dev/null
+++ b/packages/rich-text/src/is-active-list-type.js
@@ -0,0 +1,26 @@
+/**
+ * Internal dependencies
+ */
+
+import { getLineListFormats } from './get-line-list-formats';
+
+/**
+ * Wether or not the selected list has the given tag name.
+ *
+ * @param {string} tagName The tag name the list should have.
+ * @param {string} rootTagName The current root tag name, to compare with in
+ * case nothing is selected.
+ * @param {Object} value The internal rich-text value.
+ *
+ * @return {boolean} [description]
+ */
+export function isActiveListType( tagName, rootTagName, value ) {
+ const startLineFormats = getLineListFormats( value );
+ const [ deepestListFormat ] = startLineFormats.slice( -1 );
+
+ if ( ! deepestListFormat || ! deepestListFormat.type ) {
+ return tagName === rootTagName;
+ }
+
+ return deepestListFormat.type.toLowerCase() === tagName;
+}
diff --git a/packages/rich-text/src/is-list-root-selected.js b/packages/rich-text/src/is-list-root-selected.js
new file mode 100644
index 00000000000000..ba823ebbf60d7c
--- /dev/null
+++ b/packages/rich-text/src/is-list-root-selected.js
@@ -0,0 +1,18 @@
+/**
+ * Internal dependencies
+ */
+
+import { getLineListFormats } from './get-line-list-formats';
+
+/**
+ * Whether or not the root list is selected.
+ *
+ * @param {Object} value The internal rich-text value.
+ *
+ * @return {boolean} True if the root list or nothing is selected, false if an
+ * inner list is selected.
+ */
+export function isListRootSelected( value ) {
+ const startLineFormats = getLineListFormats( value );
+ return startLineFormats.length < 1;
+}