Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RichText: unify active formats, 'selectedFormat' and 'placeholderFormat' #14411

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
this.props.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 ) {
ellatrix marked this conversation as resolved.
Show resolved Hide resolved
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';
ellatrix marked this conversation as resolved.
Show resolved Hide resolved
export { getActiveFormats as __unstableGetActiveFormats } from './get-active-formats';
Loading