@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
JavaScript
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 });
};