@getdom/studio 0.1.0
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/LICENSE.md +109 -0
- package/README.md +44 -0
- package/SKILL.md +83 -0
- package/package.json +73 -0
- package/public/llms.txt +57 -0
- package/public/skills.sh +11 -0
- package/src/pages/_layout/componentCatalog.generated.js +4514 -0
- package/src/pages/_layout/componentInspector.js +60 -0
- package/src/pages/_layout/componentManager.js +192 -0
- package/src/pages/_layout/componentRuntimeRecords.js +49 -0
- package/src/pages/_layout/inspector/componentRegistry.js +639 -0
- package/src/pages/_layout/inspector/serialize.js +134 -0
- package/src/pages/_layout/inspector/useInspector.js +293 -0
- package/src/pages/components/accordion/DomAccordion.vue +39 -0
- package/src/pages/components/breadcrumbs/DomBreadcrumbs.vue +347 -0
- package/src/pages/components/button/DomButton.vue +132 -0
- package/src/pages/components/command-palette/DomCommandPalette.vue +482 -0
- package/src/pages/components/context-menu/DomContextMenu.vue +178 -0
- package/src/pages/components/dialog/DomDialog.vue +271 -0
- package/src/pages/components/dialog/DomDialogSchemaForm.vue +72 -0
- package/src/pages/components/dialog/DomDialogStack.vue +289 -0
- package/src/pages/components/dialog/useDialogs.js +253 -0
- package/src/pages/components/drawer/DomDrawer.vue +159 -0
- package/src/pages/components/dropdown/DomDropdown.vue +261 -0
- package/src/pages/components/icon-button/DomIconButton.vue +155 -0
- package/src/pages/components/lightbox/DomLightbox.vue +1561 -0
- package/src/pages/components/menu/DomMenu.vue +359 -0
- package/src/pages/components/menu/DomMenuItem.vue +233 -0
- package/src/pages/components/menu/DomMenuLabel.vue +19 -0
- package/src/pages/components/menu/DomMenuSeparator.vue +14 -0
- package/src/pages/components/menu/menuContext.js +11 -0
- package/src/pages/components/menu/menuPlacement.js +26 -0
- package/src/pages/components/menu/submenu.js +58 -0
- package/src/pages/components/pagination/DomPagination.vue +265 -0
- package/src/pages/components/playground/DomRenderer.vue +48 -0
- package/src/pages/components/popover/DomPopover.vue +244 -0
- package/src/pages/components/profile-menu/DomProfileMenu.vue +408 -0
- package/src/pages/components/profile-menu/DomProfileMenuItemContent.vue +45 -0
- package/src/pages/components/scroll-menu/DomScrollMenu.vue +430 -0
- package/src/pages/components/scroll-menu/DomScrollMenuItem.vue +212 -0
- package/src/pages/components/scroll-menu/scrollMenuContext.js +22 -0
- package/src/pages/components/splitter-panel/DomSplitterPanel.vue +283 -0
- package/src/pages/components/stepper/DomStepper.vue +386 -0
- package/src/pages/components/tabs/DomTabs.vue +149 -0
- package/src/pages/components/theme-switcher/DomThemeSwitcher.vue +226 -0
- package/src/pages/components/toast/DomToastItem.vue +52 -0
- package/src/pages/components/toast/DomToastStack.vue +768 -0
- package/src/pages/components/toast/useToasts.js +237 -0
- package/src/pages/components/toolbar/DomToolbar.vue +158 -0
- package/src/pages/components/tooltip/DomTooltip.vue +69 -0
- package/src/pages/components/tooltip/vDomTooltip.js +106 -0
- package/src/pages/components/tree-view/DomTreeView.vue +961 -0
- package/src/pages/components/tree-view/treeSchemas.js +129 -0
- package/src/pages/components/tree-view/treeUtils.js +228 -0
- package/src/pages/data/_shared/calculateVirtualWindow.js +101 -0
- package/src/pages/data/_shared/resourceRecord.js +29 -0
- package/src/pages/data/activity-timeline/DomActivityTimeline.vue +417 -0
- package/src/pages/data/agent-chat/DomAgentChatShell.vue +565 -0
- package/src/pages/data/agent-chat/DomAgentConversation.vue +737 -0
- package/src/pages/data/agent-chat/DomAgentConversationList.vue +319 -0
- package/src/pages/data/agent-chat/examples/agentChatMockApi.js +620 -0
- package/src/pages/data/agent-chat/examples/useAgentChatDemoStore.js +730 -0
- package/src/pages/data/data-grid/DataGridVirtualRows.vue +220 -0
- package/src/pages/data/data-grid/DomDataGrid.vue +1643 -0
- package/src/pages/data/document-editor/DomDocumentEditor.vue +3030 -0
- package/src/pages/data/document-editor/documentEditorCodeBlock.js +282 -0
- package/src/pages/data/document-editor/examples/documentEditorMockApi.js +272 -0
- package/src/pages/data/document-editor/examples/useDocumentEditorDemoStore.js +284 -0
- package/src/pages/data/filter-bar/DomFilterBar.vue +533 -0
- package/src/pages/data/kanban/DomKanban.vue +1703 -0
- package/src/pages/data/kanban/kanbanApi.js +80 -0
- package/src/pages/data/media-browser/DomMediaBrowser.vue +2344 -0
- package/src/pages/data/media-browser/examples/mediaBrowserMockApi.js +653 -0
- package/src/pages/data/media-browser/examples/useMediaBrowserDemoStore.js +1157 -0
- package/src/pages/data/month-calendar/DomMonthCalendar.vue +1718 -0
- package/src/pages/data/week-planner/DomWeekPlanner.vue +2308 -0
- package/src/pages/dev/code-block/DomCodeBlock.vue +263 -0
- package/src/pages/dev/json-viewer/DomJsonViewer.vue +962 -0
- package/src/pages/forms/_shared/DomFieldLoadingSpinner.vue +12 -0
- package/src/pages/forms/_shared/options.js +61 -0
- package/src/pages/forms/_shared/useOptionMenu.js +59 -0
- package/src/pages/forms/autocomplete/DomAutocomplete.vue +180 -0
- package/src/pages/forms/calendar/DomCalendar.vue +442 -0
- package/src/pages/forms/checkbox/DomCheckbox.vue +69 -0
- package/src/pages/forms/class-toggle-input/DomClassToggleInput.vue +316 -0
- package/src/pages/forms/code-input/DomCodeInput.vue +202 -0
- package/src/pages/forms/color-input/DomColorInput.vue +118 -0
- package/src/pages/forms/combobox/DomCombobox.vue +237 -0
- package/src/pages/forms/confirmation-code-input/DomConfirmationCodeInput.vue +345 -0
- package/src/pages/forms/date-picker/DomDatePicker.vue +299 -0
- package/src/pages/forms/date-time-picker/DomDateTimePicker.vue +630 -0
- package/src/pages/forms/email-input/DomEmailInput.vue +55 -0
- package/src/pages/forms/field/DomField.vue +267 -0
- package/src/pages/forms/field/fieldLayout.js +1 -0
- package/src/pages/forms/field/fieldProps.js +114 -0
- package/src/pages/forms/field/layouts/DomGridFieldLayout.vue +38 -0
- package/src/pages/forms/field/useField.js +343 -0
- package/src/pages/forms/field/validators.js +364 -0
- package/src/pages/forms/file-upload/DomFileUpload.vue +361 -0
- package/src/pages/forms/form/DomForm.vue +985 -0
- package/src/pages/forms/form/adapters/fieldBuilder.js +45 -0
- package/src/pages/forms/form/adapters/jsonSchema.js +264 -0
- package/src/pages/forms/form/adapters/shared.js +53 -0
- package/src/pages/forms/form/adapters/zod.js +235 -0
- package/src/pages/forms/form/formApi.js +38 -0
- package/src/pages/forms/form/formDefinition.js +392 -0
- package/src/pages/forms/form/schemaAdapter.js +54 -0
- package/src/pages/forms/icon-selector/DomIconSelector.vue +460 -0
- package/src/pages/forms/icon-selector/iconSelectorItems.js +490 -0
- package/src/pages/forms/input-group/DomInputGroup.vue +232 -0
- package/src/pages/forms/json-input/DomJsonInput.vue +227 -0
- package/src/pages/forms/json-list-input/DomJsonListInput.vue +206 -0
- package/src/pages/forms/listbox/DomListbox.vue +105 -0
- package/src/pages/forms/money-input/DomMoneyInput.vue +147 -0
- package/src/pages/forms/native-select/DomNativeSelect.vue +67 -0
- package/src/pages/forms/number-input/DomNumberInput.vue +47 -0
- package/src/pages/forms/password-input/DomPasswordInput.vue +218 -0
- package/src/pages/forms/password-input/pwnedPasswords.js +47 -0
- package/src/pages/forms/password-strength/DomPasswordStrength.vue +128 -0
- package/src/pages/forms/password-strength/passwordStrength.js +92 -0
- package/src/pages/forms/position-input/DomPositionInput.vue +83 -0
- package/src/pages/forms/radio-group/DomRadioGroup.vue +102 -0
- package/src/pages/forms/range-input/DomRangeInput.vue +296 -0
- package/src/pages/forms/select/DomSelect.vue +479 -0
- package/src/pages/forms/select-input/DomSelectInput.vue +155 -0
- package/src/pages/forms/tag-combobox/DomTagCombobox.vue +358 -0
- package/src/pages/forms/text-input/DomTextInput.vue +48 -0
- package/src/pages/forms/textarea-composer/DomTextareaComposer.vue +234 -0
- package/src/pages/forms/textarea-input/DomTextareaInput.vue +86 -0
- package/src/pages/forms/textarea-input/useTextareaAutosize.js +147 -0
- package/src/pages/forms/time-selector/DomTimeSelector.vue +389 -0
- package/src/pages/forms/time-selector/timeUtils.js +194 -0
- package/src/pages/forms/toggle/DomToggle.vue +158 -0
- package/src/pages/forms/toggle-button/DomToggleButton.vue +88 -0
- package/src/pages/forms/toggle-button-group/DomToggleButtonGroup.vue +340 -0
- package/src/pages/forms/url-input/DomUrlInput.vue +55 -0
- package/src/pages/forms/wysiwyg/DomWysiwyg.vue +204 -0
- package/src/pages/lib/css/color.js +245 -0
- package/src/pages/lib/headless/accordion.d.ts +7 -0
- package/src/pages/lib/headless/accordion.js +246 -0
- package/src/pages/lib/headless/autocomplete.d.ts +4 -0
- package/src/pages/lib/headless/autocomplete.js +33 -0
- package/src/pages/lib/headless/base.d.ts +25 -0
- package/src/pages/lib/headless/base.js +127 -0
- package/src/pages/lib/headless/checkbox.d.ts +19 -0
- package/src/pages/lib/headless/checkbox.js +99 -0
- package/src/pages/lib/headless/combobox.d.ts +7 -0
- package/src/pages/lib/headless/combobox.js +488 -0
- package/src/pages/lib/headless/dialog.d.ts +23 -0
- package/src/pages/lib/headless/dialog.js +384 -0
- package/src/pages/lib/headless/drawer.d.ts +21 -0
- package/src/pages/lib/headless/drawer.js +682 -0
- package/src/pages/lib/headless/dropdown.d.ts +19 -0
- package/src/pages/lib/headless/dropdown.js +412 -0
- package/src/pages/lib/headless/floating-effect.js +118 -0
- package/src/pages/lib/headless/floating.d.ts +34 -0
- package/src/pages/lib/headless/floating.js +382 -0
- package/src/pages/lib/headless/index.d.ts +19 -0
- package/src/pages/lib/headless/index.js +19 -0
- package/src/pages/lib/headless/listbox.d.ts +19 -0
- package/src/pages/lib/headless/listbox.js +126 -0
- package/src/pages/lib/headless/menu.d.ts +18 -0
- package/src/pages/lib/headless/menu.js +117 -0
- package/src/pages/lib/headless/popover-panel.d.ts +18 -0
- package/src/pages/lib/headless/popover-panel.js +309 -0
- package/src/pages/lib/headless/popover.d.ts +19 -0
- package/src/pages/lib/headless/popover.js +391 -0
- package/src/pages/lib/headless/radio-group.d.ts +19 -0
- package/src/pages/lib/headless/radio-group.js +100 -0
- package/src/pages/lib/headless/tabs.d.ts +18 -0
- package/src/pages/lib/headless/tabs.js +119 -0
- package/src/pages/lib/headless/toast.d.ts +13 -0
- package/src/pages/lib/headless/toast.js +65 -0
- package/src/pages/lib/headless/toggle.d.ts +19 -0
- package/src/pages/lib/headless/toggle.js +162 -0
- package/src/pages/lib/headless/tooltip-controller.d.ts +22 -0
- package/src/pages/lib/headless/tooltip-controller.js +549 -0
- package/src/pages/lib/headless/tooltip.d.ts +19 -0
- package/src/pages/lib/headless/tooltip.js +153 -0
- package/src/pages/lib/headless/transitions.d.ts +5 -0
- package/src/pages/lib/headless/transitions.js +89 -0
- package/src/pages/lib/vue/_internal/DomLiquidGlassSurface.vue +310 -0
- package/src/pages/lib/vue/_internal/liquidGlass.js +112 -0
- package/src/pages/lib/vue/_internal/liquidGlassConfig.js +10 -0
- package/src/pages/lib/vue/codeHighlighter.js +132 -0
- package/src/pages/lib/vue/floatingTeleportTarget.js +23 -0
- package/src/pages/lib/vue/index.d.ts +6806 -0
- package/src/pages/lib/vue/index.js +218 -0
- package/src/pages/lib/vue/useTheme.js +116 -0
- package/src/pages/mobile/action-sheet/DomActionSheet.vue +186 -0
- package/src/pages/mobile/bottom-nav/DomAppBottomNav.vue +94 -0
- package/src/pages/mobile/list-item/DomAppListItem.vue +90 -0
- package/src/pages/mobile/safe-area/DomSafeArea.vue +39 -0
- package/src/pages/mobile/shell/DomAppShell.vue +51 -0
- package/src/pages/mobile/stack/AppStackScreen.vue +63 -0
- package/src/pages/mobile/stack/DomAppStack.vue +711 -0
- package/src/pages/mobile/top-bar/DomAppTopBar.vue +67 -0
- package/src/pages/visual/alert/DomAlert.vue +363 -0
- package/src/pages/visual/avatar/DomAvatar.vue +110 -0
- package/src/pages/visual/avatar-stack/DomAvatarStack.vue +89 -0
- package/src/pages/visual/badge/DomBadge.vue +105 -0
- package/src/pages/visual/card/DomCard.vue +65 -0
- package/src/pages/visual/card-rail/DomCardRail.vue +99 -0
- package/src/pages/visual/empty-state/DomEmptyState.vue +88 -0
- package/src/pages/visual/progress/DomProgress.vue +212 -0
- package/src/pages/visual/progress/DomRadialProgress.vue +186 -0
- package/src/pages/visual/progress/DomSegmentedProgress.vue +350 -0
- package/src/pages/visual/skeleton/DomSkeleton.vue +174 -0
- package/src/pages/visual/spinner/DomSpinner.vue +122 -0
- package/src/pages/visual/status-pill/DomStatusPill.vue +75 -0
- package/src/pages/visual/text-diff/DomTextDiff.vue +577 -0
- package/src/pages/visual/text-diff/textDiff.js +300 -0
- package/src/style.css +446 -0
|
@@ -0,0 +1,985 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { computed, defineAsyncComponent, defineComponent, h, inject, provide, reactive, toRaw, watch } from 'vue';
|
|
3
|
+
import DomAutocomplete from '../autocomplete/DomAutocomplete.vue';
|
|
4
|
+
import DomCalendar from '../calendar/DomCalendar.vue';
|
|
5
|
+
import DomCheckbox from '../checkbox/DomCheckbox.vue';
|
|
6
|
+
import DomCodeInput from '../code-input/DomCodeInput.vue';
|
|
7
|
+
import DomCombobox from '../combobox/DomCombobox.vue';
|
|
8
|
+
import DomDateTimePicker from '../date-time-picker/DomDateTimePicker.vue';
|
|
9
|
+
import DomDatePicker from '../date-picker/DomDatePicker.vue';
|
|
10
|
+
import DomEmailInput from '../email-input/DomEmailInput.vue';
|
|
11
|
+
import DomFileUpload from '../file-upload/DomFileUpload.vue';
|
|
12
|
+
import DomInputGroup from '../input-group/DomInputGroup.vue';
|
|
13
|
+
import DomMoneyInput from '../money-input/DomMoneyInput.vue';
|
|
14
|
+
import DomNativeSelect from '../native-select/DomNativeSelect.vue';
|
|
15
|
+
import DomNumberInput from '../number-input/DomNumberInput.vue';
|
|
16
|
+
import DomPasswordInput from '../password-input/DomPasswordInput.vue';
|
|
17
|
+
import DomPasswordStrength from '../password-strength/DomPasswordStrength.vue';
|
|
18
|
+
import DomRadioGroup from '../radio-group/DomRadioGroup.vue';
|
|
19
|
+
import DomSelect from '../select/DomSelect.vue';
|
|
20
|
+
import DomSelectInput from '../select-input/DomSelectInput.vue';
|
|
21
|
+
import DomTagCombobox from '../tag-combobox/DomTagCombobox.vue';
|
|
22
|
+
import DomTextInput from '../text-input/DomTextInput.vue';
|
|
23
|
+
import DomTextareaComposer from '../textarea-composer/DomTextareaComposer.vue';
|
|
24
|
+
import DomTextareaInput from '../textarea-input/DomTextareaInput.vue';
|
|
25
|
+
import DomTimeSelector from '../time-selector/DomTimeSelector.vue';
|
|
26
|
+
import DomToggle from '../toggle/DomToggle.vue';
|
|
27
|
+
import DomUrlInput from '../url-input/DomUrlInput.vue';
|
|
28
|
+
import { formFieldProviderKey, normalizeErrors } from '../field/useField.js';
|
|
29
|
+
import { deletePathValue, forms, getPathValue, setPathValue } from './formApi.js';
|
|
30
|
+
import { formNodeDefaultValue, normalizeFormChildren, normalizeFormNode } from './formDefinition.js';
|
|
31
|
+
import { schemaToChildren } from './schemaAdapter.js';
|
|
32
|
+
|
|
33
|
+
const DomJsonInput = defineAsyncComponent(() => import('../json-input/DomJsonInput.vue'));
|
|
34
|
+
const DomJsonListInput = defineAsyncComponent(() => import('../json-list-input/DomJsonListInput.vue'));
|
|
35
|
+
|
|
36
|
+
const formComponents = {
|
|
37
|
+
DomAutocomplete,
|
|
38
|
+
DomCalendar,
|
|
39
|
+
DomCheckbox,
|
|
40
|
+
DomCodeInput,
|
|
41
|
+
DomCombobox,
|
|
42
|
+
DomDateTimePicker,
|
|
43
|
+
DomDatePicker,
|
|
44
|
+
DomEmailInput,
|
|
45
|
+
DomFileUpload,
|
|
46
|
+
DomInputGroup,
|
|
47
|
+
DomJsonInput,
|
|
48
|
+
DomJsonListInput,
|
|
49
|
+
DomMoneyInput,
|
|
50
|
+
DomNativeSelect,
|
|
51
|
+
DomNumberInput,
|
|
52
|
+
DomPasswordInput,
|
|
53
|
+
DomPasswordStrength,
|
|
54
|
+
DomRadioGroup,
|
|
55
|
+
DomSelect,
|
|
56
|
+
DomSelectInput,
|
|
57
|
+
DomTagCombobox,
|
|
58
|
+
DomTextInput,
|
|
59
|
+
DomTextareaComposer,
|
|
60
|
+
DomTextareaInput,
|
|
61
|
+
DomTimeSelector,
|
|
62
|
+
DomToggle,
|
|
63
|
+
DomUrlInput,
|
|
64
|
+
};
|
|
65
|
+
let DomFormComponent;
|
|
66
|
+
|
|
67
|
+
function joinPath(scope, name) {
|
|
68
|
+
if (!scope) return name || '';
|
|
69
|
+
if (!name) return scope;
|
|
70
|
+
return `${scope}.${name}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function htmlNameFromPath(path) {
|
|
74
|
+
const parts = String(path || '').split('.').filter(Boolean);
|
|
75
|
+
if (!parts.length) return '';
|
|
76
|
+
return parts.slice(1).reduce((name, part) => `${name}[${part}]`, parts[0]);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function htmlIdFromPath(path) {
|
|
80
|
+
return String(path || '')
|
|
81
|
+
.split('.')
|
|
82
|
+
.filter(Boolean)
|
|
83
|
+
.join('_')
|
|
84
|
+
.replace(/[^A-Za-z0-9_]+/g, '_')
|
|
85
|
+
.replace(/^_+|_+$/g, '');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function ownField(path, scope) {
|
|
89
|
+
return !scope || path === scope || String(path).startsWith(`${scope}.`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function clone(value) {
|
|
93
|
+
const next = toRaw(value);
|
|
94
|
+
if (next == null || typeof next !== 'object') return next;
|
|
95
|
+
if (Array.isArray(next)) return next.map((item) => clone(item));
|
|
96
|
+
if (next instanceof Date) return new Date(next);
|
|
97
|
+
return Object.fromEntries(Object.entries(next).map(([key, item]) => [key, clone(item)]));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function snapshot(value) {
|
|
101
|
+
return clone(value ?? {});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function replaceReactiveValue(target, nextValue) {
|
|
105
|
+
if (Array.isArray(target)) {
|
|
106
|
+
const next = Array.isArray(nextValue) ? snapshot(nextValue) : [];
|
|
107
|
+
target.splice(0, target.length, ...next);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const next = nextValue && typeof nextValue === 'object' && !Array.isArray(nextValue) ? snapshot(nextValue) : {};
|
|
111
|
+
for (const key of Object.keys(target)) delete target[key];
|
|
112
|
+
Object.assign(target, next);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function syncReactiveObject(target, nextValue) {
|
|
116
|
+
const incoming = nextValue && typeof nextValue === 'object' && !Array.isArray(nextValue) ? nextValue : {};
|
|
117
|
+
for (const key of Object.keys(target)) {
|
|
118
|
+
if (!(key in incoming)) delete target[key];
|
|
119
|
+
}
|
|
120
|
+
Object.assign(target, incoming);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function singularLabel(label) {
|
|
124
|
+
const normalized = String(label || 'row').trim().toLowerCase();
|
|
125
|
+
if (normalized === 'children') return 'child';
|
|
126
|
+
if (normalized.endsWith('ies')) return `${normalized.slice(0, -3)}y`;
|
|
127
|
+
if (normalized.endsWith('s')) return normalized.slice(0, -1);
|
|
128
|
+
return normalized;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function jsonListSchemaForNode(node) {
|
|
132
|
+
if (node.props?.schema) return node.props.schema;
|
|
133
|
+
if (node.type !== 'array') return undefined;
|
|
134
|
+
return {
|
|
135
|
+
type: 'array',
|
|
136
|
+
...(node.props?.label ? { label: node.props.label } : {}),
|
|
137
|
+
...(node.items ? { items: node.items } : {}),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function renderedComponentProps(props = {}) {
|
|
142
|
+
const {
|
|
143
|
+
default: _schemaDefault,
|
|
144
|
+
...renderedProps
|
|
145
|
+
} = props;
|
|
146
|
+
return renderedProps;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function normalizeFieldState(state = {}) {
|
|
150
|
+
const errors = normalizeErrors(state.errors);
|
|
151
|
+
const hasValidationSignal = (
|
|
152
|
+
'validating' in state
|
|
153
|
+
|| 'invalid' in state
|
|
154
|
+
|| 'valid' in state
|
|
155
|
+
|| 'errors' in state
|
|
156
|
+
);
|
|
157
|
+
const interaction = state.interaction || (state.focused ? 'focused' : state.touched ? 'blurred' : 'untouched');
|
|
158
|
+
const modification = state.modification || (state.dirty ? 'changed' : 'clean');
|
|
159
|
+
let validation = 'unknown';
|
|
160
|
+
|
|
161
|
+
if (state.validating) validation = 'validating';
|
|
162
|
+
else if (state.invalid || errors.length) validation = 'invalid';
|
|
163
|
+
else if (state.valid) validation = 'valid';
|
|
164
|
+
else if (state.validation && state.validation !== 'unknown') validation = state.validation;
|
|
165
|
+
else if (state.validation === 'unknown') validation = 'unknown';
|
|
166
|
+
else if (hasValidationSignal) validation = 'valid';
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
...state,
|
|
170
|
+
interaction,
|
|
171
|
+
modification,
|
|
172
|
+
validation,
|
|
173
|
+
focused: interaction === 'focused',
|
|
174
|
+
touched: interaction !== 'untouched',
|
|
175
|
+
dirty: modification === 'changed',
|
|
176
|
+
validating: validation === 'validating',
|
|
177
|
+
invalid: validation === 'invalid',
|
|
178
|
+
valid: validation === 'valid',
|
|
179
|
+
errors,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function mergeFieldState(previous = {}, patch = {}) {
|
|
184
|
+
const next = {
|
|
185
|
+
...previous,
|
|
186
|
+
...patch,
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
if (!('interaction' in patch)) {
|
|
190
|
+
if (patch.focused === true) next.interaction = 'focused';
|
|
191
|
+
else if (patch.touched === true) next.interaction = 'blurred';
|
|
192
|
+
else if (patch.focused === false && patch.touched === false) next.interaction = 'untouched';
|
|
193
|
+
else if ('focused' in patch || 'touched' in patch) delete next.interaction;
|
|
194
|
+
}
|
|
195
|
+
if (!('modification' in patch) && 'dirty' in patch) {
|
|
196
|
+
next.modification = patch.dirty ? 'changed' : 'clean';
|
|
197
|
+
}
|
|
198
|
+
if (!('validation' in patch) && ('validating' in patch || 'invalid' in patch || 'valid' in patch || 'errors' in patch)) {
|
|
199
|
+
const patchErrors = 'errors' in patch ? normalizeErrors(patch.errors) : null;
|
|
200
|
+
if (patch.validating === true) next.validation = 'validating';
|
|
201
|
+
else if (patch.invalid === true || patchErrors?.length) next.validation = 'invalid';
|
|
202
|
+
else if (patch.valid === true || patch.invalid === false || (patchErrors && !patchErrors.length)) next.validation = 'valid';
|
|
203
|
+
else delete next.validation;
|
|
204
|
+
}
|
|
205
|
+
if ('validation' in patch || 'validating' in patch || 'invalid' in patch || 'valid' in patch || 'errors' in patch) {
|
|
206
|
+
if (patch.validating !== true) delete next.validating;
|
|
207
|
+
if (patch.invalid !== true) delete next.invalid;
|
|
208
|
+
if (patch.valid !== true) delete next.valid;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return normalizeFieldState(next);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Returns the Vue slot name assigned to a declarative form node.
|
|
216
|
+
*
|
|
217
|
+
* @param {unknown} node Declarative child node.
|
|
218
|
+
* @returns {string} Named slot or the default slot.
|
|
219
|
+
*/
|
|
220
|
+
function formNodeSlotName(node) {
|
|
221
|
+
if (!node || typeof node !== 'object' || typeof node.slot !== 'string') return 'default';
|
|
222
|
+
return node.slot || 'default';
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Renders declarative child nodes as a default slot or a named-slot object.
|
|
227
|
+
*
|
|
228
|
+
* @param {Array<Record<string, unknown>>} children Normalized declarative child nodes.
|
|
229
|
+
* @param {boolean} [useSlots=true] Whether to build Vue component slots instead of native children.
|
|
230
|
+
* @returns {undefined | Array<unknown> | (() => Array<unknown>) | Record<string, () => Array<unknown>>} Vue children argument.
|
|
231
|
+
*/
|
|
232
|
+
function renderFormNodeChildren(children = [], useSlots = true) {
|
|
233
|
+
if (!children.length) return undefined;
|
|
234
|
+
const renderedChildren = children.map((child, index) => ({
|
|
235
|
+
node: h(RenderNode, { node: child, key: child.id || index }),
|
|
236
|
+
slot: formNodeSlotName(child),
|
|
237
|
+
}));
|
|
238
|
+
if (!useSlots) return renderedChildren.map((child) => child.node);
|
|
239
|
+
if (renderedChildren.every((child) => child.slot === 'default')) {
|
|
240
|
+
return () => renderedChildren.map((child) => child.node);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const slots = {};
|
|
244
|
+
for (const child of renderedChildren) {
|
|
245
|
+
slots[child.slot] ||= [];
|
|
246
|
+
slots[child.slot].push(child.node);
|
|
247
|
+
}
|
|
248
|
+
return Object.fromEntries(Object.entries(slots).map(([name, nodes]) => [name, () => nodes]));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const RenderNode = defineComponent({
|
|
252
|
+
name: 'DomFormRenderNode',
|
|
253
|
+
props: {
|
|
254
|
+
node: { type: [Object, String], required: true },
|
|
255
|
+
},
|
|
256
|
+
setup(props) {
|
|
257
|
+
return () => {
|
|
258
|
+
const node = props.node;
|
|
259
|
+
if (typeof node === 'string') return node;
|
|
260
|
+
if (node.type === 'text' || node.text != null) return node.text || '';
|
|
261
|
+
const componentName = node.component || node.type || 'div';
|
|
262
|
+
const component = componentName === 'DomForm' ? DomFormComponent : formComponents[componentName] || componentName;
|
|
263
|
+
const usesNativeChildren = typeof component === 'string' && /^[a-z]/.test(component);
|
|
264
|
+
const nodeProps = {
|
|
265
|
+
...renderedComponentProps(node.props),
|
|
266
|
+
...(componentName === 'DomForm' ? {
|
|
267
|
+
type: node.type,
|
|
268
|
+
items: node.items,
|
|
269
|
+
} : {}),
|
|
270
|
+
...(componentName === 'DomJsonListInput' ? {
|
|
271
|
+
schema: jsonListSchemaForNode(node),
|
|
272
|
+
} : {}),
|
|
273
|
+
};
|
|
274
|
+
return h(component, nodeProps, renderFormNodeChildren(node.children, !usesNativeChildren));
|
|
275
|
+
};
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
DomFormComponent = defineComponent({
|
|
280
|
+
name: 'DomForm',
|
|
281
|
+
inheritAttrs: false,
|
|
282
|
+
__doc: {
|
|
283
|
+
name: 'Form',
|
|
284
|
+
tag: '<DomForm>',
|
|
285
|
+
description: 'A Pinia-friendly form provider that gathers child field values, errors, and field state.',
|
|
286
|
+
icon: 'M6 4h12v16H6V4Zm3 5h6M9 13h6M9 17h4',
|
|
287
|
+
slots: [
|
|
288
|
+
{ name: '(default)', description: 'Form fields. Slot props include values, errors, state, validate, and reset.' },
|
|
289
|
+
],
|
|
290
|
+
events: [
|
|
291
|
+
{ name: 'update:modelValue', payload: 'Record<string, unknown>', description: 'Fired when any child field updates the form value.' },
|
|
292
|
+
{ name: 'update:children', payload: 'Array', description: 'Fired when the programmatic schema children change.' },
|
|
293
|
+
{ name: 'schema-change', payload: '{ children, form }', description: 'Fired when children are added, removed, or replaced through the form API.' },
|
|
294
|
+
{ name: 'change', payload: '{ name, value, values, errors, state }', description: 'Fired whenever a child field changes.' },
|
|
295
|
+
{ name: 'submit', payload: '{ values, errors, state }', description: 'Fired after successful validation when the form is submitted.' },
|
|
296
|
+
{ name: 'invalid', payload: '{ values, errors, state }', description: 'Fired when submit validation fails.' },
|
|
297
|
+
],
|
|
298
|
+
},
|
|
299
|
+
props: {
|
|
300
|
+
modelValue: {
|
|
301
|
+
type: [Array, Object],
|
|
302
|
+
default: () => ({}),
|
|
303
|
+
_edit: { component: 'DomJsonInput', description: 'Current form value keyed by field path.' },
|
|
304
|
+
},
|
|
305
|
+
type: {
|
|
306
|
+
type: String,
|
|
307
|
+
default: 'object',
|
|
308
|
+
_edit: { options: ['object', 'array'], description: 'Render a single object form or an array of form rows.' },
|
|
309
|
+
},
|
|
310
|
+
multiple: {
|
|
311
|
+
type: Boolean,
|
|
312
|
+
default: false,
|
|
313
|
+
_edit: { description: 'Render this form as a repeatable array of itself.' },
|
|
314
|
+
},
|
|
315
|
+
items: {
|
|
316
|
+
type: [Array, Object],
|
|
317
|
+
default: null,
|
|
318
|
+
_edit: { component: 'DomJsonInput', description: 'Schema used for each row when type is array.' },
|
|
319
|
+
},
|
|
320
|
+
addLabel: {
|
|
321
|
+
type: String,
|
|
322
|
+
default: '',
|
|
323
|
+
_edit: { description: 'Button label for adding a row in array mode.' },
|
|
324
|
+
},
|
|
325
|
+
compact: {
|
|
326
|
+
type: Boolean,
|
|
327
|
+
default: false,
|
|
328
|
+
_edit: { description: 'Reduce spacing for inspectors and narrow layouts.' },
|
|
329
|
+
},
|
|
330
|
+
label: {
|
|
331
|
+
type: String,
|
|
332
|
+
default: '',
|
|
333
|
+
_edit: { description: 'Optional group label.' },
|
|
334
|
+
},
|
|
335
|
+
description: {
|
|
336
|
+
type: String,
|
|
337
|
+
default: '',
|
|
338
|
+
_edit: { description: 'Optional group description.' },
|
|
339
|
+
},
|
|
340
|
+
required: {
|
|
341
|
+
type: Boolean,
|
|
342
|
+
default: false,
|
|
343
|
+
_edit: { description: 'Show a required marker on the group label.' },
|
|
344
|
+
},
|
|
345
|
+
visible: {
|
|
346
|
+
type: Boolean,
|
|
347
|
+
default: true,
|
|
348
|
+
_edit: { description: 'Show or hide the rendered form.' },
|
|
349
|
+
},
|
|
350
|
+
tag: {
|
|
351
|
+
type: String,
|
|
352
|
+
default: '',
|
|
353
|
+
_edit: { description: 'Override the rendered root element.' },
|
|
354
|
+
},
|
|
355
|
+
isolated: {
|
|
356
|
+
type: Boolean,
|
|
357
|
+
default: false,
|
|
358
|
+
_edit: { description: 'Create a standalone provider even when nested inside another form.' },
|
|
359
|
+
},
|
|
360
|
+
name: {
|
|
361
|
+
type: String,
|
|
362
|
+
default: '',
|
|
363
|
+
_edit: { description: 'Root forms use name as the global registry key. Nested forms use name as their local path segment.' },
|
|
364
|
+
},
|
|
365
|
+
children: {
|
|
366
|
+
type: [Array, Object],
|
|
367
|
+
default: () => [],
|
|
368
|
+
_edit: {
|
|
369
|
+
component: 'DomJsonInput',
|
|
370
|
+
description: 'Server/studio-style field definitions to render when slot children are not supplied. Accepts an array or keyed object.',
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
schema: {
|
|
374
|
+
type: [Array, Object],
|
|
375
|
+
default: null,
|
|
376
|
+
_edit: {
|
|
377
|
+
component: 'DomJsonInput',
|
|
378
|
+
description: 'Schema source used to generate children. Accepts JSON Schema, Zod-like schemas, native form nodes, arrays, or keyed objects.',
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
validateOnSubmit: {
|
|
382
|
+
type: Boolean,
|
|
383
|
+
default: true,
|
|
384
|
+
_edit: { description: 'Run all registered field validators before emitting submit.' },
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
emits: ['update:modelValue', 'update:children', 'schema-change', 'change', 'submit', 'invalid'],
|
|
388
|
+
setup(props, { attrs, emit, expose, slots }) {
|
|
389
|
+
const injectedParent = inject(formFieldProviderKey, null);
|
|
390
|
+
const parent = props.isolated ? null : injectedParent;
|
|
391
|
+
const isRoot = !parent;
|
|
392
|
+
const initialValue = props.multiple || props.type === 'array'
|
|
393
|
+
? (Array.isArray(props.modelValue) ? props.modelValue : [])
|
|
394
|
+
: (props.modelValue || {});
|
|
395
|
+
const values = isRoot ? reactive(snapshot(initialValue)) : parent.values;
|
|
396
|
+
const fieldStates = isRoot ? reactive({}) : parent.fieldStates;
|
|
397
|
+
const fields = isRoot ? reactive(new Map()) : parent.fields;
|
|
398
|
+
const errors = isRoot ? reactive({}) : parent.errors;
|
|
399
|
+
const subforms = isRoot ? reactive({}) : parent.subforms;
|
|
400
|
+
const schemaChildren = reactive([]);
|
|
401
|
+
const scopePath = computed(() => (isRoot ? '' : joinPath(parent?.scopePath || '', props.name)));
|
|
402
|
+
const isArrayMode = computed(() => props.multiple || props.type === 'array');
|
|
403
|
+
const normalizedItems = computed(() => {
|
|
404
|
+
if (props.items) return normalizeFormNode(props.items);
|
|
405
|
+
if (schemaChildren.length) {
|
|
406
|
+
return normalizeFormNode({
|
|
407
|
+
type: 'DomForm',
|
|
408
|
+
children: schemaChildren,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
return normalizeFormNode({ type: 'DomForm', children: [] });
|
|
412
|
+
});
|
|
413
|
+
const arrayRows = computed(() => {
|
|
414
|
+
const value = isRoot ? values : getPathValue(values, scopePath.value);
|
|
415
|
+
return Array.isArray(value) ? value : [];
|
|
416
|
+
});
|
|
417
|
+
const arrayAddLabel = computed(() => props.addLabel || `+ Add ${singularLabel(props.label || props.name || 'row')}`);
|
|
418
|
+
|
|
419
|
+
const scopedFieldEntries = computed(() => Array.from(fields.entries()).filter(([path]) => ownField(path, scopePath.value)));
|
|
420
|
+
const scopedErrorEntries = computed(() => Object.entries(errors).filter(([path]) => ownField(path, scopePath.value)));
|
|
421
|
+
const errorCount = computed(() => scopedErrorEntries.value.reduce((count, [, fieldErrors]) => (
|
|
422
|
+
count + (Array.isArray(fieldErrors) ? fieldErrors.length : fieldErrors ? 1 : 0)
|
|
423
|
+
), 0));
|
|
424
|
+
|
|
425
|
+
const state = computed(() => {
|
|
426
|
+
const states = Object.entries(fieldStates)
|
|
427
|
+
.filter(([path]) => ownField(path, scopePath.value))
|
|
428
|
+
.map(([, field]) => normalizeFieldState(field));
|
|
429
|
+
const fieldErrorCount = states.reduce((count, field) => count + field.errors.length, 0);
|
|
430
|
+
const nextErrorCount = Math.max(errorCount.value, fieldErrorCount);
|
|
431
|
+
const validation = states.some((field) => field.validation === 'validating')
|
|
432
|
+
? 'validating'
|
|
433
|
+
: nextErrorCount > 0 || states.some((field) => field.validation === 'invalid')
|
|
434
|
+
? 'invalid'
|
|
435
|
+
: states.length && states.every((field) => field.validation === 'valid')
|
|
436
|
+
? 'valid'
|
|
437
|
+
: 'unknown';
|
|
438
|
+
const interaction = states.some((field) => field.interaction === 'focused')
|
|
439
|
+
? 'focused'
|
|
440
|
+
: states.some((field) => field.interaction === 'blurred')
|
|
441
|
+
? 'blurred'
|
|
442
|
+
: 'untouched';
|
|
443
|
+
const modification = states.some((field) => field.modification === 'changed') ? 'changed' : 'clean';
|
|
444
|
+
return {
|
|
445
|
+
interaction,
|
|
446
|
+
modification,
|
|
447
|
+
validation,
|
|
448
|
+
dirty: modification === 'changed',
|
|
449
|
+
touched: interaction !== 'untouched',
|
|
450
|
+
focused: interaction === 'focused',
|
|
451
|
+
validating: validation === 'validating',
|
|
452
|
+
invalid: validation === 'invalid',
|
|
453
|
+
valid: validation === 'valid',
|
|
454
|
+
errorCount: nextErrorCount,
|
|
455
|
+
fieldCount: scopedFieldEntries.value.length,
|
|
456
|
+
path: scopePath.value,
|
|
457
|
+
};
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
function modelSnapshot() {
|
|
461
|
+
return snapshot(values);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function payload(name = '') {
|
|
465
|
+
return {
|
|
466
|
+
name,
|
|
467
|
+
value: name ? getPathValue(values, name) : undefined,
|
|
468
|
+
values: modelSnapshot(),
|
|
469
|
+
errors: { ...errors },
|
|
470
|
+
state: state.value,
|
|
471
|
+
form: provider,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function emitModel() {
|
|
476
|
+
if (isRoot) emit('update:modelValue', modelSnapshot());
|
|
477
|
+
else parent.emitModel?.();
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function emitChange(name) {
|
|
481
|
+
emit('change', payload(name));
|
|
482
|
+
if (!isRoot) parent.emitChange?.(name);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function setArrayRows(nextRows) {
|
|
486
|
+
const next = Array.isArray(nextRows) ? snapshot(nextRows) : [];
|
|
487
|
+
if (isRoot) {
|
|
488
|
+
replaceReactiveValue(values, next);
|
|
489
|
+
} else if (scopePath.value) {
|
|
490
|
+
setPathValue(values, scopePath.value, next);
|
|
491
|
+
}
|
|
492
|
+
emitModel();
|
|
493
|
+
emitChange(scopePath.value);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function addArrayRow() {
|
|
497
|
+
const index = arrayRows.value.length;
|
|
498
|
+
setArrayRows([...arrayRows.value, formNodeDefaultValue(normalizedItems.value, { index })]);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function removeArrayRow(rowIndex) {
|
|
502
|
+
setArrayRows(arrayRows.value.filter((_, index) => index !== rowIndex));
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function moveArrayRow(rowIndex, dir) {
|
|
506
|
+
const nextIndex = rowIndex + dir;
|
|
507
|
+
if (nextIndex < 0 || nextIndex >= arrayRows.value.length) return;
|
|
508
|
+
const next = [...arrayRows.value];
|
|
509
|
+
[next[rowIndex], next[nextIndex]] = [next[nextIndex], next[rowIndex]];
|
|
510
|
+
setArrayRows(next);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function emitSchemaChange() {
|
|
514
|
+
const nextChildren = snapshot(schemaChildren);
|
|
515
|
+
emit('update:children', nextChildren);
|
|
516
|
+
emit('schema-change', {
|
|
517
|
+
children: nextChildren,
|
|
518
|
+
form: provider,
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function setChildren(nextChildren = []) {
|
|
523
|
+
schemaChildren.splice(0, schemaChildren.length, ...snapshot(normalizeFormChildren(nextChildren)));
|
|
524
|
+
emitSchemaChange();
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function hasChildDefinitions(children) {
|
|
528
|
+
if (!children) return false;
|
|
529
|
+
if (Array.isArray(children)) return children.length > 0;
|
|
530
|
+
if (typeof children === 'object') return Object.keys(children).length > 0;
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function syncChildren() {
|
|
535
|
+
const nextChildren = hasChildDefinitions(props.children)
|
|
536
|
+
? normalizeFormChildren(props.children)
|
|
537
|
+
: schemaToChildren(props.schema);
|
|
538
|
+
schemaChildren.splice(0, schemaChildren.length, ...snapshot(nextChildren));
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function getChildren() {
|
|
542
|
+
return snapshot(schemaChildren);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function addChild(child, index = schemaChildren.length) {
|
|
546
|
+
const at = Math.max(0, Math.min(index, schemaChildren.length));
|
|
547
|
+
schemaChildren.splice(at, 0, snapshot(normalizeFormNode(child)));
|
|
548
|
+
emitSchemaChange();
|
|
549
|
+
return schemaChildren[at];
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function childIndex(match) {
|
|
553
|
+
if (typeof match === 'number') return match;
|
|
554
|
+
return schemaChildren.findIndex((child) => (
|
|
555
|
+
child.id === match
|
|
556
|
+
|| child.props?.name === match
|
|
557
|
+
));
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function removeChild(match) {
|
|
561
|
+
const index = childIndex(match);
|
|
562
|
+
if (index < 0) return null;
|
|
563
|
+
const [removed] = schemaChildren.splice(index, 1);
|
|
564
|
+
emitSchemaChange();
|
|
565
|
+
return removed;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function replaceChild(match, child) {
|
|
569
|
+
const index = childIndex(match);
|
|
570
|
+
if (index < 0) return addChild(child);
|
|
571
|
+
schemaChildren.splice(index, 1, snapshot(normalizeFormNode(child)));
|
|
572
|
+
emitSchemaChange();
|
|
573
|
+
return schemaChildren[index];
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function addSubform(path, children = [], options = {}) {
|
|
577
|
+
const node = {
|
|
578
|
+
id: options.id || `subform-${String(path).replace(/\W+/g, '-')}`,
|
|
579
|
+
component: 'DomForm',
|
|
580
|
+
props: {
|
|
581
|
+
name: path,
|
|
582
|
+
class: options.class || 'space-y-3 rounded-xl border border-border bg-secondary/25 p-4',
|
|
583
|
+
...(options.props || {}),
|
|
584
|
+
},
|
|
585
|
+
children,
|
|
586
|
+
};
|
|
587
|
+
if ('value' in options) setFieldValue(path, options.value);
|
|
588
|
+
return addChild(node, options.index ?? schemaChildren.length);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function setFieldErrors(name, nextErrors) {
|
|
592
|
+
if (Array.isArray(nextErrors) && nextErrors.length) {
|
|
593
|
+
setPathValue(errors, name, nextErrors);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
if (nextErrors && !Array.isArray(nextErrors)) {
|
|
597
|
+
setPathValue(errors, name, nextErrors);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
deletePathValue(errors, name);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function getFieldPath(name) {
|
|
604
|
+
return joinPath(scopePath.value, name);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function getHtmlName(name) {
|
|
608
|
+
return htmlNameFromPath(getFieldPath(name));
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function getHtmlId(name) {
|
|
612
|
+
return htmlIdFromPath(getFieldPath(name));
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function registerField(field) {
|
|
616
|
+
if (!field?.name) return;
|
|
617
|
+
const path = getFieldPath(field.name);
|
|
618
|
+
fields.set(path, {
|
|
619
|
+
...field,
|
|
620
|
+
name: field.name,
|
|
621
|
+
path,
|
|
622
|
+
htmlName: htmlNameFromPath(path),
|
|
623
|
+
htmlId: htmlIdFromPath(path),
|
|
624
|
+
kind: 'field',
|
|
625
|
+
getValue: () => getPathValue(values, path),
|
|
626
|
+
setValue: (value) => {
|
|
627
|
+
setPathValue(values, path, value);
|
|
628
|
+
emitModel();
|
|
629
|
+
emitChange(path);
|
|
630
|
+
},
|
|
631
|
+
getState: () => normalizeFieldState(getPathValue(fieldStates, path) || {}),
|
|
632
|
+
setState: (patch) => {
|
|
633
|
+
setPathValue(fieldStates, path, mergeFieldState(getPathValue(fieldStates, path) || {}, patch));
|
|
634
|
+
if ('errors' in patch) setFieldErrors(path, patch.errors);
|
|
635
|
+
},
|
|
636
|
+
});
|
|
637
|
+
if (getPathValue(values, path) === undefined) {
|
|
638
|
+
setPathValue(values, path, field.initialValue ?? '');
|
|
639
|
+
emitModel();
|
|
640
|
+
}
|
|
641
|
+
if (!getPathValue(fieldStates, path)) setPathValue(fieldStates, path, normalizeFieldState());
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function unregisterField(name) {
|
|
645
|
+
const path = getFieldPath(name);
|
|
646
|
+
fields.delete(path);
|
|
647
|
+
deletePathValue(fieldStates, path);
|
|
648
|
+
deletePathValue(errors, path);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function getFieldValue(name) {
|
|
652
|
+
return getPathValue(values, getFieldPath(name));
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function setFieldValue(name, value) {
|
|
656
|
+
const path = getFieldPath(name);
|
|
657
|
+
setPathValue(values, path, value);
|
|
658
|
+
emitModel();
|
|
659
|
+
emitChange(path);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function getValue(name) {
|
|
663
|
+
return getFieldValue(name);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function setValue(name, value) {
|
|
667
|
+
setFieldValue(name, value);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function getFieldState(name) {
|
|
671
|
+
return normalizeFieldState(getPathValue(fieldStates, getFieldPath(name)) || {});
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function getSubform(name) {
|
|
675
|
+
return subforms[getFieldPath(name)];
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function getField(name) {
|
|
679
|
+
return fields.get(getFieldPath(name)) || null;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function getForm(name = '') {
|
|
683
|
+
if (!name) return provider;
|
|
684
|
+
return getSubform(name) || null;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function get(name) {
|
|
688
|
+
return getForm(name) || getField(name);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function isFormApi(value = provider) {
|
|
692
|
+
return value?.kind === 'form';
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function isFieldApi(value) {
|
|
696
|
+
return value?.kind === 'field';
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function setFieldState(name, patch) {
|
|
700
|
+
const path = getFieldPath(name);
|
|
701
|
+
setPathValue(fieldStates, path, mergeFieldState(getPathValue(fieldStates, path) || {}, patch));
|
|
702
|
+
if ('errors' in patch) setFieldErrors(path, patch.errors);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function getState() {
|
|
706
|
+
const fieldEntries = scopedFieldEntries.value.map(([path, field]) => {
|
|
707
|
+
const fieldState = normalizeFieldState(getPathValue(fieldStates, path) || {});
|
|
708
|
+
return [path, {
|
|
709
|
+
name: field.name,
|
|
710
|
+
path,
|
|
711
|
+
label: field.label,
|
|
712
|
+
required: Boolean(field.required),
|
|
713
|
+
htmlName: field.htmlName,
|
|
714
|
+
htmlId: field.htmlId,
|
|
715
|
+
value: typeof field.getValue === 'function' ? field.getValue() : getPathValue(values, path),
|
|
716
|
+
state: fieldState,
|
|
717
|
+
errors: fieldState.errors,
|
|
718
|
+
}];
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
return {
|
|
722
|
+
path: scopePath.value,
|
|
723
|
+
state: state.value,
|
|
724
|
+
values: snapshot(scopePath.value ? getPathValue(values, scopePath.value) : values),
|
|
725
|
+
errors: snapshot(errors),
|
|
726
|
+
fieldStates: Object.fromEntries(scopedFieldEntries.value.map(([path]) => [
|
|
727
|
+
path,
|
|
728
|
+
normalizeFieldState(getPathValue(fieldStates, path) || {}),
|
|
729
|
+
])),
|
|
730
|
+
fields: Object.fromEntries(fieldEntries),
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
async function validate() {
|
|
735
|
+
const results = await Promise.all(scopedFieldEntries.value.map(async ([, field]) => {
|
|
736
|
+
if (typeof field.validate !== 'function') return true;
|
|
737
|
+
return field.validate({ form: provider });
|
|
738
|
+
}));
|
|
739
|
+
return results.every(Boolean);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function reset(nextValues = props.modelValue || {}) {
|
|
743
|
+
if (isRoot) {
|
|
744
|
+
replaceReactiveValue(values, isArrayMode.value ? (Array.isArray(nextValues) ? nextValues : []) : nextValues);
|
|
745
|
+
for (const key of Object.keys(fieldStates)) delete fieldStates[key];
|
|
746
|
+
for (const key of Object.keys(errors)) delete errors[key];
|
|
747
|
+
} else {
|
|
748
|
+
setPathValue(values, scopePath.value, nextValues);
|
|
749
|
+
for (const [path] of scopedFieldEntries.value) {
|
|
750
|
+
deletePathValue(fieldStates, path);
|
|
751
|
+
deletePathValue(errors, path);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
emitModel();
|
|
755
|
+
emitChange(scopePath.value);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
async function onSubmit(event) {
|
|
759
|
+
if (event) event.preventDefault();
|
|
760
|
+
const valid = props.validateOnSubmit ? await validate() : state.value.valid;
|
|
761
|
+
emit(valid ? 'submit' : 'invalid', payload(scopePath.value));
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const provider = {
|
|
765
|
+
kind: 'form',
|
|
766
|
+
values,
|
|
767
|
+
errors,
|
|
768
|
+
fieldStates,
|
|
769
|
+
fields,
|
|
770
|
+
subforms,
|
|
771
|
+
state,
|
|
772
|
+
children: schemaChildren,
|
|
773
|
+
scopePath: scopePath.value,
|
|
774
|
+
getValue,
|
|
775
|
+
setValue,
|
|
776
|
+
getFieldPath,
|
|
777
|
+
getHtmlName,
|
|
778
|
+
getHtmlId,
|
|
779
|
+
getFieldState,
|
|
780
|
+
setFieldState,
|
|
781
|
+
getState,
|
|
782
|
+
getFieldValue,
|
|
783
|
+
setFieldValue,
|
|
784
|
+
get,
|
|
785
|
+
getField,
|
|
786
|
+
getForm,
|
|
787
|
+
isForm: isFormApi,
|
|
788
|
+
isField: isFieldApi,
|
|
789
|
+
getSubform,
|
|
790
|
+
getChildren,
|
|
791
|
+
setChildren,
|
|
792
|
+
addChild,
|
|
793
|
+
removeChild,
|
|
794
|
+
replaceChild,
|
|
795
|
+
addSubform,
|
|
796
|
+
addArrayRow,
|
|
797
|
+
removeArrayRow,
|
|
798
|
+
moveArrayRow,
|
|
799
|
+
registerField,
|
|
800
|
+
unregisterField,
|
|
801
|
+
validate,
|
|
802
|
+
reset,
|
|
803
|
+
emitModel,
|
|
804
|
+
emitChange,
|
|
805
|
+
};
|
|
806
|
+
|
|
807
|
+
watch(scopePath, (nextPath) => {
|
|
808
|
+
provider.scopePath = nextPath;
|
|
809
|
+
}, { immediate: true });
|
|
810
|
+
|
|
811
|
+
watch(() => props.modelValue, (nextValue) => {
|
|
812
|
+
if (!isRoot) return;
|
|
813
|
+
if (isArrayMode.value) {
|
|
814
|
+
replaceReactiveValue(values, Array.isArray(nextValue) ? nextValue : []);
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
syncReactiveObject(values, nextValue);
|
|
818
|
+
}, { deep: true });
|
|
819
|
+
|
|
820
|
+
watch([isArrayMode, scopePath], () => {
|
|
821
|
+
if (!isArrayMode.value || isRoot || !scopePath.value) return;
|
|
822
|
+
if (!Array.isArray(getPathValue(values, scopePath.value))) {
|
|
823
|
+
setPathValue(values, scopePath.value, []);
|
|
824
|
+
emitModel();
|
|
825
|
+
}
|
|
826
|
+
}, { immediate: true });
|
|
827
|
+
|
|
828
|
+
watch([() => props.children, () => props.schema], syncChildren, { deep: true, immediate: true });
|
|
829
|
+
|
|
830
|
+
watch(() => props.name, (nextName, previousName) => {
|
|
831
|
+
if (previousName && forms[previousName] === provider) delete forms[previousName];
|
|
832
|
+
if (nextName && isRoot) forms[nextName] = provider;
|
|
833
|
+
}, { immediate: true });
|
|
834
|
+
|
|
835
|
+
watch(scopePath, (nextPath, previousPath) => {
|
|
836
|
+
if (previousPath && subforms[previousPath] === provider) delete subforms[previousPath];
|
|
837
|
+
if (nextPath && !isRoot) {
|
|
838
|
+
subforms[nextPath] = provider;
|
|
839
|
+
const directName = props.name;
|
|
840
|
+
if (directName && !String(directName).includes('.')) parent[directName] = provider;
|
|
841
|
+
}
|
|
842
|
+
}, { immediate: true });
|
|
843
|
+
|
|
844
|
+
provide(formFieldProviderKey, provider);
|
|
845
|
+
expose(provider);
|
|
846
|
+
|
|
847
|
+
function renderGroupHeader() {
|
|
848
|
+
if (!props.label && !props.description) return [];
|
|
849
|
+
return [
|
|
850
|
+
h('div', { class: 'space-y-0.5' }, [
|
|
851
|
+
props.label
|
|
852
|
+
? h('p', { class: 'text-[11px] font-semibold uppercase tracking-wider text-muted-fg' }, [
|
|
853
|
+
props.label,
|
|
854
|
+
props.required ? h('span', { class: 'text-destructive' }, '*') : null,
|
|
855
|
+
])
|
|
856
|
+
: null,
|
|
857
|
+
props.description
|
|
858
|
+
? h('p', { class: 'text-[11px] leading-snug text-muted-fg' }, props.description)
|
|
859
|
+
: null,
|
|
860
|
+
]),
|
|
861
|
+
];
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function renderRoot(children) {
|
|
865
|
+
if (props.visible === false) return null;
|
|
866
|
+
const tag = props.tag || (isRoot ? 'form' : 'fieldset');
|
|
867
|
+
const rootProps = {
|
|
868
|
+
...attrs,
|
|
869
|
+
...(tag === 'form' ? {
|
|
870
|
+
noValidate: attrs.noValidate ?? attrs.novalidate ?? true,
|
|
871
|
+
onSubmit,
|
|
872
|
+
} : {}),
|
|
873
|
+
};
|
|
874
|
+
return h(tag, rootProps, children);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function rowButton(label, title, onClick) {
|
|
878
|
+
return h('button', {
|
|
879
|
+
type: 'button',
|
|
880
|
+
class: [
|
|
881
|
+
'grid place-items-center rounded-md text-muted-fg hover:bg-secondary hover:text-canvas-fg',
|
|
882
|
+
props.compact ? 'size-6' : 'size-7',
|
|
883
|
+
],
|
|
884
|
+
title,
|
|
885
|
+
'aria-label': title,
|
|
886
|
+
onClick,
|
|
887
|
+
}, label);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function renderArrayItem(row, rowIndex) {
|
|
891
|
+
const item = normalizedItems.value || normalizeFormNode({ type: 'json' });
|
|
892
|
+
const itemContent = item.type === 'object'
|
|
893
|
+
? h(DomFormComponent, {
|
|
894
|
+
name: String(rowIndex),
|
|
895
|
+
type: 'object',
|
|
896
|
+
children: item.children || [],
|
|
897
|
+
compact: props.compact,
|
|
898
|
+
tag: 'fieldset',
|
|
899
|
+
class: props.compact ? 'space-y-1.5' : 'space-y-2',
|
|
900
|
+
})
|
|
901
|
+
: h(RenderNode, {
|
|
902
|
+
node: {
|
|
903
|
+
...item,
|
|
904
|
+
props: {
|
|
905
|
+
...(item.props || {}),
|
|
906
|
+
name: String(rowIndex),
|
|
907
|
+
label: item.props?.label || `Row ${rowIndex + 1}`,
|
|
908
|
+
},
|
|
909
|
+
},
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
return h('div', {
|
|
913
|
+
key: rowIndex,
|
|
914
|
+
class: [
|
|
915
|
+
'rounded-lg border border-border bg-canvas',
|
|
916
|
+
props.compact ? 'space-y-1.5 p-1.5' : 'space-y-2 p-2',
|
|
917
|
+
],
|
|
918
|
+
}, [
|
|
919
|
+
h('div', { class: 'flex items-center justify-between gap-2' }, [
|
|
920
|
+
h('span', { class: 'text-[11px] font-semibold uppercase tracking-wider text-muted-fg' }, `Row ${rowIndex + 1}`),
|
|
921
|
+
h('span', { class: 'flex items-center gap-1' }, [
|
|
922
|
+
rowButton('↑', `Move row ${rowIndex + 1} up`, () => moveArrayRow(rowIndex, -1)),
|
|
923
|
+
rowButton('↓', `Move row ${rowIndex + 1} down`, () => moveArrayRow(rowIndex, 1)),
|
|
924
|
+
h('button', {
|
|
925
|
+
type: 'button',
|
|
926
|
+
class: [
|
|
927
|
+
'grid place-items-center rounded-md text-destructive hover:bg-destructive/10',
|
|
928
|
+
props.compact ? 'size-6' : 'size-7',
|
|
929
|
+
],
|
|
930
|
+
title: 'Remove',
|
|
931
|
+
'aria-label': `Remove row ${rowIndex + 1}`,
|
|
932
|
+
onClick: () => removeArrayRow(rowIndex),
|
|
933
|
+
}, '×'),
|
|
934
|
+
]),
|
|
935
|
+
]),
|
|
936
|
+
itemContent,
|
|
937
|
+
]);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function renderArray(slotChildren) {
|
|
941
|
+
return [
|
|
942
|
+
...renderGroupHeader(),
|
|
943
|
+
h('div', { class: props.compact ? 'space-y-1.5' : 'space-y-2' }, [
|
|
944
|
+
...arrayRows.value.map((row, index) => renderArrayItem(row, index)),
|
|
945
|
+
h('button', {
|
|
946
|
+
type: 'button',
|
|
947
|
+
class: [
|
|
948
|
+
'flex w-full items-center justify-center gap-1 rounded-lg border border-dashed border-border bg-canvas text-xs font-medium text-muted-fg hover:border-primary/40 hover:text-canvas-fg',
|
|
949
|
+
props.compact ? 'py-1.5' : 'py-2',
|
|
950
|
+
],
|
|
951
|
+
onClick: addArrayRow,
|
|
952
|
+
}, arrayAddLabel.value),
|
|
953
|
+
]),
|
|
954
|
+
...slotChildren,
|
|
955
|
+
];
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
return () => {
|
|
959
|
+
const slotProps = {
|
|
960
|
+
values,
|
|
961
|
+
errors,
|
|
962
|
+
state: state.value,
|
|
963
|
+
form: provider,
|
|
964
|
+
children: schemaChildren,
|
|
965
|
+
validate,
|
|
966
|
+
reset,
|
|
967
|
+
};
|
|
968
|
+
const renderedChildren = schemaChildren.map((node, index) => (
|
|
969
|
+
h(RenderNode, { node, key: `schema-${node.id || index}` })
|
|
970
|
+
));
|
|
971
|
+
const slotChildren = slots.default ? slots.default(slotProps) : [];
|
|
972
|
+
|
|
973
|
+
return renderRoot(isArrayMode.value
|
|
974
|
+
? renderArray(slotChildren)
|
|
975
|
+
: [
|
|
976
|
+
...renderGroupHeader(),
|
|
977
|
+
...renderedChildren,
|
|
978
|
+
...slotChildren,
|
|
979
|
+
]);
|
|
980
|
+
};
|
|
981
|
+
},
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
export default DomFormComponent;
|
|
985
|
+
</script>
|