vanilla-jui 1.4.5 → 1.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/index.d.ts +1273 -1896
- package/dist/index.js +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/style.css +1 -1
- package/package.json +21 -2
- package/src/components/accordion.ts +297 -216
- package/src/components/flow.ts +222 -258
- package/src/components/form.ts +44 -102
- package/src/components/menu.ts +268 -356
- package/src/components/modal.ts +181 -641
- package/src/components/offcanvas.ts +225 -106
- package/src/components/pagination.ts +296 -326
- package/src/components/sticky.ts +55 -54
- package/src/components/swiper.ts +237 -247
- package/src/components/tabs.ts +278 -271
- package/src/components/toc.ts +42 -46
- package/src/core/Component.ts +69 -47
- package/src/css/accordion.css +2 -2
- package/src/css/animation.css +1 -5
- package/src/css/avatar.css +1 -1
- package/src/css/badge.css +1 -1
- package/src/css/breadcrumb.css +9 -10
- package/src/css/button.css +1 -1
- package/src/css/card.css +2 -2
- package/src/css/color.css +974 -1002
- package/src/css/color_p3.css +974 -1004
- package/src/css/components.css +1 -1
- package/src/css/content.css +72 -63
- package/src/css/drop.css +1 -1
- package/src/css/editor.css +14 -14
- package/src/css/flow.css +4 -14
- package/src/css/font.css +5 -4
- package/src/css/form.css +76 -52
- package/src/css/gradient.css +299 -222
- package/src/css/icon.css +57 -57
- package/src/css/index.css +1 -1
- package/src/css/layout.css +13 -10
- package/src/css/loading.css +60 -12
- package/src/css/menu.css +262 -188
- package/src/css/modal.css +1 -1
- package/src/css/offcanvas.css +9 -13
- package/src/css/pagination.css +5 -1
- package/src/css/popup.css +1 -1
- package/src/css/skeleton.css +3 -2
- package/src/css/status.css +2 -3
- package/src/css/swiper.css +1 -1
- package/src/css/table.css +1 -1
- package/src/css/tabs.css +12 -11
- package/src/css/tag.css +1 -1
- package/src/css/theme-palette.css +19 -5
- package/src/css/themes/default/accordion.css +61 -61
- package/src/css/themes/default/avatar.css +54 -54
- package/src/css/themes/default/badge.css +62 -62
- package/src/css/themes/default/breadcrumb.css +23 -23
- package/src/css/themes/default/button.css +203 -203
- package/src/css/themes/default/content.css +53 -53
- package/src/css/themes/default/drop.css +10 -10
- package/src/css/themes/default/editor.css +9 -9
- package/src/css/themes/default/index.css +1 -1
- package/src/css/themes/default/loading.css +26 -20
- package/src/css/themes/default/menu.css +75 -11
- package/src/css/themes/default/modal.css +32 -36
- package/src/css/themes/default/pagination.css +7 -7
- package/src/css/themes/default/popup.css +6 -6
- package/src/css/themes/default/swiper.css +32 -32
- package/src/css/themes/default/table.css +16 -16
- package/src/css/themes/default/tabs.css +30 -30
- package/src/css/themes/default/tag.css +71 -71
- package/src/css/themes/default/theme-palette.css +11 -11
- package/src/css/themes/default/tip.css +42 -42
- package/src/css/themes/default/toast.css +60 -60
- package/src/css/themes/default/toolbar.css +23 -29
- package/src/css/themes/default/tooltip.css +43 -13
- package/src/css/themes/index.css +1 -1
- package/src/css/tip.css +2 -2
- package/src/css/toast.css +3 -3
- package/src/css/tokens.css +12 -17
- package/src/css/toolbar.css +9 -10
- package/src/css/tooltip.css +3 -2
- package/src/index.ts +22 -19
- package/src/{components → primitives}/drop.ts +58 -49
- package/src/{components → primitives}/icons.ts +1 -1
- package/src/primitives/loading.ts +23 -0
- package/src/primitives/parabola.ts +388 -0
- package/src/primitives/popup.ts +61 -0
- package/src/{components → primitives}/theme.ts +89 -57
- package/src/{components → primitives}/toast.ts +18 -25
- package/src/{components → primitives}/tooltip.ts +100 -25
- package/src/utilities/browser.ts +23 -49
- package/src/utilities/class-name.ts +5 -0
- package/src/utilities/dom.ts +14 -32
- package/src/utilities/function.ts +4 -0
- package/src/utilities/http.ts +7 -7
- package/src/utilities/id.ts +57 -0
- package/src/utilities/object.ts +5 -0
- package/src/utilities/timer.ts +38 -0
- package/src/utilities/types.ts +420 -0
- package/src/{components → validation}/validator.ts +64 -53
- package/src/components/parabola.ts +0 -335
- package/src/utilities/cache.ts +0 -57
- package/src/utilities/core.ts +0 -614
- package/src/utilities/storage.ts +0 -46
package/src/components/modal.ts
CHANGED
|
@@ -8,63 +8,55 @@ import {
|
|
|
8
8
|
render,
|
|
9
9
|
untrack,
|
|
10
10
|
} from 'vanilla-signal';
|
|
11
|
+
import { t } from 'vanilla-signal-i18n';
|
|
11
12
|
|
|
12
13
|
import Component, {
|
|
13
14
|
type ComponentDOM,
|
|
14
15
|
type ComponentRuntime,
|
|
15
|
-
type ComponentUpdateOptions,
|
|
16
16
|
} from '../core/Component.ts';
|
|
17
|
-
import
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
resolveProps,
|
|
23
|
-
validateParam,
|
|
24
|
-
} from '../utilities/core.ts';
|
|
17
|
+
import locales from '../locales/index.ts';
|
|
18
|
+
import { icon } from '../primitives/icons.ts';
|
|
19
|
+
import { createLoading } from '../primitives/loading.ts';
|
|
20
|
+
import { createPopup } from '../primitives/popup.ts';
|
|
21
|
+
import { joinClasses } from '../utilities/class-name.ts';
|
|
25
22
|
import {
|
|
26
23
|
all,
|
|
27
24
|
q,
|
|
28
|
-
createLoading,
|
|
29
|
-
isRenderableContent,
|
|
30
25
|
normalizeContentNodes,
|
|
31
26
|
type RenderableContent,
|
|
32
27
|
} from '../utilities/dom.ts';
|
|
28
|
+
import { randomId } from '../utilities/id.ts';
|
|
29
|
+
import { isPlainObject } from '../utilities/object.ts';
|
|
30
|
+
import {
|
|
31
|
+
type ResolveSchema,
|
|
32
|
+
resolveProps,
|
|
33
|
+
validateParam,
|
|
34
|
+
} from '../utilities/types.ts';
|
|
33
35
|
import {
|
|
34
|
-
|
|
36
|
+
createForm,
|
|
35
37
|
type FormDataRecord,
|
|
36
38
|
type FormField,
|
|
37
39
|
type FormProps,
|
|
38
40
|
} from './form.ts';
|
|
39
|
-
import { icon } from './icons.ts';
|
|
40
41
|
|
|
41
42
|
const HIDE_DURATION = 300;
|
|
42
43
|
|
|
43
|
-
type ModalStyle =
|
|
44
|
-
| string
|
|
45
|
-
| Record<string, string | number | null | undefined>
|
|
46
|
-
| null;
|
|
47
44
|
type ModalTextInput = Partial<ModalText> & Record<string, unknown>;
|
|
48
45
|
type ModalContent = RenderableContent<Modal>;
|
|
49
|
-
type
|
|
46
|
+
type FormInstance = ReturnType<typeof createForm>;
|
|
47
|
+
export type ModalMode = 'content' | 'form';
|
|
50
48
|
|
|
51
49
|
export interface ModalClassNames {
|
|
52
50
|
layout: string;
|
|
53
51
|
modal: string;
|
|
54
|
-
fullscreen: string;
|
|
55
52
|
header: string;
|
|
56
53
|
body: string;
|
|
57
54
|
footer: string;
|
|
58
55
|
title: string;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
56
|
+
closeBtn: string;
|
|
57
|
+
cancelBtn: string;
|
|
58
|
+
confirmBtn: string;
|
|
62
59
|
button: string;
|
|
63
|
-
buttonIcon: string;
|
|
64
|
-
buttonGhost: string;
|
|
65
|
-
buttonPrimary: string;
|
|
66
|
-
buttonSmall: string;
|
|
67
|
-
formContainer: string;
|
|
68
60
|
}
|
|
69
61
|
|
|
70
62
|
export type ModalClassNameConfig = Partial<ModalClassNames>;
|
|
@@ -75,55 +67,13 @@ export interface ModalText {
|
|
|
75
67
|
cancel: string;
|
|
76
68
|
}
|
|
77
69
|
|
|
78
|
-
interface FlowStep {
|
|
79
|
-
id?: string;
|
|
80
|
-
title?: RenderableContent<Modal>;
|
|
81
|
-
content?: ModalContent;
|
|
82
|
-
modal?: ModalFlowView | ModalFlowViewFactory;
|
|
83
|
-
view?: ModalFlowView | ModalFlowViewFactory;
|
|
84
|
-
[key: string]: unknown;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
interface FlowSnapshot {
|
|
88
|
-
data?: Record<string, unknown> | null;
|
|
89
|
-
currentData?: Record<string, unknown> | null;
|
|
90
|
-
currentStep?: FlowStep;
|
|
91
|
-
[key: string]: unknown;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
interface FlowLike {
|
|
95
|
-
currentStep?: FlowStep;
|
|
96
|
-
next: (
|
|
97
|
-
payload: FormDataRecord | null
|
|
98
|
-
) => Promise<FlowSnapshot> | FlowSnapshot;
|
|
99
|
-
back: (
|
|
100
|
-
payload: FormDataRecord | null
|
|
101
|
-
) => Promise<FlowSnapshot> | FlowSnapshot;
|
|
102
|
-
snapshot: () => FlowSnapshot;
|
|
103
|
-
reset?: () => void;
|
|
104
|
-
destroy?: () => void;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
interface ModalFlowView extends Partial<ModalProps> {
|
|
108
|
-
[key: string]: unknown;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
type ModalFlowViewFactory = (context: {
|
|
112
|
-
flow: FlowLike;
|
|
113
|
-
snapshot: FlowSnapshot | null;
|
|
114
|
-
step: FlowStep | undefined;
|
|
115
|
-
modal: Modal;
|
|
116
|
-
data: Record<string, unknown> | null;
|
|
117
|
-
currentData: Record<string, unknown> | null;
|
|
118
|
-
}) => unknown;
|
|
119
|
-
|
|
120
70
|
export interface ModalProps extends Record<string, unknown> {
|
|
71
|
+
mode?: ModalMode | null;
|
|
121
72
|
content?: ModalContent;
|
|
122
73
|
position?: string;
|
|
123
74
|
showCancel?: boolean;
|
|
124
75
|
showClose?: boolean;
|
|
125
76
|
fullscreen?: boolean;
|
|
126
|
-
flow?: FlowLike | null;
|
|
127
77
|
text?: ModalTextInput;
|
|
128
78
|
onShow?: ((modal: Modal) => void | Promise<void>) | null;
|
|
129
79
|
onShown?: ((modal: Modal) => void | Promise<void>) | null;
|
|
@@ -137,21 +87,19 @@ export interface ModalProps extends Record<string, unknown> {
|
|
|
137
87
|
fields?: readonly FormField[] | null;
|
|
138
88
|
header?: boolean;
|
|
139
89
|
footer?: boolean;
|
|
140
|
-
style?: ModalStyle;
|
|
141
90
|
id?: string | null;
|
|
142
91
|
escClose?: boolean;
|
|
143
92
|
bgClose?: boolean;
|
|
144
|
-
lazy?: boolean;
|
|
145
93
|
className?: ModalClassNameConfig;
|
|
146
94
|
}
|
|
147
95
|
|
|
148
96
|
interface ResolvedModalProps extends Record<string, unknown> {
|
|
97
|
+
mode: ModalMode;
|
|
149
98
|
content: ModalContent;
|
|
150
99
|
position: string;
|
|
151
100
|
showCancel: boolean;
|
|
152
101
|
showClose: boolean;
|
|
153
102
|
fullscreen: boolean;
|
|
154
|
-
flow: FlowLike | null;
|
|
155
103
|
text: ModalText;
|
|
156
104
|
onShow: NonNullable<ModalProps['onShow']> | null;
|
|
157
105
|
onShown: NonNullable<ModalProps['onShown']> | null;
|
|
@@ -163,17 +111,18 @@ interface ResolvedModalProps extends Record<string, unknown> {
|
|
|
163
111
|
fields: FormField[] | null;
|
|
164
112
|
header: boolean;
|
|
165
113
|
footer: boolean;
|
|
166
|
-
style: ModalStyle;
|
|
167
114
|
id: string;
|
|
168
115
|
escClose: boolean;
|
|
169
116
|
bgClose: boolean;
|
|
170
|
-
lazy: boolean;
|
|
171
117
|
className: ModalClassNames;
|
|
172
118
|
}
|
|
173
119
|
|
|
174
|
-
interface ModalState extends
|
|
120
|
+
interface ModalState extends Record<string, unknown> {
|
|
121
|
+
mode: ModalMode;
|
|
122
|
+
content: ModalContent;
|
|
123
|
+
fields: FormField[] | null;
|
|
175
124
|
loading: boolean;
|
|
176
|
-
|
|
125
|
+
processing: boolean;
|
|
177
126
|
visible: boolean;
|
|
178
127
|
data: FormDataRecord | null;
|
|
179
128
|
extraData: FormDataRecord | null;
|
|
@@ -185,7 +134,7 @@ interface ModalDOM extends ComponentDOM {
|
|
|
185
134
|
header: HTMLElement | null;
|
|
186
135
|
body: HTMLElement | null;
|
|
187
136
|
footer: HTMLElement | null;
|
|
188
|
-
form:
|
|
137
|
+
form: FormInstance | null;
|
|
189
138
|
formContainer: HTMLElement | null;
|
|
190
139
|
}
|
|
191
140
|
|
|
@@ -197,7 +146,6 @@ interface ModalRuntime extends ComponentRuntime {
|
|
|
197
146
|
interface ModalCache {
|
|
198
147
|
initial: ResolvedModalProps | null;
|
|
199
148
|
fieldIds: Map<string, string> | null;
|
|
200
|
-
baseStyle: string;
|
|
201
149
|
previousActiveElement: HTMLElement | null;
|
|
202
150
|
formId: string;
|
|
203
151
|
}
|
|
@@ -208,26 +156,19 @@ interface ModalCleanupExtras {
|
|
|
208
156
|
hideTimer?: ReturnType<typeof setTimeout> | null;
|
|
209
157
|
}
|
|
210
158
|
|
|
211
|
-
type ModalPatch = Partial<ModalProps>;
|
|
212
159
|
type ModalStatePatch = Partial<ModalState>;
|
|
213
160
|
|
|
214
161
|
const DEFAULT_CLASS_NAMES: ModalClassNames = {
|
|
215
162
|
layout: 'j-popup-layout',
|
|
216
163
|
modal: 'j-modal',
|
|
217
|
-
fullscreen: 'is-fullscreen',
|
|
218
164
|
header: 'modal-header',
|
|
219
165
|
body: 'modal-body',
|
|
220
166
|
footer: 'modal-footer',
|
|
221
167
|
title: 'modal-title',
|
|
222
|
-
close: 'modal-close',
|
|
223
|
-
cancel: 'modal-cancel',
|
|
224
|
-
confirm: 'modal-confirm',
|
|
225
168
|
button: 'j-button',
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
buttonSmall: 'is-sm',
|
|
230
|
-
formContainer: 'modal-form-container',
|
|
169
|
+
closeBtn: 'is-icon is-sm is-ghost',
|
|
170
|
+
cancelBtn: 'is-ghost',
|
|
171
|
+
confirmBtn: 'is-primary',
|
|
231
172
|
};
|
|
232
173
|
|
|
233
174
|
function cloneFields(
|
|
@@ -254,66 +195,13 @@ function cloneProps(props: ResolvedModalProps): ResolvedModalProps {
|
|
|
254
195
|
};
|
|
255
196
|
}
|
|
256
197
|
|
|
257
|
-
function isUpdateProps(value: unknown): value is ModalPatch {
|
|
258
|
-
return isPlainObject(value);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function isFlowLike(value: unknown): value is FlowLike | null {
|
|
262
|
-
return (
|
|
263
|
-
value == null ||
|
|
264
|
-
(typeof value === 'object' &&
|
|
265
|
-
typeof (value as Partial<FlowLike>).next === 'function' &&
|
|
266
|
-
typeof (value as Partial<FlowLike>).back === 'function' &&
|
|
267
|
-
typeof (value as Partial<FlowLike>).snapshot === 'function')
|
|
268
|
-
);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function hydrateFields(
|
|
272
|
-
fields: readonly FormField[] | null,
|
|
273
|
-
data: Record<string, unknown> | null | undefined
|
|
274
|
-
): FormField[] | null {
|
|
275
|
-
if (!Array.isArray(fields) || !isPlainObject(data))
|
|
276
|
-
return fields ? cloneFields(fields) : null;
|
|
277
|
-
|
|
278
|
-
return fields.map((field) => {
|
|
279
|
-
if (!field?.name || !hasOwn(data, field.name)) return { ...field };
|
|
280
|
-
const value = data[field.name];
|
|
281
|
-
|
|
282
|
-
if (field.type === 'checkbox' || field.type === 'radio') {
|
|
283
|
-
const optionValue = field.value ?? 'on';
|
|
284
|
-
const checked = Array.isArray(value)
|
|
285
|
-
? value.map(String).includes(String(optionValue))
|
|
286
|
-
: value === true || String(value) === String(optionValue);
|
|
287
|
-
return { ...field, checked };
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
return {
|
|
291
|
-
...field,
|
|
292
|
-
value: Array.isArray(value)
|
|
293
|
-
? value.map(String)
|
|
294
|
-
: (value as FormField['value']),
|
|
295
|
-
};
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function createTextState(
|
|
300
|
-
props: Partial<ModalProps> | Partial<ResolvedModalProps>
|
|
301
|
-
): ModalText {
|
|
302
|
-
const text = isPlainObject(props?.text) ? props.text : {};
|
|
303
|
-
return {
|
|
304
|
-
title: typeof text.title === 'string' ? text.title : 'Tip',
|
|
305
|
-
confirm: typeof text.confirm === 'string' ? text.confirm : 'Confirm',
|
|
306
|
-
cancel: typeof text.cancel === 'string' ? text.cancel : 'Cancel',
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
|
|
310
198
|
function createModalState(props: ResolvedModalProps): ModalState {
|
|
311
199
|
return {
|
|
312
|
-
|
|
200
|
+
mode: props.mode,
|
|
201
|
+
content: props.content,
|
|
313
202
|
fields: Array.isArray(props.fields) ? cloneFields(props.fields) : null,
|
|
314
|
-
text: createTextState(props),
|
|
315
203
|
loading: false,
|
|
316
|
-
|
|
204
|
+
processing: false,
|
|
317
205
|
visible: false,
|
|
318
206
|
data: null,
|
|
319
207
|
extraData: null,
|
|
@@ -328,33 +216,16 @@ function mergeExtraData(
|
|
|
328
216
|
return Object.assign(data, extraData);
|
|
329
217
|
}
|
|
330
218
|
|
|
331
|
-
function clonePlainObject(value: unknown): ModalFlowView {
|
|
332
|
-
return isPlainObject(value) ? { ...value } : {};
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
function joinClasses(
|
|
336
|
-
...classes: Array<string | false | null | undefined>
|
|
337
|
-
): string {
|
|
338
|
-
return classes.filter(Boolean).join(' ');
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
function textValue(value: RenderableContent<Modal> | undefined): string {
|
|
342
|
-
return typeof value === 'string' || typeof value === 'number'
|
|
343
|
-
? String(value)
|
|
344
|
-
: '';
|
|
345
|
-
}
|
|
346
|
-
|
|
347
219
|
const MODAL_CONTENT_RULE = {
|
|
348
|
-
|
|
349
|
-
message: 'expects string, number, Node, array, function or null.',
|
|
220
|
+
type: 'renderable',
|
|
350
221
|
};
|
|
351
222
|
|
|
352
223
|
const MODAL_TEXT_RULE = {
|
|
353
224
|
default: {},
|
|
354
|
-
|
|
355
|
-
|
|
225
|
+
type: 'object',
|
|
226
|
+
plain: true,
|
|
356
227
|
normalize: (value: unknown) => {
|
|
357
|
-
const text = isPlainObject(value) ? value : {};
|
|
228
|
+
const text = (isPlainObject(value) ? value : {}) as ModalTextInput;
|
|
358
229
|
return {
|
|
359
230
|
...text,
|
|
360
231
|
title: typeof text.title === 'string' ? text.title : 'Tip',
|
|
@@ -366,16 +237,20 @@ const MODAL_TEXT_RULE = {
|
|
|
366
237
|
|
|
367
238
|
const MODAL_FIELDS_RULE = { types: ['array', 'null'] };
|
|
368
239
|
const MODAL_PROPS_SCHEMA = {
|
|
240
|
+
mode: {
|
|
241
|
+
default: null,
|
|
242
|
+
types: ['string', 'null'],
|
|
243
|
+
enum: ['content', 'form', null],
|
|
244
|
+
normalize: (value: unknown, context) => {
|
|
245
|
+
if (value != null) return value;
|
|
246
|
+
return Array.isArray(context.options.fields) ? 'form' : 'content';
|
|
247
|
+
},
|
|
248
|
+
},
|
|
369
249
|
content: { default: '', ...MODAL_CONTENT_RULE },
|
|
370
250
|
position: { default: 'center', type: 'string' },
|
|
371
251
|
showCancel: { default: true, type: 'boolean' },
|
|
372
252
|
showClose: { default: true, type: 'boolean' },
|
|
373
253
|
fullscreen: { default: false, type: 'boolean' },
|
|
374
|
-
flow: {
|
|
375
|
-
default: null,
|
|
376
|
-
validate: isFlowLike,
|
|
377
|
-
message: 'expects a Flow instance or null.',
|
|
378
|
-
},
|
|
379
254
|
text: MODAL_TEXT_RULE,
|
|
380
255
|
onShow: { default: null, types: ['function', 'null'] },
|
|
381
256
|
onShown: { default: null, types: ['function', 'null'] },
|
|
@@ -387,7 +262,6 @@ const MODAL_PROPS_SCHEMA = {
|
|
|
387
262
|
fields: { default: null, ...MODAL_FIELDS_RULE },
|
|
388
263
|
header: { default: true, type: 'boolean' },
|
|
389
264
|
footer: { default: true, type: 'boolean' },
|
|
390
|
-
style: { default: null, types: ['string', 'null', 'object'] },
|
|
391
265
|
id: {
|
|
392
266
|
default: null,
|
|
393
267
|
types: ['string', 'null'],
|
|
@@ -402,7 +276,6 @@ const MODAL_PROPS_SCHEMA = {
|
|
|
402
276
|
},
|
|
403
277
|
escClose: { default: false, type: 'boolean' },
|
|
404
278
|
bgClose: { default: false, type: 'boolean' },
|
|
405
|
-
lazy: { default: false, type: 'boolean' },
|
|
406
279
|
className: {
|
|
407
280
|
default: DEFAULT_CLASS_NAMES,
|
|
408
281
|
type: 'object',
|
|
@@ -414,36 +287,29 @@ const MODAL_PROPS_SCHEMA = {
|
|
|
414
287
|
} satisfies ResolveSchema<ModalProps>;
|
|
415
288
|
|
|
416
289
|
const MODAL_STATE_SCHEMA = {
|
|
417
|
-
|
|
290
|
+
mode: { type: 'string', enum: ['content', 'form'] },
|
|
291
|
+
content: MODAL_PROPS_SCHEMA.content,
|
|
292
|
+
fields: MODAL_PROPS_SCHEMA.fields,
|
|
418
293
|
loading: { default: false, type: 'boolean' },
|
|
419
|
-
|
|
294
|
+
processing: { default: false, type: 'boolean' },
|
|
420
295
|
visible: { default: false, type: 'boolean' },
|
|
421
296
|
data: { default: null, types: ['object', 'null'] },
|
|
422
297
|
extraData: { default: null, types: ['object', 'null'] },
|
|
423
298
|
};
|
|
424
299
|
|
|
425
|
-
const MODAL_UPDATE_BLOCKED_KEYS = new Set(['id', 'lazy']);
|
|
426
|
-
|
|
427
|
-
const MODAL_EXTRA_FIELDS_RULE = {
|
|
428
|
-
validate: (value: unknown) =>
|
|
429
|
-
!!value && typeof value === 'object' && !Array.isArray(value),
|
|
430
|
-
message: 'expects an object.',
|
|
431
|
-
};
|
|
432
|
-
|
|
433
300
|
const MODAL_UPDATE_RULE = {
|
|
434
|
-
|
|
435
|
-
message: 'expects a props object.',
|
|
301
|
+
type: 'plainObject',
|
|
436
302
|
};
|
|
437
303
|
|
|
438
304
|
function normalizeProps(input: ModalProps): ResolvedModalProps {
|
|
439
305
|
const props = resolveProps(input, MODAL_PROPS_SCHEMA, 'Modal');
|
|
440
306
|
return {
|
|
307
|
+
mode: props.mode as ModalMode,
|
|
441
308
|
content: props.content as ModalContent,
|
|
442
309
|
position: props.position as string,
|
|
443
310
|
showCancel: props.showCancel as boolean,
|
|
444
311
|
showClose: props.showClose as boolean,
|
|
445
312
|
fullscreen: props.fullscreen as boolean,
|
|
446
|
-
flow: props.flow as FlowLike | null,
|
|
447
313
|
text: props.text as ModalText,
|
|
448
314
|
onShow: props.onShow as ResolvedModalProps['onShow'],
|
|
449
315
|
onShown: props.onShown as ResolvedModalProps['onShown'],
|
|
@@ -457,11 +323,9 @@ function normalizeProps(input: ModalProps): ResolvedModalProps {
|
|
|
457
323
|
: null,
|
|
458
324
|
header: props.header as boolean,
|
|
459
325
|
footer: props.footer as boolean,
|
|
460
|
-
style: props.style as ModalStyle,
|
|
461
326
|
id: props.id as string,
|
|
462
327
|
escClose: props.escClose as boolean,
|
|
463
328
|
bgClose: props.bgClose as boolean,
|
|
464
|
-
lazy: props.lazy as boolean,
|
|
465
329
|
className: props.className as ModalClassNames,
|
|
466
330
|
};
|
|
467
331
|
}
|
|
@@ -469,11 +333,15 @@ function normalizeProps(input: ModalProps): ResolvedModalProps {
|
|
|
469
333
|
let modalScrollLockCount = 0;
|
|
470
334
|
let modalBodyOverflow = '';
|
|
471
335
|
|
|
472
|
-
|
|
336
|
+
class ModalComponent extends Component<
|
|
337
|
+
ResolvedModalProps,
|
|
338
|
+
ModalState,
|
|
339
|
+
ModalDOM,
|
|
340
|
+
ModalCache
|
|
341
|
+
> {
|
|
473
342
|
declare runtime: ModalRuntime;
|
|
474
343
|
declare state: ModalState;
|
|
475
344
|
declare cleanup: Component['cleanup'] & ModalCleanupExtras;
|
|
476
|
-
cache: ModalCache;
|
|
477
345
|
|
|
478
346
|
constructor(input: ModalProps = {}) {
|
|
479
347
|
const props = normalizeProps(input);
|
|
@@ -496,7 +364,6 @@ export class Modal extends Component<ResolvedModalProps, ModalState, ModalDOM> {
|
|
|
496
364
|
this.cache = {
|
|
497
365
|
initial: cloneProps(this.props),
|
|
498
366
|
fieldIds: new Map(),
|
|
499
|
-
baseStyle: '',
|
|
500
367
|
previousActiveElement: null,
|
|
501
368
|
formId: `${this.props.id}_form`,
|
|
502
369
|
};
|
|
@@ -508,33 +375,30 @@ export class Modal extends Component<ResolvedModalProps, ModalState, ModalDOM> {
|
|
|
508
375
|
|
|
509
376
|
protected override onInit(): void {
|
|
510
377
|
this.bindReactiveVisibility();
|
|
511
|
-
if (!this.state.lazy) this.buildRoot();
|
|
512
378
|
}
|
|
513
379
|
|
|
514
|
-
|
|
515
|
-
|
|
380
|
+
build(): this {
|
|
381
|
+
this.assertActive('build');
|
|
382
|
+
if (this.dom.root instanceof HTMLElement) return this;
|
|
516
383
|
|
|
517
|
-
const { id, className } = this.
|
|
384
|
+
const { id, className } = this.props;
|
|
518
385
|
const dialogChildren = [
|
|
519
386
|
jsx('div', {
|
|
520
387
|
className: className.header,
|
|
521
|
-
|
|
522
|
-
style: () => ({ display: this.state.header ? '' : 'none' }),
|
|
388
|
+
style: { display: this.props.header ? '' : 'none' },
|
|
523
389
|
ref: (element: HTMLElement) => {
|
|
524
390
|
this.dom.header = element;
|
|
525
391
|
},
|
|
526
392
|
}),
|
|
527
393
|
jsx('div', {
|
|
528
394
|
className: className.body,
|
|
529
|
-
'data-modal-body': id,
|
|
530
395
|
ref: (element: HTMLElement) => {
|
|
531
396
|
this.dom.body = element;
|
|
532
397
|
},
|
|
533
398
|
}),
|
|
534
399
|
jsx('div', {
|
|
535
400
|
className: className.footer,
|
|
536
|
-
|
|
537
|
-
style: () => ({ display: this.state.footer ? '' : 'none' }),
|
|
401
|
+
style: { display: this.props.footer ? '' : 'none' },
|
|
538
402
|
ref: (element: HTMLElement) => {
|
|
539
403
|
this.dom.footer = element;
|
|
540
404
|
},
|
|
@@ -544,35 +408,32 @@ export class Modal extends Component<ResolvedModalProps, ModalState, ModalDOM> {
|
|
|
544
408
|
const modal = jsx('div', {
|
|
545
409
|
className: () =>
|
|
546
410
|
joinClasses(
|
|
547
|
-
this.
|
|
548
|
-
this.
|
|
411
|
+
this.props.className.modal,
|
|
412
|
+
this.props.fullscreen && 'is-fullscreen'
|
|
549
413
|
),
|
|
550
414
|
id,
|
|
551
415
|
role: 'document',
|
|
552
416
|
'data-modal-dialog': id,
|
|
553
417
|
ref: (element: HTMLElement) => {
|
|
554
418
|
this.dom.modal = element;
|
|
555
|
-
this.applyStyle(element, this.state.style);
|
|
556
419
|
},
|
|
557
420
|
children: dialogChildren,
|
|
558
421
|
}) as HTMLElement;
|
|
559
422
|
|
|
560
|
-
const root =
|
|
561
|
-
className:
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
'aria-label'
|
|
569
|
-
|
|
570
|
-
children: modal,
|
|
571
|
-
}) as HTMLElement;
|
|
423
|
+
const root = createPopup({
|
|
424
|
+
className: className.layout,
|
|
425
|
+
position: this.props.position,
|
|
426
|
+
component: 'modal',
|
|
427
|
+
labelledby: this.props.header ? `${id}_title` : '',
|
|
428
|
+
content: modal,
|
|
429
|
+
});
|
|
430
|
+
if (!this.props.header) {
|
|
431
|
+
root.setAttribute('aria-label', this.props.text.title || 'Modal');
|
|
432
|
+
}
|
|
572
433
|
|
|
573
|
-
this.root = root;
|
|
434
|
+
this.dom.root = root;
|
|
574
435
|
this.mountView();
|
|
575
|
-
return
|
|
436
|
+
return this;
|
|
576
437
|
}
|
|
577
438
|
|
|
578
439
|
mountView(): void {
|
|
@@ -584,7 +445,10 @@ export class Modal extends Component<ResolvedModalProps, ModalState, ModalDOM> {
|
|
|
584
445
|
onCleanup(headerDispose);
|
|
585
446
|
}
|
|
586
447
|
|
|
587
|
-
if (this.dom.body)
|
|
448
|
+
if (this.dom.body) {
|
|
449
|
+
const bodyDispose = render(() => this.bodyView(), this.dom.body);
|
|
450
|
+
onCleanup(bodyDispose);
|
|
451
|
+
}
|
|
588
452
|
|
|
589
453
|
if (this.dom.footer) {
|
|
590
454
|
const footerDispose = render(() => this.footerView(), this.dom.footer);
|
|
@@ -592,34 +456,30 @@ export class Modal extends Component<ResolvedModalProps, ModalState, ModalDOM> {
|
|
|
592
456
|
}
|
|
593
457
|
|
|
594
458
|
this.bindReactiveLoading();
|
|
595
|
-
this.bindReactiveStyle();
|
|
596
459
|
return dispose;
|
|
597
460
|
});
|
|
598
461
|
}
|
|
599
462
|
|
|
600
463
|
headerView(): () => (Node | null)[] | null {
|
|
601
464
|
return () => {
|
|
602
|
-
if (!this.
|
|
465
|
+
if (!this.props.header) return null;
|
|
603
466
|
|
|
604
467
|
return [
|
|
605
468
|
jsx('div', {
|
|
606
|
-
className: this.
|
|
607
|
-
id: `${this.
|
|
608
|
-
'
|
|
609
|
-
children:
|
|
469
|
+
className: this.props.className.title,
|
|
470
|
+
id: `${this.props.id}_title`,
|
|
471
|
+
'aria-label': this.props.text.title,
|
|
472
|
+
children: this.props.text.title,
|
|
610
473
|
}) as Node,
|
|
611
|
-
this.
|
|
474
|
+
this.props.showClose
|
|
612
475
|
? (jsx('button', {
|
|
613
476
|
type: 'button',
|
|
614
477
|
className: joinClasses(
|
|
615
|
-
this.
|
|
616
|
-
this.
|
|
617
|
-
this.state.className.buttonGhost,
|
|
618
|
-
this.state.className.buttonSmall,
|
|
619
|
-
this.state.className.close
|
|
478
|
+
this.props.className.button,
|
|
479
|
+
this.props.className.closeBtn
|
|
620
480
|
),
|
|
621
481
|
'data-action': 'close',
|
|
622
|
-
'aria-label': '
|
|
482
|
+
'aria-label': t('Close', locales),
|
|
623
483
|
children: icon('close'),
|
|
624
484
|
}) as Node)
|
|
625
485
|
: null,
|
|
@@ -629,49 +489,44 @@ export class Modal extends Component<ResolvedModalProps, ModalState, ModalDOM> {
|
|
|
629
489
|
|
|
630
490
|
footerView(): () => (Node | null)[] | null {
|
|
631
491
|
return () => {
|
|
632
|
-
if (!this.
|
|
492
|
+
if (!this.props.footer) return null;
|
|
633
493
|
|
|
634
494
|
return [
|
|
635
|
-
this.
|
|
495
|
+
this.props.showCancel
|
|
636
496
|
? (jsx('button', {
|
|
637
497
|
type: 'button',
|
|
638
498
|
className: joinClasses(
|
|
639
|
-
this.
|
|
640
|
-
this.
|
|
641
|
-
this.state.className.cancel
|
|
499
|
+
this.props.className.button,
|
|
500
|
+
this.props.className.cancelBtn
|
|
642
501
|
),
|
|
643
|
-
'data-action': '
|
|
644
|
-
'aria-label': 'close',
|
|
502
|
+
'data-action': 'cancel',
|
|
645
503
|
disabled: () => this.isBusy(),
|
|
646
|
-
children:
|
|
504
|
+
children: this.props.text.cancel,
|
|
647
505
|
}) as Node)
|
|
648
506
|
: null,
|
|
649
507
|
jsx('button', {
|
|
650
|
-
type: () => (this.
|
|
651
|
-
form: () => (this.isFormMode() ? this.cache.formId : null),
|
|
508
|
+
type: () => (this.state.mode === 'form' ? 'submit' : 'button'),
|
|
652
509
|
className: joinClasses(
|
|
653
|
-
this.
|
|
654
|
-
this.
|
|
655
|
-
this.state.className.confirm
|
|
510
|
+
this.props.className.button,
|
|
511
|
+
this.props.className.confirmBtn
|
|
656
512
|
),
|
|
657
|
-
'data-action': () =>
|
|
513
|
+
'data-action': () =>
|
|
514
|
+
this.state.mode === 'form' ? 'submit' : 'confirm',
|
|
658
515
|
disabled: () => this.isBusy(),
|
|
659
|
-
children:
|
|
516
|
+
children: this.props.text.confirm,
|
|
660
517
|
}) as Node,
|
|
661
518
|
];
|
|
662
519
|
};
|
|
663
520
|
}
|
|
664
521
|
|
|
665
522
|
bodyView(): RenderableContent<Modal> {
|
|
666
|
-
if (this.
|
|
523
|
+
if (this.state.mode === 'form') return this.formView();
|
|
667
524
|
this.destroyForm();
|
|
668
525
|
return this.contentView(this.state.content);
|
|
669
526
|
}
|
|
670
527
|
|
|
671
528
|
formView(): HTMLElement {
|
|
672
529
|
return jsx('div', {
|
|
673
|
-
className: this.state.className.formContainer,
|
|
674
|
-
'data-modal-form-container': this.state.id,
|
|
675
530
|
ref: (element: HTMLElement) => {
|
|
676
531
|
this.mountForm(element);
|
|
677
532
|
},
|
|
@@ -684,11 +539,18 @@ export class Modal extends Component<ResolvedModalProps, ModalState, ModalDOM> {
|
|
|
684
539
|
|
|
685
540
|
const props = this.createFormProps();
|
|
686
541
|
if (this.dom.form) {
|
|
687
|
-
this.dom.form.
|
|
542
|
+
this.dom.form.setFields(props.fields || []);
|
|
543
|
+
if (
|
|
544
|
+
this.dom.form.dom.root &&
|
|
545
|
+
!container.contains(this.dom.form.dom.root)
|
|
546
|
+
) {
|
|
547
|
+
container.appendChild(this.dom.form.dom.root);
|
|
548
|
+
}
|
|
688
549
|
return;
|
|
689
550
|
}
|
|
690
551
|
|
|
691
|
-
this.dom.form =
|
|
552
|
+
this.dom.form = createForm(props).build();
|
|
553
|
+
if (this.dom.form.dom.root) container.appendChild(this.dom.form.dom.root);
|
|
692
554
|
}
|
|
693
555
|
|
|
694
556
|
createFormProps(): FormProps {
|
|
@@ -729,144 +591,8 @@ export class Modal extends Component<ResolvedModalProps, ModalState, ModalDOM> {
|
|
|
729
591
|
});
|
|
730
592
|
}
|
|
731
593
|
|
|
732
|
-
bindReactiveStyle(): void {
|
|
733
|
-
createEffect(() => {
|
|
734
|
-
const style = this.state.style;
|
|
735
|
-
if (this.dom.modal) this.applyStyle(this.dom.modal, style);
|
|
736
|
-
});
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
isFormMode(): boolean {
|
|
740
|
-
return Array.isArray(this.state.fields);
|
|
741
|
-
}
|
|
742
|
-
|
|
743
594
|
isBusy(): boolean {
|
|
744
|
-
return !!(this.state.loading || this.state.
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
validatePropsPatch(patch: ModalPatch, namespace = 'Modal.update'): void {
|
|
748
|
-
validateParam('props', patch, MODAL_UPDATE_RULE, namespace);
|
|
749
|
-
|
|
750
|
-
for (const key of Object.keys(patch)) {
|
|
751
|
-
if (!hasOwn(MODAL_PROPS_SCHEMA, key)) {
|
|
752
|
-
throw new Error(
|
|
753
|
-
`Validator: ${namespace}.${key} is not a supported modal prop.`
|
|
754
|
-
);
|
|
755
|
-
}
|
|
756
|
-
if (MODAL_UPDATE_BLOCKED_KEYS.has(key)) {
|
|
757
|
-
throw new Error(
|
|
758
|
-
`Modal.update: "${key}" cannot be updated after initialization.`
|
|
759
|
-
);
|
|
760
|
-
}
|
|
761
|
-
validateParam(
|
|
762
|
-
key,
|
|
763
|
-
patch[key],
|
|
764
|
-
MODAL_PROPS_SCHEMA[key as keyof typeof MODAL_PROPS_SCHEMA],
|
|
765
|
-
namespace
|
|
766
|
-
);
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
applyProps(
|
|
771
|
-
patch: ModalPatch,
|
|
772
|
-
{
|
|
773
|
-
validate = true,
|
|
774
|
-
force = false,
|
|
775
|
-
}: { validate?: boolean; force?: boolean } = {}
|
|
776
|
-
): this {
|
|
777
|
-
if (validate) this.validatePropsPatch(patch);
|
|
778
|
-
if (!patch || Object.keys(patch).length === 0) return this;
|
|
779
|
-
|
|
780
|
-
const hasFields = hasOwn(patch, 'fields');
|
|
781
|
-
const hasContent = hasOwn(patch, 'content');
|
|
782
|
-
|
|
783
|
-
if (hasContent && !hasFields && this.isFormMode() && !force) {
|
|
784
|
-
throw new Error(
|
|
785
|
-
'Modal.update: Cannot update content when fields are defined.'
|
|
786
|
-
);
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
const nextInput: ModalProps = {
|
|
790
|
-
...(this.props as ModalProps),
|
|
791
|
-
...patch,
|
|
792
|
-
text: (hasOwn(patch, 'text')
|
|
793
|
-
? { ...this.props.text, ...patch.text }
|
|
794
|
-
: this.props.text) as ModalTextInput,
|
|
795
|
-
className: hasOwn(patch, 'className')
|
|
796
|
-
? { ...this.props.className, ...patch.className }
|
|
797
|
-
: this.props.className,
|
|
798
|
-
};
|
|
799
|
-
const nextProps = normalizeProps(nextInput);
|
|
800
|
-
|
|
801
|
-
this.props = nextProps;
|
|
802
|
-
super.update(patch as Partial<ResolvedModalProps>, { force });
|
|
803
|
-
this.props = nextProps;
|
|
804
|
-
|
|
805
|
-
const statePatch: ModalStatePatch = {};
|
|
806
|
-
if (hasFields) {
|
|
807
|
-
this.cache.fieldIds?.clear();
|
|
808
|
-
statePatch.fields = Array.isArray(patch.fields)
|
|
809
|
-
? cloneFields(patch.fields)
|
|
810
|
-
: null;
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
for (const [key, value] of Object.entries(patch)) {
|
|
814
|
-
if (key === 'fields' || key === 'text' || key === 'className') continue;
|
|
815
|
-
statePatch[key as keyof ModalStatePatch] =
|
|
816
|
-
value as ModalStatePatch[keyof ModalStatePatch];
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
if (hasOwn(patch, 'text')) {
|
|
820
|
-
statePatch.text = createTextState(nextProps);
|
|
821
|
-
}
|
|
822
|
-
if (hasOwn(patch, 'className')) {
|
|
823
|
-
statePatch.className = nextProps.className;
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
flushSync(() => {
|
|
827
|
-
for (const [key, value] of Object.entries(statePatch)) {
|
|
828
|
-
this.state[key as keyof ModalState] =
|
|
829
|
-
value as ModalState[keyof ModalState];
|
|
830
|
-
}
|
|
831
|
-
});
|
|
832
|
-
|
|
833
|
-
if (hasOwn(patch, 'style') && this.dom.modal) {
|
|
834
|
-
this.applyStyle(this.dom.modal, this.state.style);
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
if (
|
|
838
|
-
this.state.visible &&
|
|
839
|
-
(hasOwn(patch, 'bgClose') || hasOwn(patch, 'escClose'))
|
|
840
|
-
) {
|
|
841
|
-
this.bindEvents(this.root);
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
if (this.dom.form && hasFields) {
|
|
845
|
-
this.dom.form.update(this.createFormProps());
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
return this;
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
applyStyle(element: HTMLElement, style: ModalStyle): void {
|
|
852
|
-
element.removeAttribute('style');
|
|
853
|
-
if (!style) {
|
|
854
|
-
this.cache.baseStyle = '';
|
|
855
|
-
return;
|
|
856
|
-
}
|
|
857
|
-
if (typeof style === 'string') {
|
|
858
|
-
element.style.cssText = style;
|
|
859
|
-
this.cache.baseStyle = element.getAttribute('style') || '';
|
|
860
|
-
return;
|
|
861
|
-
}
|
|
862
|
-
Object.entries(style).forEach(([key, value]) => {
|
|
863
|
-
if (value == null) return;
|
|
864
|
-
const name = key.startsWith('--')
|
|
865
|
-
? key
|
|
866
|
-
: key.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
|
|
867
|
-
element.style.setProperty(name, String(value));
|
|
868
|
-
});
|
|
869
|
-
this.cache.baseStyle = element.getAttribute('style') || '';
|
|
595
|
+
return !!(this.state.loading || this.state.processing);
|
|
870
596
|
}
|
|
871
597
|
|
|
872
598
|
bindReactiveVisibility(): void {
|
|
@@ -893,33 +619,35 @@ export class Modal extends Component<ResolvedModalProps, ModalState, ModalDOM> {
|
|
|
893
619
|
throw new Error('Modal: The current instance has been destroyed.');
|
|
894
620
|
}
|
|
895
621
|
|
|
896
|
-
if (!this.root)
|
|
622
|
+
if (!this.dom.root) {
|
|
623
|
+
throw new Error('Modal.show: build() must be called before show().');
|
|
624
|
+
}
|
|
897
625
|
|
|
898
626
|
this.cancelHideTimer();
|
|
899
627
|
this.resetAnimationStyles();
|
|
900
628
|
|
|
901
|
-
const { onShow, onShown } = this.
|
|
629
|
+
const { onShow, onShown } = this.props;
|
|
902
630
|
void onShow?.(this);
|
|
903
631
|
|
|
904
632
|
this.cache.previousActiveElement =
|
|
905
633
|
document.activeElement instanceof HTMLElement
|
|
906
634
|
? document.activeElement
|
|
907
635
|
: null;
|
|
908
|
-
if (this.root && !this.root.parentNode)
|
|
909
|
-
document.body.appendChild(this.root);
|
|
636
|
+
if (this.dom.root && !this.dom.root.parentNode)
|
|
637
|
+
document.body.appendChild(this.dom.root);
|
|
910
638
|
this.lockScroll();
|
|
911
639
|
this.runtime.visibleApplied = true;
|
|
912
640
|
|
|
913
|
-
this.bindEvents(this.root);
|
|
641
|
+
this.bindEvents(this.dom.root);
|
|
914
642
|
this.focusFirst();
|
|
915
643
|
|
|
916
644
|
void onShown?.(this);
|
|
917
645
|
}
|
|
918
646
|
|
|
919
647
|
hideFromState(): void {
|
|
920
|
-
if (!this.runtime.visibleApplied || !this.root) return;
|
|
648
|
+
if (!this.runtime.visibleApplied || !this.dom.root) return;
|
|
921
649
|
|
|
922
|
-
const { onHide, onHidden } = this.
|
|
650
|
+
const { onHide, onHidden } = this.props;
|
|
923
651
|
void onHide?.(this);
|
|
924
652
|
|
|
925
653
|
this.runtime.visibleApplied = false;
|
|
@@ -954,7 +682,7 @@ export class Modal extends Component<ResolvedModalProps, ModalState, ModalDOM> {
|
|
|
954
682
|
bindOverlayCloseEvent(root: Element | null): void {
|
|
955
683
|
if (!root) return;
|
|
956
684
|
this.cleanup.events.on('bg', root, 'click', (event) => {
|
|
957
|
-
if (this.
|
|
685
|
+
if (this.props.bgClose && event.target === root) {
|
|
958
686
|
this.hide();
|
|
959
687
|
}
|
|
960
688
|
});
|
|
@@ -964,7 +692,7 @@ export class Modal extends Component<ResolvedModalProps, ModalState, ModalDOM> {
|
|
|
964
692
|
this.cleanup.events.on('keydown', document, 'keydown', (event) => {
|
|
965
693
|
if (!this.state.visible || !(event instanceof KeyboardEvent)) return;
|
|
966
694
|
|
|
967
|
-
if (event.key === 'Escape' && this.
|
|
695
|
+
if (event.key === 'Escape' && this.props.escClose) {
|
|
968
696
|
event.preventDefault();
|
|
969
697
|
this.hide();
|
|
970
698
|
return;
|
|
@@ -993,18 +721,8 @@ export class Modal extends Component<ResolvedModalProps, ModalState, ModalDOM> {
|
|
|
993
721
|
return;
|
|
994
722
|
}
|
|
995
723
|
|
|
996
|
-
if (action === 'back') {
|
|
997
|
-
void this.handleBack();
|
|
998
|
-
return;
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
if (action === 'next') {
|
|
1002
|
-
void this.handleNext();
|
|
1003
|
-
return;
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
724
|
if (action === 'confirm') {
|
|
1007
|
-
if (this.
|
|
725
|
+
if (this.state.mode === 'form') {
|
|
1008
726
|
this.requestSubmit();
|
|
1009
727
|
return;
|
|
1010
728
|
}
|
|
@@ -1018,7 +736,7 @@ export class Modal extends Component<ResolvedModalProps, ModalState, ModalDOM> {
|
|
|
1018
736
|
}
|
|
1019
737
|
|
|
1020
738
|
requestSubmit(): void {
|
|
1021
|
-
if (!this.dom.form) {
|
|
739
|
+
if (this.state.mode !== 'form' || !this.dom.form) {
|
|
1022
740
|
void this.handleConfirm();
|
|
1023
741
|
return;
|
|
1024
742
|
}
|
|
@@ -1038,138 +756,21 @@ export class Modal extends Component<ResolvedModalProps, ModalState, ModalDOM> {
|
|
|
1038
756
|
await this.handleSubmit(data);
|
|
1039
757
|
}
|
|
1040
758
|
|
|
1041
|
-
async handleNext(): Promise<void> {
|
|
1042
|
-
if (this.isBusy()) return;
|
|
1043
|
-
if (!this.hasFlow()) return;
|
|
1044
|
-
await this.moveFlow('next');
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
async handleBack(): Promise<void> {
|
|
1048
|
-
if (this.isBusy()) return;
|
|
1049
|
-
if (!this.hasFlow()) return;
|
|
1050
|
-
await this.moveFlow('back');
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
hasFlow(): this is this & { state: ModalState & { flow: FlowLike } } {
|
|
1054
|
-
return isFlowLike(this.state.flow) && !!this.state.flow;
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
async moveFlow(direction: FlowDirection): Promise<void> {
|
|
1058
|
-
if (!this.hasFlow()) return;
|
|
1059
|
-
const payload = this.createFlowPayload();
|
|
1060
|
-
if (payload === false) return;
|
|
1061
|
-
|
|
1062
|
-
flushSync(() => {
|
|
1063
|
-
this.state.submitting = true;
|
|
1064
|
-
});
|
|
1065
|
-
|
|
1066
|
-
try {
|
|
1067
|
-
const snapshot = await Promise.resolve(
|
|
1068
|
-
this.state.flow[direction](payload)
|
|
1069
|
-
);
|
|
1070
|
-
this.syncFlowView(this.state.flow, snapshot);
|
|
1071
|
-
} catch (error) {
|
|
1072
|
-
console.error(`Modal.flow.${direction} error:`, error);
|
|
1073
|
-
this.syncFlowView(this.state.flow);
|
|
1074
|
-
} finally {
|
|
1075
|
-
if (this.state) {
|
|
1076
|
-
flushSync(() => {
|
|
1077
|
-
this.state.submitting = false;
|
|
1078
|
-
});
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
createFlowPayload(): FormDataRecord | null | false {
|
|
1084
|
-
if (!this.isFormMode()) return null;
|
|
1085
|
-
|
|
1086
|
-
const form = this.dom.form;
|
|
1087
|
-
if (!form) return this.state.data || null;
|
|
1088
|
-
if (!form.validate()) return false;
|
|
1089
|
-
|
|
1090
|
-
const data = mergeExtraData(form.collectData(), this.state.extraData);
|
|
1091
|
-
flushSync(() => {
|
|
1092
|
-
this.state.data = data;
|
|
1093
|
-
});
|
|
1094
|
-
|
|
1095
|
-
return this.state.data;
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
resolveFlowModalView(
|
|
1099
|
-
flow: FlowLike,
|
|
1100
|
-
snapshot: FlowSnapshot | null,
|
|
1101
|
-
step?: FlowStep
|
|
1102
|
-
): ModalFlowView {
|
|
1103
|
-
const source = step?.modal ?? step?.view;
|
|
1104
|
-
if (typeof source === 'function') {
|
|
1105
|
-
const result = source({
|
|
1106
|
-
flow,
|
|
1107
|
-
snapshot,
|
|
1108
|
-
step,
|
|
1109
|
-
modal: this,
|
|
1110
|
-
data: snapshot?.data ?? null,
|
|
1111
|
-
currentData: snapshot?.currentData ?? null,
|
|
1112
|
-
});
|
|
1113
|
-
return isPlainObject(result) ? result : {};
|
|
1114
|
-
}
|
|
1115
|
-
return clonePlainObject(source);
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
syncFlowView(
|
|
1119
|
-
flow: FlowLike | null,
|
|
1120
|
-
snapshot: FlowSnapshot | null = null
|
|
1121
|
-
): void {
|
|
1122
|
-
if (!flow || !this.state) return;
|
|
1123
|
-
const state = flow.snapshot() || snapshot;
|
|
1124
|
-
const step =
|
|
1125
|
-
flow.currentStep || state?.currentStep || snapshot?.currentStep;
|
|
1126
|
-
const modalView = this.resolveFlowModalView(flow, state, step);
|
|
1127
|
-
const nextText = isPlainObject(modalView.text) ? modalView.text : {};
|
|
1128
|
-
const shouldInjectStepTitle =
|
|
1129
|
-
(!hasOwn(modalView, 'text') || !hasOwn(nextText, 'title')) &&
|
|
1130
|
-
step?.title != null;
|
|
1131
|
-
|
|
1132
|
-
const patch: ModalPatch = {
|
|
1133
|
-
...(shouldInjectStepTitle
|
|
1134
|
-
? {
|
|
1135
|
-
text: {
|
|
1136
|
-
...nextText,
|
|
1137
|
-
title: textValue(step.title),
|
|
1138
|
-
},
|
|
1139
|
-
}
|
|
1140
|
-
: {}),
|
|
1141
|
-
...(!hasOwn(modalView, 'content') && !hasOwn(modalView, 'fields')
|
|
1142
|
-
? { content: step?.content ?? null }
|
|
1143
|
-
: {}),
|
|
1144
|
-
...modalView,
|
|
1145
|
-
};
|
|
1146
|
-
|
|
1147
|
-
if (Array.isArray(patch.fields)) {
|
|
1148
|
-
patch.fields = hydrateFields(patch.fields, state?.currentData);
|
|
1149
|
-
}
|
|
1150
|
-
if (!hasOwn(patch, 'fields') && hasOwn(patch, 'content')) {
|
|
1151
|
-
patch.fields = null;
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
this.update(patch);
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
759
|
async handleConfirm(): Promise<void> {
|
|
1158
760
|
if (this.isBusy()) return;
|
|
1159
761
|
|
|
1160
762
|
flushSync(() => {
|
|
1161
|
-
this.state.
|
|
763
|
+
this.state.processing = true;
|
|
1162
764
|
});
|
|
1163
765
|
|
|
1164
766
|
try {
|
|
1165
|
-
await Promise.resolve(this.
|
|
1166
|
-
this.hide();
|
|
767
|
+
await Promise.resolve(this.props.onConfirm?.(this));
|
|
1167
768
|
} catch (error) {
|
|
1168
769
|
console.error('Modal.onConfirm error:', error);
|
|
1169
770
|
} finally {
|
|
1170
771
|
if (this.state) {
|
|
1171
772
|
flushSync(() => {
|
|
1172
|
-
this.state.
|
|
773
|
+
this.state.processing = false;
|
|
1173
774
|
});
|
|
1174
775
|
}
|
|
1175
776
|
}
|
|
@@ -1179,39 +780,39 @@ export class Modal extends Component<ResolvedModalProps, ModalState, ModalDOM> {
|
|
|
1179
780
|
if (this.isBusy()) return;
|
|
1180
781
|
|
|
1181
782
|
flushSync(() => {
|
|
1182
|
-
this.state.
|
|
783
|
+
this.state.processing = true;
|
|
1183
784
|
});
|
|
1184
785
|
|
|
1185
786
|
try {
|
|
1186
|
-
await Promise.resolve(this.
|
|
787
|
+
await Promise.resolve(this.props.onCancel?.(this));
|
|
1187
788
|
this.hide();
|
|
1188
789
|
} catch (error) {
|
|
1189
790
|
console.error('Modal.onCancel error:', error);
|
|
1190
791
|
} finally {
|
|
1191
792
|
if (this.state) {
|
|
1192
793
|
flushSync(() => {
|
|
1193
|
-
this.state.
|
|
794
|
+
this.state.processing = false;
|
|
1194
795
|
});
|
|
1195
796
|
}
|
|
1196
797
|
}
|
|
1197
798
|
}
|
|
1198
799
|
|
|
1199
800
|
async handleSubmit(data: FormDataRecord): Promise<void> {
|
|
1200
|
-
if (!this.
|
|
801
|
+
if (!this.props.onSubmit) return;
|
|
1201
802
|
|
|
1202
803
|
flushSync(() => {
|
|
1203
|
-
this.state.
|
|
804
|
+
this.state.processing = true;
|
|
1204
805
|
});
|
|
1205
806
|
|
|
1206
807
|
try {
|
|
1207
|
-
await Promise.resolve(this.
|
|
808
|
+
await Promise.resolve(this.props.onSubmit(data, this));
|
|
1208
809
|
this.state.extraData = null;
|
|
1209
810
|
} catch (error) {
|
|
1210
811
|
console.error('Modal.onSubmit error:', error);
|
|
1211
812
|
} finally {
|
|
1212
813
|
if (this.state) {
|
|
1213
814
|
flushSync(() => {
|
|
1214
|
-
this.state.
|
|
815
|
+
this.state.processing = false;
|
|
1215
816
|
});
|
|
1216
817
|
}
|
|
1217
818
|
}
|
|
@@ -1289,14 +890,14 @@ export class Modal extends Component<ResolvedModalProps, ModalState, ModalDOM> {
|
|
|
1289
890
|
|
|
1290
891
|
resetAnimationStyles(): void {
|
|
1291
892
|
if (!this.dom.modal) return;
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
893
|
+
this.dom.modal.style.removeProperty('transition');
|
|
894
|
+
this.dom.modal.style.removeProperty('opacity');
|
|
895
|
+
this.dom.modal.style.removeProperty('transform');
|
|
1295
896
|
}
|
|
1296
897
|
|
|
1297
898
|
finishHide(onHidden: ResolvedModalProps['onHidden']): void {
|
|
1298
899
|
this.cleanup.hideTimer = null;
|
|
1299
|
-
this.root?.remove();
|
|
900
|
+
this.dom.root?.remove();
|
|
1300
901
|
this.resetAnimationStyles();
|
|
1301
902
|
this.unlockScroll();
|
|
1302
903
|
this.restoreFocus();
|
|
@@ -1323,22 +924,27 @@ export class Modal extends Component<ResolvedModalProps, ModalState, ModalDOM> {
|
|
|
1323
924
|
}
|
|
1324
925
|
}
|
|
1325
926
|
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
927
|
+
protected override normalizeStatePatch(
|
|
928
|
+
patch: ModalStatePatch
|
|
929
|
+
): ModalStatePatch {
|
|
930
|
+
const nextPatch: ModalStatePatch = { ...patch };
|
|
931
|
+
if (Object.hasOwn(nextPatch, 'fields')) {
|
|
932
|
+
nextPatch.fields = Array.isArray(nextPatch.fields)
|
|
933
|
+
? cloneFields(nextPatch.fields)
|
|
934
|
+
: null;
|
|
1329
935
|
}
|
|
936
|
+
return nextPatch;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
protected override validateStatePatch(patch: ModalStatePatch): void {
|
|
940
|
+
validateParam('state', patch, MODAL_UPDATE_RULE, 'Modal.setState');
|
|
1330
941
|
|
|
1331
942
|
for (const key of Object.keys(patch)) {
|
|
1332
|
-
if (!hasOwn(MODAL_STATE_SCHEMA, key)) {
|
|
943
|
+
if (!Object.hasOwn(MODAL_STATE_SCHEMA, key)) {
|
|
1333
944
|
throw new Error(
|
|
1334
945
|
`Modal.setState: "${key}" is not a supported state key.`
|
|
1335
946
|
);
|
|
1336
947
|
}
|
|
1337
|
-
if (MODAL_UPDATE_BLOCKED_KEYS.has(key)) {
|
|
1338
|
-
throw new Error(
|
|
1339
|
-
`Modal.setState: "${key}" cannot be updated after initialization.`
|
|
1340
|
-
);
|
|
1341
|
-
}
|
|
1342
948
|
validateParam(
|
|
1343
949
|
key,
|
|
1344
950
|
patch[key as keyof ModalStatePatch],
|
|
@@ -1348,119 +954,52 @@ export class Modal extends Component<ResolvedModalProps, ModalState, ModalDOM> {
|
|
|
1348
954
|
}
|
|
1349
955
|
}
|
|
1350
956
|
|
|
1351
|
-
override
|
|
1352
|
-
|
|
1353
|
-
this.
|
|
1354
|
-
|
|
1355
|
-
flushSync(() => {
|
|
1356
|
-
for (const [key, value] of Object.entries(patch)) {
|
|
1357
|
-
if (key === 'fields') this.cache.fieldIds?.clear();
|
|
1358
|
-
this.state[key as keyof ModalState] =
|
|
1359
|
-
key === 'fields' && Array.isArray(value)
|
|
1360
|
-
? (cloneFields(
|
|
1361
|
-
value as FormField[]
|
|
1362
|
-
) as ModalState[keyof ModalState])
|
|
1363
|
-
: (value as ModalState[keyof ModalState]);
|
|
1364
|
-
}
|
|
1365
|
-
});
|
|
1366
|
-
return this;
|
|
957
|
+
protected override afterSetState(patch: ModalStatePatch): void {
|
|
958
|
+
if (Object.hasOwn(patch, 'fields')) this.cache.fieldIds?.clear();
|
|
959
|
+
if (this.state.mode === 'form' && this.dom.form)
|
|
960
|
+
this.dom.form.setFields(this.createFormProps().fields || []);
|
|
1367
961
|
}
|
|
1368
962
|
|
|
1369
963
|
show(): this {
|
|
1370
964
|
this.assertActive('show');
|
|
1371
|
-
|
|
1372
|
-
|
|
965
|
+
if (!this.dom.root) {
|
|
966
|
+
throw new Error('Modal.show: build() must be called before show().');
|
|
967
|
+
}
|
|
968
|
+
this.setState({
|
|
969
|
+
visible: true,
|
|
1373
970
|
});
|
|
1374
971
|
return this;
|
|
1375
972
|
}
|
|
1376
973
|
|
|
1377
974
|
hide(): this {
|
|
1378
975
|
this.assertActive('hide');
|
|
1379
|
-
|
|
1380
|
-
|
|
976
|
+
this.setState({
|
|
977
|
+
visible: false,
|
|
1381
978
|
});
|
|
1382
979
|
return this;
|
|
1383
980
|
}
|
|
1384
981
|
|
|
1385
|
-
setFields(data: readonly FormField[] | null, force = false): this {
|
|
1386
|
-
validateParam('data', data, MODAL_FIELDS_RULE, 'Modal.setFields');
|
|
1387
|
-
this.applyProps({ fields: data }, { validate: false, force });
|
|
1388
|
-
return this;
|
|
1389
|
-
}
|
|
1390
|
-
|
|
1391
|
-
addFields(data: FormDataRecord): this {
|
|
1392
|
-
validateParam('data', data, MODAL_EXTRA_FIELDS_RULE, 'Modal.addFields');
|
|
1393
|
-
flushSync(() => {
|
|
1394
|
-
this.state.extraData = data;
|
|
1395
|
-
});
|
|
1396
|
-
return this;
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
setContent(content: ModalContent, force = false): this {
|
|
1400
|
-
validateParam('content', content, MODAL_CONTENT_RULE, 'Modal.setContent');
|
|
1401
|
-
|
|
1402
|
-
if (this.isFormMode() && !force) {
|
|
1403
|
-
throw new Error(
|
|
1404
|
-
'Modal.setContent: Cannot setContent when fields are defined.'
|
|
1405
|
-
);
|
|
1406
|
-
}
|
|
1407
|
-
|
|
1408
|
-
this.applyProps({ content }, { validate: false, force });
|
|
1409
|
-
return this;
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
override update(
|
|
1413
|
-
patch?: Partial<ResolvedModalProps> | null,
|
|
1414
|
-
options?: ComponentUpdateOptions
|
|
1415
|
-
): this;
|
|
1416
|
-
update(patch?: ModalPatch | null, force?: boolean): this;
|
|
1417
|
-
update(
|
|
1418
|
-
patch: ModalPatch | Partial<ResolvedModalProps> | null = {},
|
|
1419
|
-
forceOrOptions: boolean | ComponentUpdateOptions = false
|
|
1420
|
-
): this {
|
|
1421
|
-
const force =
|
|
1422
|
-
typeof forceOrOptions === 'boolean'
|
|
1423
|
-
? forceOrOptions
|
|
1424
|
-
: !!forceOrOptions.force;
|
|
1425
|
-
return this.applyProps((patch || {}) as ModalPatch, {
|
|
1426
|
-
validate: true,
|
|
1427
|
-
force,
|
|
1428
|
-
});
|
|
1429
|
-
}
|
|
1430
|
-
|
|
1431
982
|
reset(): this {
|
|
1432
983
|
this.cache.fieldIds?.clear();
|
|
1433
984
|
if (!this.cache.initial) return this;
|
|
1434
985
|
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
986
|
+
this.setState({
|
|
987
|
+
mode: this.cache.initial.mode,
|
|
988
|
+
content: this.cache.initial.content,
|
|
989
|
+
fields: Array.isArray(this.cache.initial.fields)
|
|
990
|
+
? cloneFields(this.cache.initial.fields)
|
|
991
|
+
: null,
|
|
992
|
+
loading: false,
|
|
993
|
+
processing: false,
|
|
994
|
+
data: null,
|
|
995
|
+
extraData: null,
|
|
1444
996
|
});
|
|
1445
997
|
return this;
|
|
1446
998
|
}
|
|
1447
999
|
|
|
1448
|
-
resetContent(): this {
|
|
1449
|
-
return this.setContent(this.cache.initial?.content ?? '');
|
|
1450
|
-
}
|
|
1451
|
-
|
|
1452
|
-
resetFields(): this {
|
|
1453
|
-
return this.setFields(
|
|
1454
|
-
Array.isArray(this.cache.initial?.fields)
|
|
1455
|
-
? cloneFields(this.cache.initial.fields)
|
|
1456
|
-
: null
|
|
1457
|
-
);
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
1000
|
protected override onDestroy(): void {
|
|
1461
1001
|
const wasVisible = !!this.runtime.visibleApplied;
|
|
1462
|
-
const onHide = this.
|
|
1463
|
-
const onHidden = this.state?.onHidden;
|
|
1002
|
+
const { onHide, onHidden } = this.props;
|
|
1464
1003
|
|
|
1465
1004
|
if (wasVisible) void onHide?.(this);
|
|
1466
1005
|
|
|
@@ -1473,13 +1012,12 @@ export class Modal extends Component<ResolvedModalProps, ModalState, ModalDOM> {
|
|
|
1473
1012
|
this.cleanup.view?.();
|
|
1474
1013
|
this.cleanup.view = null;
|
|
1475
1014
|
|
|
1476
|
-
this.root?.remove();
|
|
1015
|
+
this.dom.root?.remove();
|
|
1477
1016
|
if (wasVisible) void onHidden?.(this);
|
|
1478
1017
|
|
|
1479
1018
|
this.cache = {
|
|
1480
1019
|
initial: null,
|
|
1481
1020
|
fieldIds: null,
|
|
1482
|
-
baseStyle: '',
|
|
1483
1021
|
previousActiveElement: null,
|
|
1484
1022
|
formId: '',
|
|
1485
1023
|
};
|
|
@@ -1492,6 +1030,8 @@ export class Modal extends Component<ResolvedModalProps, ModalState, ModalDOM> {
|
|
|
1492
1030
|
}
|
|
1493
1031
|
}
|
|
1494
1032
|
|
|
1033
|
+
export type Modal = ModalComponent;
|
|
1034
|
+
|
|
1495
1035
|
export function createModal(input: ModalProps = {}): Modal {
|
|
1496
|
-
return new
|
|
1036
|
+
return new ModalComponent(input);
|
|
1497
1037
|
}
|