@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
JavaScript
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