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

Rich text: formats: allow format to filter value when changing tag name #35516

Merged
merged 11 commits into from
Oct 20, 2021
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
18 changes: 18 additions & 0 deletions packages/format-library/src/text-color/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,5 +125,23 @@ export const textColor = {
style: 'style',
class: 'class',
},
/*
* Since this format relies on the <mark> 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( ':' );
Copy link
Member

@fullofcaffeine fullofcaffeine Oct 14, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit and curious: why the join( ':' ) and not just a single string?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably keeping with the style used in ./inline.js. But personally I'd avoid it in such a situation, since I don't know how smart the VM is.

// Prepend `addedCSS` to avoid a double `;;` as any the existing CSS
// rules will already include a `;`.
return value ? [ addedCSS, value ].join( ';' ) : addedCSS;
},
edit: TextColorEdit,
};
40 changes: 40 additions & 0 deletions packages/rich-text/src/component/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fullofcaffeine Why is this needed? We shouldn't dirty on init.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we upgrade a span to a mark, it's only visible in the editor. The front-end will still be a span and that's fine. Only when the user changes something in the rich text field will the change be made.

Copy link
Member

@fullofcaffeine fullofcaffeine Oct 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we upgrade a span to a mark, it's only visible in the editor. The front-end will still be a span and that's fine. Only when the user changes something in the rich text field will the change be made.

There was a problem with the blocks in the editor not being persisted to the block editor's HTML markup. For instance, the programmatically added background-color: rgba(0, 0, 0 ,0) rule wasn't being persisted to the code markup at all even after editing something else/saving the post and this fixed it*. Follow these steps to reproduce:

  1. Checkout v11.6.0 and build;
  2. Start a new post, go to the code editor, add two paragraphs: "Custom by 11.6" and "Preset by 11.6". With the "Text color" tool, set a custom color for the first one and a preset for the other. Save and Publish.
  3. Checkout v11.7.0 and build;
  4. Open the same post, add two paragraphs below the others: "Custom by 11.7" and "Preset by 11.7". With the "Highlight" tool, set a custom color for the first one and a preset for the other. Save and Publish.
  5. Checkout cbcdfd67f6 (a commit before I added the handleChangesUponInit), build;
  6. When you load the same post, you'll see something like this:
Peek.2021-10-22.10-55.mp4

At first glance, it all looks good in the editor. However, the second paragraph still has a yellow mark on the frontend. By looking at the code markup in the code editor, you'll see that the transparent background color is not there. If you edit the post*, it will still not sync up the markup, it will still look broken in the frontend.

*It does sync up if I edit the same paragraph, but that's less than ideal, as it'd require the user to type in each one of them to make the onChange to run. That wouldn't scale well in a long post with hundreds or maybe thousands of them.

I'm aware that the handleChangesUponInit change is hacky and less than ideal, but it seemed to work well as an interim solution. Feel free to change it as you see fit, I'll also be following up to pick up your brain on it :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what it's meant to do. It's not meant to sync up until the user edits the same paragraph. Sure, the old colours that were added will not update to mark, but that's fine. All that matters is that they look visually correct in the editor.

Copy link
Member

@fullofcaffeine fullofcaffeine Oct 25, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what it's meant to do. It's not meant to sync up until the user edits the same paragraph. Sure, the old colours that were added will not update to mark, but that's fine. All that matters is that they look visually correct in the editor.

That makes sense! I think the scale of the issue (i.e in number of paragraphs affected) wasn't as big as I thought then. I think it'd be possible to use the solution without syncing upon init, but instruct users to click each affected paragraph, then 🤔 - it'd require more work from users, though. I completely agree that for the long term this shouldn't be here, thanks for creating the follow-up!

}
} else if (
selectionStart !== record.current.start ||
selectionEnd !== record.current.end
Expand Down Expand Up @@ -153,6 +168,31 @@ export function useRichText( {
forceRender();
}

function handleChangesUponInit( newRecord ) {
Copy link
Member

@fullofcaffeine fullofcaffeine Oct 18, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is a copy of handleChanges with the selection logic stripped out. It's possible some of the code below might not be needed, I'll do some further testing, but this seems to work fine for persisting format attributes that are changed upon init, which is the case here.

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();
Copy link
Member

@fullofcaffeine fullofcaffeine Oct 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's possible this is not needed at all, didn't have time to test.

}

function applyFromProps() {
setRecordFromProps();
applyRecord( record.current );
Expand Down
38 changes: 24 additions & 14 deletions packages/rich-text/src/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 ];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are registered and unregistered attributes in this context?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unregistered attributes are attributes that the format has but not registered to it. It basically ensures that, if you e.g. add a style or data attribute to a strong tag, it will be preserved.


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 {
Expand Down