|
1 | 1 | import React, { ReactNode, createRef } from "react"; |
2 | 2 | import { create } from "zustand"; |
3 | 3 |
|
| 4 | +export interface BaseProps { |
| 5 | + zIndex?: number; |
| 6 | + children?: ReactNode; |
| 7 | +} |
| 8 | + |
| 9 | +interface ComponentDefinition<Props extends BaseProps = BaseProps> { |
| 10 | + component: React.FC<Props>; |
| 11 | + props: Props; |
| 12 | + children?: ReactNode; |
| 13 | + key: string | number; |
| 14 | +} |
| 15 | + |
4 | 16 | interface WindowManagerStoreState { |
5 | 17 | contentRef: React.RefObject<HTMLDivElement>; |
6 | | - /** |
7 | | - * A map of all windows. |
8 | | - * The key is the type of window, and the value is another map. |
9 | | - * The key of the nested map is the ID of the window, and the value |
10 | | - * is the window JSX element. |
11 | | - */ |
12 | | - windowsMap: Map<string, Map<string, ReactNode>>; |
13 | | - getWindows: () => Array<ReactNode>; |
14 | | - addWindow: (windowType: string, windowId: string, window: ReactNode) => void; |
| 18 | + windowsMap: Map<string, Map<string, ComponentDefinition>>; |
| 19 | + highestZIndex: number; |
| 20 | + getWindowDefinitions: () => Array<ComponentDefinition>; |
| 21 | + addWindow: <Props extends BaseProps = BaseProps>( |
| 22 | + windowType: string, |
| 23 | + windowId: string, |
| 24 | + definition: ComponentDefinition<Props> |
| 25 | + ) => void; |
15 | 26 | closeWindow: (windowType: string, windowId: string) => void; |
| 27 | + focusWindowsOfType: (windowType: string) => void; |
| 28 | + focusWindow: (windowType: string, windowId: string) => void; |
| 29 | + windowsOfTypeExist: (windowType: string) => boolean; |
16 | 30 | } |
| 31 | + |
17 | 32 | const useWindowManagerStore = create<WindowManagerStoreState>()((set, get) => ({ |
18 | 33 | contentRef: createRef<HTMLDivElement>(), |
19 | 34 | windowsMap: new Map(), |
20 | | - getWindows() { |
| 35 | + highestZIndex: 0, |
| 36 | + getWindowDefinitions() { |
21 | 37 | return Array.from(get().windowsMap.values()).flatMap((map) => |
22 | 38 | Array.from(map.values()) |
23 | 39 | ); |
24 | 40 | }, |
25 | | - addWindow(windowType, windowId, window) { |
| 41 | + addWindow<Props extends BaseProps = BaseProps>( |
| 42 | + windowType: string, |
| 43 | + windowId: string, |
| 44 | + definition: ComponentDefinition<Props> |
| 45 | + ) { |
26 | 46 | const windowsMap = get().windowsMap; |
27 | 47 | const windowsOfType = windowsMap.get(windowType); |
| 48 | + const highestZIndex = get().highestZIndex + 1; |
| 49 | + definition.props.zIndex = highestZIndex; |
28 | 50 |
|
| 51 | + // TODO: Need to find a better way of dealing with this rather than risky casting |
29 | 52 | if (!windowsOfType) { |
30 | | - windowsMap.set(windowType, new Map([[windowId, window]])); |
| 53 | + windowsMap.set( |
| 54 | + windowType, |
| 55 | + new Map([[windowId, definition as unknown as ComponentDefinition]]) |
| 56 | + ); |
31 | 57 | } else { |
32 | | - windowsOfType.set(windowId, window); |
| 58 | + windowsOfType.set(windowId, definition as unknown as ComponentDefinition); |
33 | 59 | } |
34 | 60 |
|
35 | | - set({ windowsMap }); |
| 61 | + set({ windowsMap, highestZIndex }); |
36 | 62 | }, |
37 | 63 | closeWindow(windowType, windowId) { |
38 | 64 | const windowsMap = get().windowsMap; |
39 | 65 | const windowsOfType = windowsMap.get(windowType); |
40 | | - if (windowsOfType) { |
41 | | - windowsOfType.delete(windowId); |
| 66 | + |
| 67 | + // Delete the window, and if there are no windows left of this type, |
| 68 | + // remove the window type map from the master window map |
| 69 | + windowsOfType?.delete(windowId); |
| 70 | + |
| 71 | + if (windowsOfType?.size === 0) { |
| 72 | + windowsMap.delete(windowType); |
42 | 73 | } |
| 74 | + |
| 75 | + // If there are no windows left open, reset the zIndex counter |
| 76 | + let highestZIndex = get().highestZIndex; |
| 77 | + if (!windowsMap.size) highestZIndex = 1; |
| 78 | + |
| 79 | + set({ windowsMap, highestZIndex }); |
| 80 | + }, |
| 81 | + focusWindowsOfType(windowType) { |
| 82 | + const windowsMap = get().windowsMap; |
| 83 | + const windowsOfType = windowsMap.get(windowType); |
| 84 | + |
| 85 | + // If no windows of this type exist, nothing to do |
| 86 | + if (!windowsOfType?.size) return; |
| 87 | + |
| 88 | + let highestZIndex = get().highestZIndex; |
| 89 | + |
| 90 | + // There may be multiple instances of this window type open, |
| 91 | + // so we want to preserve the zindex order they have relative to each other. |
| 92 | + // To do this, we sort them so we're processing the one with the lowest zindex first |
| 93 | + Array.from(windowsOfType.values()) |
| 94 | + .sort((a, b) => (a.props.zIndex ?? 0) - (b.props.zIndex ?? 0)) |
| 95 | + .forEach((window) => (window.props.zIndex = ++highestZIndex)); |
| 96 | + |
43 | 97 | set({ windowsMap }); |
44 | 98 | }, |
| 99 | + focusWindow(windowType, windowId) { |
| 100 | + let highestZIndex = get().highestZIndex; |
| 101 | + const windowsMap = get().windowsMap; |
| 102 | + const windowsOfType = windowsMap.get(windowType); |
| 103 | + const window = windowsOfType?.get(windowId); |
| 104 | + |
| 105 | + // If the window doesnt exist, nothing to do |
| 106 | + if (!windowsOfType || !window) return; |
| 107 | + |
| 108 | + // If the window is already top most, nothing to do |
| 109 | + if (window.props.zIndex === highestZIndex) return; |
| 110 | + |
| 111 | + // Increase the zindex counter and set the window to have this zindex |
| 112 | + highestZIndex++; |
| 113 | + window.props.zIndex = highestZIndex; |
| 114 | + |
| 115 | + set({ windowsMap, highestZIndex }); |
| 116 | + }, |
| 117 | + windowsOfTypeExist(windowType) { |
| 118 | + return (get().windowsMap.get(windowType)?.size ?? 0) > 0; |
| 119 | + }, |
45 | 120 | })); |
46 | 121 |
|
47 | 122 | export default useWindowManagerStore; |
0 commit comments