import * as React from 'react' import {__, _x} from '@wordpress/i18n' import { useCallback, } from '@wordpress/element' import { ToggleControl, TextControl, BaseControl, } from '@wordpress/components' import { hasBlockSupport, getBlockSupport, createBlock, // @ts-expect-error } from '@wordpress/blocks' import { InspectorAdvancedControls, store as blockEditorStore, // @ts-expect-error } from '@wordpress/block-editor' import { createHigherOrderComponent, useDebounce, } from '@wordpress/compose' import { useSelect, useDispatch, } from '@wordpress/data' import { addFilter, } from '@wordpress/hooks' import { plus, } from '@wordpress/icons' import { ButtonGroup, Button, } from '@ska/components' import type { tBlock, tBlockEditProps, tBlockNameOrType, tBlockType, } from '@ska/shared' export interface AppenderValue { /** Type of appender to render. */ type?: 'default' | 'hidden' | 'repeater' /** Repeater appender button text. */ label?: string /** Whether repeater appender should strip content. */ stripContent?: boolean } export interface AppenderAttributes { skaBlocksAppender?: AppenderValue } export const APPENDER_SUPPORT_KEY = 'skaBlocksAppender' export const APPENDER_SUPPORT_DEFAULT_ENABLED = false export const hasAppenderSupport = (block: tBlockNameOrType) => hasBlockSupport(block, APPENDER_SUPPORT_KEY, APPENDER_SUPPORT_DEFAULT_ENABLED) const defaultLabel = _x('Add item', 'Repeater appender default label', 'ska-blocks') /** * Default value can be provided in block `supports` attribute. * * @example * ```json * "supports": { * "skaBlocksAppender": { * "render": false * } * } * ``` */ const getDefaultValue = (block: tBlockType) => { const support = getBlockSupport(block, APPENDER_SUPPORT_KEY, APPENDER_SUPPORT_DEFAULT_ENABLED) return { ...(typeof support === 'object' && { ...support, }), } } const withCustomAttributes = (settings: tBlockType) => { if(!hasAppenderSupport(settings)) { return settings } if(!settings.attributes.skaBlocksAppender) { const defaultValue = getDefaultValue(settings) Object.assign(settings.attributes, { skaBlocksAppender: { type: 'object', ...(Object.keys(defaultValue).length > 0 && { default: defaultValue, }), }, }) } return settings } const useSupportsRepeater = (clientId: string) => { return useSelect(select => { const { getBlock, } = select(blockEditorStore) const block = getBlock(clientId) as tBlock return block.innerBlocks.length > 0 }, [clientId]) } const APPENDER_OPTIONS = [ { label: _x('Default', 'Block appender type', 'ska-blocks'), value: 'default', }, { label: _x('Repeater', 'Block appender type', 'ska-blocks'), value: 'repeater', }, { label: _x('Disabled', 'Block appender type', 'ska-blocks'), value: 'hidden', }, ] const AppenderInspectorControls: React.FC> = (props) => { const { clientId, attributes, setAttributes, } = props const { skaBlocksAppender = {}, } = attributes const { type, stripContent = false, label = '', } = skaBlocksAppender const supportsRepeater = useSupportsRepeater(clientId) const isRepeater = type === 'repeater' return <> value !== 'repeater')} value={type || 'default'} onChange={(value = 'default') => setAttributes({skaBlocksAppender: {...skaBlocksAppender, type: (value === 'default' ? undefined : value) as AppenderValue['type']}})} /> {isRepeater && <> setAttributes({skaBlocksAppender: {...skaBlocksAppender, stripContent: !stripContent}})} __nextHasNoMarginBottom /> setAttributes({skaBlocksAppender: {...skaBlocksAppender, label: nextValue}})} __nextHasNoMarginBottom __next40pxDefaultSize /> } } const withAppenderControls = createHigherOrderComponent( (BlockEdit: any) => (props: tBlockEditProps) => { if(hasAppenderSupport(props.name)) { return <> } /> } return }, 'withSkaBlocksAppender' ) const RepeaterAppender: React.FC> = props => { const { clientId, isSelected = false, attributes, } = props const { skaBlocksAppender = {}, } = attributes const { label = defaultLabel, stripContent = false, } = skaBlocksAppender const { template, index, hasChildSelected, } = useSelect(select => { const { getBlockIndex, getBlock, hasSelectedInnerBlock, } = select(blockEditorStore) const block = getBlock(clientId) as tBlock const template = block.innerBlocks.length ? block.innerBlocks[block.innerBlocks.length - 1] : undefined const isAncestorOfSelectedBlock = hasSelectedInnerBlock(clientId, true) return { block, template, index: template ? getBlockIndex(template.clientId) as number : 0, hasChildSelected: isAncestorOfSelectedBlock, } }, [clientId]) const { insertBlock, toggleBlockHighlight, } = useDispatch(blockEditorStore) const {clientId: templateClientId} = template || {} const debouncedToggleBlockHighlight = useDebounce(toggleBlockHighlight, 50) const onMouseEnter = useCallback(() => { if(templateClientId) { debouncedToggleBlockHighlight(templateClientId, true) } }, [templateClientId, debouncedToggleBlockHighlight]) const onMouseLeave = useCallback(() => { if(templateClientId) { debouncedToggleBlockHighlight(templateClientId, false) } }, [templateClientId, debouncedToggleBlockHighlight]) const onClick = useCallback(() => { toggleBlockHighlight(templateClientId, false) const makeBlock: any = (block: tBlock) => { const { name, attributes: blockAttributes = {}, innerBlocks = [], } = block const { content, ...attributes } = blockAttributes return [ name, { ...attributes, ...(!!content && !stripContent && { content, }), }, innerBlocks.map(block => createBlock(...makeBlock(block))), ] } insertBlock(createBlock(...makeBlock(template)), index + 1, clientId) }, [clientId, templateClientId, template, index, insertBlock, stripContent, toggleBlockHighlight]) if(!template || (!isSelected && !hasChildSelected)) { return null } return (