/** * External dependencies */ import clsx from 'clsx'; /** * WordPress dependencies */ import { __, sprintf } from '@wordpress/i18n'; import { useSelect, useDispatch } from '@wordpress/data'; import { InspectorControls, BlockControls, useBlockProps, getSpacingPresetCssVar, isValueSpacingPreset, store as blockEditorStore, __experimentalSpacingSizesControl as SpacingSizesControl, } from '@wordpress/block-editor'; import { ResizableBox, RangeControl, ToggleControl, HorizontalRule, ToolbarGroup, ToolbarButton, __experimentalToolsPanel as ToolsPanel, __experimentalToolsPanelItem as ToolsPanelItem, __experimentalParseQuantityAndUnitFromRawValue as parseQuantityAndUnitFromRawValue, } from '@wordpress/components'; import { Link, Stack } from '@wordpress/ui'; import { useEffect, useState } from '@wordpress/element'; import { View } from '@wordpress/primitives'; import { Icon, settings, mobile, tablet, desktop } from '@wordpress/icons'; import { addQueryArgs } from '@wordpress/url'; import { useViewportMatch } from '@wordpress/compose'; import type { BlockEditProps } from '@wordpress/blocks'; /** * Internal dependencies */ import { responsive } from './icons'; import { store } from './store'; import type { BlockAttributes } from './types'; import { MIN_SPACER_HEIGHT, MAX_SPACER_HEIGHT, DEFAULT_SPACER_HEIGHT, DEFAULT_SPACER_HEIGHT_UNIT, } from './constants'; type HeightValue = string | number | undefined; interface SpacerControl { label: string; icon: JSX.Element; slug: string; value: string | undefined; syncKey?: number; onChange: ( value: string | undefined ) => void; isNegative?: boolean; onNegativeChange?: ( value: boolean ) => void; hasValue: () => boolean; onDeselect: () => void; } // Renders the height input for a single device. function HeightControl( { label, icon, value = '', syncKey, onChange, onMouseOver, onMouseOut, }: { label: string; icon: JSX.Element; value?: string; syncKey?: number; onChange: ( value: string | undefined ) => void; onMouseOver: () => void; onMouseOut: () => void; } ) { const disableCustomSpacingSizes = useSelect( ( select ) => select( blockEditorStore ).getSettings().disableCustomSpacingSizes, [] ); const [ parsedQuantity, parsedUnit ] = parseQuantityAndUnitFromRawValue( value ); // With custom values disabled the core control renders the preset UI alone, so // custom values can no longer be edited. The core Spacer block behaves the same // way, but restore a numeric input here for backward compatibility. if ( disableCustomSpacingSizes ) { return (
} min={ MIN_SPACER_HEIGHT } max={ MAX_SPACER_HEIGHT } value={ parsedQuantity } onChange={ ( next ) => onChange( next === undefined ? undefined : `${ next }${ parsedUnit || DEFAULT_SPACER_HEIGHT_UNIT }` ) } __next40pxDefaultSize />
); } // An empty string renders as a blank custom input rather than as a preset // slider parked on "None". const computedValue = isValueSpacingPreset( value ) ? value : [ parsedQuantity, parsedUnit ].join( '' ); return (
onChange( all || undefined ) } label={ label } sides={ [ 'all' ] } showSideInLabel={ false } onMouseOver={ onMouseOver } onMouseOut={ onMouseOut } />
); } interface SpacerDevice { label: string; slug: string; icon: JSX.Element; isNegative: boolean; height: string; onResizeStart: () => void; onResize: () => void; onResizeStop: ( event: unknown, direction: unknown, elt: HTMLElement ) => void; isResizing: boolean; } export default function Edit( { attributes, isSelected, setAttributes, toggleSelection, }: BlockEditProps< BlockAttributes > & { // Injected by the block editor at runtime; not part of `BlockEditProps`. toggleSelection?: ( isSelectionEnabled: boolean ) => void; } ) { const { heightLg, heightMd, heightSm, isNegativeLg, isNegativeMd, isNegativeSm } = attributes; const heightAll = heightLg === heightMd && heightMd === heightSm ? heightLg : undefined; const [ activeDevice, setActiveDevice ] = useState< string | undefined >( undefined ); const [ isResizingLg, setIsResizingLg ] = useState( false ); const [ isResizingMd, setIsResizingMd ] = useState( false ); const [ isResizingSm, setIsResizingSm ] = useState( false ); // A control picks its preset or custom view when it mounts and never switches // back on its own, so remount the counterpart when the other side writes a // height. const [ deviceSyncKey, setDeviceSyncKey ] = useState( 0 ); const [ allSyncKey, setAllSyncKey ] = useState( 0 ); const isResponsive = useSelect( ( select ) => select( store ).getIsResponsive(), [] ); const { setIsResponsive } = useDispatch( store ); const isMobile = useViewportMatch( 'medium', '<' ); const isEnableMd = parseInt( fsbConf.breakpoint.md ) !== parseInt( fsbConf.breakpoint.sm ); const isShowBlock = fsbConf.showBlock; const defaultValue = fsbConf.defaultValue; // Apply default values from the settings page when inserting a block. useEffect( () => { if ( heightLg === `${ DEFAULT_SPACER_HEIGHT }${ DEFAULT_SPACER_HEIGHT_UNIT }` && heightMd === `${ DEFAULT_SPACER_HEIGHT }${ DEFAULT_SPACER_HEIGHT_UNIT }` && heightSm === `${ DEFAULT_SPACER_HEIGHT }${ DEFAULT_SPACER_HEIGHT_UNIT }` ) { setAttributes( { heightLg: defaultValue.lg + defaultValue.lg_unit, heightMd: defaultValue.md + defaultValue.md_unit, heightSm: defaultValue.sm + defaultValue.sm_unit, } ); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [] ); const defaultLgValue = defaultValue.lg + defaultValue.lg_unit; const defaultMdValue = defaultValue.md + defaultValue.md_unit; const defaultSmValue = defaultValue.sm + defaultValue.sm_unit; const settingUrl = addQueryArgs( 'options-general.php', { page: 'flexible-spacer-block-option', } ); const blockProps = useBlockProps( { className: clsx( 'fsb-flexible-spacer', { 'fsb-flexible-spacer--is-show-block': !! isShowBlock, 'fsb-flexible-spacer--is-responsive': !! isResponsive, } ), } ); function getUpdatedHeight( currentValue: HeightValue, newValue: HeightValue ): string | undefined { if ( ! newValue ) { return undefined; } const [ newParsedQuantity, newParsedUnit ] = parseQuantityAndUnitFromRawValue( newValue ); const [ , currentParsedUnit ] = parseQuantityAndUnitFromRawValue( currentValue ); const newUnit = newParsedUnit || currentParsedUnit || 'px'; return newParsedQuantity + newUnit; } // Writing a device height leaves "All heights" on the view it mounted with, // so remount it. Its own handler bumps the key in the other direction. const setDeviceHeights = ( heights: Partial< BlockAttributes > ) => { setAttributes( heights ); setAllSyncKey( ( key ) => key + 1 ); }; const onChangeHeightLg = ( currentValue: HeightValue, newValue: HeightValue ) => { const updatedHeight = getUpdatedHeight( currentValue, newValue ); const newHeights = { heightLg: updatedHeight, ...( ! isEnableMd && { heightMd: updatedHeight } ), }; setDeviceHeights( newHeights ); }; const onChangeHeightMd = ( currentValue: HeightValue, newValue: HeightValue ) => { setDeviceHeights( { heightMd: getUpdatedHeight( currentValue, newValue ) } ); }; const onChangeHeightSm = ( currentValue: HeightValue, newValue: HeightValue ) => { setDeviceHeights( { heightSm: getUpdatedHeight( currentValue, newValue ) } ); }; const resetAll = () => { setDeviceHeights( { heightLg: defaultLgValue, heightMd: isEnableMd ? defaultMdValue : defaultLgValue, heightSm: defaultSmValue, isNegativeLg: false, isNegativeMd: false, isNegativeSm: false, } ); }; const SPACER_CONTROLS: SpacerControl[] = [ { label: __( 'All heights', 'flexible-spacer-block' ), icon: settings, slug: 'all', value: heightAll, syncKey: allSyncKey, onChange: ( value ) => { setAttributes( { heightLg: value, heightMd: value, heightSm: value } ); setDeviceSyncKey( ( key ) => key + 1 ); }, hasValue: () => heightLg !== defaultLgValue || heightMd !== defaultMdValue || heightSm !== defaultSmValue || isNegativeLg || isNegativeMd || isNegativeSm, onDeselect: resetAll, }, { label: __( 'Desktop height', 'flexible-spacer-block' ), icon: desktop, slug: 'lg', value: heightLg, syncKey: deviceSyncKey, onChange: ( value ) => { const newHeights = { heightLg: value, ...( ! isEnableMd && { heightMd: value } ) }; setDeviceHeights( newHeights ); }, isNegative: isNegativeLg, onNegativeChange: ( value ) => { const newAttributes = { isNegativeLg: value, ...( ! isEnableMd && { isNegativeMd: value } ), }; setAttributes( newAttributes ); }, hasValue: () => heightLg !== defaultLgValue || isNegativeLg, onDeselect: () => setDeviceHeights( { heightLg: defaultLgValue, isNegativeLg: false } ), }, { label: __( 'Tablet height', 'flexible-spacer-block' ), icon: tablet, slug: 'md', value: heightMd, syncKey: deviceSyncKey, onChange: ( value ) => setDeviceHeights( { heightMd: value } ), isNegative: isNegativeMd, onNegativeChange: ( value ) => setAttributes( { isNegativeMd: value } ), hasValue: () => heightMd !== defaultMdValue || isNegativeMd, onDeselect: () => setDeviceHeights( { heightMd: defaultMdValue, isNegativeMd: false } ), }, { label: __( 'Mobile height', 'flexible-spacer-block' ), icon: mobile, slug: 'sm', value: heightSm, syncKey: deviceSyncKey, onChange: ( value ) => setDeviceHeights( { heightSm: value } ), isNegative: isNegativeSm, onNegativeChange: ( value ) => setAttributes( { isNegativeSm: value } ), hasValue: () => heightSm !== defaultSmValue || isNegativeSm, onDeselect: () => setDeviceHeights( { heightSm: defaultSmValue, isNegativeSm: false } ), }, ]; const SPACER_DEVICES: SpacerDevice[] = [ { label: __( 'Mobile', 'flexible-spacer-block' ), slug: 'sm', icon: mobile, isNegative: isNegativeSm, height: heightSm || defaultValue.sm, isResizing: isResizingSm, setIsResizing: setIsResizingSm, onChangeHeight: onChangeHeightSm, enabled: true, }, { label: __( 'Tablet', 'flexible-spacer-block' ), slug: 'md', icon: tablet, isNegative: isNegativeMd, height: heightMd || defaultValue.md, isResizing: isResizingMd, setIsResizing: setIsResizingMd, onChangeHeight: onChangeHeightMd, enabled: isEnableMd, }, { label: __( 'Desktop', 'flexible-spacer-block' ), slug: 'lg', icon: desktop, isNegative: isNegativeLg, height: heightLg || defaultValue.lg, isResizing: isResizingLg, setIsResizing: setIsResizingLg, onChangeHeight: onChangeHeightLg, enabled: true, }, ] .filter( ( device ) => device.enabled ) .map( ( { enabled, setIsResizing, onChangeHeight, ...device } ) => ( { ...device, onResizeStart: () => toggleSelection?.( false ), onResize: () => setIsResizing( true ), onResizeStop: ( _event: unknown, _direction: unknown, elt: HTMLElement ) => { onChangeHeight( undefined, `${ elt.clientHeight }px` ); setIsResizing( false ); }, } ) ); const dropdownMenuProps = ! isMobile ? { popoverProps: { placement: 'left-start' as const, offset: 259, }, } : {}; return ( <> setIsResponsive( ! isResponsive ) } /> { SPACER_CONTROLS.map( ( control ) => ( setActiveDevice( control.slug ) } onMouseOut={ () => setActiveDevice( undefined ) } /> { control.onNegativeChange && ( ) } ) ) } { __( 'Plugin Setting', 'flexible-spacer-block' ) }
{ sprintf( /* translators: %d: Breakpoint width in pixels. */ __( '≤ %dpx <', 'flexible-spacer-block' ), Number( fsbConf.breakpoint.sm ) ) }
{ isEnableMd && (
{ sprintf( /* translators: %d: Breakpoint width in pixels. */ __( '≤ %dpx <', 'flexible-spacer-block' ), Number( fsbConf.breakpoint.md ) ) }
) }
{ SPACER_DEVICES.map( ( device, index ) => (
{ device.label }
{ /* `children` is required by the type, but is not actually needed here, so render a dummy element. */ } { /* TODO: Remove this once https://github.com/WordPress/gutenberg/pull/79370 is merged. */ } <>
) ) }
); }