import React, { useContext, useEffect, useReducer, useRef, useState } from 'react'; import { MenuSpec } from './types'; import { LayoutFrameState } from './state'; import { BehaviorSubject } from 'rxjs'; import produce from 'immer'; import type { MenuProps } from 'antd/lib/menu'; export interface LayoutFrameInstance { readonly name; subscribe(consumer: (state: LayoutFrameState) => void): () => void; update(update: (state: LayoutFrameState) => void); getState(): LayoutFrameState; dispose(): void; } export interface LayoutFrameOptions { name?: string; menus: MenuSpec[]; link?: ({ href }) => React.ReactNode; menuProps?: Partial; } const LayoutFrameContext = React.createContext<{ layout: LayoutFrameInstance; options: LayoutFrameOptions; }>(null as any); interface TypedUseSelectorHook { ( selector: (state: TState) => TSelected, equalityFn?: (left: TSelected, right: TSelected) => boolean, ): TSelected; } export const useLayoutFrameSelector: TypedUseSelectorHook = (selector, eq?) => { const layout = useLayoutFrame(); const ref = useRef(); const [state, setState] = useState(() => (ref.current = selector(layout.getState()))); useEffect(() => { return layout.subscribe((s) => { const next = selector(s); if ((eq && !eq(next, ref.current)) || next !== ref.current) { setState((ref.current = next)); } }); }, []); return state; }; export function useLayoutFrameOptions(): LayoutFrameOptions { return useContext(LayoutFrameContext).options; } // class LayoutFrameStore implements LayoutFrameInstance { // name = 'default'; // context = LayoutStoreContext; // store: Store; // useSelector: TypedUseSelectorHook; // dispatch; // forceRootUpdate: () => void; // // constructor({ forceRootUpdate, name = 'default' }) { // this.forceRootUpdate = forceRootUpdate; // this.name = name; // // const slice = createLayoutFrameSlice(); // this.store = configureStore(slice); // this.dispatch = this.store.dispatch; // this.useSelector = createSelectorHook(this.context); // } // // selector = (s) => s; // // getLayout(): LayoutFrameInstance { // // const {name, store, context} = this; // // return { // // name, // // dispatch: store.dispatch, // // selector: s => s, // // useSelector: createSelectorHook(context), // // } // return this; // } // } export const LayoutFrameProvider: React.FC<{ layout: LayoutFrameInstance; options: LayoutFrameOptions; }> = ({ layout, options, children }) => { return {children}; }; function createLayoutFrame( options: { current?: LayoutFrameInstance; initialState?: Partial | (() => Partial); name?: string; } = {}, ): LayoutFrameInstance { const { initialState, name = 'default', current } = options; const state = new BehaviorSubject({ ...(typeof initialState === 'function' ? initialState() : initialState ?? {}), }); const layout = { get name() { return name; }, subscribe(consumer: (state: LayoutFrameState) => void): () => void { const s = state.subscribe(consumer); return s.unsubscribe.bind(s); }, getState(): LayoutFrameState { return state.value; }, update(update: ((state: LayoutFrameState) => void) | Partial) { if (typeof update !== 'function') { layout.update((s) => { Object.assign(s, update); }); return; } const current = state.value; const next = produce(current, update as (state: LayoutFrameState) => void); if (current !== next) { state.next(next); console.debug(`next layout state`, next, current); } }, dispose(): void { // }, }; return layout; } export function useLayoutFrame(options?: { layout?: LayoutFrameInstance; initialState?: Partial | (() => Partial); name?: string; }): LayoutFrameInstance { const { layout, name = 'default', initialState } = options || {}; const instanceRef = React.useRef(); const [, forceRootUpdate] = useReducer((a) => a + 1, 0); const current = useContext(LayoutFrameContext)?.layout; if (!instanceRef.current) { if (current && current.name === name) { instanceRef.current = current; } else if (layout) { instanceRef.current = layout; } else { // const layoutStore = new LayoutFrameStore({ forceRootUpdate, name }); instanceRef.current = createLayoutFrame({ name, initialState, current }); } } return instanceRef.current; }