@adaptabletools/adaptable
Version:
Powerful data-agnostic HTML5 AG Grid extension which provides advanced, cutting-edge functionality to meet all DataGrid requirements
516 lines (515 loc) • 25.1 kB
JavaScript
import * as React from 'react';
import { useState, useRef, useEffect } from 'react';
import { SortOrder } from '../../../AdaptableState/Common/Enums';
import { ListBoxFilterSortComponent } from './ListBoxFilterSortComponent';
import { ArrayExtensions } from '../../../Utilities/Extensions/ArrayExtensions';
import ListGroupItem from '../../../components/List/ListGroupItem';
import SimpleButton from '../../../components/SimpleButton';
import { Flex } from 'rebass';
import Panel from '../../../components/Panel';
import ListGroup from '../../../components/List/ListGroup';
import SelectableList from '../../../components/SelectableList';
import { useLatest } from '../../../components/utils/useLatest';
const listGroupStyle = {
overflowY: 'auto',
marginBottom: '0px',
};
const listGroupItemStyle = {
fontSize: 'small',
padding: 'var(--ab-space-1)',
};
const ButtonDirection = (props) => (React.createElement(SimpleButton, { ...props, style: { whiteSpace: 'nowrap', justifyContent: 'center', ...props.style } }));
export const DualListBoxEditor = (props) => {
const [placeholder] = useState(() => {
let placeholder = document.createElement('button');
placeholder.className = 'placeholder';
placeholder.classList.add('list-group-item');
placeholder.type = 'button';
return placeholder;
});
const refDraggedHTMLElement = useRef(null);
const refDraggedElement = useRef(null);
const refOverHTMLElement = useRef(null);
const refFirstSelected = useRef(null);
const createAvailableValuesList = (availableValues, sortOrder) => {
return ArrayExtensions.sortArray(availableValues, sortOrder);
};
const [state, updateState] = useState(() => {
let availableValues = new Array();
props.AvailableValues.forEach((x) => {
if (props.SelectedValues.indexOf(x) < 0) {
availableValues.push(x);
}
});
return {
SelectedValues: props.SelectedValues,
AvailableValues: createAvailableValuesList(availableValues, SortOrder.Asc),
UiSelectedSelectedValues: [],
UiSelectedAvailableValues: [],
FilterValue: '',
SelectedValuesFilterValue: '',
SortOrder: SortOrder.Asc,
SelectedValuesSortOrder: SortOrder.Asc,
AllValues: props.AvailableValues,
};
});
const callbackRefs = useRef([]);
const getProps = useLatest(props);
const setState = (newState, callback) => {
updateState({ ...state, ...newState });
if (newState.SelectedValues && props.SelectedValues !== newState.SelectedValues) {
getProps().onChange(newState.SelectedValues);
}
if (callback) {
callbackRefs.current.push(callback);
}
};
useEffect(() => {
const callbacks = [...callbackRefs.current];
callbacks.forEach((fn) => fn());
callbackRefs.current = [];
});
useEffect(() => {
const availableValues = [];
props.AvailableValues.forEach((x) => {
if (props.SelectedValues.indexOf(x) < 0) {
availableValues.push(x);
}
});
setState({
SelectedValues: props.SelectedValues,
AvailableValues: createAvailableValuesList(availableValues, state.SortOrder),
});
}, [props.SelectedValues, props.AvailableValues]);
const isValueFilteredOut = (item, FilterValue = state.FilterValue) => {
return (FilterValue != '' && item.toLocaleLowerCase().indexOf(FilterValue.toLocaleLowerCase()) < 0);
};
const canGoTopOrUp = () => {
return (state.UiSelectedSelectedValues.length != 0 &&
state.UiSelectedSelectedValues.every((x) => state.SelectedValues.indexOf(x) > 0));
};
const canGoDownOrBottom = () => {
return (state.UiSelectedSelectedValues.length != 0 &&
state.UiSelectedSelectedValues.every((x) => state.SelectedValues.indexOf(x) < state.SelectedValues.length - 1));
};
const ensureFirstSelectedItemVisible = (top) => {
var itemComponentDOMNode = refFirstSelected.current;
if (itemComponentDOMNode) {
itemComponentDOMNode.scrollIntoView(top);
}
};
const Top = () => {
let newSelectedValues = [].concat(state.UiSelectedSelectedValues, state.SelectedValues.filter((x) => state.UiSelectedSelectedValues.indexOf(x) < 0));
setState({
SelectedValues: newSelectedValues,
UiSelectedSelectedValues: [],
}, () => {
ensureFirstSelectedItemVisible(true);
});
};
const Up = () => {
let newSelectedValues = [...state.SelectedValues];
for (let selElement of state.UiSelectedSelectedValues) {
let index = newSelectedValues.indexOf(selElement);
ArrayExtensions.moveArray(newSelectedValues, index, index - 1);
}
setState({
SelectedValues: newSelectedValues,
}, () => {
ensureFirstSelectedItemVisible(false);
});
};
const Bottom = () => {
let newSelectedValues = [].concat(state.SelectedValues.filter((x) => state.UiSelectedSelectedValues.indexOf(x) < 0), state.UiSelectedSelectedValues);
setState({
SelectedValues: newSelectedValues,
UiSelectedSelectedValues: [],
}, () => {
ensureFirstSelectedItemVisible(true);
});
};
const Down = () => {
let newSelectedValues = [...state.SelectedValues];
for (var index = state.UiSelectedSelectedValues.length - 1; index >= 0; index--) {
let indexglob = newSelectedValues.indexOf(state.UiSelectedSelectedValues[index]);
ArrayExtensions.moveArray(newSelectedValues, indexglob, indexglob + 1);
}
setState({
SelectedValues: newSelectedValues,
}, () => {
ensureFirstSelectedItemVisible(false);
});
};
const Add = () => {
let newSelectedValues = [...state.SelectedValues];
let newAvailableValues = [...state.AvailableValues];
let valuesToAdd = state.UiSelectedAvailableValues;
valuesToAdd.forEach((x) => {
let index = newAvailableValues.indexOf(x);
newAvailableValues.splice(index, 1);
newSelectedValues.push(x);
});
newAvailableValues = createAvailableValuesList(newAvailableValues, state.SortOrder);
setState({
UiSelectedAvailableValues: [],
SelectedValues: newSelectedValues,
AvailableValues: newAvailableValues,
});
};
const AddAll = () => {
let newSelectedValues = [].concat(state.SelectedValues);
let valuesToAdd = state.AvailableValues;
valuesToAdd.forEach((x) => {
newSelectedValues.push(x);
});
setState({
UiSelectedSelectedValues: [],
UiSelectedAvailableValues: [],
SelectedValues: newSelectedValues,
AvailableValues: [],
});
};
const RemoveAll = () => {
let newSelectedValues = [];
let newAvailableValues = [].concat(state.AllValues);
newAvailableValues = createAvailableValuesList(newAvailableValues, state.SortOrder);
setState({
UiSelectedSelectedValues: [],
UiSelectedAvailableValues: [],
SelectedValues: newSelectedValues,
AvailableValues: newAvailableValues,
});
};
const Remove = () => {
let newSelectedValues = [...state.SelectedValues];
let newAvailableValues = [...state.AvailableValues];
state.UiSelectedSelectedValues.forEach((x) => {
let index = newSelectedValues.indexOf(x);
newSelectedValues.splice(index, 1);
let originalItem = state.AllValues.find((y) => y == x);
if (originalItem) {
newAvailableValues.push(originalItem);
}
});
newAvailableValues = createAvailableValuesList(newAvailableValues, state.SortOrder);
setState({
UiSelectedSelectedValues: [],
SelectedValues: newSelectedValues,
AvailableValues: newAvailableValues,
});
};
const DragSelectedStart = (e, listElement) => {
refDraggedHTMLElement.current = e.currentTarget;
refDraggedElement.current = listElement;
};
const DragSelectedEnd = () => {
if (refOverHTMLElement.current && refDraggedElement.current) {
//now we need to check in which drop area we dropped the selected item
let to;
let from = state.SelectedValues.indexOf(refDraggedElement.current);
let newSelectedArray;
let newAvailableValues;
if (refOverHTMLElement.current.classList.contains('Available')) {
to = state.AvailableValues.indexOf(refOverHTMLElement.current.innerText);
newSelectedArray = [...state.SelectedValues];
newSelectedArray.splice(from, 1);
newAvailableValues = [...state.AvailableValues];
let originalItem = state.AllValues.find((y) => y == refDraggedElement.current);
if (originalItem) {
let checkForExistig = newAvailableValues.find((x) => x == originalItem);
if (!checkForExistig) {
newAvailableValues.splice(to, 0, originalItem);
}
}
}
else if (refOverHTMLElement.current.classList.contains('ab-AvailableDropZone')) {
newSelectedArray = [...state.SelectedValues];
newSelectedArray.splice(from, 1);
newAvailableValues = [...state.AvailableValues];
let originalItem = state.AllValues.find((y) => y == refDraggedElement.current);
if (originalItem) {
let checkForExistig = newAvailableValues.find((x) => x == originalItem);
if (!checkForExistig) {
newAvailableValues.push(originalItem);
}
}
}
else if (refOverHTMLElement.current.classList.contains('Selected')) {
to = state.SelectedValues.indexOf(refOverHTMLElement.current.innerText);
newSelectedArray = [...state.SelectedValues];
newSelectedArray.splice(from, 1);
newSelectedArray.splice(to, 0, refDraggedElement.current);
newAvailableValues = [...state.AvailableValues];
}
else if (refOverHTMLElement.current.classList.contains('ab-SelectedDropZone')) {
newSelectedArray = [...state.SelectedValues];
newSelectedArray.splice(from, 1);
newSelectedArray.push(refDraggedElement.current);
newAvailableValues = [...state.AvailableValues];
}
//We remove our awesome placeholder
if (refOverHTMLElement.current.classList.contains('ab-SelectedDropZone') ||
refOverHTMLElement.current.classList.contains('ab-AvailableDropZone')) {
refOverHTMLElement.current.removeChild(placeholder);
}
else {
refOverHTMLElement.current.parentNode.removeChild(placeholder);
}
refOverHTMLElement.current = null;
refDraggedHTMLElement.current = null;
refDraggedElement.current = null;
// Update state
newAvailableValues = createAvailableValuesList(newAvailableValues, state.SortOrder);
setState({
SelectedValues: newSelectedArray,
AvailableValues: newAvailableValues,
UiSelectedSelectedValues: [],
UiSelectedAvailableValues: [],
});
}
};
const DragAvailableStart = (e, listElement) => {
refDraggedHTMLElement.current = e.currentTarget;
refDraggedElement.current = listElement;
};
const DragAvailableEnd = () => {
if (refOverHTMLElement.current && refDraggedElement.current) {
let to;
let from = state.AvailableValues.indexOf(refDraggedElement.current);
let newSelectedArray;
let newAvailableValues;
if (refOverHTMLElement.current.classList.contains('Selected')) {
from = state.AvailableValues.indexOf(refDraggedElement.current);
to = state.SelectedValues.indexOf(refOverHTMLElement.current.innerText);
newSelectedArray = [...state.SelectedValues];
newSelectedArray.splice(to, 0, refDraggedElement.current);
newAvailableValues = [...state.AvailableValues];
newAvailableValues.splice(from, 1);
}
else if (refOverHTMLElement.current.classList.contains('ab-SelectedDropZone')) {
newSelectedArray = [...state.SelectedValues];
newSelectedArray.push(refDraggedElement.current);
newAvailableValues = [...state.AvailableValues];
newAvailableValues.splice(from, 1);
}
//We remove our awesome placeholder
if (refOverHTMLElement.current.classList.contains('ab-SelectedDropZone')) {
refOverHTMLElement.current.removeChild(placeholder);
}
else {
refOverHTMLElement.current.parentNode.removeChild(placeholder);
}
refOverHTMLElement.current = null;
refDraggedHTMLElement.current = null;
refDraggedElement.current = null;
// Update state
setState({
SelectedValues: newSelectedArray,
AvailableValues: newAvailableValues,
UiSelectedSelectedValues: [],
UiSelectedAvailableValues: [],
});
}
};
const DragEnterAvailable = (e) => {
e.preventDefault();
e.stopPropagation();
};
const DragOverAvailable = (e) => {
e.preventDefault();
e.stopPropagation();
//we can only drop selected data into available
if (!refDraggedHTMLElement.current.classList.contains('Selected')) {
e.dataTransfer.dropEffect = 'none';
return;
}
let targetElement = e.target;
//we want to keep the reference of the last intem we were over to
if (targetElement.classList.contains('placeholder')) {
return;
}
refOverHTMLElement.current = targetElement;
if (refOverHTMLElement.current.classList.contains('ab-AvailableDropZone')) {
targetElement.appendChild(placeholder);
}
else {
targetElement.parentNode.insertBefore(placeholder, targetElement);
}
};
const DragLeaveAvailable = (e) => {
e.preventDefault();
e.stopPropagation();
let targetElement = e.target;
if (targetElement.classList.contains('ab-AvailableDropZone') ||
targetElement.classList.contains('placeholder')) {
if (refOverHTMLElement.current) {
if (refOverHTMLElement.current.classList.contains('ab-AvailableDropZone')) {
refOverHTMLElement.current.removeChild(placeholder);
}
else {
refOverHTMLElement.current.parentNode.removeChild(placeholder);
}
refOverHTMLElement.current = null;
}
}
};
const DragEnterSelected = (e) => {
e.preventDefault();
e.stopPropagation();
};
const DragOverSelected = (e) => {
e.preventDefault();
e.stopPropagation();
let targetElement = e.target;
//we want to keep the reference of the last intem we were over to
if (targetElement.classList.contains('placeholder')) {
return;
}
refOverHTMLElement.current = targetElement;
if (refOverHTMLElement.current.classList.contains('ab-SelectedDropZone')) {
targetElement.appendChild(placeholder);
}
else {
targetElement.parentNode.insertBefore(placeholder, targetElement);
}
};
const DragLeaveSelected = (e) => {
e.preventDefault();
e.stopPropagation();
let targetElement = e.target;
if (targetElement.classList.contains('ab-SelectedDropZone') ||
targetElement.classList.contains('placeholder')) {
if (refOverHTMLElement.current) {
if (refOverHTMLElement.current.classList.contains('ab-SelectedDropZone')) {
refOverHTMLElement.current.removeChild(placeholder);
}
else {
refOverHTMLElement.current.parentNode.removeChild(placeholder);
}
refOverHTMLElement.current = null;
}
}
};
const handleChangeFilterValue = (x) => {
setState({
FilterValue: x,
});
};
const handleChangeSelectedValuesFilterValue = (x) => {
setState({
SelectedValuesFilterValue: x,
});
};
const sortColumnValues = () => {
if (state.SortOrder == SortOrder.Asc) {
setState({
AvailableValues: ArrayExtensions.sortArray(state.AvailableValues, SortOrder.Desc),
SortOrder: SortOrder.Desc,
});
}
else {
setState({
AvailableValues: ArrayExtensions.sortArray(state.AvailableValues, SortOrder.Asc),
SortOrder: SortOrder.Asc,
});
}
};
const sortSelectedColumnValues = () => {
if (state.SelectedValuesSortOrder == SortOrder.Asc) {
setState({
SelectedValues: ArrayExtensions.sortArray(state.SelectedValues, SortOrder.Desc),
SelectedValuesSortOrder: SortOrder.Desc,
});
}
else {
setState({
SelectedValues: ArrayExtensions.sortArray(state.SelectedValues, SortOrder.Asc),
SelectedValuesSortOrder: SortOrder.Asc,
});
}
};
const getSelectedItemId = (index) => {
const item = state.SelectedValues[index];
if (!item) {
return -1;
}
let display = item;
if (isValueFilteredOut(display, state.SelectedValuesFilterValue)) {
return -1;
}
return item;
};
const onSelectedListSelectionChange = (selection) => {
const UiSelectedSelectedValues = Object.keys(selection);
UiSelectedSelectedValues.sort((a, b) => state.SelectedValues.indexOf(a) - state.SelectedValues.indexOf(b));
setState({ UiSelectedSelectedValues });
};
const getAvailableItemId = (index) => {
const item = state.AvailableValues[index];
if (!item) {
return -1;
}
let display = item;
let value = item;
if (isValueFilteredOut(display)) {
return -1;
}
return value;
};
/**
* @param selection - is a map, values being item keys (their textual representation), while values being true
*/
const onAvailableListSelectionChange = (selection) => {
const UiSelectedAvailableValues = Object.keys(selection);
const availableValues = state.AvailableValues;
UiSelectedAvailableValues.sort((a, b) => availableValues.indexOf(a) - availableValues.indexOf(b));
setState({ UiSelectedAvailableValues });
};
let setRefFirstSelectedSelected = true;
// build selected elements
const selectedElements = state.SelectedValues.map((x, index) => {
let isActive = state.UiSelectedSelectedValues.indexOf(x) >= 0;
if (isValueFilteredOut(x, state.SelectedValuesFilterValue)) {
return null;
}
const result = (React.createElement(ListGroupItem, { key: `${x}-1`, index: index, className: "Selected", draggable: true, style: listGroupItemStyle, active: isActive, ref: isActive && setRefFirstSelectedSelected ? refFirstSelected : null, onDragStart: (event) => DragSelectedStart(event, x), onDragEnd: () => DragSelectedEnd(), value: x }, x));
if (isActive && setRefFirstSelectedSelected) {
setRefFirstSelectedSelected = false;
}
return result;
});
// build available elements
const availableElements = state.AvailableValues.map((x, index) => {
let isActive = state.UiSelectedAvailableValues.indexOf(x) >= 0;
let value = x;
if (isValueFilteredOut(x)) {
return null;
}
else {
return (React.createElement(ListGroupItem, { className: "Available", style: listGroupItemStyle, active: isActive, index: index, draggable: true, key: `${value}-item`, onDragStart: (event) => DragAvailableStart(event, x), onDragEnd: () => DragAvailableEnd(), value: value }, x));
}
});
const headerFirstListBox = (React.createElement(ListBoxFilterSortComponent, { FilterValue: state.FilterValue, sortColumnValues: sortColumnValues, SortOrder: state.SortOrder, handleChangeFilterValue: handleChangeFilterValue }));
const headerSecondListBox = (React.createElement(ListBoxFilterSortComponent, { FilterValue: state.SelectedValuesFilterValue, sortColumnValues: sortSelectedColumnValues, SortOrder: state.SelectedValuesSortOrder, handleChangeFilterValue: handleChangeSelectedValuesFilterValue }));
return (React.createElement(Flex, { alignItems: "stretch", flexDirection: "row", className: "ab-DualListBoxEditor", style: { ...props.style, maxHeight: '100%', width: '100%' } },
React.createElement(Panel, { header: props.HeaderAvailable, className: "ab-DualListBoxEditor__source", bodyProps: { padding: 0 }, marginRight: 2, style: { flex: '4 0 0%' }, bodyScroll: true },
headerFirstListBox,
React.createElement(SelectableList, { getItemId: getAvailableItemId, onSelectedChange: onAvailableListSelectionChange },
React.createElement(ListGroup, { className: "ab-AvailableDropZone", style: listGroupStyle, onDragEnter: (event) => DragEnterAvailable(event), onDragOver: (event) => DragOverAvailable(event), onDragLeave: (event) => DragLeaveAvailable(event) }, availableElements))),
React.createElement(Flex, { flexDirection: "column", justifyContent: "center", className: "ab-DualListBoxEditor__action-buttons" },
React.createElement(ButtonDirection, { "data-name": "add-all", marginBottom: 2, icon: "fast-forward", iconPosition: "end", disabled: state.AvailableValues.length == 0, onClick: () => AddAll() }, "Add All"),
React.createElement(ButtonDirection, { "data-name": "add", iconPosition: "end", icon: 'arrow-right', marginBottom: 3, disabled: state.UiSelectedAvailableValues.length == 0, onClick: () => Add() }, "Add"),
React.createElement(ButtonDirection, { "data-name": "remove", icon: 'arrow-left', marginBottom: 2, iconPosition: "start", disabled: state.UiSelectedSelectedValues.length == 0, onClick: () => Remove() }, "Remove"),
React.createElement(ButtonDirection, { "data-name": "remove-all", marginBottom: 2, icon: "fast-backward", iconPosition: "start", disabled: state.SelectedValues.length == 0, onClick: () => RemoveAll() }, "Remove All")),
React.createElement(Panel, { header: props.HeaderSelected, className: "ab-DualListBoxEditor__destination", bodyScroll: true, bodyProps: {
padding: 0,
}, style: { flex: '4 0 0%' }, marginLeft: 2, marginRight: 2 },
headerSecondListBox,
React.createElement(SelectableList, { getItemId: getSelectedItemId, onSelectedChange: onSelectedListSelectionChange },
React.createElement(ListGroup, { style: listGroupStyle, className: "ab-SelectedDropZone", onDragEnter: (event) => DragEnterSelected(event), onDragOver: (event) => DragOverSelected(event), onDragLeave: (event) => DragLeaveSelected(event) }, selectedElements))),
React.createElement(Flex, { flexDirection: "column", justifyContent: "center", className: "ab-DualListBoxEditor__order-buttons" },
React.createElement(ButtonDirection, { "data-name": "top", marginBottom: 2, iconPosition: "start", icon: "triangle-up", disabled: !canGoTopOrUp(), onClick: () => Top() }, "Top"),
React.createElement(ButtonDirection, { "data-name": "up", marginBottom: 2, iconPosition: "start", icon: "arrow-up", disabled: !canGoTopOrUp(), onClick: () => Up() }, "Up"),
React.createElement(ButtonDirection, { "data-name": "down", marginBottom: 2, icon: "arrow-down", iconPosition: "start", disabled: !canGoDownOrBottom(), onClick: () => Down() }, "Down"),
React.createElement(ButtonDirection, { "data-name": "bottom", marginBottom: 2, icon: "triangle-down", iconPosition: "start", disabled: !canGoDownOrBottom(), onClick: () => Bottom() }, "Bottom"))));
};