# UILayer UIEvent ### Event Object and Layers System - systems works with tree structure of layer data - at each time exists no more than one active layer - systems can creates event for active layer and fires event with 2 phases - calls handlers on capture phase for each layer from root to active - calls handlers on bubling phase for each layer from active to root - each hadler can stop propagation (as capturing and bubbling) - layer can be enabled or disabled and that also means - if parent is disabled all his chidlren are disabled - layer is enabled only if itself is enabled and all his parents itself are enabled ```plantuml @startmindmap <style> mindmapDiagram { .layer { BackgroundColor lightgreen } .chain { BackgroundColor #FFBBCC } .active { BackgroundColor lightblue } } </style> + Layer <<chain>> ++ Pages +++ Page1 ++++ Layer <<layer>> +++++ ... +++ Page2 ++++ Layer <<layer>> +++++ SidePanel ++++++ Layer <<layer>> +++++++ ... +++++ Header ++++++ Layer <<layer>> +++++++ ... +++++ Content ++++++ Layer <<layer>> +++++++ ... ++ Popups +++ Layer <<chain>> ++++ Popup1 +++++ Layer <<layer>> ++++++ ... ++++ Popup2 +++++ "Layer Active" <<active>> ++++++ ... ++ Tooltips +++ ... @endmindmap ``` #### Public interface ```typescript= interface LayerEvent<T, TData> = { readonly eventKey: T; readonly payload: TData; readonly stopped: boolean; stopPropagation(): void; }; type LayerEventHandler<T> = (ev: LayerEvent<T>) => void|false; class Layer { // there is a node of tree structure data private parent: Layer | null = null; private readonly children: Layer[] = []; // defines if layer is enable private _selfEnable = true; setSelfEnableState(isEnable: boolean): void; // returns true if layer and all parent is '_selfEnable === true' get isEnable(): boolean; /* there is stack related data * stack===true - means all direct children are in stack * and depends on theirs 'order' and '_selfEnable' * we know who is a topest enable */ private order?: number; private stack?: boolean; // subscribe/unsubsribe for event subscribe<T>(eventKey: T, handler: LayerEventHandler<T>, capture?: boolean): void; unsubscribe<T>(eventKey: T, handler: LayerEventHandler<T>, capture?: boolean): void; emit<T>(eventKey: T, payload: TData): void; activate(): void; }; // hook to get current layer function useEventLayerContext(): Layer; // hook to create new layer (without putting to hierarchy of react components) // @obsolete function useDefineEventLayer(name?: string): Layer; // component that provides defined layer to hierarchy of react components // @obsolete const ProvideEventLayer: FC<{ children: ReactNode, eventLayer: Layer }>; // component that creates and provides layer to hierarchy of react components const EventLayer: FC<{ children: ReactNode, name?: string }> // shortcut around useEffect and 'subscribe' and 'unsubscribe' functions const useLayerEventHandler = <T>(eventKey: T, handler: LayerEventHandler<T>, dep: any[], capture?: boolean) => void; ``` #### Examples how to use ```typescript= const App: FC = () => { return <EventLayer name="GlobalStack" stack> <EventLayer name="Pages" order={0} initialEnabled> <Pages> </EventLayer> <EventLayer name="Popups" order={1}> <Popups /> </EventLayer> <EventLayer name="Tooltips" order={2}> <Tooltips /> </EventLayer> </EventLayer> }; const Popups: FC = () => { const popupService = useBetween(usePopupService); // enable/disable current layer depends on popups count const layer = useEventLayerContext(); layer.setSelfEnableState(popupService.stack.length > 0); }; const LeftPanel: FC = () => { // define and provide manually const layer = useDefineEventLayer("LeftPanel"); // add/remove EventListener manually useEffect(() => { return layer.subscribe('select', () => onSelect); }, [onSelect]); return <ProvideEventLayer eventLayer={layer}> ... </ProvideEventLayer>; } const RightPanel: FC = () => { // use layer provided before const layer = useEventLayerContext(); useEffect(() => { const onKeyDown = (event: KeyboardEvent) => { if (layer.isEnable) { // .... event.stopPropagation(); } }; window.addEventListener('keydown', onKeyDown); return () => { window.removeEventListener('keydown', onKeyDown); }; }, []); return <div> ... </div> } const SomeApp: FC = () => { // declarative way to define layers return <EventLayer name="Root"> <LeftPanel/> <EventLayer name="RightPanel"> {(layer) => { return <RightPanel/> }} </EventLayer> </EventLayer> } const MenuItem: FC = () => { // create interactive element const layer = useEventLayerContext(); useEffect(() => { return layer.subscribe('select', () => onSelect); }, [onSelect]); return <div ref={ref as any} onClick={()=>{ layer.activate(); layer.emit('select'); }}>Button</div>; }; ``` ### Event ```typescript= // auto (un/re)subsribe useLayerEventHandler( 'EVENT_POPUP_CANCEL', (e) => { close(); e.stopPropagation(); }, [close] ); // manually const layer = useEventLayerContext(); useEffect(() => { const handler: EventHandler<'EVENT_POPUP_CANCEL'> = (e) => { close(); e.stopPropagation(); }; layer.subscribe('EVENT_POPUP_CANCEL', handler); return () => { layer.unsubscribe('EVENT_POPUP_CANCEL', handler); }; }, [close]); /* - how to subsribe to root layer (aka window.addEventListener) ? - how to separate cases to handle (or not) events in non active layer? */ ``` //:::spoiler obsolete #### Events abstractions ```plantuml @startuml skinparam componentStyle rectangle component NativeEvents { component "mouse(click.down.up)" component "keybord(click.down.up)" component "gamepad()" } component BusinessEvents { component back component acept } component ComponentInterationApi { component click } NativeEvents <-- BusinessEvents BusinessEvents <-- ComponentInterationApi @enduml ``` ```typescript= function useHandlingService(events: { [customAction:string]: EvendHandler }, options?: { disabled?: boolean; }): void {...} const UncontrolledNavigation = ()=> { // finds layers in children compoenents and finds whois next and prev visually using Dom API const navigator = useNavigationVisualByClassName({loop: true, trigger:'focus'}) useHandlingService({ 'next': (ev)=> { navigator.next(); }, 'prev': (ev)=> { navigator.prev(); }, }, { disabled: false, }); useNativeEvent({ 'keyboard:left': 'prev', 'keyboard:right': 'next', 'gamepad:button1': 'next', 'gamepad:button2': 'next', }) return <EventLayer customEvents={{ 'next': (ev)=> { navigator.next(); }, 'prev': (ev)=> { navigator.prev(); }, }} nativeEventMap={{ 'keyboard:left': 'prev', 'keyboard:right': 'next', 'gamepad:button1': 'next', 'gamepad:button2': 'next', }} disabled=false> <MenuItem>Loadout</MenuItem> <MenuItem>Datacubes</MenuItem> <MenuItem>Jobs</MenuItem> <MenuItem>Hideout</MenuItem> <MenuItem>Stats</MenuItem> </div> } const MenuItem = ()=> { useHandlingService({ 'submit': (ev)=> { router.navigateTo(...); }, }); useNativeEvent({ 'keyboard:enter': 'submit', 'gamepad:button3': 'submit', 'mouse:click': 'submit', 'mouse:hover': 'focus', }) const {isFocused, isSubmitting} = useHandlingState(['isFocused', 'isSubmitting']); const classes = classNames({ 'selected': isFocused, 'active': isSubmitting, }); return <navigable-item> <div className={classes}>{children}</div> </navigable-item> } ``` ```typescript= const ControlledNavigation = ()=> { // finds layers in children compoenents and finds whois next and prev visually using Dom API const [selectedIndex, setSelectedIndex] = useState(0); useHandlingService({ 'next': (ev)=> { setSelectedIndex((selectedIndex+1)%selectedIndex) }, 'prev': (ev)=> { setSelectedIndex((2*selectedIndex-1)%selectedIndex) }, }, { disabled: false, }); useNativeEvent({ 'keyboard:left': 'prev', 'keyboard:right': 'next', 'gamepad:button1': 'next', 'gamepad:button2': 'next', }) const tabs = ['Loadout','Datacubes','Jobs','Hideout','Stats'] return <div> {tabs.map((tab, index)=>{ return <MenuItem key={index} selected={selectedIndex=index}>{tab} </MenuItem> })} </div> } const MenuItem = ({selected})=> { useHandlingService({ 'submit': (ev)=> { router.navigateTo(...); }, }); useNativeEvent({ 'keyboard:enter': 'submit', 'gamepad:button3': 'submit', 'mouse:click': 'submit', 'mouse:hover': 'focus', }) const {isFocused, isSubmitting} = useHandlingState(['isFocused', 'isSubmitting']); const classes = classNames({ 'selected': isFocused || selected, 'active': isSubmitting, }); return <div className={classes}>{children}</div> } ``` ```plantuml @startuml group useEventLayerContext ComponentA -> LayerContext: getContext LayerContext --> ComponentA: return Layer1 end ComponentA -> Layer1: emit action 'A' group useEventLayerContext ComponentB -> LayerContext: getContext LayerContext --> ComponentB: return Layer2 end ComponentB -> Layer2: subsribe @enduml ``` ::: | Index | Button .pressed Code | Button on Xbox | Button on PlayStation | |-------|-----------------------------|---------------------|-----------------------| | 0 | gamepad.buttons[0].pressed | A | X | | 1 | gamepad.buttons[1].pressed | B | O | | 2 | gamepad.buttons[2].pressed | X | Square | | 3 | gamepad.buttons[3].pressed | Y | Triangle | | 4 | gamepad.buttons[4].pressed | LB | L1 | | 5 | gamepad.buttons[5].pressed | RB | R1 | | 6 | gamepad.buttons[6].pressed | LT | L2 | | 7 | gamepad.buttons[7].pressed | RT | R2 | | 8 | gamepad.buttons[8].pressed | Show Address Bar | Share | | 9 | gamepad.buttons[9].pressed | Show Menu | Options | | 10 | gamepad.buttons[10].pressed | Left Stick Pressed | Left Stick Pressed | | 11 | gamepad.buttons[11].pressed | Right Stick Pressed | Right Stick Pressed | | 12 | gamepad.buttons[12].pressed | Directional Up | Directional Up | | 13 | gamepad.buttons[13].pressed | Directional Down | Directional Down | | 14 | gamepad.buttons[14].pressed | Directional Left | Directional Left | | 15 | gamepad.buttons[15].pressed | Directional Right | Directional Right | | 16 | gamepad.buttons[16].pressed | Xbox Light-Up Logo | PlayStation Logo |