UNPKG

@excentone/spfx-react

Version:

Contains custom ReactJs components and hooks intended to use when developing SharePoint Framework (SPFx) Web components.

285 lines (283 loc) 12.2 kB
import { SelectableOptionMenuItemType, } from "@fluentui/react"; import { useRef, useMemo, useState, useEffect, useCallback, } from "react"; import { useSafeDispatch } from "../useSafeDispatch"; import { isFunction } from "@excentone/spfx-utilities"; const { Normal, Header, Divider } = SelectableOptionMenuItemType; const GenerateDefaultOptionListHookOptions = () => ({ multiSelect: false, selectAllText: null, blankOptionText: null, autoSelectWhenOnlyOneOption: false }); export const useOptionList = (hookOptions) => { const _shouldTriggerEvent = useRef(false); const [state, setState] = useState({ items: [], options: [], selectedKey: [], selectedKeys: [], selectedItems: [], allSelected: false }); const { initialItems, selectAllText, blankOptionText, keySelector, textSelector, groupSelector, idSelector, orderBy, isSelected, isHidden, multiSelect, onChange, autoSelectWhenOnlyOneOption } = useMemo(() => ({ ...GenerateDefaultOptionListHookOptions(), ...hookOptions, textSelector: hookOptions.textSelector || hookOptions.keySelector, orderBy: hookOptions.orderBy || hookOptions.textSelector || hookOptions.keySelector, }), [hookOptions]); const handleOnChange = useSafeDispatch(onChange); const sortByField = useCallback((a, b) => orderBy(a) < orderBy(b) ? -1 : orderBy(a) > orderBy(b) ? 1 : 0, [orderBy]); const sortByGroupOrByText = useCallback((a, b) => groupSelector(a) < groupSelector(b) ? -1 : groupSelector(a) > groupSelector(b) ? 1 : sortByField(a, b), [groupSelector, sortByField]); const groupItems = useCallback((group, item) => { const groupName = groupSelector(item); if (!group[groupName]) group[groupName] = []; group[groupName].push(item); return group; }, [groupSelector]); const createOption = useCallback((item, index, selected, groupName) => ({ groupName, data: item, id: idSelector ? idSelector(item) : null, text: textSelector ? textSelector(item) : keySelector(item), key: keySelector(item), selected: selected && selected(item, index) && !(isHidden && isHidden(item)), hidden: isHidden && isHidden(item), itemType: Normal }), [idSelector, keySelector, textSelector, isHidden]); const createOptions = useCallback((sourceItems, selected) => { const opts = []; if (!(sourceItems && sourceItems.length)) return opts; if (blankOptionText) { opts.push({ itemType: Normal, key: null, text: blankOptionText, }); opts.push({ key: `divider_blank`, text: '', itemType: Divider }); } if (selectAllText) { opts.push({ itemType: Normal, key: selectAllText, text: selectAllText, }); opts.push({ key: `divider_all`, text: '', itemType: Divider }); } if (groupSelector) { const groups = sourceItems .sort(sortByGroupOrByText) .reduce(groupItems, {}); Object .keys(groups) .forEach((groupName, index) => { const itemsInGroup = groups[groupName]; opts.push({ key: `group_${index}`, text: groupName, itemType: Header }); opts.push(...itemsInGroup .sort(sortByField) .map((itm, ndx) => createOption(itm, ndx, selected))); opts.push({ key: `divider_${index}`, text: '', itemType: Divider }); }); } else { opts.push(...sourceItems .sort(sortByField) .map((itm, ndx) => createOption(itm, ndx, selected))); } if (blankOptionText) { const blankOption = opts .find(o => o.text === blankOptionText); if (blankOption) blankOption.selected = opts .filter(o => o.itemType === Normal && o.text !== blankOptionText) .every(o => !o.selected); } if (selectAllText) { const selectAllOpt = opts .find(o => o.key === selectAllText); if (selectAllOpt) selectAllOpt.selected = opts .filter(o => o.itemType === Normal && o.key !== selectAllText) .every(o => o.selected); } return opts; }, [blankOptionText, sortByField, sortByGroupOrByText, groupItems, selectAllText]); const getSelectedOptionsProp = useCallback((options, selector) => options .filter(o => o.selected && o.key !== selectAllText) .map(selector), [selectAllText]); const setItems = useCallback((newItems, selected, shouldTriggerEvent = false) => { _shouldTriggerEvent.current = shouldTriggerEvent; setState(prev => { const items = isFunction(newItems) ? newItems(prev && prev.items) : newItems; const predicate = item => { const userDefinedPredicate = selected || isSelected; //return (!autoSelectWhenOnlyOneOption || items.length === 1) || (userDefinedPredicate && userDefinedPredicate(item)); //Wrong logic return (autoSelectWhenOnlyOneOption && items.length === 1) || (userDefinedPredicate && userDefinedPredicate(item)); //Correct logic }; const options = createOptions(items, predicate); const keys = getSelectedOptionsProp(options, o => o.key); const selectedItems = getSelectedOptionsProp(options, o => o.data); const allSelected = keys.length === items.length; return { items, options, allSelected, selectedItems, selectedKey: keys, selectedKeys: keys, }; }); }, [setState, createOptions, getSelectedOptionsProp, isSelected]); const initState = useCallback(() => { if (initialItems && initialItems.length) setItems(initialItems, isSelected); }, [initialItems, setItems, isSelected]); const nextState = useCallback(({ items, options: oldOptions }, setOptions) => { // _shouldTriggerEvent.current = true; const options = isFunction(setOptions) ? setOptions(oldOptions) : setOptions; const keys = getSelectedOptionsProp(options, o => o.key); const allSelected = items.length === keys.length; const selectAllOpt = options.find(o => o.key === selectAllText); if (selectAllOpt) selectAllOpt.selected = allSelected; const blankOption = options.find(o => o.text === blankOptionText); if (blankOption) blankOption.selected = options .filter(o => o.itemType === Normal) .every(o => !o.selected); return { items, options, allSelected, selectedKey: keys, selectedKeys: keys, selectedItems: getSelectedOptionsProp(options, o => o.data), }; }, [getSelectedOptionsProp, selectAllText]); const selectAll = useCallback((shouldTriggerEvent = false) => { _shouldTriggerEvent.current = shouldTriggerEvent; setState(prev => nextState(prev, options => options.map(opt => ({ ...opt, selected: opt.itemType === Normal })))); }, [setState, nextState]); const unselectAll = useCallback((shouldTriggerEvent = false) => { _shouldTriggerEvent.current = shouldTriggerEvent; setState(prev => nextState(prev, options => options.map(opt => ({ ...opt, selected: false })))); }, [setState, nextState, blankOptionText]); const selectOne = useCallback((predicateOrOption, shouldTriggerEvent = true) => { _shouldTriggerEvent.current = false; if (predicateOrOption) { _shouldTriggerEvent.current = shouldTriggerEvent; setState(prev => nextState(prev, options => { const predicate = isFunction(predicateOrOption) ? predicateOrOption : (option) => option.key === predicateOrOption.key; const index = options.findIndex((opt, ndx) => opt.itemType === Normal && predicate(opt, ndx)); return index >= 0 ? options.map((opt, ndx) => ({ ...opt, selected: ndx === index })) : options; })); } }, [setState, nextState]); const selectMany = useCallback((predicate, shouldTriggerEvent = true) => { _shouldTriggerEvent.current = shouldTriggerEvent; setState(prev => nextState(prev, options => { options = options .map((opt, ndx) => ({ ...opt, selected: opt.itemType === Normal && predicate(opt, ndx) })); return options; })); }, [setState, nextState, selectAllText]); const filter = useCallback((predicate, shouldTriggerEvent = true) => { _shouldTriggerEvent.current = shouldTriggerEvent; setState(prev => nextState(prev, options => options.map(({ itemType, ...opt }, ndx) => { const hidden = itemType === Normal && !predicate(opt, ndx); const selected = !hidden && opt.selected; return { ...opt, itemType, hidden, selected, disabled: hidden }; }))); }, [setState, nextState]); const filterByGroup = useCallback((groups, shouldTriggerEvent = true) => { _shouldTriggerEvent.current = shouldTriggerEvent; setState(prev => nextState(prev, options => { const groupNames = groups.map(grp => typeof grp === 'string' ? grp : grp.text); return options.map(({ groupName, ...opt }) => { const hidden = !groupNames.includes(groupName); return { ...opt, hidden }; }); })); }, [setState, nextState]); const tryTriggerEvent = useCallback(() => { if (state && _shouldTriggerEvent.current && onChange) { const { selectedItems } = state; const items = selectedItems && selectedItems.length > 0 ? selectedItems : []; handleOnChange(items, items.map(keySelector).map(key => key.toLocaleString())); } _shouldTriggerEvent.current = false; }, [state, _shouldTriggerEvent.current, onChange, keySelector, handleOnChange]); const setSelected = useCallback((_, option) => { if (multiSelect) { selectMany(o => option && (option.key === selectAllText || option.key === o.key) ? option.selected : o.selected); } else { selectOne(option); } }, [multiSelect, onChange]); const refresh = useCallback(() => { if (isSelected) initState(); }, [isSelected, initState]); const props = useMemo(() => { const { options, selectedKey, selectedKeys } = state; return { options, multiSelect, selectedKey, selectedKeys, text: multiSelect && options.filter(o => o.itemType === Normal).every(o => o.selected) && selectAllText ? selectAllText : null }; }, [state, multiSelect]); useEffect(tryTriggerEvent, [state]); useEffect(initState, [initialItems]); return [ { props, state }, { setSelected, selectOne, selectMany, selectAll, unselectAll, setItems, filter, filterByGroup, refresh } ]; }; //# sourceMappingURL=useOptionList.js.map