Skip to content

Commit

Permalink
Implement list block in React Native (#14636)
Browse files Browse the repository at this point in the history
* Make sure multiline property is filtered out of props on save.

* Send block edit parameters using the context.

* Add multiline variables to allow proper parsing and saving of properties.

* Add list edit toolbar options

* Add multiline property.

* Add list block to mobile gb.

* Move list-edit.native.js to new location.

* Make block edit send down the onFocus property.

* Handle case where unstableSplit is passed has prop.

* Pass multiline tags to serialiser.

* Use the format-lib for handling "Enter" in lists

* Force selection reset on split

* Add multiline wrapper tags to formatToValue.

* Remove unnecessary code.

* Force rich-text text update on list type change

* Disable indent and outdent.

* Enable toggling list type of nested lists

* Update list type toolbar button on native mobile

* Include diff missed by previous commit

* Rename to denote that it's about lines

* Split into separate functions and mark unstable

* Add missing JSDoc param

* Update snapshot for BlockControls

* Move isActiveListType, isListRootSelected to rich-text package

* Remove excess empty line
  • Loading branch information
SergioEstevao authored Apr 5, 2019
1 parent 37bc735 commit 45da3cf
Show file tree
Hide file tree
Showing 9 changed files with 230 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ exports[`BlockControls should render a dynamic toolbar of controls 1`] = `
"focusedElement": null,
"isSelected": true,
"name": undefined,
"onFocus": undefined,
"setFocusedElement": [Function],
}
}
Expand Down
3 changes: 2 additions & 1 deletion packages/block-editor/src/components/block-edit/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}

Expand Down
123 changes: 106 additions & 17 deletions packages/block-editor/src/components/rich-text/index.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ import {
split,
toHTMLString,
insert,
insertLineSeparator,
insertLineBreak,
isEmptyLine,
isCollapsed,
} from '@wordpress/rich-text';
import { decodeEntities } from '@wordpress/html-entities';
Expand All @@ -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';

Expand Down Expand Up @@ -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 );
Expand Down Expand Up @@ -120,9 +143,7 @@ export class RichText extends Component {
*
*/
splitContent( currentRecord, blocks = [], isPasted = false ) {
const { onSplit } = this.props;

if ( ! onSplit ) {
if ( ! this.onSplit ) {
return;
}

Expand Down Expand Up @@ -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 ) {
Expand All @@ -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
Expand All @@ -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();
}
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -446,7 +489,8 @@ export class RichText extends Component {
...create( {
html: innerContent,
range: null,
multilineTag: false,
multilineTag: this.multilineTag,
multilineWrapperTags: this.multilineWrapperTags,
} ),
};

Expand All @@ -459,13 +503,15 @@ export class RichText extends Component {
return create( {
html: children.toHTML( value ),
multilineTag: this.multilineTag,
multilineWrapperTags: this.multilineWrapperTags,
} );
}

if ( this.props.format === 'string' ) {
return create( {
html: value,
multilineTag: this.multilineTag,
multilineWrapperTags: this.multilineWrapperTags,
} );
}

Expand Down Expand Up @@ -525,6 +571,7 @@ export class RichText extends Component {
style,
formattingControls,
isSelected,
onTagNameChange,
} = this.props;

const record = this.getRecord();
Expand All @@ -546,6 +593,14 @@ export class RichText extends Component {

return (
<View>
{ isSelected && this.multilineTag === 'li' && (
<ListEdit
onTagNameChange={ onTagNameChange }
tagName={ tagName }
value={ record }
onChange={ this.onFormatChangeForceChild }
/>
) }
{ isSelected && (
<BlockFormatControls>
<FormatToolbar controls={ formattingControls } />
Expand Down Expand Up @@ -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 && <FormatEdit value={ record } onChange={ this.onFormatChange } /> }
</View>
Expand All @@ -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 = <RawHTML>{ value }</RawHTML>;
content = <RawHTML>{ html }</RawHTML>;
break;
}

Expand Down
55 changes: 55 additions & 0 deletions packages/block-editor/src/components/rich-text/list-edit.native.js
Original file line number Diff line number Diff line change
@@ -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,
} ) => (
<BlockFormatControls>
<Toolbar
controls={ [
onTagNameChange && {
icon: 'editor-ul',
title: __( 'Convert to unordered list' ),
isActive: __unstableIsActiveListType( 'ul', tagName, value ),
onClick() {
onChange( changeListType( value, { type: 'ul' } ) );

if ( __unstableIsListRootSelected( value ) ) {
onTagNameChange( 'ul' );
}
},
},
onTagNameChange && {
icon: 'editor-ol',
title: __( 'Convert to ordered list' ),
isActive: __unstableIsActiveListType( 'ol', tagName, value ),
onClick() {
onChange( changeListType( value, { type: 'ol' } ) );

if ( __unstableIsListRootSelected( value ) ) {
onTagNameChange( 'ol' );
}
},
},
].filter( Boolean ) }
/>
</BlockFormatControls>
);
1 change: 1 addition & 0 deletions packages/block-library/src/index.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ export const registerCoreBlocks = () => {
more,
image,
nextpage,
list,
].forEach( ( { name, settings } ) => {
registerBlockType( name, settings );
} );
Expand Down
19 changes: 19 additions & 0 deletions packages/rich-text/src/get-line-list-formats.js
Original file line number Diff line number Diff line change
@@ -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;
}
2 changes: 2 additions & 0 deletions packages/rich-text/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
26 changes: 26 additions & 0 deletions packages/rich-text/src/is-active-list-type.js
Original file line number Diff line number Diff line change
@@ -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;
}
Loading

0 comments on commit 45da3cf

Please sign in to comment.