UNPKG

@wordpress/block-editor

Version:
359 lines (337 loc) 10.1 kB
/** * External dependencies */ import clsx from 'clsx'; /** * WordPress dependencies */ import { __, sprintf } from '@wordpress/i18n'; import { useState, useMemo, useCallback, createInterpolateElement, } from '@wordpress/element'; import { Button, CheckboxControl, Flex, FlexItem, Icon, Modal, } from '@wordpress/components'; import { useDispatch, useSelect } from '@wordpress/data'; import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; import { store as noticesStore } from '@wordpress/notices'; /** * Internal dependencies */ import { BLOCK_VISIBILITY_VIEWPORT_ENTRIES, BLOCK_VISIBILITY_VIEWPORTS, } from './constants'; import { store as blockEditorStore } from '../../store'; import { cleanEmptyObject } from '../../hooks/utils'; import { getViewportCheckboxState, getHideEverywhereCheckboxState, } from './utils'; import './style.scss'; const DEFAULT_VIEWPORT_CHECKBOX_VALUES = { [ BLOCK_VISIBILITY_VIEWPORTS.mobile.key ]: false, [ BLOCK_VISIBILITY_VIEWPORTS.tablet.key ]: false, [ BLOCK_VISIBILITY_VIEWPORTS.desktop.key ]: false, }; const EMPTY_BLOCKS = []; /** * Modal component for configuring block visibility across viewports. * * Allows users to hide blocks on specific viewport sizes (mobile, tablet, desktop) * or hide them everywhere. When editing multiple blocks, checkboxes only show as * checked if ALL selected blocks share the same setting to avoid ambiguity. * * @param {Object} props Component props. * @param {Array} props.clientIds The client IDs of the blocks to hide. * @param {Function} props.onClose Callback function invoked when the modal is closed. * @return {JSX.Element} The modal component. */ export default function BlockVisibilityModal( { clientIds, onClose } ) { const { createSuccessNotice } = useDispatch( noticesStore ); const { updateBlockAttributes } = useDispatch( blockEditorStore ); const blocks = useSelect( ( select ) => select( blockEditorStore ).getBlocksByClientId( clientIds ) ?? EMPTY_BLOCKS, [ clientIds ] ); const listViewShortcut = useSelect( ( select ) => { return select( keyboardShortcutsStore ).getShortcutRepresentation( 'core/editor/toggle-list-view' ); }, [] ); const initialViewportValues = useMemo( () => { if ( blocks?.length === 0 ) { return { hideEverywhere: false, viewportChecked: {}, }; } const viewportValues = {}; BLOCK_VISIBILITY_VIEWPORT_ENTRIES.forEach( ( [ , { key } ] ) => { viewportValues[ key ] = getViewportCheckboxState( blocks, key ); } ); return { hideEverywhere: getHideEverywhereCheckboxState( blocks ), viewportChecked: viewportValues, }; }, [ blocks ] ); const [ viewportChecked, setViewportChecked ] = useState( initialViewportValues?.viewportChecked ?? {} ); const [ hideEverywhere, setHideEverywhere ] = useState( initialViewportValues?.hideEverywhere ?? false ); const handleViewportCheckboxChange = useCallback( ( viewport, isChecked ) => { setViewportChecked( { ...viewportChecked, [ viewport ]: isChecked, } ); }, [ viewportChecked ] ); const noticeMessage = useMemo( () => { if ( ! hideEverywhere ) { return sprintf( // translators: %s: The shortcut key to access the List View. __( 'Block visibility settings saved. You can access them via the List View (%s).' ), listViewShortcut ); } const message = blocks?.length > 1 ? // translators: %s: The shortcut key to access the List View. __( 'Blocks hidden. You can access them via the List View (%s).' ) : // translators: %s: The shortcut key to access the List View. __( 'Block hidden. You can access it via the List View (%s).' ); return sprintf( message, listViewShortcut ); }, [ hideEverywhere, blocks?.length, listViewShortcut ] ); const isAnyViewportChecked = useMemo( () => Object.values( viewportChecked ).some( ( checked ) => checked === true || checked === null ), [ viewportChecked ] ); const hasIndeterminateValues = useMemo( () => { if ( hideEverywhere === null ) { return true; } return Object.values( viewportChecked ).some( ( checked ) => checked === null ); }, [ hideEverywhere, viewportChecked ] ); const handleSubmit = useCallback( ( event ) => { event.preventDefault(); const newVisibility = hideEverywhere ? false : BLOCK_VISIBILITY_VIEWPORT_ENTRIES.reduce( ( acc, [ , { key } ] ) => { if ( viewportChecked[ key ] ) { // Values are inverted to hide the block on the selected viewport. // In the UI, the checkbox is checked (true) when the block is hidden on the selected viewport, // so 'false' means hide the block on the selected viewport. acc[ key ] = false; } return acc; }, {} ); const attributesByClientId = Object.fromEntries( blocks.map( ( { clientId, attributes } ) => [ clientId, { metadata: cleanEmptyObject( { ...attributes?.metadata, blockVisibility: newVisibility, } ), }, ] ) ); updateBlockAttributes( clientIds, attributesByClientId, { uniqueByBlock: true, } ); createSuccessNotice( noticeMessage, { id: hideEverywhere ? 'block-visibility-hidden' : 'block-visibility-viewports-saved', type: 'snackbar', } ); onClose(); }, [ blocks, clientIds, createSuccessNotice, hideEverywhere, noticeMessage, onClose, updateBlockAttributes, viewportChecked, ] ); const hasMultipleBlocks = blocks?.length > 1; return ( <Modal title={ clientIds?.length > 1 ? __( 'Hide blocks' ) : __( 'Hide block' ) } onRequestClose={ onClose } overlayClassName="block-editor-block-visibility-modal" size="small" > <form onSubmit={ handleSubmit }> <fieldset> <legend> { hasMultipleBlocks ? __( 'Select the viewport sizes for which you want to hide the blocks. Changes will apply to all selected blocks.' ) : __( 'Select the viewport size for which you want to hide the block.' ) } </legend> <ul className="block-editor-block-visibility-modal__options"> <li className="block-editor-block-visibility-modal__options-item block-editor-block-visibility-modal__options-item--everywhere"> <CheckboxControl className="block-editor-block-visibility-modal__options-checkbox--everywhere" label={ __( 'Omit from published content' ) } checked={ hideEverywhere === true } indeterminate={ hideEverywhere === null } onChange={ ( checked ) => { setHideEverywhere( checked ); // Reset viewport checkboxes when hide everywhere is checked. setViewportChecked( DEFAULT_VIEWPORT_CHECKBOX_VALUES ); } } /> { hideEverywhere !== true && ( <ul className="block-editor-block-visibility-modal__sub-options"> { BLOCK_VISIBILITY_VIEWPORT_ENTRIES.map( ( [ , { label, icon, key } ] ) => ( <li key={ key } className="block-editor-block-visibility-modal__options-item" > <CheckboxControl label={ sprintf( // translators: %s: The viewport name. __( 'Hide on %s' ), label ) } checked={ viewportChecked[ key ] ?? false } indeterminate={ viewportChecked[ key ] === null } onChange={ ( checked ) => handleViewportCheckboxChange( key, checked ) } /> <Icon icon={ icon } className={ clsx( { 'block-editor-block-visibility-modal__options-icon--checked': viewportChecked[ key ], } ) } /> </li> ) ) } </ul> ) } </li> </ul> { hasMultipleBlocks && hasIndeterminateValues && ( <p className="block-editor-block-visibility-modal__description"> { __( 'Selected blocks have different visibility settings. The checkboxes show an indeterminate state when settings differ.' ) } </p> ) } { ! hasMultipleBlocks && hideEverywhere === true && ( <p className="block-editor-block-visibility-modal__description"> { sprintf( // translators: %s: The shortcut key to access the List View. __( 'Block will be hidden in the editor, and omitted from the published markup on the frontend. You can configure it again by selecting it in the List View (%s).' ), listViewShortcut ) } </p> ) } { ! hasMultipleBlocks && ! hideEverywhere && isAnyViewportChecked && ( <p className="block-editor-block-visibility-modal__description"> { createInterpolateElement( sprintf( // translators: %s: The shortcut key to access the List View __( 'Block will be hidden according to the selected viewports. It will be <strong>included in the published markup on the frontend</strong>. You can configure it again by selecting it in the List View (%s).' ), listViewShortcut ), { strong: <strong />, } ) } </p> ) } </fieldset> <Flex className="block-editor-block-visibility-modal__actions" justify="flex-end" expanded={ false } > <FlexItem> <Button variant="tertiary" onClick={ onClose } __next40pxDefaultSize > { __( 'Cancel' ) } </Button> </FlexItem> <FlexItem> <Button variant="primary" type="submit" __next40pxDefaultSize > { __( 'Apply' ) } </Button> </FlexItem> </Flex> </form> </Modal> ); }