UNPKG

@onehat/ui

Version:
437 lines (397 loc) 14.3 kB
import { forwardRef, useState, useEffect, useRef, } from 'react'; import { SELECTION_MODE_SINGLE, SELECTION_MODE_MULTI, SELECT_UP, SELECT_DOWN, } from '../../../Constants/Selection.js'; import useForceUpdate from '../../../Hooks/useForceUpdate.js'; import inArray from '../../../Functions/inArray.js'; import _ from 'lodash'; // NOTE: This is a modified version of @onehat/ui/src/Hoc/withSelection export default function withSelection(WrappedComponent) { return forwardRef((props, ref) => { if (props.secondaryDisableWithSelection || props.secondaryAlreadyHasWithSelection) { return <WrappedComponent {...props} />; } if (props.secondarySetSelection) { // bypass everything, since we're already using withSelection() in hierarchy. // For example, Combo has withSelection(), and intenally it uses Grid which also has withSelection(), // but we only need it defined once for the whole thing. return <WrappedComponent {...props} />; } const { secondarySelection, secondaryDefaultSelection, secondaryOnChangeSelection, secondarySelectionMode = SELECTION_MODE_SINGLE, // SELECTION_MODE_MULTI, SELECTION_MODE_SINGLE secondaryAutoSelectFirstItem = false, fireEvent, // withComponent self, // withSecondaryValue secondaryValue, secondarySetValue, // withSecondaryData SecondaryRepository, secondaryData, secondaryIdIx, secondaryDisplayIx, } = props, usesWithValue = !!secondarySetValue, initialSelection = secondarySelection || secondaryDefaultSelection || [], forceUpdate = useForceUpdate(), secondarySelectionRef = useRef(initialSelection), SecondaryRepositoryRef = useRef(SecondaryRepository), [isReady, setIsReady] = useState(secondarySelection || false), // if secondarySelection is already defined, or secondaryValue is not null and we don't need to load repository, it's ready secondarySetSelection = (secondarySelection) => { if (_.isEqual(secondarySelection, secondaryGetSelection())) { return; } secondarySelectionRef.current = secondarySelection; if (secondaryOnChangeSelection) { secondaryOnChangeSelection(secondarySelection); } if (fireEvent) { fireEvent('secondaryChangeSelection', secondarySelection); } forceUpdate(); }, secondaryGetSelection = () => { return secondarySelectionRef.current; }, secondaryGetRepository = () => { return SecondaryRepositoryRef.current; }, secondarySelectPrev = () => { secondarySelectDirection(SELECT_UP); }, secondarySelectNext = () => { secondarySelectDirection(SELECT_DOWN); }, secondaryAddPrevToSelection = () => { secondarySelectDirection(SELECT_UP, true); }, secondaryAddNextToSelection = () => { secondarySelectDirection(SELECT_DOWN, true); }, secondarySelectDirection = (which, isAdd = false) => { const { items, max, min, noSelection, } = getMaxMinSelectionIndices(); let newIx; if (which === SELECT_DOWN) { if (noSelection || max === items.length -1) { // select first newIx = 0; } else { newIx = max +1; } } else if (which === SELECT_UP) { if (noSelection || min === 0) { // select last newIx = items.length -1; } else { newIx = min -1; } } if (items[newIx]) { if (isAdd) { secondaryAddToSelection(items[newIx]); } else { secondarySetSelection([items[newIx]]); } } }, secondaryAddToSelection = (item) => { const newSelection = [...secondaryGetSelection()]; // so we get a new object, so descendants rerender newSelection.push(item); secondarySetSelection(newSelection); }, secondaryRemoveFromSelection = (item) => { const SecondaryRepository = secondaryGetRepository(); let newSelection = []; if (SecondaryRepository) { newSelection = _.remove(secondaryGetSelection(), (sel) => sel !== item); } else { newSelection = _.remove(secondaryGetSelection(), (sel) => sel[secondaryIdIx] !== item[secondaryIdIx]); } secondarySetSelection(newSelection); }, secondaryDeselectAll = () => { if (!_.isEmpty(secondaryGetSelection())) { secondarySetSelection([]); } }, secondaryRefreshSelection = () => { // When Repository reloads, the entities get destroyed. // Loop through these destroyed entities and see if new ones exist with same ids. // If so, select these new ones. // That way, after a load event, we'll keep the same selection, if possible. const newSelection = [], ids = _.map(secondaryGetSelection(), (item) => item.id), SecondaryRepository = secondaryGetRepository(); _.each(ids, (id) => { const found = SecondaryRepository.getById(id); if (found) { newSelection.push(found); } }); secondarySetSelection(newSelection); }, getMaxMinSelectionIndices = () => { let items, currentlySelectedRowIndices = []; const SecondaryRepository = secondaryGetRepository(); if (SecondaryRepository) { items = SecondaryRepository.getEntitiesOnPage(); } else { items = secondaryData; } _.each(items, (item, ix) => { if (secondaryIsInSelection(item)) { currentlySelectedRowIndices.push(ix); } }); if (currentlySelectedRowIndices.length === 0) { return { items, noSelection: true, }; } const max = Math.max(...currentlySelectedRowIndices), min = Math.min(...currentlySelectedRowIndices); return { items, max, min, noSelection: false, }; }, secondarySelectRangeTo = (item) => { // Select above max or below min to this one const currentSelectionLength = secondaryGetSelection().length, index = getIndexOfSelectedItem(item); let newSelection = [...secondaryGetSelection()]; // so we get a new object, so descendants rerender if (currentSelectionLength) { const { items, max, min, } = getMaxMinSelectionIndices(); let i, itemAtIx; if (max < index) { // all other secondarySelections are below the current; // Range is from max+1 up to index for (i = max +1; i < index; i++) { itemAtIx = items[i]; newSelection.push(itemAtIx); } } else if (min > index) { // all other secondarySelections are above the current; // Range is from min-1 down to index for (i = min -1; i > index; i--) { itemAtIx = items[i]; newSelection.push(itemAtIx); } } } newSelection.push(item); secondarySetSelection(newSelection); }, secondaryIsInSelection = (item) => { const SecondaryRepository = secondaryGetRepository(); if (SecondaryRepository) { return inArray(item, secondaryGetSelection()); } const found = _.find(secondaryGetSelection(), (selectedItem) => { return selectedItem[secondaryIdIx] === item[secondaryIdIx]; }); return !!found; }, getIndexOfSelectedItem = (item) => { const SecondaryRepository = secondaryGetRepository(); // Gets ix of entity on page, or element in secondaryData array if (SecondaryRepository) { const entities = SecondaryRepository.getEntitiesOnPage(); return entities.indexOf(item); } let found; _.each(secondaryData, (datum, ix) => { if (datum[secondaryIdIx] === item[secondaryIdIx]) { found = ix; return false; // break loop } }); return found; }, secondaryGetIdsFromLocalSelection = () => { if (!secondaryGetSelection()[0]) { return null; } const SecondaryRepository = secondaryGetRepository(), secondaryValues = _.map(secondaryGetSelection(), (item) => { if (SecondaryRepository) { return item.id; } return item[secondaryIdIx]; }); if (secondaryValues.length === 1) { return secondaryValues[0]; } return secondaryValues; }, secondaryGetDisplayValuesFromLocalSelection = (secondarySelection) => { if (!secondarySelection[0]) { return ''; } const SecondaryRepository = secondaryGetRepository(); return _.map(secondarySelection, (item) => { if (SecondaryRepository) { return item.displayValue; } return item[secondaryDisplayIx]; }) .join(', '); }, conformValueToLocalSelection = () => { if (!secondarySetValue) { return; } const localValue = secondaryGetIdsFromLocalSelection(); if (!_.isEqual(localValue, secondaryValue)) { secondarySetValue(localValue); } }, conformSelectionToValue = async () => { const SecondaryRepository = secondaryGetRepository(); let newSelection = []; if (SecondaryRepository) { if (SecondaryRepository.isLoading) { await SecondaryRepository.waitUntilDoneLoading(); } // Get entity or entities that match secondaryValue if ((_.isArray(secondaryValue) && !_.isEmpty(secondaryValue)) || !!secondaryValue) { if (_.isArray(secondaryValue)) { newSelection = SecondaryRepository.getBy((entity) => inArray(entity.id, secondaryValue)); } else { let found = SecondaryRepository.getById(secondaryValue); if (found) { newSelection.push(found); // } else if (SecondaryRepository?.isRemote && SecondaryRepository?.entities.length) { // // Value cannot be found in SecondaryRepository, but actually exists on server // // Try to get this secondaryValue from the server directly // SecondaryRepository.filter(SecondaryRepository.schema.model.idProperty, secondaryValue); // await SecondaryRepository.load(); // found = SecondaryRepository.getById(secondaryValue); // if (found) { // newSelection.push(found); // } } } } } else { // Get secondaryData item or items that match secondaryValue if (!_.isNil(secondaryValue) && (_.isBoolean(secondaryValue) || _.isNumber(secondaryValue) || !_.isEmpty(secondaryValue))) { let currentValue = secondaryValue; if (!_.isArray(currentValue)) { currentValue = [currentValue]; } _.each(currentValue, (val) => { // Search through secondaryData const found = _.find(secondaryData, (item) => { if (_.isString(item[secondaryIdIx]) && _.isString(val)) { return item[secondaryIdIx].toLowerCase() === val.toLowerCase(); } return item[secondaryIdIx] === val; }); if (found) { newSelection.push(found); } }); } } if (!_.isEqual(newSelection, secondaryGetSelection())) { secondarySetSelection(newSelection); } }; if (SecondaryRepository) { useEffect(() => { SecondaryRepository.on('load', secondaryRefreshSelection); return () => { SecondaryRepository.off('load', secondaryRefreshSelection); }; }, []); } useEffect(() => { (async () => { const SecondaryRepository = secondaryGetRepository(); if (usesWithValue && SecondaryRepository?.isRemote && !SecondaryRepository.isAutoLoad && !SecondaryRepository.isLoaded && !SecondaryRepository.isLoading && (!_.isNil(secondaryValue) || !_.isEmpty(secondarySelection)) || secondaryAutoSelectFirstItem) { // on initialization, we can't conformSelectionToValue if the repository is not yet loaded, // so first load repo, then conform to secondaryValue await SecondaryRepository.load(); } if (!_.isNil(secondaryValue)) { await conformSelectionToValue(); } else if (!_.isEmpty(secondarySelection)) { conformValueToLocalSelection(); } else if (secondaryAutoSelectFirstItem) { let newSelection = []; if (SecondaryRepository) { const entitiesOnPage = SecondaryRepository.getEntitiesOnPage(); newSelection = entitiesOnPage[0] ? [entitiesOnPage[0]] : []; } else { newSelection = secondaryData[0] ? [secondaryData[0]] : []; } secondarySetSelection(newSelection); } setIsReady(true); })(); }, [secondaryValue]); if (self) { self.secondarySelection = secondaryGetSelection(); self.secondarySetSelection = secondarySetSelection; self.secondarySelectPrev = secondarySelectPrev; self.secondarySelectNext = secondarySelectNext; self.secondaryAddPrevToSelection = secondaryAddPrevToSelection; self.secondaryAddNextToSelection = secondaryAddNextToSelection; self.secondaryAddToSelection = secondaryAddToSelection; self.secondaryRemoveFromSelection = secondaryRemoveFromSelection; self.secondaryDeselectAll = secondaryDeselectAll; self.secondarySelectRangeTo = secondarySelectRangeTo; self.secondaryIsInSelection = secondaryIsInSelection; self.secondaryGetIdsFromLocalSelection = secondaryGetIdsFromLocalSelection; self.secondaryGetDisplayValuesFromSelection = secondaryGetDisplayValuesFromLocalSelection; } if (usesWithValue) { useEffect(() => { if (!isReady) { return () => {}; } conformSelectionToValue(); }, [secondaryValue]); useEffect(() => { if (!isReady) { return () => {}; } conformValueToLocalSelection(); }, [secondarySelection]); } if (!isReady) { return null; } return <WrappedComponent {...props} secondaryDisableWithSelection={false} secondaryAlreadyHasWithSelection={true} ref={ref} secondarySelection={secondaryGetSelection()} secondaryGetSelection={secondaryGetSelection} secondarySetSelection={secondarySetSelection} secondarySelectionMode={secondarySelectionMode} secondarySelectPrev={secondarySelectPrev} secondarySelectNext={secondarySelectNext} secondaryAddNextToSelection={secondaryAddNextToSelection} secondaryAddPrevToSelection={secondaryAddPrevToSelection} secondaryRemoveFromSelection={secondaryRemoveFromSelection} secondaryAddToSelection={secondaryAddToSelection} secondaryDeselectAll={secondaryDeselectAll} secondarySelectRangeTo={secondarySelectRangeTo} secondaryIsInSelection={secondaryIsInSelection} secondaryGetIdsFromSelection={secondaryGetIdsFromLocalSelection} secondaryGetDisplayValuesFromSelection={secondaryGetDisplayValuesFromLocalSelection} />; }); }