import React, {Fragment, useState, ReactNode, useCallback, useMemo, memo} from 'react' import nanoid from 'nanoid' import {isFunctionalUpdate, useStyle, cssRule} from '@karma.run/react' import {isValueConstructor, ValueConstructor, UnionToIntersection} from '@karma.run/utility' import { MaterialIconDeleteOutlined, MaterialIconKeyboardArrowUp, MaterialIconKeyboardArrowDown } from '@karma.run/icons' import {BlockProps, BlockConstructorFn} from './block' import {IconElement, Icon} from '../data/icon' import {AddBlockInput} from '../input/addBlockInput' import {Box} from '../layout/box' import {Spacing} from '../style/helpers' import {cssRuleWithTheme, useThemeStyle} from '../style/themeContext' import {IconButton} from '../buttons/iconButton' import {Card} from '../data/card' export interface BlockCaseProps { label: string icon: IconElement defaultValue: ValueConstructor field: BlockConstructorFn } export interface BlockListValue { key: string type: T value: V } export type BlockMap = Record export type BlockMapForValue = UnionToIntersection< R extends BlockListValue ? {[K in T]: BlockCaseProps} : never > const BlockListStyle = cssRule({ width: '100%' }) export interface BlockListItemProps { index: number value: BlockListValue icon: IconElement autofocus: boolean disabled?: boolean onChange: (index: number, value: React.SetStateAction>) => void onDelete: (index: number) => void onMoveUp?: (index: number) => void onMoveDown?: (index: number) => void children: (props: BlockProps) => JSX.Element } const BlockListItem = memo(function BlockListItem({ index, value, icon, autofocus, disabled, children, onChange, onDelete, onMoveUp, onMoveDown }: BlockListItemProps) { const handleValueChange = useCallback( (fieldValue: React.SetStateAction) => { onChange(index, value => ({ ...value, value: isFunctionalUpdate(fieldValue) ? fieldValue(value.value) : fieldValue })) }, [onChange, index] ) return ( onDelete(index)} onMoveUp={onMoveUp ? () => onMoveUp(index) : undefined} onMoveDown={onMoveDown ? () => onMoveDown(index) : undefined}> {children({value: value.value, onChange: handleValueChange, autofocus, disabled})} ) }) export function useBlockMap( map: () => BlockMapForValue, deps: ReadonlyArray | undefined ) { return useMemo(map, deps) } export interface BlockListProps extends BlockProps { children: BlockMapForValue } export function BlockList({ value: values, children, disabled, onChange }: BlockListProps) { const [focusIndex, setFocusIndex] = useState(null) const css = useStyle() const blockMap = children as BlockMap const handleItemChange = useCallback( (index: number, itemValue: React.SetStateAction) => { onChange(value => Object.assign([], value, { [index]: isFunctionalUpdate(itemValue) ? itemValue(value[index]) : itemValue }) ) }, [onChange] ) const handleAdd = useCallback( (index: number, type: string) => { setFocusIndex(index) onChange(values => { const {defaultValue} = blockMap[type] const valuesCopy = values.slice() valuesCopy.splice(index, 0, { key: nanoid(), type, value: isValueConstructor(defaultValue) ? defaultValue() : defaultValue } as V) return valuesCopy }) }, [blockMap, onChange] ) const handleRemove = useCallback( (itemIndex: number) => { onChange(value => value.filter((value, index) => index !== itemIndex)) }, [onChange] ) const handleMoveIndex = useCallback( (from: number, to: number) => { onChange(values => { const valuesCopy = values.slice() const [value] = valuesCopy.splice(from, 1) valuesCopy.splice(to, 0, value) return valuesCopy }) }, [onChange] ) const handleMoveUp = useCallback( (index: number) => { handleMoveIndex(index, index - 1) }, [handleMoveIndex] ) const handleMoveDown = useCallback( (index: number) => { handleMoveIndex(index, index + 1) }, [handleMoveIndex] ) function addButtonForIndex(index: number) { return ( ({ id: type, icon, label }))} onMenuItemClick={({id}) => handleAdd(index, id)} subtle={index !== values.length || disabled} disabled={disabled} /> ) } function listItemForIndex(value: V, index: number) { const hasPrevIndex = index - 1 >= 0 const hasNextIndex = index + 1 < values.length const blockDef = blockMap[value.type] return ( {blockDef.field} {addButtonForIndex(index + 1)} ) } return (
{addButtonForIndex(0)} {values.map((value, index) => listItemForIndex(value, index))}
) } const ListItemWrapperStyle = cssRule({ display: 'flex', width: '100%' }) const ListItemWrapperActionStyle = cssRule({ display: 'flex', flexDirection: 'column', marginRight: Spacing.ExtraSmall }) const ListItemWrapperAccessoryStyle = cssRuleWithTheme(({theme}) => ({ display: 'flex', flexDirection: 'column', marginLeft: Spacing.ExtraSmall, fontSize: 24, fill: theme.colors.gray })) const ListItemWrapperContentStyle = cssRule({ display: 'flex', width: '100%' }) interface ListItemWrapperProps { children?: ReactNode icon?: IconElement disabled?: boolean onDelete?: () => void onMoveUp?: () => void onMoveDown?: () => void } function ListItemWrapper({ children, icon, disabled, onDelete, onMoveUp, onMoveDown }: ListItemWrapperProps) { const css = useThemeStyle() return (
{children}
{icon && }
) }