From d4eb1bfae473c36d099d3fc50db8b9ad8cd841ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20van=C2=A0Durpe?= <4710635+ellatrix@users.noreply.github.com> Date: Wed, 20 Oct 2021 22:00:37 +0300 Subject: [PATCH] Rich text: formats: allow format to filter value when changing tag name (#35516) * Rich text: formats: allow format to filter value when changing tag name * Do not add a transparent background-color if already set * Fix undefined values in the list of attributes, which would cause the rich-text component to break * Do not skip the entire loop iteration if key does not exist in _attributes * Format library: text-color: Explain use of __unstableFilterAttributeValue * Remove `export` for `parseCSS` * Make sure to delete undefined registeredAttributes * Handle format attributes on rich-text init * Only handle changes for `text-color` formats upon init * Be a bit more defensive when accessing record.current.formats * Prepend the `addedCSS` to prevent double `;` Existing CSS rules already end with a semicolon, and joining it with the `background-color` rule would result in something like: ``` color:#e511be;;background-color:rgba(0, 0, 0, 0) ``` Which is benign AFAIK, but doesn't look right. Best to prepend the `background-color` rule, since it doesn't include a `;`, but the `join` will take care of adding it when joining it with the other existing rules, if any. Co-authored-by: Marcelo Serpa <81248+fullofcaffeine@users.noreply.github.com> Co-authored-by: Miguel Fonseca --- .../format-library/src/text-color/index.js | 18 +++++++++ packages/rich-text/src/component/index.js | 40 +++++++++++++++++++ packages/rich-text/src/create.js | 38 +++++++++++------- 3 files changed, 82 insertions(+), 14 deletions(-) diff --git a/packages/format-library/src/text-color/index.js b/packages/format-library/src/text-color/index.js index a582197503f3cd..77824a26d69ba5 100644 --- a/packages/format-library/src/text-color/index.js +++ b/packages/format-library/src/text-color/index.js @@ -125,5 +125,23 @@ export const textColor = { style: 'style', class: 'class', }, + /* + * Since this format relies on the tag, it's important to + * prevent the default yellow background color applied by most + * browsers. The solution is to detect when this format is used with a + * text color but no background color, and in such cases to override + * the default styling with a transparent background. + * + * @see https://github.com/WordPress/gutenberg/pull/35516 + */ + __unstableFilterAttributeValue( key, value ) { + if ( key !== 'style' ) return value; + // We should not add a background-color if it's already set + if ( value && value.includes( 'background-color' ) ) return value; + const addedCSS = [ 'background-color', 'rgba(0, 0, 0, 0)' ].join( ':' ); + // Prepend `addedCSS` to avoid a double `;;` as any the existing CSS + // rules will already include a `;`. + return value ? [ addedCSS, value ].join( ';' ) : addedCSS; + }, edit: TextColorEdit, }; diff --git a/packages/rich-text/src/component/index.js b/packages/rich-text/src/component/index.js index b5c4f081490e57..d73e89f2e398cb 100644 --- a/packages/rich-text/src/component/index.js +++ b/packages/rich-text/src/component/index.js @@ -101,6 +101,21 @@ export function useRichText( { if ( ! record.current ) { setRecordFromProps(); + // Sometimes formats are added programmatically and we need to make + // sure it's persisted to the block store / markup. If these formats + // are not applied, they could cause inconsistencies between the data + // in the visual editor and the frontend. Right now, it's only relevant + // to the `core/text-color` format, which is applied at runtime in + // certain circunstances. See the `__unstableFilterAttributeValue` + // function in `packages/format-library/src/text-color/index.js`. + // @todo find a less-hacky way of solving this. + + const hasRelevantInitFormat = + record.current?.formats[ 0 ]?.[ 0 ]?.type === 'core/text-color'; + + if ( hasRelevantInitFormat ) { + handleChangesUponInit( record.current ); + } } else if ( selectionStart !== record.current.start || selectionEnd !== record.current.end @@ -153,6 +168,31 @@ export function useRichText( { forceRender(); } + function handleChangesUponInit( newRecord ) { + record.current = newRecord; + + _value.current = toHTMLString( { + value: __unstableBeforeSerialize + ? { + ...newRecord, + formats: __unstableBeforeSerialize( newRecord ), + } + : newRecord, + multilineTag, + preserveWhiteSpace, + } ); + + const { formats, text } = newRecord; + + registry.batch( () => { + onChange( _value.current, { + __unstableFormats: formats, + __unstableText: text, + } ); + } ); + forceRender(); + } + function applyFromProps() { setRecordFromProps(); applyRecord( record.current ); diff --git a/packages/rich-text/src/create.js b/packages/rich-text/src/create.js index 39f0a3708c75f1..02cdda5d5af58d 100644 --- a/packages/rich-text/src/create.js +++ b/packages/rich-text/src/create.js @@ -43,14 +43,6 @@ function createEmptyValue() { }; } -function simpleFindKey( object, value ) { - for ( const key in object ) { - if ( object[ key ] === value ) { - return key; - } - } -} - function toFormat( { type, attributes } ) { let formatType; @@ -94,15 +86,33 @@ function toFormat( { type, attributes } ) { const registeredAttributes = {}; const unregisteredAttributes = {}; + const _attributes = { ...attributes }; - for ( const name in attributes ) { - const key = simpleFindKey( formatType.attributes, name ); + for ( const key in formatType.attributes ) { + const name = formatType.attributes[ key ]; - if ( key ) { - registeredAttributes[ key ] = attributes[ name ]; - } else { - unregisteredAttributes[ name ] = attributes[ name ]; + registeredAttributes[ key ] = _attributes[ name ]; + + if ( formatType.__unstableFilterAttributeValue ) { + registeredAttributes[ + key + ] = formatType.__unstableFilterAttributeValue( + key, + registeredAttributes[ key ] + ); } + + // delete the attribute and what's left is considered + // to be unregistered. + delete _attributes[ name ]; + + if ( typeof registeredAttributes[ key ] === 'undefined' ) { + delete registeredAttributes[ key ]; + } + } + + for ( const name in _attributes ) { + unregisteredAttributes[ name ] = attributes[ name ]; } return {