import { Arr, Cell, Obj, Optional, Optionals, Singleton } from '@ephox/katamari'; import type { AlloyComponent } from '../../api/component/ComponentApi'; import type { MenuPreparation } from '../../ui/single/TieredMenuSpec'; import * as MenuPathing from './MenuPathing'; // Object indexed by menu value. Each entry has a list of item values. export type MenuDirectory = Record; // A tuple of (item, menu). This can be used to refresh the menu and position them next to the right // triggering items. export interface LayeredItemTrigger { readonly triggeringItem: AlloyComponent; readonly triggeredMenu: AlloyComponent; readonly triggeringPath: string[]; } export interface LayeredState { setContents: (sPrimary: string, sMenus: Record, sExpansions: Record, dir: MenuDirectory) => void; setMenuBuilt: (menuName: string, built: AlloyComponent) => void; expand: (itemValue: string) => Optional; refresh: (itemValue: string) => Optional; collapse: (itemValue: string) => Optional; lookupMenu: (menuValue: string) => Optional; lookupItem: (itemValue: string) => Optional; otherMenus: (path: string[]) => string[]; getPrimary: () => Optional; getMenus: () => Record; clear: () => void; isClear: () => boolean; getTriggeringPath: (itemValue: string, getItemByValue: (itemValue: string) => Optional) => Optional; } const init = (): LayeredState => { const expansions: Cell> = Cell({ }); const menus: Cell> = Cell({ }); const paths: Cell> = Cell({ }); const primary = Singleton.value(); // Probably think of a better way to store this information. const directory: Cell = Cell({ }); const clear = (): void => { expansions.set({}); menus.set({}); paths.set({}); primary.clear(); }; const isClear = (): boolean => primary.get().isNone(); const setMenuBuilt = (menuName: string, built: AlloyComponent) => { menus.set({ ...menus.get(), [menuName]: { type: 'prepared', menu: built } }); }; const setContents = (sPrimary: string, sMenus: Record, sExpansions: Record, dir: MenuDirectory): void => { primary.set(sPrimary); expansions.set(sExpansions); menus.set(sMenus); directory.set(dir); const sPaths = MenuPathing.generate(dir, sExpansions); paths.set(sPaths); }; const getTriggeringItem = (menuValue: string): Optional => Obj.find(expansions.get(), (v, _k) => v === menuValue); const getTriggerData = (menuValue: string, getItemByValue: (v: string) => Optional, path: string[]): Optional => getPreparedMenu(menuValue).bind((menu) => getTriggeringItem(menuValue).bind((triggeringItemValue) => getItemByValue(triggeringItemValue).map((triggeredItem) => ({ triggeredMenu: menu, triggeringItem: triggeredItem, triggeringPath: path })))); const getTriggeringPath = (itemValue: string, getItemByValue: (v: string) => Optional): Optional => { // Get the path up to the last item const extraPath: string[] = Arr.filter(lookupItem(itemValue).toArray(), (menuValue) => getPreparedMenu(menuValue).isSome()); return Obj.get(paths.get(), itemValue).bind((path) => { // remember the path is [ most-recent-menu, next-most-recent-menu ] // convert each menu identifier into { triggeringItem: comp, menu: comp } // could combine into a fold ... probably a left to reverse ... but we'll take the // straightforward version when prototyping const revPath = Arr.reverse(extraPath.concat(path)); const triggers: Array> = Arr.bind(revPath, (menuValue, menuIndex) => // finding menuValue, it should match the trigger getTriggerData(menuValue, getItemByValue, revPath.slice(0, menuIndex + 1)).fold( () => Optionals.is(primary.get(), menuValue) ? [ ] : [ Optional.none() ], (data) => [ Optional.some(data) ] ) ); // Convert List> to Optional> if ALL are Some return Optionals.sequence(triggers); }); }; // Given an item, return a list of all menus including the one that it triggered (if there is one) const expand = (itemValue: string): Optional => Obj.get(expansions.get(), itemValue).map((menu: string) => { const current: string[] = Obj.get(paths.get(), itemValue).getOr([ ]); return [ menu ].concat(current); }); const collapse = (itemValue: string): Optional => // Look up which key has the itemValue Obj.get(paths.get(), itemValue).bind((path) => path.length > 1 ? Optional.some(path.slice(1)) : Optional.none()); const refresh = (itemValue: string): Optional => Obj.get(paths.get(), itemValue); const getPreparedMenu = (menuValue: string): Optional => lookupMenu(menuValue).bind(extractPreparedMenu); const lookupMenu = (menuValue: string): Optional => Obj.get( menus.get(), menuValue ); const lookupItem = (itemValue: string): Optional => Obj.get( expansions.get(), itemValue ); const otherMenus = (path: string[]): string[] => { const menuValues = directory.get(); return Arr.difference(Obj.keys(menuValues), path); }; const getPrimary = (): Optional => primary.get().bind(getPreparedMenu); const getMenus = (): Record => menus.get(); return { setMenuBuilt, setContents, expand, refresh, collapse, lookupMenu, lookupItem, otherMenus, getPrimary, getMenus, clear, isClear, getTriggeringPath }; }; const extractPreparedMenu = (prep: MenuPreparation): Optional => prep.type === 'prepared' ? Optional.some(prep.menu) : Optional.none(); export const LayeredState = { init, extractPreparedMenu };