Skip to content

Commit

Permalink
RichText: unify active formats, 'selectedFormat' and 'placeholderForm…
Browse files Browse the repository at this point in the history
…at' (#14411)

* RichText: unify active formats, 'selectedFormat' and 'placeholderFormat'

* Add extra e2e test

* Only should boundary style when focused

* Update docs
  • Loading branch information
ellatrix committed Apr 3, 2019
1 parent dee166a commit 2141e22
Show file tree
Hide file tree
Showing 14 changed files with 288 additions and 143 deletions.
133 changes: 49 additions & 84 deletions packages/block-editor/src/components/rich-text/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ import {
isCollapsed,
LINE_SEPARATOR,
indentListItems,
__unstableGetActiveFormats,
__unstableUpdateFormats,
} from '@wordpress/rich-text';
import { decodeEntities } from '@wordpress/html-entities';
import { withFilters, IsolatedEventContainer } from '@wordpress/components';
Expand Down Expand Up @@ -186,9 +188,9 @@ export class RichText extends Component {
*/
getRecord() {
const { formats, replacements, text } = this.formatToValue( this.props.value );
const { start, end, selectedFormat } = this.state;
const { start, end, activeFormats } = this.state;

return { formats, replacements, text, start, end, selectedFormat };
return { formats, replacements, text, start, end, activeFormats };
}

createRecord() {
Expand Down Expand Up @@ -365,6 +367,8 @@ export class RichText extends Component {
unstableOnFocus();
}

this.recalculateBoundaryStyle();

document.addEventListener( 'selectionchange', this.onSelectionChange );
}

Expand Down Expand Up @@ -403,48 +407,27 @@ export class RichText extends Component {
}
}

let { selectedFormat } = this.state;
const { formats, replacements, text, start, end } = this.createRecord();

if ( this.formatPlaceholder ) {
selectedFormat = this.formatPlaceholder.length;

if ( selectedFormat > 0 ) {
formats[ this.state.start ] = this.formatPlaceholder;
} else {
delete formats[ this.state.start ];
}
} else if ( selectedFormat > 0 ) {
const formatsBefore = formats[ start - 1 ] || [];
const formatsAfter = formats[ start ] || [];

let source = formatsBefore;

if ( formatsAfter.length > formatsBefore.length ) {
source = formatsAfter;
}

source = source.slice( 0, selectedFormat );

formats[ this.state.start ] = source;
} else {
delete formats[ this.state.start ];
}

const change = { formats, replacements, text, start, end, selectedFormat };
const value = this.createRecord();
const { activeFormats = [], start } = this.state;

this.onChange( change, {
withoutHistory: true,
// Update the formats between the last and new caret position.
const change = __unstableUpdateFormats( {
value,
start,
end: value.start,
formats: activeFormats,
} );

this.onChange( change, { withoutHistory: true } );

const transformed = this.patterns.reduce(
( accumlator, transform ) => transform( accumlator ),
change
);

if ( transformed !== change ) {
this.onCreateUndoLevel();
this.onChange( { ...transformed, selectedFormat } );
this.onChange( { ...transformed, activeFormats } );
}

// Create an undo level when input stops for over a second.
Expand All @@ -464,39 +447,23 @@ export class RichText extends Component {
* Handles the `selectionchange` event: sync the selection to local state.
*/
onSelectionChange() {
if ( this.ignoreSelectionChange ) {
delete this.ignoreSelectionChange;
return;
}

const value = this.createRecord();
const { start, end, formats } = value;
const { start, end } = value;

if ( start !== this.state.start || end !== this.state.end ) {
const isCaretWithinFormattedText = this.props.isCaretWithinFormattedText;
const { isCaretWithinFormattedText } = this.props;
const activeFormats = __unstableGetActiveFormats( value );

if ( ! isCaretWithinFormattedText && formats[ start ] ) {
if ( ! isCaretWithinFormattedText && activeFormats.length ) {
this.props.onEnterFormattedText();
} else if ( isCaretWithinFormattedText && ! formats[ start ] ) {
} else if ( isCaretWithinFormattedText && ! activeFormats.length ) {
this.props.onExitFormattedText();
}

let selectedFormat;
const formatsAfter = formats[ start ] || [];
const collapsed = isCollapsed( value );

if ( collapsed ) {
const formatsBefore = formats[ start - 1 ] || [];

selectedFormat = Math.min( formatsBefore.length, formatsAfter.length );
}

this.setState( { start, end, selectedFormat } );
this.applyRecord( { ...value, selectedFormat }, { domOnly: true } );

delete this.formatPlaceholder;
this.setState( { start, end, activeFormats } );
this.applyRecord( { ...value, activeFormats }, { domOnly: true } );

if ( collapsed ? selectedFormat > 0 : formatsAfter.length > 0 ) {
if ( activeFormats.length > 0 ) {
this.recalculateBoundaryStyle();
}
}
Expand All @@ -513,7 +480,7 @@ export class RichText extends Component {
.replace( 'rgb', 'rgba' );

globalStyle.innerHTML =
`${ boundarySelector }{background-color: ${ newColor }}`;
`*:focus ${ boundarySelector }{background-color: ${ newColor }}`;
}
}

Expand Down Expand Up @@ -541,14 +508,12 @@ export class RichText extends Component {
onChange( record, { withoutHistory } = {} ) {
this.applyRecord( record );

const { start, end, formatPlaceholder, selectedFormat } = record;
const { start, end, activeFormats = [] } = record;

this.formatPlaceholder = formatPlaceholder;
this.onChangeEditableValue( record );

this.savedContent = this.valueToFormat( record );
this.props.onChange( this.savedContent );
this.setState( { start, end, selectedFormat } );
this.setState( { start, end, activeFormats } );

if ( ! withoutHistory ) {
this.onCreateUndoLevel();
Expand Down Expand Up @@ -764,17 +729,15 @@ export class RichText extends Component {
handleHorizontalNavigation( event ) {
const value = this.createRecord();
const { formats, text, start, end } = value;
const { selectedFormat } = this.state;
const { activeFormats = [] } = this.state;
const collapsed = isCollapsed( value );
const isReverse = event.keyCode === LEFT;

delete this.formatPlaceholder;

// If the selection is collapsed and at the very start, do nothing if
// navigating backward.
// If the selection is collapsed and at the very end, do nothing if
// navigating forward.
if ( collapsed && selectedFormat === 0 ) {
if ( collapsed && activeFormats.length === 0 ) {
if ( start === 0 && isReverse ) {
return;
}
Expand All @@ -794,41 +757,43 @@ export class RichText extends Component {
// In all other cases, prevent default behaviour.
event.preventDefault();

// Ignore the selection change handler when setting selection, all state
// will be set here.
this.ignoreSelectionChange = true;

const formatsBefore = formats[ start - 1 ] || [];
const formatsAfter = formats[ start ] || [];

let newSelectedFormat = selectedFormat;
let newActiveFormatsLength = activeFormats.length;
let source = formatsAfter;

if ( formatsBefore.length > formatsAfter.length ) {
source = formatsBefore;
}

// If the amount of formats before the caret and after the caret is
// different, the caret is at a format boundary.
if ( formatsBefore.length < formatsAfter.length ) {
if ( ! isReverse && selectedFormat < formatsAfter.length ) {
newSelectedFormat++;
if ( ! isReverse && activeFormats.length < formatsAfter.length ) {
newActiveFormatsLength++;
}

if ( isReverse && selectedFormat > formatsBefore.length ) {
newSelectedFormat--;
if ( isReverse && activeFormats.length > formatsBefore.length ) {
newActiveFormatsLength--;
}
} else if ( formatsBefore.length > formatsAfter.length ) {
if ( ! isReverse && selectedFormat > formatsAfter.length ) {
newSelectedFormat--;
if ( ! isReverse && activeFormats.length > formatsAfter.length ) {
newActiveFormatsLength--;
}

if ( isReverse && selectedFormat < formatsBefore.length ) {
newSelectedFormat++;
if ( isReverse && activeFormats.length < formatsBefore.length ) {
newActiveFormatsLength++;
}
}

// Wait for boundary class to be added.
setTimeout( () => this.recalculateBoundaryStyle() );

if ( newSelectedFormat !== selectedFormat ) {
this.applyRecord( { ...value, selectedFormat: newSelectedFormat } );
this.setState( { selectedFormat: newSelectedFormat } );
if ( newActiveFormatsLength !== activeFormats.length ) {
const newActiveFormats = source.slice( 0, newActiveFormatsLength );
this.applyRecord( { ...value, activeFormats: newActiveFormats } );
this.setState( { activeFormats: newActiveFormats } );
return;
}

Expand All @@ -839,7 +804,7 @@ export class RichText extends Component {
...value,
start: newPos,
end: newPos,
selectedFormat: isReverse ? formatsBefore.length : formatsAfter.length,
activeFormats: isReverse ? formatsBefore : formatsAfter,
} );
}

Expand Down
6 changes: 6 additions & 0 deletions packages/e2e-tests/specs/__snapshots__/rich-text.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ exports[`RichText should apply formatting with primary shortcut 1`] = `
<!-- /wp:paragraph -->"
`;

exports[`RichText should apply multiple formats when selection is collapsed 1`] = `
"<!-- wp:paragraph -->
<p><strong><em>1</em></strong>.</p>
<!-- /wp:paragraph -->"
`;

exports[`RichText should handle change in tag name gracefully 1`] = `
"<!-- wp:heading {\\"level\\":3} -->
<h3></h3>
Expand Down
28 changes: 28 additions & 0 deletions packages/e2e-tests/specs/rich-text.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,34 @@ describe( 'RichText', () => {
expect( await getEditedPostContent() ).toMatchSnapshot();
} );

it( 'should apply multiple formats when selection is collapsed', async () => {
await clickBlockAppender();
await pressKeyWithModifier( 'primary', 'b' );
await pressKeyWithModifier( 'primary', 'i' );
await page.keyboard.type( '1' );
await pressKeyWithModifier( 'primary', 'i' );
await pressKeyWithModifier( 'primary', 'b' );
await page.keyboard.type( '.' );

expect( await getEditedPostContent() ).toMatchSnapshot();
} );

it( 'should not highlight more than one format', async () => {
await clickBlockAppender();
await pressKeyWithModifier( 'primary', 'b' );
await page.keyboard.type( '1' );
await pressKeyWithModifier( 'primary', 'b' );
await page.keyboard.type( ' 2' );
await pressKeyWithModifier( 'shift', 'ArrowLeft' );
await pressKeyWithModifier( 'primary', 'b' );

const count = await page.evaluate( () => document.querySelectorAll(
'*[data-rich-text-format-boundary]'
).length );

expect( count ).toBe( 1 );
} );

it( 'should return focus when pressing formatting button', async () => {
await clickBlockAppender();
await page.keyboard.type( 'Some ' );
Expand Down
7 changes: 3 additions & 4 deletions packages/rich-text/src/apply-format.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ export function applyFormat(
startIndex = value.start,
endIndex = value.end
) {
const newFormats = value.formats.slice( 0 );
const { formats, activeFormats = [] } = value;
const newFormats = formats.slice();

// The selection is collapsed.
if ( startIndex === endIndex ) {
Expand All @@ -51,11 +52,9 @@ export function applyFormat(
// Otherwise, insert a placeholder with the format so new input appears
// with the format applied.
} else {
const previousFormat = newFormats[ startIndex - 1 ] || [];

return {
...value,
formatPlaceholder: [ ...previousFormat, format ],
activeFormats: [ ...activeFormats, format ],
};
}
} else {
Expand Down
24 changes: 17 additions & 7 deletions packages/rich-text/src/get-active-formats.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,29 @@
*
* @return {?Object} Active format objects.
*/
export function getActiveFormats( { formats, start, selectedFormat } ) {
export function getActiveFormats( { formats, start, end, activeFormats } ) {
if ( start === undefined ) {
return [];
}

const formatsBefore = formats[ start - 1 ] || [];
const formatsAfter = formats[ start ] || [];
if ( start === end ) {
// For a collapsed caret, it is possible to override the active formats.
if ( activeFormats ) {
return activeFormats;
}

let source = formatsAfter;
const formatsBefore = formats[ start - 1 ] || [];
const formatsAfter = formats[ start ] || [];

if ( formatsBefore.length > formatsAfter.length ) {
source = formatsBefore;
// By default, select the lowest amount of formats possible (which means
// the caret is positioned outside the format boundary). The user can
// then use arrow keys to define `activeFormats`.
if ( formatsBefore.length < formatsAfter.length ) {
return formatsBefore;
}

return formatsAfter;
}

return source.slice( 0, selectedFormat );
return formats[ start ] || [];
}
2 changes: 2 additions & 0 deletions packages/rich-text/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,5 @@ export { unregisterFormatType } from './unregister-format-type';
export { indentListItems } from './indent-list-items';
export { outdentListItems } from './outdent-list-items';
export { changeListType } from './change-list-type';
export { updateFormats as __unstableUpdateFormats } from './update-formats';
export { getActiveFormats as __unstableGetActiveFormats } from './get-active-formats';
Loading

0 comments on commit 2141e22

Please sign in to comment.