import {__, _x, sprintf} from '@wordpress/i18n' import cx from 'classnames' import { useEffect, useMemo, useRef, useState, } from '@wordpress/element' import { useSelect, useDispatch, } from '@wordpress/data' import { store as coreDataStore, } from '@wordpress/core-data' import { store as noticesStore, } from '@wordpress/notices' import { useDebounce, } from 'react-use' import Fuse from 'fuse.js' import { usePluginPreference, } from '../../store' import { useTailwindConfigVersion, } from '../../tailwind' import { resolvePreset, resolvePresetSlug, usePresetOptionsEdit, getPresetSlugs, type BlockPreset, type ResolvedBlockPreset, type PresetOptions, type Presets, } from '.' import type { PresetFilters, } from '../../types' import type { tBlockAttributes, } from '@ska/shared' import { presetOptions, } from '../../data' import { triggerCompiledEvent, } from '../../plugins/options/plugin-icon' import { debugLog, parseJson, } from '@ska/utils' const NO_PRESETS = {} as Presets /** All presets. */ export const usePresets = () => { const [select] = usePresetOptionsEdit() return select.getOption('presets') as PresetOptions['presets'] } /** All preset slugs. */ export const usePresetSlugs = () => { return Object.keys(usePresets()) } /** Slugs of presets applied to a block. */ export const useBlocksPresetSlugs = (attributes: tBlockAttributes, filter?: PresetFilters) => { return getPresetSlugs(attributes, filter) } /** Presets applied to a block. */ export const useBlocksPresets = (attributes: tBlockAttributes, filter?: PresetFilters) => { const slugs = useBlocksPresetSlugs(attributes, filter) return useSelect(select => { if(!slugs.length) { return NO_PRESETS } const {getEditedEntityRecord} = select(coreDataStore) const record = getEditedEntityRecord('root', 'site', undefined!) as any const {ska_blocks_presets = {}} = record || {} const {presets: livePresets} = ska_blocks_presets const presets = livePresets || presetOptions.presets return Object.keys(presets).reduce((acc, cur) => { if(slugs.includes(cur)) { acc[cur] = presets[cur] } return acc }, {} as Presets) }, [slugs]) } /** Preset by slug. */ export const usePreset = (slug: BlockPreset['id']) => { const presets = usePresets() if(Object(presets).hasOwnProperty(slug)) { return presets[slug] } return resolvePreset(slug, presets) } /** All valid presets of a block. */ export const useResolvedBlockPresets = (blockPresets: BlockPreset[]): ResolvedBlockPreset[] => { const presets = usePresets() return blockPresets.map(blockPreset => { const slug = resolvePresetSlug(blockPreset.id, presets) return slug ? { ...blockPreset, id: slug, } : false }).filter(Boolean) as ResolvedBlockPreset[] } /** All presets of a block, including missing ones. */ export const useBlockPresets = (blockPresets: BlockPreset[]): ResolvedBlockPreset[] => { const presets = usePresets() return blockPresets.map(blockPreset => { const slug = resolvePresetSlug(blockPreset.id, presets) return slug ? { ...blockPreset, id: slug, } : { id: blockPreset.id.toString(), ref: '404', title: `Not found (${blockPreset.id.toString()})`, attrs: '{}', cx: '', css: '', } }) } // Note: has PHP equivalent @ Preset.php const KEPT_WHEN_IS = ['not-prose', 'group', 'peer'] const KEPT_WHEN_STARTS_WITH = ['group/', 'peer/'] const isKeptStaticPresetClassName = (className: string) => { if(KEPT_WHEN_IS.includes(className)) { return true } return KEPT_WHEN_STARTS_WITH.some(cx => className.indexOf(cx) === 0) } export const useStaticPresetsClassNames = (attributes: tBlockAttributes) => { const staticPresets = useBlocksPresets(attributes, 'static') const classNames = Object.entries(staticPresets).reduce((acc, [slug, {cx = ''}]) => { acc.push(`ska-preset--${slug}`) cx.split(' ').forEach(className => { if(!acc.includes(className) && isKeptStaticPresetClassName(className)) { acc.push(className) } }) return acc }, [] as string[]) return cx(classNames) } /** Recompile all presets when Tailwind config has been modified. */ export const useRecompilePresets = (presets: Presets) => { const ver = useTailwindConfigVersion() const [, presetsDispatch] = usePresetOptionsEdit() const compiledRef = useRef>({}) const hasEntityConfig = useSelect(select => !!select(coreDataStore).getEntityConfig('root', 'site'), []) // Entity config needs to be loaded to be able to update presets const { createInfoNotice, } = useDispatch(noticesStore) useEffect(() => { if(ver < 0 || !hasEntityConfig) { return } const recompiled = Object.entries(presets).filter(([slug, preset]) => { const { v = 0, } = preset if(v === ver) { return false } if(Object(compiledRef.current).hasOwnProperty(slug) && compiledRef.current[slug] === v) { return false } compiledRef.current[slug] = v const { attrs = '{}', } = preset const attributes = parseJson(attrs, `Preset: ${slug}`) as tBlockAttributes presetsDispatch.updatePresetAttributes(slug, attributes) return true }) if(!recompiled.length) { return } triggerCompiledEvent() debugLog(`Triggered recompile for ${recompiled.length} of ${Object.keys(presets).length} presets.`) if(recompiled.length > 1) { createInfoNotice(sprintf(__(`Recompiled %1$d of %2$d presets.`, 'ska-blocks'), recompiled.length, Object.keys(presets).length), {type: 'snackbar'}) } }, [ver, hasEntityConfig, presets, presetsDispatch, createInfoNotice]) return null } type FilterTerm = string type UpdateFilterTerm = (nextFilter: FilterTerm) => void export const useFilteredPresets = (presets: Presets) => { const [initialFilterTerm, updateFilterTerm] = usePluginPreference('presetFilterTerm') const [filterTerm, setFilterTerm] = useState(initialFilterTerm) const fuse = useMemo(() => { const searchableData = Object.entries(presets).map(([slug, preset]) => { const {title, cx} = preset return { slug, title, cx, preset, } }) return new Fuse(searchableData, { keys: [ {name: 'title', weight: 1}, {name: 'slug', weight: 0.5}, {name: 'cx', weight: 0.1}, ], }) }, [presets]) useDebounce(() => { if(initialFilterTerm !== filterTerm) { updateFilterTerm(filterTerm) } }, 1000, [filterTerm, initialFilterTerm, updateFilterTerm]) const results = useMemo(() => { return fuse.search(filterTerm || '').reduce((acc, cur) => { const {item} = cur const {slug, preset} = item acc[slug] = preset return acc }, {} as Presets) }, [fuse, filterTerm]) const totalCount = Object.keys(presets).length const resultsCount = Object.keys(results).length return { presets: resultsCount > 0 ? results : presets, info: resultsCount > 0 ? sprintf(__('%1$d of %2$d presets', 'ska-blocks'), resultsCount, totalCount) : sprintf(__('%d presets', 'ska-blocks'), totalCount), filterTerm: filterTerm as FilterTerm, setFilterTerm: setFilterTerm as UpdateFilterTerm, } }