import { TagLabelFactory } from "$lib-core-types"; import { isEmpty, isNil, flatten } from "lodash"; import { DetailsGroup, OptionDetails } from "$lib-core-types/OptionDetails.types"; const getParentKey = ({ parents }: OptionDetails) => parents.join("."); const getCurrentKey = ({ parents, option: { id } }: OptionDetails) => [...parents, id].join("."); const getOptionId = ({ option: { id } }: OptionDetails) => id; const prepareIsEqualToDetails = (details1: OptionDetails) => { const optionId1 = getOptionId(details1); return (details2: OptionDetails) => optionId1 === getOptionId(details2); }; export interface GroupedOptions { parentKey: string; parentTag: string | TagLabelFactory | null; anyLabel?: () => string | JSX.Element | null; isFull: boolean; parentDetails?: OptionDetails; optionsDetails: OptionDetails[]; } type PartialDetailsGroup = { [K in keyof DetailsGroup]?: DetailsGroup[K] }; export class OptionsDetailsCache { selectedOptions = new Map(); add(details: OptionDetails) { const key = getParentKey(details); this.ensureGroup(key); const selectedSiblings = this.getOptionDetails(key); this.addOptionDetails(key, { optionsDetails: [...selectedSiblings, details] }); } addFullGroup(children: OptionDetails[], parent: OptionDetails) { const { option: { tagAnyLabel } } = parent; const key = getCurrentKey(parent); this.ensureGroup(key); this.addOptionDetails(key, { optionsDetails: children, isFull: true, parentDetails: parent, anyLabel: isNil(tagAnyLabel) ? undefined : () => typeof tagAnyLabel === "string" ? tagAnyLabel : tagAnyLabel(parent) }); } remove(details: OptionDetails) { const key = getParentKey(details); const isEqual = prepareIsEqualToDetails(details); const optionDetailsGroup = this.getOptionDetails(key); const filtered = optionDetailsGroup.filter(d => !isEqual(d)); if (isEmpty(filtered)) { this.selectedOptions.delete(key); } else { this.addOptionDetails(key, { optionsDetails: filtered }); } } removeGroup(parentKey: string) { const removed = this.selectedOptions.get(parentKey); this.selectedOptions.delete(parentKey); return removed; } getSubOptions(details: OptionDetails): OptionDetails[] { if (!details.isCategory) { const key = getCurrentKey(details); return this.getOptionDetails(key); } const category = details.option.id; const allSelected = flatten( Array.from(this.selectedOptions, ([parentKey, group]) => group) ).map(({ optionsDetails }) => optionsDetails); return flatten(allSelected).filter( ({ option: { category: currentCategory } }) => category === currentCategory ); } getGroupedOptions(): GroupedOptions[] { const result = Array.from(this.selectedOptions, ([parentKey, group]) => { const [first] = group.optionsDetails; return { parentKey, parentTag: first && first.tagParentLabel, isFull: group.isFull, parentDetails: group.parentDetails, optionsDetails: group.optionsDetails, anyLabel: group.anyLabel }; }); return result.filter( groupedOptions => !isEmpty(groupedOptions.optionsDetails) ); } isCached(details: OptionDetails) { const key = getParentKey(details); const optionDetailsGroup = this.getOptionDetails(key); const isEqual = prepareIsEqualToDetails(details); return !!(optionDetailsGroup && optionDetailsGroup.find(isEqual)); } private getOptionDetails(key: string) { const group = this.selectedOptions.get(key); return group ? group.optionsDetails : []; } private ensureGroup(key: string) { const group = this.selectedOptions.get(key); if (!group) { this.selectedOptions.set(key, { isFull: false, optionsDetails: [] }); } } private addOptionDetails( key: string, { optionsDetails, isFull, parentDetails, anyLabel }: PartialDetailsGroup ) { const group = this.selectedOptions.get(key); if (!group) { throw new Error(`Group for key ${key} is empty`); } this.selectedOptions.set(key, { isFull: isFull || group.isFull, parentDetails, optionsDetails: optionsDetails || [], anyLabel }); } }