-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Add option to add text color to specific text inside RichText #16014
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
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 } | ||
|
||
| 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', | ||
|
||
| 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.