UNPKG

@wordpress/block-editor

Version:
384 lines (339 loc) 11.2 kB
/** * External dependencies */ import { extend } from 'colord'; import namesPlugin from 'colord/plugins/names'; /** * WordPress dependencies */ import { getBlockSupport, getBlockType, hasBlockSupport, } from '@wordpress/blocks'; import { useInstanceId } from '@wordpress/compose'; import { addFilter } from '@wordpress/hooks'; import { useMemo, useEffect } from '@wordpress/element'; /** * Internal dependencies */ import { BlockControls, InspectorControls, __experimentalDuotoneControl as DuotoneControl, useSettings, } from '../components'; import { getDuotoneFilter, getDuotoneStylesheet, getDuotoneUnsetStylesheet, } from '../components/duotone/utils'; import { getBlockCSSSelector } from '../components/global-styles/get-block-css-selector'; import { scopeSelector } from '../components/global-styles/utils'; import { useBlockSettings, usePrivateStyleOverride } from './utils'; import { default as StylesFiltersPanel } from '../components/global-styles/filters-panel'; import { useBlockEditingMode } from '../components/block-editing-mode'; import { useBlockElement } from '../components/block-list/use-block-props/use-block-refs'; const EMPTY_ARRAY = []; // Safari does not always update the duotone filter when the duotone colors // are changed. This browser check is later used to force a re-render of the block // element to ensure the duotone filter is updated. The check is included at the // root of this file as it only needs to be run once per page load. const isSafari = window?.navigator.userAgent && window.navigator.userAgent.includes( 'Safari' ) && ! window.navigator.userAgent.includes( 'Chrome' ) && ! window.navigator.userAgent.includes( 'Chromium' ); extend( [ namesPlugin ] ); function useMultiOriginPresets( { presetSetting, defaultSetting } ) { const [ enableDefault, userPresets, themePresets, defaultPresets ] = useSettings( defaultSetting, `${ presetSetting }.custom`, `${ presetSetting }.theme`, `${ presetSetting }.default` ); return useMemo( () => [ ...( userPresets || EMPTY_ARRAY ), ...( themePresets || EMPTY_ARRAY ), ...( ( enableDefault && defaultPresets ) || EMPTY_ARRAY ), ], [ enableDefault, 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 DuotonePanelPure( { style, setAttributes, name } ) { const duotoneStyle = style?.color?.duotone; const settings = useBlockSettings( name ); const blockEditingMode = useBlockEditingMode(); const duotonePalette = useMultiOriginPresets( { presetSetting: 'color.duotone', defaultSetting: 'color.defaultDuotone', } ); const colorPalette = useMultiOriginPresets( { presetSetting: 'color.palette', defaultSetting: 'color.defaultPalette', } ); const [ enableCustomColors, enableCustomDuotone ] = useSettings( 'color.custom', 'color.customDuotone' ); const disableCustomColors = ! enableCustomColors; const disableCustomDuotone = ! enableCustomDuotone || ( colorPalette?.length === 0 && disableCustomColors ); if ( duotonePalette?.length === 0 && disableCustomDuotone ) { return null; } if ( blockEditingMode !== 'default' ) { 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> </> ); } export default { shareWithChildBlocks: true, edit: DuotonePanelPure, useBlockProps, attributeKeys: [ 'style' ], hasSupport( name ) { return hasBlockSupport( name, 'filter.duotone' ); }, }; /** * 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; } function useDuotoneStyles( { clientId, id: filterId, selector: duotoneSelector, attribute: duotoneAttr, } ) { 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 ) => { // 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 `.${ filterId }${ selectorPart.trim() }`; } ); const selector = selectorsScoped.join( ', ' ); const isValidFilter = Array.isArray( colors ) || colors === 'unset'; usePrivateStyleOverride( isValidFilter ? { css: colors !== 'unset' ? getDuotoneStylesheet( selector, filterId ) : getDuotoneUnsetStylesheet( selector ), __unstableType: 'presets', } : undefined ); usePrivateStyleOverride( isValidFilter ? { assets: colors !== 'unset' ? getDuotoneFilter( filterId, colors ) : '', __unstableType: 'svgs', } : undefined ); const blockElement = useBlockElement( clientId ); useEffect( () => { if ( ! isValidFilter ) { return; } // Safari does not always update the duotone filter when the duotone // colors are changed. When using Safari, force the block element to be // repainted by the browser to ensure any changes are reflected // visually. This logic matches that used on the site frontend in // `block-supports/duotone.php`. if ( blockElement && isSafari ) { const display = blockElement.style.display; // Switch to `inline-block` to force a repaint. In the editor, // `inline-block` is used instead of `none` to ensure that scroll // position is not affected, as `none` results in the editor // scrolling to the top of the block. blockElement.style.setProperty( 'display', 'inline-block' ); // Simply accessing el.offsetHeight flushes layout and style changes // in WebKit without having to wait for setTimeout. // eslint-disable-next-line no-unused-expressions blockElement.offsetHeight; blockElement.style.setProperty( 'display', display ); } // `colors` must be a dependency so this effect runs when the colors // change in Safari. }, [ isValidFilter, blockElement, colors ] ); } // Used for generating the instance ID const DUOTONE_BLOCK_PROPS_REFERENCE = {}; function useBlockProps( { clientId, name, style } ) { const id = useInstanceId( DUOTONE_BLOCK_PROPS_REFERENCE ); const selector = useMemo( () => { const blockType = getBlockType( 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, } ); } }, [ name ] ); const attribute = style?.color?.duotone; const filterClass = `wp-duotone-${ id }`; const shouldRender = selector && attribute; useDuotoneStyles( { clientId, id: filterClass, selector, attribute, } ); return { className: shouldRender ? filterClass : '', }; } addFilter( 'blocks.registerBlockType', 'core/editor/duotone/add-attributes', addDuotoneAttributes );