1*af8fd012Sandyimport * as React from 'react'; 2*af8fd012Sandy 3*af8fd012Sandy// utility function for capturing all push and pop stack events 4*af8fd012Sandy// components can subscribe to internal state to push and pop their own views depending on use case 5*af8fd012Sandy// e.g a modal stack, a screen stack, a toast stack, etc 6*af8fd012Sandy 7*af8fd012Sandyexport type IStackEvent = 'pushstart' | 'pushend' | 'popstart' | 'popend' | 'replace'; 8*af8fd012Sandy 9*af8fd012Sandyexport type StackItemStatus = 'pushing' | 'popping' | 'settled'; 10*af8fd012Sandy 11*af8fd012Sandyexport type ListenerFn<T> = ({ 12*af8fd012Sandy items, 13*af8fd012Sandy}: { 14*af8fd012Sandy action: IStackEvent; 15*af8fd012Sandy key: string; 16*af8fd012Sandy items: StackItem<T>[]; 17*af8fd012Sandy getItemByKey: (key: string) => T | undefined; 18*af8fd012Sandy}) => void; 19*af8fd012Sandy 20*af8fd012Sandyexport type StackItem<T> = T & { key: string; status: StackItemStatus }; 21*af8fd012Sandyexport type IReplaceOptions<T> = T & { replaceAmount?: number; key?: string }; 22*af8fd012Sandyexport type IPushOptions<T> = T & { key?: string }; 23*af8fd012Sandyexport interface IStack<T> { 24*af8fd012Sandy push: (pushOptions: IPushOptions<T>) => Promise<string>; 25*af8fd012Sandy pop: (amount?: number) => Promise<string[]>; 26*af8fd012Sandy replace: (replaceOptions: IReplaceOptions<T>) => Promise<any>; 27*af8fd012Sandy onPushEnd: (key: string) => void; 28*af8fd012Sandy onPopEnd: (key: string) => void; 29*af8fd012Sandy subscribe: (listener: ListenerFn<T>) => () => void; 30*af8fd012Sandy getState: () => { 31*af8fd012Sandy items: StackItem<T>[]; 32*af8fd012Sandy getItemByKey: (key: string) => T | undefined; 33*af8fd012Sandy }; 34*af8fd012Sandy} 35*af8fd012Sandy 36*af8fd012Sandyconst generateRouteKey = () => `${new Date().getTime()}`; 37*af8fd012Sandy 38*af8fd012Sandyexport function createAsyncStack<T>(): IStack<T> { 39*af8fd012Sandy let keys: string[] = []; 40*af8fd012Sandy const lookup: Record<string, StackItem<T>> = {}; 41*af8fd012Sandy 42*af8fd012Sandy const pushResolvers: Record<string, Function> = {}; 43*af8fd012Sandy const popResolvers: Record<string, Function> = {}; 44*af8fd012Sandy 45*af8fd012Sandy let listeners: any[] = []; 46*af8fd012Sandy 47*af8fd012Sandy function push(pushOptions: IPushOptions<T>) { 48*af8fd012Sandy const key = pushOptions.key || generateRouteKey(); 49*af8fd012Sandy 50*af8fd012Sandy if (keys.includes(key)) { 51*af8fd012Sandy return Promise.resolve(key); 52*af8fd012Sandy } 53*af8fd012Sandy 54*af8fd012Sandy keys.push(key); 55*af8fd012Sandy 56*af8fd012Sandy lookup[key] = { 57*af8fd012Sandy ...pushOptions, 58*af8fd012Sandy key, 59*af8fd012Sandy status: 'pushing', 60*af8fd012Sandy }; 61*af8fd012Sandy 62*af8fd012Sandy const promise = new Promise<string>((resolve) => { 63*af8fd012Sandy pushResolvers[key] = resolve; 64*af8fd012Sandy }); 65*af8fd012Sandy 66*af8fd012Sandy emit('pushstart', key); 67*af8fd012Sandy 68*af8fd012Sandy return promise; 69*af8fd012Sandy } 70*af8fd012Sandy 71*af8fd012Sandy function onPushEnd(key: string) { 72*af8fd012Sandy const item = lookup[key]; 73*af8fd012Sandy 74*af8fd012Sandy if (item) { 75*af8fd012Sandy item.status = 'settled'; 76*af8fd012Sandy 77*af8fd012Sandy emit('pushend', key); 78*af8fd012Sandy 79*af8fd012Sandy const resolver = pushResolvers[key]; 80*af8fd012Sandy 81*af8fd012Sandy if (resolver) { 82*af8fd012Sandy resolver(key); 83*af8fd012Sandy } 84*af8fd012Sandy } 85*af8fd012Sandy } 86*af8fd012Sandy 87*af8fd012Sandy function pop(amount = 1, startIndex = 0) { 88*af8fd012Sandy const promises = []; 89*af8fd012Sandy 90*af8fd012Sandy if (amount === -1) { 91*af8fd012Sandy // pop them all 92*af8fd012Sandy amount = keys.length; 93*af8fd012Sandy } 94*af8fd012Sandy 95*af8fd012Sandy for (let i = 1; i <= amount; i++) { 96*af8fd012Sandy const key = keys[keys.length - startIndex - i]; 97*af8fd012Sandy const item = lookup[key]; 98*af8fd012Sandy 99*af8fd012Sandy if (item) { 100*af8fd012Sandy item.status = 'popping'; 101*af8fd012Sandy 102*af8fd012Sandy const promise = new Promise((resolve) => { 103*af8fd012Sandy popResolvers[key] = resolve; 104*af8fd012Sandy }); 105*af8fd012Sandy 106*af8fd012Sandy promises.push(promise); 107*af8fd012Sandy emit('popstart', key); 108*af8fd012Sandy } 109*af8fd012Sandy } 110*af8fd012Sandy 111*af8fd012Sandy return Promise.all(promises) as Promise<string[]>; 112*af8fd012Sandy } 113*af8fd012Sandy 114*af8fd012Sandy function onPopEnd(key: string) { 115*af8fd012Sandy keys = keys.filter((k) => k !== key); 116*af8fd012Sandy 117*af8fd012Sandy const resolver = popResolvers[key]; 118*af8fd012Sandy 119*af8fd012Sandy if (resolver) { 120*af8fd012Sandy resolver(key); 121*af8fd012Sandy } 122*af8fd012Sandy 123*af8fd012Sandy delete popResolvers[key]; 124*af8fd012Sandy delete pushResolvers[key]; 125*af8fd012Sandy 126*af8fd012Sandy emit('popend', key); 127*af8fd012Sandy } 128*af8fd012Sandy 129*af8fd012Sandy async function replace(replaceOptions: IReplaceOptions<T>) { 130*af8fd012Sandy const itemsToPop = replaceOptions.replaceAmount != null ? replaceOptions.replaceAmount : 1; 131*af8fd012Sandy 132*af8fd012Sandy const promise2 = await push(replaceOptions); 133*af8fd012Sandy const promise1 = await pop(itemsToPop, 1); 134*af8fd012Sandy 135*af8fd012Sandy return Promise.all([promise2, promise1]); 136*af8fd012Sandy } 137*af8fd012Sandy 138*af8fd012Sandy function subscribe(listener: any) { 139*af8fd012Sandy listeners.push(listener); 140*af8fd012Sandy 141*af8fd012Sandy return () => { 142*af8fd012Sandy listeners = listeners.filter((l) => l !== listener); 143*af8fd012Sandy }; 144*af8fd012Sandy } 145*af8fd012Sandy 146*af8fd012Sandy function emit(action: IStackEvent, key: string) { 147*af8fd012Sandy listeners.forEach((listener) => { 148*af8fd012Sandy const state = getState(); 149*af8fd012Sandy listener({ ...state, key, action }); 150*af8fd012Sandy }); 151*af8fd012Sandy } 152*af8fd012Sandy 153*af8fd012Sandy function getItemByKey(key: string) { 154*af8fd012Sandy return lookup[key]; 155*af8fd012Sandy } 156*af8fd012Sandy 157*af8fd012Sandy function getState() { 158*af8fd012Sandy const items = keys.map((key) => lookup[key]); 159*af8fd012Sandy 160*af8fd012Sandy return { 161*af8fd012Sandy items, 162*af8fd012Sandy getItemByKey, 163*af8fd012Sandy }; 164*af8fd012Sandy } 165*af8fd012Sandy 166*af8fd012Sandy return { 167*af8fd012Sandy push, 168*af8fd012Sandy onPushEnd, 169*af8fd012Sandy pop, 170*af8fd012Sandy onPopEnd, 171*af8fd012Sandy replace, 172*af8fd012Sandy subscribe, 173*af8fd012Sandy getState, 174*af8fd012Sandy }; 175*af8fd012Sandy} 176*af8fd012Sandy 177*af8fd012Sandyexport function useStackItems<T>(stack: IStack<T>) { 178*af8fd012Sandy const [items, setItems] = React.useState(() => stack.getState().items); 179*af8fd012Sandy 180*af8fd012Sandy React.useEffect(() => { 181*af8fd012Sandy const unsubscribe = stack.subscribe(({ items }) => { 182*af8fd012Sandy setItems(items); 183*af8fd012Sandy }); 184*af8fd012Sandy 185*af8fd012Sandy return () => { 186*af8fd012Sandy unsubscribe && unsubscribe(); 187*af8fd012Sandy }; 188*af8fd012Sandy }, [stack]); 189*af8fd012Sandy 190*af8fd012Sandy return items; 191*af8fd012Sandy} 192