/
ModalManager.ts
143 lines (121 loc) · 3.6 KB
/
ModalManager.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
import {
App,
defineComponent,
h,
inject,
InjectionKey,
shallowReactive,
} from "vue";
import {
ModalComponentNotProvidedError,
ModalManagerInjectionError,
} from "./errors";
import { freezeBody, unfreezeBody } from "./freeze";
import { ModalComponent, ModalInstance, ModalKey, ModalTypes } from "./types";
import { incrementor } from "./utils";
export class ModalManager<
Types extends ModalTypes<Key> = ModalTypes<ModalKey>,
Key extends ModalKey = keyof Types
> {
readonly #id = incrementor();
readonly #stack = shallowReactive<ModalInstance<Types, Key>[]>([]);
readonly #components = new Map<Key, ModalComponent>();
/**
* The stack of modal instances.
*/
get stack(): readonly Readonly<ModalInstance<Types, Key>>[] {
return this.#stack;
}
/**
* The top of stack, which means current rendered modal.
*/
get top(): Readonly<ModalInstance<Types, Key>> | null {
return this.stack.slice(-1).pop() ?? null;
}
/**
* Register a modal component and associate it with key to call it.
* @param key Modal key
* @param component Modal component
*/
addComponent(key: Key, component: ModalComponent): void {
this.#components.set(key, component);
}
private createModalInstance<K extends Key>(
key: K,
args: Types[K]
): ModalInstance<Types, Key> {
const component = this.#components.get(key);
if (!component) {
throw new ModalComponentNotProvidedError(key);
}
const instanceId = `VueModal[${this.#id.next().value}]::${key.toString()}`;
const namedComponent = defineComponent({
name: instanceId,
setup(props, { attrs }) {
return () => h(component, attrs);
},
});
// TODO: Capture current focused element to restore focusing when closing the modal.
const instance: ModalInstance<Types, Key> = {
key,
instanceId,
component: namedComponent,
args,
};
return instance;
}
/**
* Create new modal instance and push it into the stack. If there are some instances in the stack, new modal will be instead of currently displayed.
* @param key Modal key
* @param args A value passed to the modal component as `args` prop
* @returns Pushed modal instance
* @throws {ModalComponentNotProvidedError} The modal component specified with `key` was not provided.
*/
push<K extends Key>(key: K, args: Types[K]): ModalInstance<Types, Key> {
const instance = this.createModalInstance(key, args);
this.#stack.push(instance);
if (this.stack.length === 1) {
freezeBody();
}
return instance;
}
/**
* Remove the modal currently rendered. If it is remained some modal instances in the stack, the next one is rendered.
* @returns Popped modal instance
*/
pop(): ModalInstance<Types, Key> | null {
const popped = this.#stack.pop();
if (!popped) {
return null;
}
if (this.stack.length === 0) {
unfreezeBody();
}
return popped;
}
/**
* Wipe all modal instances, resulting no modals will be rendered.
*/
flush(): void {
this.#stack.splice(0, Infinity);
unfreezeBody();
}
install(app: App): void {
app.provide(injectionKey, this);
}
}
const injectionKey: InjectionKey<ModalManager> = Symbol();
/**
* Get ModalManager instance provided in current context.
* @returns ModalManager instance
* @throws {ModalManagerInjectionError} Injection failure
*/
export const useModal = <
Types extends ModalTypes<keyof Types>
>(): ModalManager<Types> => {
const manager = inject<ModalManager<Types>>(injectionKey);
if (!manager) {
throw new ModalManagerInjectionError();
}
return manager;
};