UNPKG

@adaptabletools/adaptable

Version:

Powerful AG Grid extension which provides advanced, cutting-edge functionality to meet all DataGrid requirements

262 lines (261 loc) 17.7 kB
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; import * as React from 'react'; import { useState, useMemo, useCallback, useEffect, useRef } from 'react'; import { defaultDragProxyMove, DragDropProvider, DragList } from '../../../components/dnd'; import { CheckBox } from '../../../components/CheckBox'; import { Icon } from '../../../components/icons'; import Radio from '../../../components/Radio'; import { AdaptableFormControlTextClear } from '../Forms/AdaptableFormControlTextClear'; import { Box, Flex } from '../../../components/Flex'; import { cn } from '../../../lib/utils'; function useValuesMap({ options, value, toIdentifier, selectedMap, }) { const optionsMap = useMemo(() => { if (selectedMap) { return; } const map = new Map([]); options.forEach((option) => { map.set(toIdentifier(option), option); }); return map; }, [options, selectedMap]); const result = useMemo(() => { if (selectedMap) { return selectedMap; } const map = new Map([]); value.forEach((id) => { map.set(id, optionsMap.get(id) ?? null); }); return map; }, [optionsMap, selectedMap, value]); return { selectedMap: result, optionsMap }; } const baseClassName = 'ab-ValueSelector'; const ROW_CLICK_IGNORE_SELECTOR = 'button, input, select, textarea, a, [data-drag-handle], [data-slot=select-trigger], [data-slot=select-content], [data-slot=select-item], .ab-Select'; const ROW_TOGGLE_SUPPRESS_MS = 150; export function ValueSelector(props) { const { options, value, filter, onChange, allowReorder = true, singleSelect, toLabel, toListLabel, toIdentifier, renderOptionTrailing, toggleSelectionOnRowClick, showSelectedOnlyLabel, showFilterInput, filterPlaceholder, compact, optionLayout = 'label-in-checkbox', hideShowSelectedOnly, compactHeaderClassName, compactFilterClassName, onShowSelectedOnlyChange, isOptionDisabled, disabled, optionClassName, className, style, } = props; const [searchInputValue, setSearchInputValue] = React.useState(''); const suppressRowToggleUntilRef = useRef(0); useEffect(() => { const markSelectInteraction = (event) => { const target = event.target; if (target.closest(ROW_CLICK_IGNORE_SELECTOR)) { suppressRowToggleUntilRef.current = Date.now() + ROW_TOGGLE_SUPPRESS_MS; } }; document.addEventListener('pointerdown', markSelectInteraction, true); return () => document.removeEventListener('pointerdown', markSelectInteraction, true); }, []); const preparedToLabel = toListLabel ?? toLabel; const [selectedOnly, doSetSelectedOnly] = useState(false); const setSelectedOnly = useCallback((selectedOnly) => { doSetSelectedOnly(selectedOnly); onShowSelectedOnlyChange?.(selectedOnly); }, [onShowSelectedOnlyChange]); const { selectedMap } = useValuesMap({ options, toIdentifier, value, }); const reorderEnabled = allowReorder !== false; const commitSelection = useCallback((nextMap) => { const newSelection = [...nextMap.keys()]; if (!newSelection.length && selectedOnly) { setSelectedOnly(false); } onChange(newSelection, nextMap); }, [onChange, selectedOnly, setSelectedOnly]); const updateSelection = useCallback((updater) => { const nextMap = new Map(selectedMap); updater(nextMap); commitSelection(nextMap); }, [commitSelection, selectedMap]); const filteredOptions = useMemo(() => { return options.filter((option) => { let result = true; if (filter) { result = filter(option, searchInputValue); } result = result && (selectedOnly ? selectedMap.has(toIdentifier(option)) : true); return result; }); }, [filter, options, searchInputValue, selectedMap, selectedOnly, toIdentifier]); const handleOptionRowClick = useCallback((option, identifier, optionDisabled, event) => { if (Date.now() < suppressRowToggleUntilRef.current) { return; } const target = event.target; if (target.closest(ROW_CLICK_IGNORE_SELECTOR)) { return; } if (optionDisabled) { return; } updateSelection((next) => { if (next.has(identifier)) { next.delete(identifier); } else { next.set(identifier, option); } }); }, [updateSelection]); const renderOptionRow = (option, index, reorderable, itemDomProps) => { const identifier = toIdentifier(option); const label = preparedToLabel(option); const { onPointerDown, onClick: itemOnClick, className: itemClassName, ...restDomProps } = itemDomProps ?? {}; const optionDisabled = disabled || (isOptionDisabled ? isOptionDisabled(option) : false); return (_jsxs(Flex, { alignItems: "center", "data-index": index, "data-id": identifier, "data-name": "option", ...restDomProps, onClick: toggleSelectionOnRowClick ? (event) => { itemOnClick?.(event); handleOptionRowClick(option, identifier, optionDisabled, event); } : itemOnClick, className: cn(toggleSelectionOnRowClick && 'twa:cursor-pointer', reorderable ? 'ab-DraggableListItem' : 'twa:bg-primary twa:text-primary-foreground', 'twa:rounded-standard', compact ? (singleSelect ? 'twa:py-1.5 twa:px-1.5' : 'twa:py-1 twa:px-1.5') : 'twa:p-2', { 'twa:mt-0.5': compact && index, 'twa:mt-1': !compact && index, 'twa:mt-0': !index, }, itemClassName, optionClassName, `${baseClassName}__option`), children: [_jsxs(Flex, { flexDirection: "row", alignItems: "center", className: "twa:flex-1 twa:min-w-0", children: [reorderable ? (_jsx(Box, { "data-drag-handle": true, onPointerDown: onPointerDown, className: cn('twa:shrink-0', compact ? 'twa:mr-2' : 'twa:mr-3'), children: _jsx(Icon, { name: "drag", style: { cursor: 'grab' } }) })) : null, singleSelect ? (_jsx(Radio, { className: cn('twa:flex-1 twa:min-w-0', compact && !singleSelect && 'twa:my-0!'), checked: selectedMap.has(identifier), "data-name": identifier, disabled: optionDisabled, onClick: toggleSelectionOnRowClick ? (event) => event.stopPropagation() : undefined, onChange: (checked) => { updateSelection((next) => { next.clear(); if (checked) { next.set(identifier, option); } }); }, children: label })) : optionLayout === 'label-beside-checkbox' ? (_jsxs(Flex, { alignItems: "center", className: "twa:flex-1 twa:min-w-0", children: [_jsx(CheckBox, { className: "twa:shrink-0", "data-name": identifier, disabled: optionDisabled, onClick: toggleSelectionOnRowClick ? (event) => event.stopPropagation() : undefined, onChange: (checked) => { updateSelection((next) => { if (checked) { next.set(identifier, option); } else { next.delete(identifier); } }); }, checked: selectedMap.has(identifier) }), _jsx(Flex, { alignItems: "center", className: "twa:mx-1.5 twa:text-3 twa:min-w-0", "data-name": "option-label", children: label })] })) : (_jsx(CheckBox, { className: cn('twa:flex-1 twa:min-w-0', compact && 'twa:my-0!'), "data-name": identifier, disabled: optionDisabled, onClick: toggleSelectionOnRowClick ? (event) => event.stopPropagation() : undefined, onChange: (checked) => { updateSelection((next) => { if (checked) { next.set(identifier, option); } else { next.delete(identifier); } }); }, checked: selectedMap.has(identifier), children: label }))] }), renderOptionTrailing ? (_jsx(Box, { className: "twa:shrink-0", children: renderOptionTrailing(option) })) : null] })); }; const renderOption = (option, index) => { const identifier = toIdentifier(option); const reorderable = typeof allowReorder === 'function' ? allowReorder(option) : allowReorder; if (!reorderEnabled) { return (_jsx(React.Fragment, { children: renderOptionRow(option, index, false) }, identifier)); } return (_jsx(DragList.DraggableItem, { id: `${identifier}`, children: (itemDomProps) => { const { onPointerDown, ...restDomProps } = itemDomProps; return renderOptionRow(option, index, reorderable, { onPointerDown: reorderable ? onPointerDown : undefined, ...restDomProps, }); } }, identifier)); }; const showOnlySelectedCheckbox = (_jsx(CheckBox, { disabled: !value.length, checked: selectedOnly, onChange: setSelectedOnly, children: showSelectedOnlyLabel ?? 'Show Selected Only' })); const selectionSectionProps = { ...props, showOnlySelectedCheckbox, onSelectAll: () => { updateSelection((next) => { options.forEach((option) => { next.set(toIdentifier(option), option); }); }); }, onClearOption: (id) => { updateSelection((next) => { next.delete(id); }); }, onClear: () => { updateSelection((next) => { next.clear(); }); }, }; const optionList = (_jsx(Flex, { className: `${baseClassName}__body twa:flex-1 twa:min-h-0 twa:overflow-auto`, flexDirection: "column", children: _jsx("div", { children: filteredOptions.map(renderOption) }) })); return (_jsxs(Flex, { style: style, className: cn('twa:flex-1 twa:min-h-0', compact && 'twa:overflow-hidden', baseClassName, compact && `${baseClassName}--compact`, className), flexDirection: "column", children: [!compact && (_jsx(Flex, { className: "twa:mb-1", children: showFilterInput && filter ? (_jsx(AdaptableFormControlTextClear, { value: searchInputValue, OnTextChange: setSearchInputValue, placeholder: filterPlaceholder ?? 'Type to search', inputClassName: "twa:p-3", className: "twa:flex-1 twa:m-[3px]" })) : (_jsx(Box, { className: "twa:flex-1" })) })), renderSelectionSection({ ...selectionSectionProps, compact, showFilterInput, filter, searchInputValue, setSearchInputValue, filterPlaceholder, hideShowSelectedOnly, compactHeaderClassName, compactFilterClassName, }), reorderEnabled ? (_jsx(DragDropProvider, { children: _jsx(Flex, { className: `${baseClassName}__body twa:flex-1 twa:min-h-0 twa:overflow-auto`, flexDirection: "column", children: _jsx(DragList, { dragListId: "value-selector", orientation: "vertical", onDragProxyMove: defaultDragProxyMove, onDragProxySetup: ({ proxyElement }) => { proxyElement.classList.add('twa:shadow-md'); }, onDrop: (_sortedIndexes) => { const selection = []; const extraKeys = []; for (const [key, mapValue] of selectedMap) { if (mapValue != null) { selection.push(key); } else { extraKeys.push(key); } } const newSelection = _sortedIndexes .map((index) => selection[index]) .filter((x) => x != null); newSelection.push(...extraKeys); const nextMap = new Map(); newSelection.forEach((key) => { nextMap.set(key, selectedMap.get(key)); }); commitSelection(nextMap); }, children: (listDomProps) => _jsx("div", { ...listDomProps, children: filteredOptions.map(renderOption) }) }) }) })) : (optionList)] })); } export const renderSelectionSection = (props) => { const { value, options, singleSelect, toLabel, toIdentifier, compact, showFilterInput, filter, searchInputValue, setSearchInputValue, filterPlaceholder, hideShowSelectedOnly, compactHeaderClassName, compactFilterClassName, } = props; const compactFilterClass = compactFilterClassName ?? 'twa:flex-1 twa:min-w-0'; const compactFilterUsesSpacer = compactFilterClassName != null && !/\bflex-1\b/.test(compactFilterClassName); const headerRowClassName = cn('twa:mb-1', 'twa:p-1', 'twa:border-b twa:border-primarydark/30', compact ? cn('twa:w-full twa:min-w-0 twa:gap-2 twa:font-bold twa:shrink-0', compactHeaderClassName) : 'twa:flex twa:flex-row '); const selectionBox = compact ? (_jsxs(Flex, { alignItems: "center", className: headerRowClassName, children: [singleSelect ? (_jsx(Box, { className: "twa:flex twa:flex-row twa:text-3 twa:font-normal twa:items-center", children: (() => { if (!value.length) { return _jsx("span", { className: "twa:opacity-70", children: "(none selected)" }); } const selectedId = value[0]; const selectedOption = options.find((o) => toIdentifier(o) === selectedId); if (!selectedOption) { return _jsx("span", { className: "twa:opacity-70", children: "(none selected)" }); } return (_jsxs(_Fragment, { children: [_jsx("span", { className: "twa:opacity-70 twa:mr-1", children: "Selected:" }), _jsx("span", { className: "twa:font-medium", children: toLabel(selectedOption) })] })); })() })) : (_jsx(CheckBox, { className: "twa:shrink-0", checked: !value.length ? false : value.length === options.length ? true : null, onChange: (checked) => { if (checked) { props.onSelectAll(); } else { props.onClear(); } }, children: "Select All" })), hideShowSelectedOnly ? null : (_jsx(Box, { className: "twa:shrink-0 twa:text-2 twa:font-normal", children: props.showOnlySelectedCheckbox })), showFilterInput && filter ? (_jsxs(_Fragment, { children: [compactFilterUsesSpacer ? _jsx(Box, { className: "twa:flex-1 twa:min-w-0" }) : null, _jsx(AdaptableFormControlTextClear, { value: searchInputValue ?? '', OnTextChange: setSearchInputValue ?? (() => undefined), placeholder: filterPlaceholder ?? 'Search...', className: compactFilterClass })] })) : null] })) : (_jsx(Box, { className: "twa:flex twa:flex-col twa:gap-2 twa:py-2", children: _jsxs(Box, { className: headerRowClassName, children: [singleSelect ? (_jsx(Box, { className: "twa:flex twa:flex-row twa:text-3 twa:font-normal twa:items-center", children: (() => { if (!value.length) { return _jsx("span", { className: "twa:opacity-70", children: "(none selected)" }); } const selectedId = value[0]; const selectedOption = options.find((o) => toIdentifier(o) === selectedId); if (!selectedOption) { return _jsx("span", { className: "twa:opacity-70", children: "(none selected)" }); } return (_jsxs(_Fragment, { children: [_jsx("span", { className: "twa:opacity-70 twa:mr-1", children: "Selected:" }), _jsx("span", { className: "twa:font-medium", children: toLabel(selectedOption) })] })); })() })) : (_jsx(CheckBox, { className: "twa:ml-0.5 twa:font-bold", checked: !value.length ? false : value.length === options.length ? true : null, onChange: (checked) => { if (checked) { props.onSelectAll(); } else { props.onClear(); } }, children: _jsxs(Box, { className: "twa:flex twa:flex-row twa:items-center twa:gap-3", children: ["Select All", _jsxs(Box, { className: "twa:text-2 twa:font-normal", children: ["(", value.length, " of ", options.length, " selected)"] })] }) })), hideShowSelectedOnly ? null : _jsx("div", { className: "twa:flex-1" }), hideShowSelectedOnly ? null : (_jsx(Box, { className: "twa:mr-2 twa:text-2 twa:font-normal", children: props.showOnlySelectedCheckbox }))] }) })); return _jsx(Box, { className: cn(`${baseClassName}__header`), children: selectionBox }); };