@adaptabletools/adaptable
Version:
Powerful data-agnostic HTML5 AG Grid extension which provides advanced, cutting-edge functionality to meet all DataGrid requirements
205 lines (204 loc) • 12.3 kB
JavaScript
import * as React from 'react';
import { useState, useMemo, useCallback } from 'react';
import { Box, Flex, Text } from 'rebass';
import { DragDropContext, Draggable, Droppable } from '../../../components/dnd';
import { CheckBox } from '../../../components/CheckBox';
import { Icon } from '../../../components/icons';
import { EllipsisContainer } from '../../../components/EllipsisContainer';
import SimpleButton from '../../../components/SimpleButton';
import Radio from '../../../components/Radio';
import { Tag } from '../../../components/Tag';
import { AdaptableFormControlTextClear } from '../Forms/AdaptableFormControlTextClear';
import ArrayExtensions from '../../../Utilities/Extensions/ArrayExtensions';
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';
export function ValueSelector(props) {
const { options, value, filter, onChange, allowReorder = true, singleSelect, toLabel, toListLabel, toIdentifier, showSelectedOnlyLabel, showFilterInput, selectionBoxPosition = 'bottom', onShowSelectedOnlyChange, isOptionDisabled, disabled, } = props;
const [searchInputValue, setSearchInputValue] = React.useState('');
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 notifyChange = useCallback(() => {
const newSelection = [...selectedMap.keys()];
if (!newSelection.length && selectedOnly) {
setSelectedOnly(false);
}
onChange(newSelection, new Map(selectedMap));
}, [onChange, selectedOnly, selectedMap]);
const renderOption = (option, index) => {
const identifier = toIdentifier(option);
const label = !allowReorder ? preparedToLabel(option) : null;
const reorderable = typeof allowReorder === 'function' ? allowReorder(option) : allowReorder;
const renderNode = (props, dragHandleProps) => {
return (React.createElement(Flex, { className: `${baseClassName}__option`, alignItems: "center", mt: index ? 1 : 0, key: identifier ?? index, backgroundColor: 'primary', padding: 2, "data-index": index, "data-id": identifier, "data-name": "option", ...props },
React.createElement(Flex, { flex: 1, flexDirection: "row", alignItems: "center" },
reorderable ? (React.createElement(Box, { mr: 3, ...dragHandleProps },
React.createElement(Icon, { name: "drag", style: { cursor: 'grab' } }))) : null,
singleSelect ? (React.createElement(Radio, { checked: selectedMap.has(identifier), "data-name": identifier, onChange: (checked) => {
if (checked) {
selectedMap.clear();
selectedMap.set(identifier, option);
}
else {
selectedMap.delete(identifier);
}
notifyChange();
} }, label)) : (React.createElement(CheckBox, { "data-name": identifier, disabled: disabled || (isOptionDisabled ? isOptionDisabled(option) : false), onChange: (checked) => {
if (checked) {
selectedMap.set(identifier, option);
}
else {
selectedMap.delete(identifier);
}
notifyChange();
}, checked: selectedMap.has(identifier) }, label)),
React.createElement(Text, { flex: 1, ml: 2 }, allowReorder ? preparedToLabel(option) : null))));
};
return (React.createElement(Draggable, { key: identifier, index: index, draggableId: `${identifier}`, isDragDisabled: !reorderable }, (draggableProvided) => {
return renderNode({
ref: draggableProvided.innerRef,
...draggableProvided.draggableProps,
style: draggableProvided.draggableProps.style,
}, draggableProvided.dragHandleProps);
}));
};
const showOnlySelectedCheckbox = (React.createElement(CheckBox, { disabled: !value.length, checked: selectedOnly, onChange: setSelectedOnly }, showSelectedOnlyLabel ?? 'Show Selected Only'));
const showSelectedOnlyPosition = props.showSelectedOnlyPosition ?? 'floating';
const selectionSectionProps = {
...props,
onSelectAll: () => {
options.forEach((option) => {
selectedMap.set(toIdentifier(option), option);
});
notifyChange();
},
onClearOption: (id) => {
selectedMap.delete(id);
notifyChange();
},
onClear: () => {
selectedMap.clear();
notifyChange();
},
};
return (React.createElement(Flex, { style: props.style, className: baseClassName, flexDirection: "column", flex: 1 },
React.createElement(Flex, { mb: 1 },
showFilterInput && filter ? (React.createElement(AdaptableFormControlTextClear, { value: searchInputValue, OnTextChange: setSearchInputValue, placeholder: "Type to search", style: { flex: 1, border: 0, margin: 3 } })) : (React.createElement(Box, { flex: 1 })),
showSelectedOnlyPosition === 'top' && React.createElement(Box, { ml: 20 }, showOnlySelectedCheckbox)),
selectionBoxPosition === 'top' && renderSelectionSection(selectionSectionProps),
React.createElement(DragDropContext, { onDragEnd: (result) => {
const { source, destination } = result;
const selection = [];
const extraKeys = [];
for (let [key, value] of selectedMap) {
if (value != null) {
selection.push(key);
}
else {
// null/non-existent keys have to be stored separately
extraKeys.push(key);
}
}
const clone = new Map(selectedMap);
const newSelection = ArrayExtensions.reorderArray(selection, source.index, destination.index);
// and then pushed back in the new order, at the end
newSelection.push(...extraKeys);
selectedMap.clear();
newSelection.forEach((key) => {
selectedMap.set(key, clone.get(key));
});
notifyChange();
} },
React.createElement(Flex, { className: `${baseClassName}__body`, flexDirection: "column", flex: 1, style: { overflow: 'auto' } },
showSelectedOnlyPosition === 'floating' && (React.createElement(Box, { className: `${baseClassName}__show-selected-only-checkbox` },
React.createElement(Box, { className: `${baseClassName}__show-selected-only-checkbox__text`, "data-name": "show-selected-only", backgroundColor: "defaultbackground" }, showOnlySelectedCheckbox))),
React.createElement(Droppable, { droppableId: "droppable" }, (droppableProvided) => {
return (React.createElement("div", { ref: droppableProvided.innerRef, ...droppableProvided.droppableProps },
options
.filter((option) => {
let result = true;
if (filter) {
result = filter(option, searchInputValue);
}
result =
result && (selectedOnly ? selectedMap.has(toIdentifier(option)) : true);
return result;
})
.map(renderOption),
droppableProvided.placeholder));
}))),
selectionBoxPosition === 'bottom' && renderSelectionSection(selectionSectionProps)));
}
export const renderSelectionSection = (props) => {
const { noSelectionLabel, clearSelectionLabel, value, options, disabled, singleSelect, toLabel, toIdentifier, selectionBoxPosition = 'bottom', } = props;
const selectionBox = (React.createElement(Box, { fontSize: 2 }, !value.length ? (React.createElement(React.Fragment, null,
noSelectionLabel ?? 'No selected options',
!singleSelect ? (React.createElement(SimpleButton, { px: 1, disabled: disabled, variant: "text", style: { textDecoration: 'underline', display: 'inline-block' }, onClick: props.onSelectAll }, "Select all")) : null)) : (React.createElement(React.Fragment, null,
React.createElement(SimpleButton, { disabled: disabled, tabIndex: 0, px: 1, mr: 1, variant: "text", style: { textDecoration: 'underline', display: 'inline-block' }, onClick: props.onClear }, clearSelectionLabel ?? 'Clear selection.'),
props.xSelectedLabel
? props.xSelectedLabel(value.length)
: `Your selected ${value.length} ${value.length > 1 ? 'options' : 'option'}.`))));
return (React.createElement(Box, { className: `${baseClassName}__header`, mt: selectionBoxPosition === 'bottom' ? 3 : 0, mb: selectionBoxPosition === 'top' ? 3 : 0 },
selectionBox,
React.createElement(ValueOptionsTags, { options: options, value: value, toLabel: toLabel, toIdentifier: toIdentifier, onClearOption: props.onClearOption, readOnly: disabled, isOptionDisabled: props.isOptionDisabled })));
};
export function ValueOptionsTags(props) {
const { allowWrap, value, toLabel, isOptionDisabled, onChange, onClearOption, readOnly: isReadOnly, renderLabel, } = props;
const { selectedMap } = useValuesMap(props);
const renderOptionTag = (index) => {
const optionId = value[index];
const clear = () => {
selectedMap.delete(optionId);
onClearOption?.(optionId);
if (onChange) {
onChange([...selectedMap.keys()], new Map(selectedMap));
}
};
const option = selectedMap.get(optionId);
const readOnly = isReadOnly || (isOptionDisabled ? isOptionDisabled(option) : false);
const label = option != null ? toLabel(option) : `${optionId}`;
return (React.createElement(Tag, { flexDirection: "row", alignItems: "center", "data-name": "selected-option", "data-id": optionId, mt: allowWrap ? 1 : 0, mr: 1, className: "ab-ValueSelector__tag", onClick: (e) => {
e.target?.lastChild?.focus?.();
}, style: { flex: 'none' } },
renderLabel ? renderLabel(label) : label,
React.createElement(SimpleButton, { icon: "close", ml: 2, iconSize: 14, variant: "text", style: { border: 'none', visibility: readOnly ? 'hidden' : 'visible' }, onClick: clear })));
};
const renderEllipsis = useCallback(({ remaining }) => {
return (React.createElement(Text, { fontSize: 2, style: { whiteSpace: 'nowrap' } },
"+",
remaining,
" ",
remaining > 1 ? 'others' : 'other'));
}, []);
return (React.createElement(EllipsisContainer, { style: props.style, marginRight: 4, my: 1, allowWrap: allowWrap, count: value.length, direction: "horizontal", renderItem: renderOptionTag, renderEllipsis: renderEllipsis }));
}