UNPKG

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