-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Add option to add text color to specific text inside RichText #16014
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
@import "./image/style.scss"; | ||
@import "./link/style.scss"; | ||
@import "./text-color/style.scss"; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { get } from 'lodash'; | ||
|
||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { __ } from '@wordpress/i18n'; | ||
import { useSelect } from '@wordpress/data'; | ||
import { useCallback, useMemo, useState } from '@wordpress/element'; | ||
import { RichTextToolbarButton } from '@wordpress/block-editor'; | ||
import { Dashicon } from '@wordpress/components'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import { default as InlineColorUI, getActiveColor } from './inline'; | ||
|
||
const name = 'core/text-color'; | ||
const title = __( 'Text Color' ); | ||
|
||
const EMPTY_ARRAY = []; | ||
|
||
function TextColorEdit( { value, onChange, isActive, activeAttributes } ) { | ||
const colors = useSelect( ( select ) => { | ||
const { getSettings } = select( 'core/block-editor' ); | ||
if ( getSettings ) { | ||
return get( getSettings(), [ 'colors' ], EMPTY_ARRAY ); | ||
} | ||
return EMPTY_ARRAY; | ||
} ); | ||
const [ isAddingColor, setIsAddingColor ] = useState( false ); | ||
const enableIsAddingColor = useCallback( () => setIsAddingColor( true ), [ | ||
setIsAddingColor, | ||
] ); | ||
const disableIsAddingColor = useCallback( () => setIsAddingColor( false ), [ | ||
setIsAddingColor, | ||
] ); | ||
const colorIndicatorStyle = useMemo( () => { | ||
const activeColor = getActiveColor( name, value, colors ); | ||
if ( ! activeColor ) { | ||
return undefined; | ||
} | ||
return { | ||
backgroundColor: activeColor, | ||
}; | ||
}, [ value, colors ] ); | ||
return ( | ||
<> | ||
<RichTextToolbarButton | ||
key={ isActive ? 'text-color' : 'text-color-not-active' } | ||
className="format-library-text-color-button" | ||
name={ isActive ? 'text-color' : undefined } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jorgefilipecosta - what's even more surprising is that the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's no There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hi @gziolo, @epiqueras, the key is not going to be RichText.ToolbarControls.undefined, because the key is the fillName and fill name has the following logic:
So here when we pass undefined the key is going to be RichText.ToolbarControl. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need this logic because we want to show the format on the main toolbar when there is a color set, but we want to hide it from the main toolbar when color is not set. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @epiqueras, @gziolo I think we may have a bug in slot fill, this case basically passes a dynamic name to the fill and maybe slot fill when the name changes does not remove previous fills. Setting a key seems to be a workaround for the issue. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
So it will be in the dropdown.
Yes, see #16014 (comment). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should address the bug in slot&fill. But I think using a key is an ok workaround so we can merge this PR in WordPress 5.4. A key in this situation does not seem to cause problems. Any thoughts on this @gziolo, @epiqueras? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, but I’d rather have the key in the button itself to avoid this pattern from masking the need for a proper fix. |
||
icon={ | ||
<> | ||
<Dashicon icon="editor-textcolor" /> | ||
{ isActive && ( | ||
<span | ||
className="format-library-text-color-button__indicator" | ||
style={ colorIndicatorStyle } | ||
/> | ||
) } | ||
</> | ||
} | ||
title={ title } | ||
onClick={ enableIsAddingColor } | ||
/> | ||
{ isAddingColor && ( | ||
<InlineColorUI | ||
name={ name } | ||
addingColor={ isAddingColor } | ||
onClose={ disableIsAddingColor } | ||
isActive={ isActive } | ||
activeAttributes={ activeAttributes } | ||
value={ value } | ||
onChange={ onChange } | ||
/> | ||
) } | ||
</> | ||
); | ||
} | ||
|
||
export const textColor = { | ||
name, | ||
title, | ||
tagName: 'span', | ||
className: 'has-inline-color', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was hoping a class wouldn't be needed when we do this: But I don't think it should block this feature. Let's work that out later. |
||
attributes: { | ||
style: 'style', | ||
class: 'class', | ||
}, | ||
edit: TextColorEdit, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { get } from 'lodash'; | ||
|
||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { useCallback, useMemo } from '@wordpress/element'; | ||
import { useSelect } from '@wordpress/data'; | ||
import { withSpokenMessages } from '@wordpress/components'; | ||
import { getRectangleFromRange } from '@wordpress/dom'; | ||
import { | ||
applyFormat, | ||
removeFormat, | ||
getActiveFormat, | ||
} from '@wordpress/rich-text'; | ||
import { | ||
ColorPalette, | ||
URLPopover, | ||
getColorClassName, | ||
getColorObjectByColorValue, | ||
getColorObjectByAttributeValues, | ||
} from '@wordpress/block-editor'; | ||
|
||
export function getActiveColor( formatName, formatValue, colors ) { | ||
const activeColorFormat = getActiveFormat( formatValue, formatName ); | ||
if ( ! activeColorFormat ) { | ||
return; | ||
} | ||
const styleColor = activeColorFormat.attributes.style; | ||
if ( styleColor ) { | ||
return styleColor.replace( new RegExp( `^color:\\s*` ), '' ); | ||
} | ||
const currentClass = activeColorFormat.attributes.class; | ||
if ( currentClass ) { | ||
const colorSlug = currentClass.replace( /.*has-(.*?)-color.*/, '$1' ); | ||
return getColorObjectByAttributeValues( colors, colorSlug ).color; | ||
} | ||
} | ||
|
||
const ColorPopoverAtLink = ( { isActive, addingColor, value, ...props } ) => { | ||
const anchorRect = useMemo( () => { | ||
const selection = window.getSelection(); | ||
const range = | ||
selection.rangeCount > 0 ? selection.getRangeAt( 0 ) : null; | ||
if ( ! range ) { | ||
return; | ||
} | ||
|
||
if ( addingColor ) { | ||
return getRectangleFromRange( range ); | ||
} | ||
|
||
let element = range.startContainer; | ||
|
||
// If the caret is right before the element, select the next element. | ||
element = element.nextElementSibling || element; | ||
|
||
while ( element.nodeType !== window.Node.ELEMENT_NODE ) { | ||
element = element.parentNode; | ||
} | ||
|
||
const closest = element.closest( 'span' ); | ||
if ( closest ) { | ||
return closest.getBoundingClientRect(); | ||
} | ||
}, [ isActive, addingColor, value.start, value.end ] ); | ||
|
||
if ( ! anchorRect ) { | ||
return null; | ||
} | ||
|
||
return <URLPopover anchorRect={ anchorRect } { ...props } />; | ||
}; | ||
|
||
const ColorPicker = ( { name, value, onChange } ) => { | ||
const colors = useSelect( ( select ) => { | ||
const { getSettings } = select( 'core/block-editor' ); | ||
return get( getSettings(), [ 'colors' ], [] ); | ||
} ); | ||
const onColorChange = useCallback( | ||
( color ) => { | ||
if ( color ) { | ||
const colorObject = getColorObjectByColorValue( colors, color ); | ||
onChange( | ||
applyFormat( value, { | ||
type: name, | ||
attributes: colorObject | ||
? { | ||
class: getColorClassName( | ||
'color', | ||
colorObject.slug | ||
), | ||
} | ||
: { | ||
style: `color:${ color }`, | ||
}, | ||
} ) | ||
); | ||
} else { | ||
onChange( removeFormat( value, name ) ); | ||
} | ||
}, | ||
[ colors, onChange ] | ||
); | ||
const activeColor = useMemo( () => getActiveColor( name, value, colors ), [ | ||
name, | ||
value, | ||
colors, | ||
] ); | ||
|
||
return <ColorPalette value={ activeColor } onChange={ onColorChange } />; | ||
}; | ||
|
||
const InlineColorUI = ( { | ||
name, | ||
value, | ||
onChange, | ||
onClose, | ||
isActive, | ||
addingColor, | ||
} ) => { | ||
return ( | ||
<ColorPopoverAtLink | ||
value={ value } | ||
isActive={ isActive } | ||
addingColor={ addingColor } | ||
onClose={ onClose } | ||
className="components-inline-color-popover" | ||
> | ||
<ColorPicker name={ name } value={ value } onChange={ onChange } /> | ||
</ColorPopoverAtLink> | ||
); | ||
}; | ||
|
||
export default withSpokenMessages( InlineColorUI ); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
.components-inline-color__indicator { | ||
position: absolute; | ||
background: #000; | ||
height: 3px; | ||
width: 20px; | ||
bottom: 6px; | ||
left: auto; | ||
right: auto; | ||
margin: 0 5px; | ||
} | ||
|
||
.components-inline-color-popover { | ||
|
||
.components-popover__content { | ||
padding: 20px 18px; | ||
|
||
.components-color-palette { | ||
margin-top: 0.6rem; | ||
} | ||
|
||
.components-base-control__title { | ||
display: block; | ||
margin-bottom: 16px; | ||
font-weight: 600; | ||
color: #191e23; | ||
} | ||
|
||
.component-color-indicator { | ||
vertical-align: text-bottom; | ||
} | ||
} | ||
} | ||
|
||
.format-library-text-color-button { | ||
position: relative; | ||
} | ||
.format-library-text-color-button__indicator { | ||
height: 4px; | ||
width: 20px; | ||
position: absolute; | ||
bottom: 6px; | ||
left: 8px; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any way not to depend on the the block editor package?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried to make at least nothing crashes if core/block-editor is not available. I guess as a follow up we may try to see if a better solution is possible.