# 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 |