UNPKG

@wordpress/block-editor

Version:
409 lines (363 loc) 11.8 kB
/** * External dependencies */ import classnames from 'classnames'; import { extend } from 'colord'; import namesPlugin from 'colord/plugins/names'; /** * WordPress dependencies */ import { getBlockSupport, getBlockType, hasBlockSupport, } from '@wordpress/blocks'; import { createHigherOrderComponent, useInstanceId } from '@wordpress/compose'; import { addFilter } from '@wordpress/hooks'; import { useMemo, useContext, createPortal } from '@wordpress/element'; /** * Internal dependencies */ import { BlockControls, InspectorControls, __experimentalDuotoneControl as DuotoneControl, useSetting, } from '../components'; import BlockList from '../components/block-list'; import { __unstableDuotoneFilter as DuotoneFilter, __unstableDuotoneStylesheet as DuotoneStylesheet, __unstableDuotoneUnsetStylesheet as DuotoneUnsetStylesheet, } from '../components/duotone'; import { getBlockCSSSelector } from '../components/global-styles/get-block-css-selector'; import { scopeSelector } from '../components/global-styles/utils'; import { useBlockSettings } from './utils'; import { default as StylesFiltersPanel } from '../components/global-styles/filters-panel'; import { useBlockEditingMode } from '../components/block-editing-mode'; const EMPTY_ARRAY = []; extend( [ namesPlugin ] ); /** * SVG and stylesheet needed for rendering the duotone filter. * * @param {Object} props Duotone props. * @param {string} props.selector Selector to apply the filter to. * @param {string} props.id Unique id for this duotone filter. * @param {string[]|"unset"} props.colors Array of RGB color strings ordered from dark to light. * * @return {WPElement} Duotone element. */ function InlineDuotone( { selector, id, colors } ) { if ( colors === 'unset' ) { return <DuotoneUnsetStylesheet selector={ selector } />; } return ( <> <DuotoneFilter id={ id } colors={ colors } /> <DuotoneStylesheet id={ id } selector={ selector } /> </> ); } function useMultiOriginPresets( { presetSetting, defaultSetting } ) { const disableDefault = ! useSetting( defaultSetting ); const userPresets = useSetting( `${ presetSetting }.custom` ) || EMPTY_ARRAY; const themePresets = useSetting( `${ presetSetting }.theme` ) || EMPTY_ARRAY; const defaultPresets = useSetting( `${ presetSetting }.default` ) || EMPTY_ARRAY; return useMemo( () => [ ...userPresets, ...themePresets, ...( disableDefault ? EMPTY_ARRAY : defaultPresets ), ], [ disableDefault, userPresets, themePresets, defaultPresets ] ); } export function getColorsFromDuotonePreset( duotone, duotonePalette ) { if ( ! duotone ) { return; } const preset = duotonePalette?.find( ( { slug } ) => { return duotone === `var:preset|duotone|${ slug }`; } ); return preset ? preset.colors : undefined; } export function getDuotonePresetFromColors( colors, duotonePalette ) { if ( ! colors || ! Array.isArray( colors ) ) { return; } const preset = duotonePalette?.find( ( duotonePreset ) => { return duotonePreset?.colors?.every( ( val, index ) => val === colors[ index ] ); } ); return preset ? `var:preset|duotone|${ preset.slug }` : undefined; } function DuotonePanel( { attributes, setAttributes, name } ) { const style = attributes?.style; const duotoneStyle = style?.color?.duotone; const settings = useBlockSettings( name ); const duotonePalette = useMultiOriginPresets( { presetSetting: 'color.duotone', defaultSetting: 'color.defaultDuotone', } ); const colorPalette = useMultiOriginPresets( { presetSetting: 'color.palette', defaultSetting: 'color.defaultPalette', } ); const disableCustomColors = ! useSetting( 'color.custom' ); const disableCustomDuotone = ! useSetting( 'color.customDuotone' ) || ( colorPalette?.length === 0 && disableCustomColors ); if ( duotonePalette?.length === 0 && disableCustomDuotone ) { return null; } const duotonePresetOrColors = ! Array.isArray( duotoneStyle ) ? getColorsFromDuotonePreset( duotoneStyle, duotonePalette ) : duotoneStyle; return ( <> <InspectorControls group="filter"> <StylesFiltersPanel value={ { filter: { duotone: duotonePresetOrColors } } } onChange={ ( newDuotone ) => { const newStyle = { ...style, color: { ...newDuotone?.filter, }, }; setAttributes( { style: newStyle } ); } } settings={ settings } /> </InspectorControls> <BlockControls group="block" __experimentalShareWithChildBlocks> <DuotoneControl duotonePalette={ duotonePalette } colorPalette={ colorPalette } disableCustomDuotone={ disableCustomDuotone } disableCustomColors={ disableCustomColors } value={ duotonePresetOrColors } onChange={ ( newDuotone ) => { const maybePreset = getDuotonePresetFromColors( newDuotone, duotonePalette ); const newStyle = { ...style, color: { ...style?.color, duotone: maybePreset ?? newDuotone, // use preset or fallback to custom colors. }, }; setAttributes( { style: newStyle } ); } } settings={ settings } /> </BlockControls> </> ); } /** * Filters registered block settings, extending attributes to include * the `duotone` attribute. * * @param {Object} settings Original block settings. * * @return {Object} Filtered block settings. */ function addDuotoneAttributes( settings ) { // Previous `color.__experimentalDuotone` support flag is migrated via // block_type_metadata_settings filter in `lib/block-supports/duotone.php`. if ( ! hasBlockSupport( settings, 'filter.duotone' ) ) { return settings; } // Allow blocks to specify their own attribute definition with default // values if needed. if ( ! settings.attributes.style ) { Object.assign( settings.attributes, { style: { type: 'object', }, } ); } return settings; } /** * Override the default edit UI to include toolbar controls for duotone if the * block supports duotone. * * @param {Function} BlockEdit Original component. * * @return {Function} Wrapped component. */ const withDuotoneControls = createHigherOrderComponent( ( BlockEdit ) => ( props ) => { // Previous `color.__experimentalDuotone` support flag is migrated via // block_type_metadata_settings filter in `lib/block-supports/duotone.php`. const hasDuotoneSupport = hasBlockSupport( props.name, 'filter.duotone' ); const blockEditingMode = useBlockEditingMode(); // CAUTION: code added before this line will be executed // for all blocks, not just those that support duotone. Code added // above this line should be carefully evaluated for its impact on // performance. return ( <> { hasDuotoneSupport && blockEditingMode === 'default' && ( <DuotonePanel { ...props } /> ) } <BlockEdit { ...props } /> </> ); }, 'withDuotoneControls' ); function DuotoneStyles( { id: filterId, selector: duotoneSelector, attribute: duotoneAttr, } ) { const element = useContext( BlockList.__unstableElementContext ); const duotonePalette = useMultiOriginPresets( { presetSetting: 'color.duotone', defaultSetting: 'color.defaultDuotone', } ); // Possible values for duotone attribute: // 1. Array of colors - e.g. ['#000000', '#ffffff']. // 2. Variable for an existing Duotone preset - e.g. 'var:preset|duotone|green-blue' or 'var(--wp--preset--duotone--green-blue)'' // 3. A CSS string - e.g. 'unset' to remove globally applied duotone. const isCustom = Array.isArray( duotoneAttr ); const duotonePreset = isCustom ? undefined : getColorsFromDuotonePreset( duotoneAttr, duotonePalette ); const isPreset = typeof duotoneAttr === 'string' && duotonePreset; const isCSS = typeof duotoneAttr === 'string' && ! isPreset; // Match the structure of WP_Duotone_Gutenberg::render_duotone_support() in PHP. let colors = null; if ( isPreset ) { // Array of colors. colors = duotonePreset; } else if ( isCSS ) { // CSS filter property string (e.g. 'unset'). colors = duotoneAttr; } else if ( isCustom ) { // Array of colors. colors = duotoneAttr; } // Build the CSS selectors to which the filter will be applied. const selectors = duotoneSelector.split( ',' ); const selectorsScoped = selectors.map( ( selectorPart ) => { // Extra .editor-styles-wrapper specificity is needed in the editor // since we're not using inline styles to apply the filter. We need to // override duotone applied by global styles and theme.json. // Assuming the selector part is a subclass selector (not a tag name) // so we can prepend the filter id class. If we want to support elements // such as `img` or namespaces, we'll need to add a case for that here. return `.editor-styles-wrapper .${ filterId }${ selectorPart.trim() }`; } ); const selector = selectorsScoped.join( ', ' ); const isValidFilter = Array.isArray( colors ) || colors === 'unset'; return ( element && isValidFilter && createPortal( <InlineDuotone selector={ selector } id={ filterId } colors={ colors } />, element ) ); } /** * Override the default block element to include duotone styles. * * @param {Function} BlockListBlock Original component. * * @return {Function} Wrapped component. */ const withDuotoneStyles = createHigherOrderComponent( ( BlockListBlock ) => ( props ) => { const id = useInstanceId( BlockListBlock ); const selector = useMemo( () => { const blockType = getBlockType( props.name ); if ( blockType ) { // Backwards compatibility for `supports.color.__experimentalDuotone` // is provided via the `block_type_metadata_settings` filter. If // `supports.filter.duotone` has not been set and the // experimental property has been, the experimental property // value is copied into `supports.filter.duotone`. const duotoneSupport = getBlockSupport( blockType, 'filter.duotone', false ); if ( ! duotoneSupport ) { return null; } // If the experimental duotone support was set, that value is // to be treated as a selector and requires scoping. const experimentalDuotone = getBlockSupport( blockType, 'color.__experimentalDuotone', false ); if ( experimentalDuotone ) { const rootSelector = getBlockCSSSelector( blockType ); return typeof experimentalDuotone === 'string' ? scopeSelector( rootSelector, experimentalDuotone ) : rootSelector; } // Regular filter.duotone support uses filter.duotone selectors with fallbacks. return getBlockCSSSelector( blockType, 'filter.duotone', { fallback: true, } ); } }, [ props.name ] ); const attribute = props?.attributes?.style?.color?.duotone; const filterClass = `wp-duotone-${ id }`; const shouldRender = selector && attribute; const className = shouldRender ? classnames( props?.className, filterClass ) : props?.className; // CAUTION: code added before this line will be executed // for all blocks, not just those that support duotone. Code added // above this line should be carefully evaluated for its impact on // performance. return ( <> { shouldRender && ( <DuotoneStyles id={ filterClass } selector={ selector } attribute={ attribute } /> ) } <BlockListBlock { ...props } className={ className } /> </> ); }, 'withDuotoneStyles' ); addFilter( 'blocks.registerBlockType', 'core/editor/duotone/add-attributes', addDuotoneAttributes ); addFilter( 'editor.BlockEdit', 'core/editor/duotone/with-editor-controls', withDuotoneControls ); addFilter( 'editor.BlockListBlock', 'core/editor/duotone/with-styles', withDuotoneStyles );