vanilla-jui 1.3.8 → 1.3.10
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/dist/index.js +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/style.css +1 -1
- package/package.json +3 -2
- package/src/components/accordion.js +373 -0
- package/src/components/components.js +0 -0
- package/src/components/drop.js +437 -0
- package/src/components/flow.js +1133 -0
- package/src/components/form.js +585 -0
- package/src/components/icons.js +83 -0
- package/src/components/menu.js +422 -0
- package/src/components/modal.js +1174 -0
- package/src/components/offcanvas.js +332 -0
- package/src/components/pagination.js +536 -0
- package/src/components/parabola.js +295 -0
- package/src/components/sticky.js +271 -0
- package/src/components/swiper.js +1123 -0
- package/src/components/tabs.js +746 -0
- package/src/components/theme.js +353 -0
- package/src/components/toast.js +329 -0
- package/src/components/toc.js +226 -0
- package/src/components/tooltip.js +86 -0
- package/src/components/validator.js +586 -0
- package/src/core/Component.js +248 -0
- package/src/css/accordion.css +72 -0
- package/src/css/animation.css +177 -0
- package/src/css/avatar.css +62 -0
- package/src/css/badge.css +66 -0
- package/src/css/breadcrumb.css +106 -0
- package/src/css/button.css +296 -0
- package/src/css/card.css +151 -0
- package/src/css/color.css +1176 -0
- package/src/css/color_p3.css +1184 -0
- package/src/css/components.css +26 -0
- package/src/css/content.css +716 -0
- package/src/css/divider.css +9 -0
- package/src/css/drop.css +28 -0
- package/src/css/flow.css +86 -0
- package/src/css/font.css +23 -0
- package/src/css/form.css +919 -0
- package/src/css/gradient.css +211 -0
- package/src/css/icon.css +63 -0
- package/src/css/index.css +12 -0
- package/src/css/layout.css +782 -0
- package/src/css/loading.css +38 -0
- package/src/css/menu.css +564 -0
- package/src/css/modal.css +111 -0
- package/src/css/offcanvas.css +104 -0
- package/src/css/pagination.css +25 -0
- package/src/css/popup.css +69 -0
- package/src/css/skeleton.css +76 -0
- package/src/css/status.css +56 -0
- package/src/css/swiper.css +160 -0
- package/src/css/table.css +52 -0
- package/src/css/tabs.css +95 -0
- package/src/css/tag.css +98 -0
- package/src/css/theme-palette.css +59 -0
- package/src/css/tip.css +84 -0
- package/src/css/toast.css +154 -0
- package/src/css/toast.min.css +1 -0
- package/src/css/toc.css +38 -0
- package/src/css/tokens.css +1324 -0
- package/src/css/toolbar.css +115 -0
- package/src/css/tooltip.css +19 -0
- package/src/icons/index.js +30 -0
- package/src/index.js +45 -0
- package/src/locales/index.js +45 -0
- package/src/locales/index.min.js +1 -0
- package/src/utilities/browser.js +111 -0
- package/src/utilities/cache.js +159 -0
- package/src/utilities/core.js +502 -0
- package/src/utilities/debug.js +103 -0
- package/src/utilities/dom.js +313 -0
- package/src/utilities/events.js +69 -0
- package/src/utilities/http.js +30 -0
- package/src/utilities/legacy-dom.js +179 -0
- package/src/utilities/storage.js +46 -0
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createDeepStore,
|
|
3
|
+
createRoot,
|
|
4
|
+
flushSync,
|
|
5
|
+
jsx,
|
|
6
|
+
onCleanup,
|
|
7
|
+
render,
|
|
8
|
+
} from 'vanilla-signal';
|
|
9
|
+
|
|
10
|
+
import Component from '../core/Component.js';
|
|
11
|
+
import {
|
|
12
|
+
hasOwn,
|
|
13
|
+
isPlainObject,
|
|
14
|
+
randomId,
|
|
15
|
+
resolveProps,
|
|
16
|
+
validateParam,
|
|
17
|
+
} from '../utilities/core.js';
|
|
18
|
+
import {
|
|
19
|
+
isRenderableContent,
|
|
20
|
+
normalizeContentNodes,
|
|
21
|
+
requireContainer,
|
|
22
|
+
requireRenderDOM,
|
|
23
|
+
} from '../utilities/dom.js';
|
|
24
|
+
import { Validator } from './validator.js';
|
|
25
|
+
|
|
26
|
+
const DEFAULT_BUTTONS = [
|
|
27
|
+
{ type: 'submit', text: 'Submit', theme: 'primary', action: 'submit' },
|
|
28
|
+
{ type: 'reset', text: 'Reset', theme: 'ghost', action: 'reset' },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const FORM_PROPS_SCHEMA = {
|
|
32
|
+
id: {
|
|
33
|
+
default: null,
|
|
34
|
+
types: ['string', 'null'],
|
|
35
|
+
normalize: (value) => {
|
|
36
|
+
if (typeof value === 'string')
|
|
37
|
+
return value.trim() ? value.trim() : randomId();
|
|
38
|
+
if (value == null) return randomId();
|
|
39
|
+
return value;
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
vertical: { default: true, type: 'boolean' },
|
|
43
|
+
itemVertical: { default: true, type: 'boolean' },
|
|
44
|
+
style: { default: '', types: ['string', 'object', 'null'] },
|
|
45
|
+
fields: { default: [], type: 'array' },
|
|
46
|
+
buttons: {
|
|
47
|
+
default: DEFAULT_BUTTONS,
|
|
48
|
+
types: ['boolean', 'array'],
|
|
49
|
+
normalize: (value) => {
|
|
50
|
+
if (value === false) return [];
|
|
51
|
+
if (value === true) return cloneButtons(DEFAULT_BUTTONS);
|
|
52
|
+
if (Array.isArray(value)) return cloneButtons(value);
|
|
53
|
+
return value;
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
validator: { default: {}, type: 'object' },
|
|
57
|
+
onSubmit: { default: null, types: ['function', 'null'] },
|
|
58
|
+
onReset: { default: null, types: ['function', 'null'] },
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const FORM_UPDATE_RULE = {
|
|
62
|
+
validate: (value) => isPlainObject(value),
|
|
63
|
+
message: 'expects a props object.',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function cloneOptions(options) {
|
|
67
|
+
if (!Array.isArray(options)) return options;
|
|
68
|
+
return options.map((option) =>
|
|
69
|
+
option && typeof option === 'object' ? { ...option } : option
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function cloneFields(fields) {
|
|
74
|
+
if (!Array.isArray(fields)) return [];
|
|
75
|
+
return fields.map((field) => ({
|
|
76
|
+
...field,
|
|
77
|
+
options: cloneOptions(field.options),
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function cloneButtons(buttons) {
|
|
82
|
+
if (!Array.isArray(buttons)) return [];
|
|
83
|
+
return buttons.map((button) => ({ ...button }));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function normalizeOption(option) {
|
|
87
|
+
if (option && typeof option === 'object') return option;
|
|
88
|
+
return { value: option, text: option };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function contentView(content, context) {
|
|
92
|
+
if (!isRenderableContent(content)) return null;
|
|
93
|
+
return normalizeContentNodes(content, context);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function fieldIsRequired(field, rules) {
|
|
97
|
+
return !!field.required || !!rules?.[field.name]?.required;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function resolveClassName(...parts) {
|
|
101
|
+
return parts.filter(Boolean).join(' ');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function setElementStyle(element, style) {
|
|
105
|
+
element.removeAttribute('style');
|
|
106
|
+
if (!style) return;
|
|
107
|
+
if (typeof style === 'string') {
|
|
108
|
+
element.style.cssText = style;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (!isPlainObject(style)) return;
|
|
112
|
+
for (const [key, value] of Object.entries(style)) {
|
|
113
|
+
if (value == null) continue;
|
|
114
|
+
const name = key.startsWith('--')
|
|
115
|
+
? key
|
|
116
|
+
: key.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
|
|
117
|
+
element.style.setProperty(name, String(value));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function cloneValidator(validator) {
|
|
122
|
+
const source = isPlainObject(validator) ? validator : {};
|
|
123
|
+
return {
|
|
124
|
+
...source,
|
|
125
|
+
rules: isPlainObject(source.rules) ? { ...source.rules } : {},
|
|
126
|
+
messages: isPlainObject(source.messages) ? { ...source.messages } : {},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export class Form extends Component {
|
|
131
|
+
constructor(input = {}, container = false) {
|
|
132
|
+
const props = resolveProps(input, FORM_PROPS_SCHEMA, 'Form.props');
|
|
133
|
+
super(props);
|
|
134
|
+
|
|
135
|
+
this.dom.container = container;
|
|
136
|
+
this.dom.fields = new Map();
|
|
137
|
+
this.validator = null;
|
|
138
|
+
this.cleanup.view = null;
|
|
139
|
+
|
|
140
|
+
this.cache = {
|
|
141
|
+
initial: this.cloneProps(props),
|
|
142
|
+
fieldIds: new Map(),
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
this.state = createDeepStore({
|
|
146
|
+
...this.cloneProps(props),
|
|
147
|
+
submitting: false,
|
|
148
|
+
data: null,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
onInit() {
|
|
153
|
+
this.mount(this.dom.container);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
build(container = this.dom.container) {
|
|
157
|
+
if (this.runtime.destroyed)
|
|
158
|
+
throw new Error('Form.build: instance destroyed');
|
|
159
|
+
if (this.cleanup.view) return this;
|
|
160
|
+
|
|
161
|
+
requireRenderDOM('Form');
|
|
162
|
+
this.dom.container =
|
|
163
|
+
container === false
|
|
164
|
+
? document.createDocumentFragment()
|
|
165
|
+
: requireContainer(container, 'Form.container');
|
|
166
|
+
this.init(this.props);
|
|
167
|
+
return this;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
mount(container = this.dom.container) {
|
|
171
|
+
if (this.cleanup.view) return this;
|
|
172
|
+
|
|
173
|
+
this.cleanup.view = createRoot((dispose) => {
|
|
174
|
+
const viewDispose = render(() => this.view(), container);
|
|
175
|
+
onCleanup(viewDispose);
|
|
176
|
+
return dispose;
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
return this;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
view() {
|
|
183
|
+
return jsx('form', {
|
|
184
|
+
className: () =>
|
|
185
|
+
resolveClassName(
|
|
186
|
+
'j-form',
|
|
187
|
+
this.state.vertical ? 'is-vertical' : 'is-horizontal',
|
|
188
|
+
this.state.itemVertical ? 'is-item-vertical' : 'is-item-horizontal'
|
|
189
|
+
),
|
|
190
|
+
id: () => this.state.id,
|
|
191
|
+
style: () => this.state.style,
|
|
192
|
+
ref: (element) => {
|
|
193
|
+
const changed = this.root !== element;
|
|
194
|
+
this.root = element;
|
|
195
|
+
setElementStyle(element, this.state.style);
|
|
196
|
+
if (changed || !this.validator) this.syncValidator();
|
|
197
|
+
},
|
|
198
|
+
onSubmit: (event) => this.handleSubmit(event),
|
|
199
|
+
onReset: (event) => this.handleReset(event),
|
|
200
|
+
children: () => [
|
|
201
|
+
...this.state.fields.map((field, index) =>
|
|
202
|
+
this.fieldView(field, index)
|
|
203
|
+
),
|
|
204
|
+
this.buttonsView(),
|
|
205
|
+
],
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
fieldView(field, index) {
|
|
210
|
+
const id = this.resolveFieldId(field, index);
|
|
211
|
+
const hidden = field.type === 'hidden';
|
|
212
|
+
|
|
213
|
+
return jsx('div', {
|
|
214
|
+
className: 'form-item',
|
|
215
|
+
style: { display: hidden ? 'none' : '' },
|
|
216
|
+
children: [
|
|
217
|
+
this.labelView(field, id),
|
|
218
|
+
jsx('div', {
|
|
219
|
+
className: 'form-control',
|
|
220
|
+
children: [
|
|
221
|
+
this.controlView(field, id, index),
|
|
222
|
+
field.help
|
|
223
|
+
? jsx('div', { className: 'help-block', children: field.help })
|
|
224
|
+
: null,
|
|
225
|
+
],
|
|
226
|
+
}),
|
|
227
|
+
],
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
labelView(field, id) {
|
|
232
|
+
if (field.label === false || field.label === undefined) return null;
|
|
233
|
+
|
|
234
|
+
return jsx('label', {
|
|
235
|
+
className: resolveClassName(
|
|
236
|
+
'item-label',
|
|
237
|
+
fieldIsRequired(field, this.state.validator?.rules) ? 'is-required' : ''
|
|
238
|
+
),
|
|
239
|
+
for: id,
|
|
240
|
+
children: field.label,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
controlView(field, id, index) {
|
|
245
|
+
switch (field.type) {
|
|
246
|
+
case 'textarea':
|
|
247
|
+
return this.textareaView(field, id);
|
|
248
|
+
case 'select':
|
|
249
|
+
return this.selectView(field, id);
|
|
250
|
+
case 'radio':
|
|
251
|
+
return this.choiceGroupView(field, id, 'radio');
|
|
252
|
+
case 'checkbox':
|
|
253
|
+
return Array.isArray(field.options)
|
|
254
|
+
? this.choiceGroupView(field, id, 'checkbox')
|
|
255
|
+
: this.inputView(field, id);
|
|
256
|
+
case 'switch':
|
|
257
|
+
return this.switchView(field, id);
|
|
258
|
+
case 'custom':
|
|
259
|
+
return contentView(field.content, { form: this, field, index });
|
|
260
|
+
default:
|
|
261
|
+
return this.inputView(field, id);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
inputView(field, id) {
|
|
266
|
+
const type = field.type || 'text';
|
|
267
|
+
const props = this.controlProps(field, id, {
|
|
268
|
+
type,
|
|
269
|
+
className: field.className || 'j-input',
|
|
270
|
+
autocomplete: field.autocomplete || this.autoComplete(type),
|
|
271
|
+
value: field.value ?? '',
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
if (field.checked !== undefined) props.checked = !!field.checked;
|
|
275
|
+
return jsx('input', props);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
textareaView(field, id) {
|
|
279
|
+
return jsx(
|
|
280
|
+
'textarea',
|
|
281
|
+
this.controlProps(field, id, {
|
|
282
|
+
className: field.className || 'j-textarea',
|
|
283
|
+
autocomplete: field.autocomplete,
|
|
284
|
+
value: field.value ?? '',
|
|
285
|
+
})
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
selectView(field, id) {
|
|
290
|
+
const value = field.value;
|
|
291
|
+
return jsx(
|
|
292
|
+
'select',
|
|
293
|
+
this.controlProps(field, id, {
|
|
294
|
+
className: field.className || 'j-select',
|
|
295
|
+
autocomplete: field.autocomplete || 'off',
|
|
296
|
+
multiple: !!field.multiple,
|
|
297
|
+
children: (field.options || []).map((option) => {
|
|
298
|
+
const item = normalizeOption(option);
|
|
299
|
+
return jsx('option', {
|
|
300
|
+
value: item.value ?? '',
|
|
301
|
+
disabled: !!item.disabled,
|
|
302
|
+
selected: this.isSelected(value, item.value),
|
|
303
|
+
children: item.text ?? item.label ?? item.value ?? '',
|
|
304
|
+
});
|
|
305
|
+
}),
|
|
306
|
+
})
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
choiceGroupView(field, id, type) {
|
|
311
|
+
const direction = field.vertical ? 'vertical' : 'horizontal';
|
|
312
|
+
const className = resolveClassName(
|
|
313
|
+
type === 'radio' ? 'j-radio' : 'j-checkbox',
|
|
314
|
+
`is-${direction}`,
|
|
315
|
+
field.group ? 'is-group' : '',
|
|
316
|
+
field.size ? `is-${field.size}` : ''
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
return jsx('div', {
|
|
320
|
+
className,
|
|
321
|
+
children: (field.options || []).map((option, optionIndex) => {
|
|
322
|
+
const item = normalizeOption(option);
|
|
323
|
+
const optionId = `${id}_${optionIndex}`;
|
|
324
|
+
return jsx('label', {
|
|
325
|
+
className: type === 'radio' ? 'radio-label' : '',
|
|
326
|
+
for: optionId,
|
|
327
|
+
children: [
|
|
328
|
+
jsx('input', {
|
|
329
|
+
type,
|
|
330
|
+
id: optionId,
|
|
331
|
+
name: field.name,
|
|
332
|
+
value: item.value ?? '',
|
|
333
|
+
checked:
|
|
334
|
+
type === 'radio'
|
|
335
|
+
? this.isSelected(field.value, item.value)
|
|
336
|
+
: this.isChecked(field.value, item.value, item.checked),
|
|
337
|
+
disabled: !!item.disabled || !!field.disabled,
|
|
338
|
+
required: !!field.required,
|
|
339
|
+
}),
|
|
340
|
+
jsx('span', {
|
|
341
|
+
className: type === 'radio' ? 'radio-text' : '',
|
|
342
|
+
children: item.text ?? item.label ?? item.value ?? '',
|
|
343
|
+
}),
|
|
344
|
+
],
|
|
345
|
+
});
|
|
346
|
+
}),
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
switchView(field, id) {
|
|
351
|
+
return jsx('label', {
|
|
352
|
+
className: resolveClassName(
|
|
353
|
+
'j-switch',
|
|
354
|
+
field.variant ? `is-${field.variant}` : 'is-default',
|
|
355
|
+
field.size ? `is-${field.size}` : 'is-md'
|
|
356
|
+
),
|
|
357
|
+
for: id,
|
|
358
|
+
children: [
|
|
359
|
+
jsx('input', {
|
|
360
|
+
type: 'checkbox',
|
|
361
|
+
id,
|
|
362
|
+
name: field.name,
|
|
363
|
+
value: field.value ?? 'on',
|
|
364
|
+
checked: !!field.checked,
|
|
365
|
+
disabled: !!field.disabled,
|
|
366
|
+
required: !!field.required,
|
|
367
|
+
}),
|
|
368
|
+
jsx('span', { className: 'switch-slider' }),
|
|
369
|
+
],
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
buttonsView() {
|
|
374
|
+
if (!this.state.buttons.length) return null;
|
|
375
|
+
|
|
376
|
+
return jsx('div', {
|
|
377
|
+
className: 'form-buttons',
|
|
378
|
+
children: this.state.buttons.map((button) =>
|
|
379
|
+
jsx('button', {
|
|
380
|
+
type: button.type || 'button',
|
|
381
|
+
className: resolveClassName(
|
|
382
|
+
'j-button',
|
|
383
|
+
button.theme ? `is-${button.theme}` : '',
|
|
384
|
+
button.className || ''
|
|
385
|
+
),
|
|
386
|
+
'data-action': button.action || button.type || 'button',
|
|
387
|
+
disabled: () => !!this.state.submitting || !!button.disabled,
|
|
388
|
+
children: button.text ?? button.label ?? '',
|
|
389
|
+
})
|
|
390
|
+
),
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
controlProps(field, id, extra = {}) {
|
|
395
|
+
return {
|
|
396
|
+
...extra,
|
|
397
|
+
name: field.name,
|
|
398
|
+
id,
|
|
399
|
+
placeholder: field.placeholder || '',
|
|
400
|
+
required: !!field.required,
|
|
401
|
+
disabled: !!field.disabled,
|
|
402
|
+
readonly: !!field.readonly,
|
|
403
|
+
ref: (element) => {
|
|
404
|
+
if (field.name) this.dom.fields.set(field.name, element);
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
resolveFieldId(field, index) {
|
|
410
|
+
if (field.id) return field.id;
|
|
411
|
+
|
|
412
|
+
const key = field.name || index;
|
|
413
|
+
if (!this.cache.fieldIds.has(key)) {
|
|
414
|
+
this.cache.fieldIds.set(key, `${this.state.id}_field_${index}`);
|
|
415
|
+
}
|
|
416
|
+
return this.cache.fieldIds.get(key);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
syncValidator() {
|
|
420
|
+
if (!this.root) return;
|
|
421
|
+
const options = {
|
|
422
|
+
...this.state.validator,
|
|
423
|
+
onSubmit: null,
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
if (this.validator?.root === this.root) {
|
|
427
|
+
this.validator.options = options;
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
this.validator?.destroy();
|
|
432
|
+
this.validator = new Validator(this.root, options, false);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
validate() {
|
|
436
|
+
if (!this.validator) return true;
|
|
437
|
+
return this.validator.validate();
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
reset() {
|
|
441
|
+
this.validator?.reset();
|
|
442
|
+
flushSync(() => {
|
|
443
|
+
this.state.data = null;
|
|
444
|
+
});
|
|
445
|
+
return this;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
async handleSubmit(event) {
|
|
449
|
+
event.preventDefault();
|
|
450
|
+
if (this.state.submitting) return;
|
|
451
|
+
if (!this.validate()) return;
|
|
452
|
+
|
|
453
|
+
const data = this.collectData();
|
|
454
|
+
flushSync(() => {
|
|
455
|
+
this.state.submitting = true;
|
|
456
|
+
this.state.data = data;
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
try {
|
|
460
|
+
await Promise.resolve(this.state.onSubmit?.(data, this));
|
|
461
|
+
} finally {
|
|
462
|
+
if (this.state) {
|
|
463
|
+
flushSync(() => {
|
|
464
|
+
this.state.submitting = false;
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
handleReset(event) {
|
|
471
|
+
this.validator?.reset();
|
|
472
|
+
flushSync(() => {
|
|
473
|
+
this.state.data = null;
|
|
474
|
+
});
|
|
475
|
+
this.state.onReset?.(event, this);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
collectData() {
|
|
479
|
+
if (!this.root) return {};
|
|
480
|
+
return this.collectFormData(this.root);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
collectFormData(form) {
|
|
484
|
+
const data = {};
|
|
485
|
+
const formData = new FormData(form);
|
|
486
|
+
|
|
487
|
+
for (const [key, value] of formData.entries()) {
|
|
488
|
+
if (hasOwn(data, key)) {
|
|
489
|
+
data[key] = Array.isArray(data[key])
|
|
490
|
+
? [...data[key], value]
|
|
491
|
+
: [data[key], value];
|
|
492
|
+
} else {
|
|
493
|
+
data[key] = value;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return data;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
requestSubmit() {
|
|
501
|
+
if (!this.root) return this;
|
|
502
|
+
if (typeof this.root.requestSubmit === 'function') {
|
|
503
|
+
this.root.requestSubmit();
|
|
504
|
+
return this;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const event = new Event('submit', { bubbles: true, cancelable: true });
|
|
508
|
+
this.root.dispatchEvent(event);
|
|
509
|
+
return this;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
update(patch = {}) {
|
|
513
|
+
validateParam('props', patch, FORM_UPDATE_RULE, 'Form.update');
|
|
514
|
+
const nextProps = resolveProps(
|
|
515
|
+
Object.assign({}, this.props, patch),
|
|
516
|
+
FORM_PROPS_SCHEMA,
|
|
517
|
+
'Form.props'
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
this.props = this.cloneProps(nextProps);
|
|
521
|
+
this.cache.fieldIds.clear();
|
|
522
|
+
super.update(patch);
|
|
523
|
+
|
|
524
|
+
flushSync(() => {
|
|
525
|
+
for (const [key, value] of Object.entries(this.cloneProps(nextProps))) {
|
|
526
|
+
this.state[key] = value;
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
return this;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
setFields(fields) {
|
|
533
|
+
validateParam('fields', fields, 'array', 'Form.setFields');
|
|
534
|
+
return this.update({ fields });
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
resetFields() {
|
|
538
|
+
return this.setFields(this.cache.initial.fields);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
cloneProps(props) {
|
|
542
|
+
return {
|
|
543
|
+
...props,
|
|
544
|
+
fields: cloneFields(props.fields),
|
|
545
|
+
buttons: cloneButtons(props.buttons),
|
|
546
|
+
validator: cloneValidator(props.validator),
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
autoComplete(type) {
|
|
551
|
+
switch (type) {
|
|
552
|
+
case 'password':
|
|
553
|
+
return 'current-password';
|
|
554
|
+
case 'email':
|
|
555
|
+
return 'email';
|
|
556
|
+
default:
|
|
557
|
+
return 'on';
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
isSelected(value, optionValue) {
|
|
562
|
+
if (Array.isArray(value))
|
|
563
|
+
return value.map(String).includes(String(optionValue));
|
|
564
|
+
return value == optionValue;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
isChecked(value, optionValue, checked) {
|
|
568
|
+
if (checked !== undefined) return !!checked;
|
|
569
|
+
if (Array.isArray(value))
|
|
570
|
+
return value.map(String).includes(String(optionValue));
|
|
571
|
+
return value === true || String(value) === String(optionValue);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
onDestroy() {
|
|
575
|
+
this.validator?.destroy();
|
|
576
|
+
this.validator = null;
|
|
577
|
+
this.cleanup.view?.();
|
|
578
|
+
this.cleanup.view = null;
|
|
579
|
+
this.cache = { initial: null, fieldIds: null };
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
export function createForm(props = {}, container = false) {
|
|
584
|
+
return new Form(props, container);
|
|
585
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import iconPath from '../icons/index.js';
|
|
2
|
+
import { requireRenderDOM } from '../utilities/dom.js';
|
|
3
|
+
|
|
4
|
+
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
5
|
+
|
|
6
|
+
const defaultSvgObjects = iconPath;
|
|
7
|
+
|
|
8
|
+
function assertIconName(name) {
|
|
9
|
+
if (!(name in defaultSvgObjects)) {
|
|
10
|
+
throw new Error(`Icon "${name}" not found.`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function svgMarkup(name) {
|
|
15
|
+
assertIconName(name);
|
|
16
|
+
return `<svg xmlns="${SVG_NS}" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false">${defaultSvgObjects[name]}</svg>`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 获取内置 SVG 图标节点。
|
|
21
|
+
*
|
|
22
|
+
* 返回值是 SVGElement,可直接作为 vanilla-signal jsx/html/render 的 children 使用。
|
|
23
|
+
* @param {string} name 图标名称。
|
|
24
|
+
* @param {object} [props={}] SVG 属性。
|
|
25
|
+
* @returns {SVGElement}
|
|
26
|
+
* @throws {Error} 图标不存在或非 DOM 环境时抛出。
|
|
27
|
+
*/
|
|
28
|
+
export function icon(name, props = {}) {
|
|
29
|
+
requireRenderDOM('icon()');
|
|
30
|
+
|
|
31
|
+
const template = document.createElement('template');
|
|
32
|
+
template.innerHTML = svgMarkup(name);
|
|
33
|
+
const svg = template.content.firstElementChild;
|
|
34
|
+
|
|
35
|
+
for (const [key, value] of Object.entries(props || {})) {
|
|
36
|
+
if (value == null || value === false) continue;
|
|
37
|
+
const attr = key === 'className' ? 'class' : key;
|
|
38
|
+
svg.setAttribute(attr, value === true ? '' : String(value));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return svg;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 获取完整 SVG 字符串。
|
|
46
|
+
*
|
|
47
|
+
* 仅在必须拼接字符串或写入 innerHTML 时使用;响应式渲染优先使用 icon(name)。
|
|
48
|
+
* @param {string} name 图标名称。
|
|
49
|
+
* @returns {string}
|
|
50
|
+
*/
|
|
51
|
+
export function iconHtml(name) {
|
|
52
|
+
return svgMarkup(name);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const iconMarkup = iconHtml;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 获取当前已注册图标的 path 片段浅拷贝。
|
|
59
|
+
* @returns {Record<string, string>}
|
|
60
|
+
*/
|
|
61
|
+
export function getRegistedIconPath() {
|
|
62
|
+
return { ...defaultSvgObjects };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 批量注册自定义图标。
|
|
67
|
+
*
|
|
68
|
+
* 传入值应为 SVG path 片段,不需要包含外层 svg。
|
|
69
|
+
* @param {Record<string, string>} svgPathObjects 图标名称到 SVG path 的映射。
|
|
70
|
+
* @returns {void}
|
|
71
|
+
*/
|
|
72
|
+
export function addIcons(svgPathObjects) {
|
|
73
|
+
if (typeof svgPathObjects !== 'object' || svgPathObjects === null) {
|
|
74
|
+
throw new Error('Icons must be a valid object.');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (const [name, path] of Object.entries(svgPathObjects)) {
|
|
78
|
+
if (typeof path !== 'string' || !path.trim().startsWith('<')) {
|
|
79
|
+
throw new Error(`Invalid SVG path for icon "${name}".`);
|
|
80
|
+
}
|
|
81
|
+
defaultSvgObjects[name] = path;
|
|
82
|
+
}
|
|
83
|
+
}
|