import * as React from 'react' import {__, _x} from '@wordpress/i18n' import { useContext, useMemo, } from '@wordpress/element' import { plus as plusIcon, redo as redoIcon, brush as brushIcon, } from '@wordpress/icons' import { store as noticesStore, } from '@wordpress/notices' import { useDispatch, } from '@wordpress/data' import { Search as SkaSearch, } from '@ska/components' import { focusElement, } from '@ska/utils' import Fuse from 'fuse.js' import { INSTANTIATED_DEFAULT_VALUE, } from '../../attributes' import { useParse, } from '../parse' import { useSkaBlocks, } from '../../hooks' import { useSkaBlocksDispatch, } from '../../reducer' import { ApplyTailwindContext, } from '../../applyTailwind' import { useBlocksPresetSlugs, usePresets, } from '../../../supports/presets' import { useNavigateToSelector, } from '../../../supports/selectors' import { handlePasteVariables, } from '../../../supports/variables' import type { tBlockAttributes, tBlockEditProps, tBlockName, } from '@ska/shared' import './style.scss' export interface SearchProps { /** Block name. */ name: tBlockName | undefined /** Block attributes. */ attributes: tBlockAttributes /** Block attributes setter. */ setAttributes: tBlockEditProps['setAttributes'] } export const TAILWIND_VALUE_CLIPBOARD_PREFIX = 'ska-blocks:' export const SEARCH_PLACEHOLDER = __('Add classes or utility…', 'ska-blocks') const Search: React.FC = ({ name, attributes, setAttributes, }) => { const navigateToSelector = useNavigateToSelector() const shouldApplyTailwind = useContext(ApplyTailwindContext) // Don't offer presets when `false`. const { createSuccessNotice, createErrorNotice, } = useDispatch(noticesStore) const features = useSkaBlocks().tailwind.getFeaturesList(attributes) const presets = usePresets() const currentPresets = useBlocksPresetSlugs(attributes) const [parse, ParseErrors] = useParse({name, attributes, setAttributes}) const dispatch = useSkaBlocksDispatch(attributes, setAttributes) const [fuse, fuseKey] = useMemo(() => { const searchablePresets = shouldApplyTailwind ? Object .entries(presets) .filter(([slug]) => !currentPresets.includes(slug)) .map(([slug, preset]) => ({ type: 'preset', presetSlug: slug, presetTitle: preset.title, })) : [] // @ts-ignore const searchData = features.concat(searchablePresets) return [ new Fuse(searchData, { keys: [ 'label', 'groupLabel', 'presetTitle', 'presetSlug', ], }), searchData.length, ] }, [features, presets, currentPresets, shouldApplyTailwind]) const getResults = (searchTerm: string) => { if(!searchTerm) { return [] } const searchResults = fuse.search(searchTerm).map(r => r.item).map((result: any) => { switch(result.type) { case 'preset': return { id: result.presetSlug, presetSlug: result.presetSlug, title: result.presetTitle, content: __('Add preset', 'ska-blocks'), } default: return { id: result.id, title: result.label, content: result.groupLabel, hasValue: result.hasValue, } } }) const probablySelector = searchTerm.endsWith(':') const addClasses = { id: 'parse', title: __('Add classes', 'ska-blocks'), content: searchTerm, hasValue: false, } const addSelector = { id: 'add-selector', title: __('Add selector', 'ska-blocks'), content: searchTerm, hasValue: false, } const isVariable = searchTerm.startsWith('--') return [ ...(isVariable ? [{ id: 'add-variable', title: __('Add variable', 'ska-blocks'), content: searchTerm, hasValue: false, }] : []), ...(probablySelector ? [addSelector, addClasses] : [addClasses, addSelector]), ...searchResults.slice(0, 15), ] } const applyPastedValue = (text: string) => { const invalidError = () => createErrorNotice(__('Invalid value.', 'ska-blocks'), {type: 'snackbar'}) if(text.indexOf(TAILWIND_VALUE_CLIPBOARD_PREFIX) !== 0) { invalidError() return } let pastedAttributes: tBlockAttributes = {} try { pastedAttributes = JSON.parse(text.replace(TAILWIND_VALUE_CLIPBOARD_PREFIX, '')) } catch(e) { invalidError() return } const { skaBlocksSelectors = {}, ...rest } = pastedAttributes dispatch.setAttributes({ skaBlocksSelectors: { ...attributes.skaBlocksSelectors, ...skaBlocksSelectors, }, ...rest, }) createSuccessNotice(__('Pasted value.', 'ska-blocks'), {type: 'snackbar'}) } const onPaste = (e: React.ClipboardEvent) => { const text = e?.clipboardData?.getData('Text') if(!text) { return } if(handlePasteVariables({ content: text, apply: pastedRecord => { const { skaBlocksVariables = {}, } = attributes const { record = {}, } = skaBlocksVariables dispatch.setAttributes({ skaBlocksVariables: { ...skaBlocksVariables, record: { ...record, ...pastedRecord, }, }, }) }, onSuccess: () => { createSuccessNotice(__('Pasted variables.', 'ska-blocks'), {type: 'snackbar'}) }, onError: () => { createErrorNotice(__('Invalid value.', 'ska-blocks'), {type: 'snackbar'}) }, })) { e.preventDefault() return } if(text.indexOf(TAILWIND_VALUE_CLIPBOARD_PREFIX) !== 0) { return } e.preventDefault() applyPastedValue(text) } return <> { const { id, content, presetSlug, } = result if(id === 'parse') { parse(content) return } if(id === 'add-selector') { dispatch.addSelector(content) navigateToSelector(content) return } if(id === 'add-variable') { dispatch.addVariable(content) return } if(presetSlug) { dispatch.addPreset(presetSlug) return } const feature = features.find(f => f.id === id) if(!feature) { return } if(feature.hasValue) { focusElement(`#${feature.id}`) return } dispatch.setAttributes({[feature.id]: INSTANTIATED_DEFAULT_VALUE}) focusElement(`#${feature.id}`, 50) }} onPaste={onPaste} getResultIcon={({hasValue, presetSlug}) => { if(presetSlug) { return brushIcon } return hasValue ? redoIcon : plusIcon }} actionInputProps={{ label: __('Search for Tailwind features to add the the block', 'ska-blocks'), placeholder: SEARCH_PLACEHOLDER, actionLabel: __('Search', 'ska-blocks'), }} /> {ParseErrors} } export default Search