@omnitend/dashboard-for-laravel 0.33.0 → 0.34.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/dist/charts.js +82 -90
- package/dist/charts.js.map +1 -1
- package/dist/charts.umd.cjs +1 -1
- package/dist/charts.umd.cjs.map +1 -1
- package/dist/components/base/DButton.vue.d.ts +1 -1
- package/dist/components/charts/DXBarChart.vue.d.ts +3 -18
- package/dist/components/charts/DXLineChart.vue.d.ts +3 -18
- package/dist/components/charts/useThemedChart.d.ts +35 -0
- package/dist/components/extended/DXChoiceField.vue.d.ts +26 -0
- package/dist/components/extended/DXDashboardSidebar.vue.d.ts +1 -1
- package/dist/components/extended/DXField.vue.d.ts +10 -113
- package/dist/components/extended/DXFieldShell.vue.d.ts +73 -0
- package/dist/components/extended/DXNumericField.vue.d.ts +23 -0
- package/dist/components/extended/DXRepeater.vue.d.ts +4 -4
- package/dist/components/extended/DXRepeaterCards.vue.d.ts +45 -0
- package/dist/components/extended/DXTable.vue.d.ts +91 -2
- package/dist/components/extended/DXTableEditorModal.vue.d.ts +17 -2
- package/dist/components/extended/DXUserAvatar.vue.d.ts +1 -1
- package/dist/composables/useAsyncOptions.d.ts +20 -0
- package/dist/composables/useResourceEditor.d.ts +2 -1
- package/dist/dashboard-for-laravel.js +9987 -10054
- package/dist/dashboard-for-laravel.js.map +1 -1
- package/dist/dashboard-for-laravel.umd.cjs +7 -7
- package/dist/dashboard-for-laravel.umd.cjs.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/style.css +1 -1
- package/dist/types/index.d.ts +146 -0
- package/dist/utils/formSchema.d.ts +59 -0
- package/docs/public/api-reference.json +464 -30
- package/docs/public/docs-map.md +1 -1
- package/docs/public/llms.txt +5 -2
- package/package.json +1 -1
- package/resources/js/components/charts/DXBarChart.vue +4 -43
- package/resources/js/components/charts/DXLineChart.vue +4 -43
- package/resources/js/components/charts/useThemedChart.ts +69 -0
- package/resources/js/components/extended/DXChoiceField.vue +114 -0
- package/resources/js/components/extended/DXField.vue +328 -615
- package/resources/js/components/extended/DXFieldShell.vue +124 -0
- package/resources/js/components/extended/DXForm.vue +9 -5
- package/resources/js/components/extended/DXNumericField.vue +144 -0
- package/resources/js/components/extended/DXRepeater.vue +53 -175
- package/resources/js/components/extended/DXRepeaterCards.vue +133 -0
- package/resources/js/components/extended/DXTable.vue +250 -37
- package/resources/js/components/extended/DXTableEditorModal.vue +34 -2
- package/resources/js/composables/defineForm.ts +8 -44
- package/resources/js/composables/useAsyncOptions.ts +61 -0
- package/resources/js/composables/useResourceEditor.ts +24 -38
- package/resources/js/index.ts +33 -0
- package/resources/js/types/index.ts +231 -0
- package/resources/js/utils/formSchema.ts +126 -0
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
@component
|
|
3
3
|
Renders a single `FieldDefinition` of any type (text, select, autocomplete, checkbox, switch, repeater, currency, percentage, file/image, component, etc.) with its label, validation error, hint and help. Used by `DXForm` and `DXTable`'s edit modal; exposes `value`, `span`, `info`, `info-popover`, `hint` and `repeater-row` slots for per-field customisation.
|
|
4
|
+
|
|
5
|
+
Structure (#135): a thin DISPATCHER. The shared chrome (label + validation
|
|
6
|
+
feedback + hint + help + info) lives in `DXFieldShell`; the numeric controls
|
|
7
|
+
(currency/percentage/number) in `DXNumericField`; the choice controls
|
|
8
|
+
(select/searchable/radio/checkbox-group) in `DXChoiceField`; and the async
|
|
9
|
+
option loading in the `useAsyncOptions` composable. This file decides which
|
|
10
|
+
control to render inside the shell by `field.type`. None of those pieces are
|
|
11
|
+
exported — they exist only to keep this component factored.
|
|
4
12
|
-->
|
|
5
13
|
<template>
|
|
6
|
-
<!-- Full-width span field: delegate entirely to the #span slot
|
|
14
|
+
<!-- Full-width span field: delegate entirely to the #span slot, bypassing
|
|
15
|
+
the label and built-in control (and the shell). -->
|
|
7
16
|
<div v-if="field.span" :class="field.class || 'mb-3'">
|
|
8
17
|
<!--
|
|
9
18
|
@slot Replaces the entire field with custom full-width content when `field.span` is set, bypassing the label and built-in control.
|
|
@@ -21,329 +30,45 @@
|
|
|
21
30
|
/>
|
|
22
31
|
</div>
|
|
23
32
|
|
|
24
|
-
<!--
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
:
|
|
36
|
-
|
|
33
|
+
<!-- Every other field type shares the same chrome via DXFieldShell; the
|
|
34
|
+
control that goes inside is chosen by field.type in the default slot. -->
|
|
35
|
+
<DXFieldShell
|
|
36
|
+
v-else
|
|
37
|
+
:field="field"
|
|
38
|
+
:form="form"
|
|
39
|
+
:error-key="errorKey"
|
|
40
|
+
:model="model"
|
|
41
|
+
:mode="shellMode"
|
|
42
|
+
:wrapper-class="field.class || 'mb-3'"
|
|
43
|
+
:group-attrs="horizontalAttrs"
|
|
44
|
+
:label="resolvedLabel"
|
|
45
|
+
:info="resolvedInfo"
|
|
46
|
+
:hide-label="hideLabel"
|
|
47
|
+
:hint-in-label="hintInLabel"
|
|
48
|
+
:resolved-hint="resolvedHint"
|
|
37
49
|
>
|
|
38
|
-
<!--
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
<template v-if="!hideLabel" #label>
|
|
43
|
-
<DXFieldLabel :label="resolvedLabel" :info="resolvedInfo" />
|
|
44
|
-
<small
|
|
45
|
-
v-if="isHorizontal && (resolvedHint || $slots.hint)"
|
|
46
|
-
class="form-text text-muted d-block dx-field-hint"
|
|
47
|
-
>
|
|
48
|
-
<slot name="hint" :field="field" :model="model">{{
|
|
49
|
-
resolvedHint
|
|
50
|
-
}}</slot>
|
|
51
|
-
</small>
|
|
52
|
-
</template>
|
|
53
|
-
<slot
|
|
54
|
-
v-if="$slots.value"
|
|
55
|
-
name="value"
|
|
56
|
-
:field="field"
|
|
57
|
-
:model="model"
|
|
58
|
-
:value="fieldValue"
|
|
59
|
-
:update="setValue"
|
|
60
|
-
/>
|
|
61
|
-
<DFormCheckbox
|
|
62
|
-
v-else
|
|
63
|
-
v-model="fieldValue"
|
|
64
|
-
:disabled="isDisabled || isReadonly || isPlaintext"
|
|
65
|
-
v-bind="controlPropsWithGuards"
|
|
66
|
-
>
|
|
67
|
-
<DXFieldLabel :label="resolvedLabel" class="visually-hidden" />
|
|
68
|
-
</DFormCheckbox>
|
|
69
|
-
|
|
70
|
-
<DFormInvalidFeedback v-if="form.hasError(errorKey)" force-show>
|
|
71
|
-
{{ form.getError(errorKey) }}
|
|
72
|
-
</DFormInvalidFeedback>
|
|
73
|
-
<slot name="info" :field="field" :model="model" />
|
|
74
|
-
<DFormText v-if="!hintInLabel && (resolvedHint || $slots.hint)" class="text-muted">
|
|
75
|
-
<slot name="hint" :field="field" :model="model">{{ resolvedHint }}</slot>
|
|
76
|
-
</DFormText>
|
|
77
|
-
<DFormText v-if="field.help">{{ field.help }}</DFormText>
|
|
78
|
-
</DFormGroup>
|
|
79
|
-
|
|
80
|
-
<!-- Checkbox (vertical, default): no label wrapper, label sits beside the control -->
|
|
81
|
-
<div v-else-if="field.type === 'checkbox'" :class="field.class || 'mb-3'">
|
|
82
|
-
<!--
|
|
83
|
-
@slot Replaces the built-in control with a custom value editor.
|
|
84
|
-
@binding {FieldDefinition} field The field definition being rendered.
|
|
85
|
-
@binding {any} model The model passed to field predicates (defaults to the live form data).
|
|
86
|
-
@binding {any} value The current field value.
|
|
87
|
-
@binding {(value: any) => void} update Setter that writes the value back and clears the field's validation error.
|
|
88
|
-
-->
|
|
89
|
-
<slot
|
|
90
|
-
v-if="$slots.value"
|
|
91
|
-
name="value"
|
|
92
|
-
:field="field"
|
|
93
|
-
:model="model"
|
|
94
|
-
:value="fieldValue"
|
|
95
|
-
:update="setValue"
|
|
96
|
-
/>
|
|
97
|
-
<DFormCheckbox
|
|
98
|
-
v-else
|
|
99
|
-
v-model="fieldValue"
|
|
100
|
-
:disabled="isDisabled || isReadonly || isPlaintext"
|
|
101
|
-
v-bind="controlPropsWithGuards"
|
|
102
|
-
>
|
|
103
|
-
<DXFieldLabel :label="resolvedLabel" :info="resolvedInfo" />
|
|
104
|
-
</DFormCheckbox>
|
|
105
|
-
|
|
106
|
-
<DFormInvalidFeedback v-if="form.hasError(errorKey)" force-show>
|
|
107
|
-
{{ form.getError(errorKey) }}
|
|
108
|
-
</DFormInvalidFeedback>
|
|
109
|
-
<!--
|
|
110
|
-
@slot Rich info block rendered below the control.
|
|
111
|
-
@binding {FieldDefinition} field The field definition being rendered.
|
|
112
|
-
@binding {any} model The model passed to field predicates (defaults to the live form data).
|
|
113
|
-
-->
|
|
114
|
-
<slot name="info" :field="field" :model="model" />
|
|
115
|
-
<DFormText v-if="resolvedHint || $slots.hint" class="text-muted">
|
|
50
|
+
<!-- Forward the field's hint slot (content + props) only when present,
|
|
51
|
+
so the shell's `$slots.hint` reflects reality (it gates the hint
|
|
52
|
+
rendering); the info slot is a pure pass-through. -->
|
|
53
|
+
<template v-if="$slots.hint" #hint="hintProps">
|
|
116
54
|
<!--
|
|
117
|
-
@slot Overrides the field's hint text shown below the control.
|
|
55
|
+
@slot Overrides the field's hint text shown below the control (or, in horizontal layout, in the label column).
|
|
118
56
|
@binding {FieldDefinition} field The field definition being rendered.
|
|
119
57
|
@binding {any} model The model passed to field predicates (defaults to the live form data).
|
|
120
58
|
-->
|
|
121
|
-
<slot name="hint"
|
|
122
|
-
</DFormText>
|
|
123
|
-
<DFormText v-if="field.help">{{ field.help }}</DFormText>
|
|
124
|
-
</div>
|
|
125
|
-
|
|
126
|
-
<!-- Switch (horizontal layout): same column-split treatment as checkbox
|
|
127
|
-
above, including omitting `info` from the inner label to avoid a
|
|
128
|
-
duplicated popover trigger. -->
|
|
129
|
-
<DFormGroup
|
|
130
|
-
v-else-if="field.type === 'switch' && isHorizontal"
|
|
131
|
-
:class="field.class || 'mb-3'"
|
|
132
|
-
v-bind="horizontalAttrs"
|
|
133
|
-
>
|
|
134
|
-
<!-- The row label (left column) names the control. The switch's own
|
|
135
|
-
inner text is shown only when the field opts into contextual on/off
|
|
136
|
-
text (e.g. "Product is current"); otherwise it falls back to the row
|
|
137
|
-
label and is hidden visually (kept for screen readers). In horizontal
|
|
138
|
-
layout the hint sits beneath the label here rather than below the
|
|
139
|
-
control. -->
|
|
140
|
-
<template v-if="!hideLabel" #label>
|
|
141
|
-
<DXFieldLabel :label="resolvedLabel" :info="resolvedInfo" />
|
|
142
|
-
<small
|
|
143
|
-
v-if="isHorizontal && (resolvedHint || $slots.hint)"
|
|
144
|
-
class="form-text text-muted d-block dx-field-hint"
|
|
145
|
-
>
|
|
146
|
-
<slot name="hint" :field="field" :model="model">{{
|
|
147
|
-
resolvedHint
|
|
148
|
-
}}</slot>
|
|
149
|
-
</small>
|
|
59
|
+
<slot name="hint" v-bind="hintProps" />
|
|
150
60
|
</template>
|
|
151
|
-
<
|
|
152
|
-
v-if="$slots.value"
|
|
153
|
-
name="value"
|
|
154
|
-
:field="field"
|
|
155
|
-
:model="model"
|
|
156
|
-
:value="fieldValue"
|
|
157
|
-
:update="setValue"
|
|
158
|
-
/>
|
|
159
|
-
<DXSwitch
|
|
160
|
-
v-else
|
|
161
|
-
v-model="switchModel"
|
|
162
|
-
:on-variant="field.switchVariant"
|
|
163
|
-
:disabled="isDisabled || isReadonly || isPlaintext"
|
|
164
|
-
v-bind="controlPropsWithGuards"
|
|
165
|
-
>
|
|
166
|
-
<DXFieldLabel
|
|
167
|
-
:label="switchText"
|
|
168
|
-
:class="switchHasContextualText ? undefined : 'visually-hidden'"
|
|
169
|
-
/>
|
|
170
|
-
</DXSwitch>
|
|
171
|
-
|
|
172
|
-
<DFormInvalidFeedback v-if="form.hasError(errorKey)" force-show>
|
|
173
|
-
{{ form.getError(errorKey) }}
|
|
174
|
-
</DFormInvalidFeedback>
|
|
175
|
-
<slot name="info" :field="field" :model="model" />
|
|
176
|
-
<DFormText v-if="!hintInLabel && (resolvedHint || $slots.hint)" class="text-muted">
|
|
177
|
-
<slot name="hint" :field="field" :model="model">{{ resolvedHint }}</slot>
|
|
178
|
-
</DFormText>
|
|
179
|
-
<DFormText v-if="field.help">{{ field.help }}</DFormText>
|
|
180
|
-
</DFormGroup>
|
|
181
|
-
|
|
182
|
-
<!-- Switch (vertical, default): toggle with contextual on/off text (DXSwitch owns the filled-box on-state style) -->
|
|
183
|
-
<div
|
|
184
|
-
v-else-if="field.type === 'switch'"
|
|
185
|
-
:class="field.class || 'mb-3'"
|
|
186
|
-
>
|
|
187
|
-
<!--
|
|
188
|
-
@slot Replaces the built-in control with a custom value editor.
|
|
189
|
-
@binding {FieldDefinition} field The field definition being rendered.
|
|
190
|
-
@binding {any} model The model passed to field predicates (defaults to the live form data).
|
|
191
|
-
@binding {any} value The current field value.
|
|
192
|
-
@binding {(value: any) => void} update Setter that writes the value back and clears the field's validation error.
|
|
193
|
-
-->
|
|
194
|
-
<slot
|
|
195
|
-
v-if="$slots.value"
|
|
196
|
-
name="value"
|
|
197
|
-
:field="field"
|
|
198
|
-
:model="model"
|
|
199
|
-
:value="fieldValue"
|
|
200
|
-
:update="setValue"
|
|
201
|
-
/>
|
|
202
|
-
<DXSwitch
|
|
203
|
-
v-else
|
|
204
|
-
v-model="switchModel"
|
|
205
|
-
:on-variant="field.switchVariant"
|
|
206
|
-
:disabled="isDisabled || isReadonly || isPlaintext"
|
|
207
|
-
v-bind="controlPropsWithGuards"
|
|
208
|
-
>
|
|
209
|
-
<DXFieldLabel :label="switchText" :info="resolvedInfo" />
|
|
210
|
-
</DXSwitch>
|
|
211
|
-
|
|
212
|
-
<DFormInvalidFeedback v-if="form.hasError(errorKey)" force-show>
|
|
213
|
-
{{ form.getError(errorKey) }}
|
|
214
|
-
</DFormInvalidFeedback>
|
|
215
|
-
<!--
|
|
216
|
-
@slot Rich info block rendered below the control.
|
|
217
|
-
@binding {FieldDefinition} field The field definition being rendered.
|
|
218
|
-
@binding {any} model The model passed to field predicates (defaults to the live form data).
|
|
219
|
-
-->
|
|
220
|
-
<slot name="info" :field="field" :model="model" />
|
|
221
|
-
<DFormText v-if="resolvedHint || $slots.hint" class="text-muted">
|
|
61
|
+
<template #info="infoProps">
|
|
222
62
|
<!--
|
|
223
|
-
@slot
|
|
63
|
+
@slot Rich info block rendered below the control.
|
|
224
64
|
@binding {FieldDefinition} field The field definition being rendered.
|
|
225
65
|
@binding {any} model The model passed to field predicates (defaults to the live form data).
|
|
226
66
|
-->
|
|
227
|
-
<slot name="
|
|
228
|
-
</DFormText>
|
|
229
|
-
<DFormText v-if="field.help">{{ field.help }}</DFormText>
|
|
230
|
-
</div>
|
|
231
|
-
|
|
232
|
-
<!-- Switch-list (#160): a list of labelled boolean toggles as config, not
|
|
233
|
-
markup. One real DFormGroup per option row — the ONLY correct source of
|
|
234
|
-
the label/content grid (hardcoding col classes is exactly the drift
|
|
235
|
-
this field exists to end) — with the option text in the label column
|
|
236
|
-
and a compact track switch (not the filled-box DXSwitch, too heavy in
|
|
237
|
-
a dense list) in the content column. The model is an array of selected
|
|
238
|
-
option values, like checkbox-group. -->
|
|
239
|
-
<div v-else-if="field.type === 'switch-list'" :class="field.class || 'mb-3'">
|
|
240
|
-
<div
|
|
241
|
-
v-if="!hideLabel && field.label"
|
|
242
|
-
class="dx-switch-list-heading text-muted fw-semibold"
|
|
243
|
-
>
|
|
244
|
-
<DXFieldLabel :label="resolvedLabel" :info="resolvedInfo" />
|
|
245
|
-
</div>
|
|
246
|
-
<div
|
|
247
|
-
v-for="option in resolvedOptions ?? []"
|
|
248
|
-
:key="String(option.value)"
|
|
249
|
-
class="dx-switch-list-row"
|
|
250
|
-
>
|
|
251
|
-
<DFormGroup v-bind="switchListRowAttrs" class="mb-0 align-items-center">
|
|
252
|
-
<template #label>
|
|
253
|
-
<DXFieldLabel
|
|
254
|
-
:label="option.text"
|
|
255
|
-
:title="option.description"
|
|
256
|
-
/>
|
|
257
|
-
</template>
|
|
258
|
-
<div class="dx-switch-list-control">
|
|
259
|
-
<DFormCheckbox
|
|
260
|
-
switch
|
|
261
|
-
:class="field.switchVariant === 'neutral' ? 'switch-neutral' : undefined"
|
|
262
|
-
:model-value="switchListIsOn(option)"
|
|
263
|
-
:disabled="isDisabled || isReadonly || isPlaintext || option.disabled === true"
|
|
264
|
-
:aria-label="option.text"
|
|
265
|
-
@update:model-value="setSwitchListOn(option, $event === true)"
|
|
266
|
-
/>
|
|
267
|
-
<!--
|
|
268
|
-
@slot Extra per-row content beside a switch-list row's toggle (e.g. a notes input the consumer binds to their own model).
|
|
269
|
-
@binding {FieldOption} option The option this row renders.
|
|
270
|
-
@binding {boolean} on Whether the row's switch is currently on.
|
|
271
|
-
-->
|
|
272
|
-
<slot
|
|
273
|
-
name="switch-list-item"
|
|
274
|
-
:option="option"
|
|
275
|
-
:on="switchListIsOn(option)"
|
|
276
|
-
/>
|
|
277
|
-
</div>
|
|
278
|
-
</DFormGroup>
|
|
279
|
-
</div>
|
|
280
|
-
|
|
281
|
-
<DFormInvalidFeedback v-if="form.hasError(errorKey)" force-show>
|
|
282
|
-
{{ form.getError(errorKey) }}
|
|
283
|
-
</DFormInvalidFeedback>
|
|
284
|
-
<slot name="info" :field="field" :model="model" />
|
|
285
|
-
<DFormText v-if="resolvedHint || $slots.hint" class="text-muted">
|
|
286
|
-
<slot name="hint" :field="field" :model="model">{{ resolvedHint }}</slot>
|
|
287
|
-
</DFormText>
|
|
288
|
-
<DFormText v-if="field.help">{{ field.help }}</DFormText>
|
|
289
|
-
</div>
|
|
290
|
-
|
|
291
|
-
<!-- Repeater: nested, repeatable sub-form -->
|
|
292
|
-
<div v-else-if="field.type === 'repeater'" :class="field.class || 'mb-3'">
|
|
293
|
-
<DFormGroup v-bind="repeaterHorizontalAttrs">
|
|
294
|
-
<template v-if="!hideLabel" #label>
|
|
295
|
-
<DXFieldLabel :label="resolvedLabel" :info="resolvedInfo" />
|
|
296
|
-
</template>
|
|
297
|
-
<DXRepeater
|
|
298
|
-
:form="form"
|
|
299
|
-
:field="field"
|
|
300
|
-
:key-path="keyPath"
|
|
301
|
-
:model="model"
|
|
302
|
-
>
|
|
303
|
-
<!-- Forward repeater row slot for custom row layouts -->
|
|
304
|
-
<template v-if="$slots['repeater-row']" #row="rowProps">
|
|
305
|
-
<!-- @slot Custom layout for a single repeater row, forwarded to `DXRepeater`'s `row` slot with its row props (row index, item, remove handler, etc.). -->
|
|
306
|
-
<slot name="repeater-row" v-bind="rowProps" />
|
|
307
|
-
</template>
|
|
308
|
-
</DXRepeater>
|
|
309
|
-
</DFormGroup>
|
|
310
|
-
<!--
|
|
311
|
-
@slot Rich info block rendered below the control.
|
|
312
|
-
@binding {FieldDefinition} field The field definition being rendered.
|
|
313
|
-
@binding {any} model The model passed to field predicates (defaults to the live form data).
|
|
314
|
-
-->
|
|
315
|
-
<slot name="info" :field="field" :model="model" />
|
|
316
|
-
<DFormText v-if="resolvedHint || $slots.hint" class="text-muted">
|
|
317
|
-
<!--
|
|
318
|
-
@slot Overrides the field's hint text shown below the control.
|
|
319
|
-
@binding {FieldDefinition} field The field definition being rendered.
|
|
320
|
-
@binding {any} model The model passed to field predicates (defaults to the live form data).
|
|
321
|
-
-->
|
|
322
|
-
<slot name="hint" :field="field" :model="model">{{ resolvedHint }}</slot>
|
|
323
|
-
</DFormText>
|
|
324
|
-
<DFormText v-if="field.help">{{ field.help }}</DFormText>
|
|
325
|
-
</div>
|
|
326
|
-
|
|
327
|
-
<!-- Standard labelled field -->
|
|
328
|
-
<DFormGroup v-else :class="field.class || 'mb-3'" v-bind="horizontalAttrs">
|
|
329
|
-
<!-- Label with optional info popover. Omitted entirely (not just
|
|
330
|
-
emptied) when hideLabel is set, so BFormGroup doesn't reserve a
|
|
331
|
-
label column/row for it (see DXRepeater's table layout). In
|
|
332
|
-
horizontal layout the hint sits beneath the label here rather than
|
|
333
|
-
below the control. -->
|
|
334
|
-
<template v-if="!hideLabel" #label>
|
|
335
|
-
<DXFieldLabel :label="resolvedLabel" :info="resolvedInfo" />
|
|
336
|
-
<small
|
|
337
|
-
v-if="isHorizontal && (resolvedHint || $slots.hint)"
|
|
338
|
-
class="form-text text-muted d-block dx-field-hint"
|
|
339
|
-
>
|
|
340
|
-
<slot name="hint" :field="field" :model="model">{{
|
|
341
|
-
resolvedHint
|
|
342
|
-
}}</slot>
|
|
343
|
-
</small>
|
|
67
|
+
<slot name="info" v-bind="infoProps" />
|
|
344
68
|
</template>
|
|
345
69
|
|
|
346
|
-
<!-- Custom value slot overrides the built-in control
|
|
70
|
+
<!-- Custom value slot overrides the built-in control (checkbox / switch
|
|
71
|
+
/ standard types; not switch-list or repeater). -->
|
|
347
72
|
<!--
|
|
348
73
|
@slot Replaces the built-in control with a custom value editor.
|
|
349
74
|
@binding {FieldDefinition} field The field definition being rendered.
|
|
@@ -352,256 +77,288 @@
|
|
|
352
77
|
@binding {(value: any) => void} update Setter that writes the value back and clears the field's validation error.
|
|
353
78
|
-->
|
|
354
79
|
<slot
|
|
355
|
-
v-if="$slots.value"
|
|
80
|
+
v-if="showValueSlot && $slots.value"
|
|
356
81
|
name="value"
|
|
357
82
|
:field="field"
|
|
358
83
|
:model="model"
|
|
359
84
|
:value="fieldValue"
|
|
360
85
|
:update="setValue"
|
|
361
86
|
/>
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
:field="field"
|
|
369
|
-
:model="model"
|
|
370
|
-
:disabled="isDisabled || isReadonly || isPlaintext"
|
|
371
|
-
v-bind="controlPropsWithGuards"
|
|
372
|
-
/>
|
|
373
|
-
|
|
374
|
-
<!-- Textarea. Error clearing happens in the v-model setter. -->
|
|
375
|
-
<DFormTextarea
|
|
376
|
-
v-else-if="field.type === 'textarea'"
|
|
377
|
-
v-model="fieldValue"
|
|
378
|
-
:required="field.required"
|
|
379
|
-
:placeholder="field.placeholder"
|
|
380
|
-
:rows="field.rows || 3"
|
|
381
|
-
:state="fieldState"
|
|
382
|
-
:disabled="isDisabled"
|
|
383
|
-
:readonly="isReadonly || isPlaintext"
|
|
384
|
-
:plaintext="isPlaintext"
|
|
385
|
-
v-bind="inputPropsWithGuards"
|
|
386
|
-
/>
|
|
387
|
-
|
|
388
|
-
<!-- Searchable select: type to filter a long option list, while the
|
|
389
|
-
model keeps the option's VALUE (an id), not the typed text. The
|
|
390
|
-
`autocomplete` type below models the text, so it can't back a
|
|
391
|
-
foreign key. -->
|
|
392
|
-
<template v-else-if="field.type === 'select' && field.searchable">
|
|
393
|
-
<DAutocomplete
|
|
394
|
-
v-if="searchableOptionsReady"
|
|
87
|
+
<template v-else>
|
|
88
|
+
<!-- Checkbox (horizontal): the row label (shell #label) names the
|
|
89
|
+
control; the checkbox's own label is kept for screen readers
|
|
90
|
+
but hidden visually so it doesn't duplicate the row label. -->
|
|
91
|
+
<DFormCheckbox
|
|
92
|
+
v-if="field.type === 'checkbox' && isHorizontal"
|
|
395
93
|
v-model="fieldValue"
|
|
396
|
-
:options="resolvedOptions"
|
|
397
|
-
:placeholder="field.placeholder"
|
|
398
|
-
:required="field.required"
|
|
399
|
-
:state="fieldState"
|
|
400
94
|
:disabled="isDisabled || isReadonly || isPlaintext"
|
|
401
|
-
open-on-focus
|
|
402
95
|
v-bind="controlPropsWithGuards"
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
so an async list would leave the raw id showing ("51" instead
|
|
407
|
-
of "Waitrose") — and a long list is exactly the case that loads
|
|
408
|
-
async. Hold the control until the options are in. -->
|
|
409
|
-
<DFormInput v-else placeholder="Loading…" disabled />
|
|
410
|
-
</template>
|
|
411
|
-
|
|
412
|
-
<!-- Select (sync or async options) -->
|
|
413
|
-
<DFormSelect
|
|
414
|
-
v-else-if="field.type === 'select'"
|
|
415
|
-
v-model="fieldValue"
|
|
416
|
-
:required="field.required"
|
|
417
|
-
:options="resolvedOptions"
|
|
418
|
-
:state="fieldState"
|
|
419
|
-
:disabled="isDisabled || isReadonly || isPlaintext"
|
|
420
|
-
v-bind="controlPropsWithGuards"
|
|
421
|
-
/>
|
|
422
|
-
|
|
423
|
-
<!-- Radio group -->
|
|
424
|
-
<DFormRadioGroup
|
|
425
|
-
v-else-if="field.type === 'radio'"
|
|
426
|
-
v-model="fieldValue"
|
|
427
|
-
:options="resolvedOptions"
|
|
428
|
-
:required="field.required"
|
|
429
|
-
:state="fieldState"
|
|
430
|
-
:disabled="isDisabled || isReadonly || isPlaintext"
|
|
431
|
-
v-bind="controlPropsWithGuards"
|
|
432
|
-
/>
|
|
433
|
-
|
|
434
|
-
<!-- Checkbox group: "pick any of N" — the model is an ARRAY of the
|
|
435
|
-
checked option values (default the field to []). -->
|
|
436
|
-
<DFormCheckboxGroup
|
|
437
|
-
v-else-if="field.type === 'checkbox-group'"
|
|
438
|
-
v-model="fieldValue"
|
|
439
|
-
:options="resolvedOptions"
|
|
440
|
-
:state="fieldState"
|
|
441
|
-
:disabled="isDisabled || isReadonly || isPlaintext"
|
|
442
|
-
v-bind="controlPropsWithGuards"
|
|
443
|
-
/>
|
|
96
|
+
>
|
|
97
|
+
<DXFieldLabel :label="resolvedLabel" class="visually-hidden" />
|
|
98
|
+
</DFormCheckbox>
|
|
444
99
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
:accept="field.accept || (field.type === 'image' ? 'image/*' : undefined)"
|
|
450
|
-
:required="field.required"
|
|
451
|
-
:state="fieldState"
|
|
100
|
+
<!-- Checkbox (vertical): label sits beside the control -->
|
|
101
|
+
<DFormCheckbox
|
|
102
|
+
v-else-if="field.type === 'checkbox'"
|
|
103
|
+
v-model="fieldValue"
|
|
452
104
|
:disabled="isDisabled || isReadonly || isPlaintext"
|
|
453
105
|
v-bind="controlPropsWithGuards"
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
:step="field.step"
|
|
474
|
-
:required="field.required"
|
|
475
|
-
:placeholder="field.placeholder"
|
|
476
|
-
:min="field.min"
|
|
477
|
-
:max="field.max"
|
|
478
|
-
:state="fieldState"
|
|
479
|
-
:disabled="isDisabled"
|
|
480
|
-
:readonly="isReadonly || isPlaintext"
|
|
481
|
-
:plaintext="isPlaintext"
|
|
482
|
-
v-bind="inputPropsWithGuards"
|
|
483
|
-
@update:model-value="setValue($event)"
|
|
484
|
-
/>
|
|
106
|
+
>
|
|
107
|
+
<DXFieldLabel :label="resolvedLabel" :info="resolvedInfo" />
|
|
108
|
+
</DFormCheckbox>
|
|
109
|
+
|
|
110
|
+
<!-- Switch (horizontal): the switch's inner text shows only when the
|
|
111
|
+
field opts into contextual on/off text; otherwise it falls back
|
|
112
|
+
to the row label and is hidden visually (kept for a11y). -->
|
|
113
|
+
<DXSwitch
|
|
114
|
+
v-else-if="field.type === 'switch' && isHorizontal"
|
|
115
|
+
v-model="switchModel"
|
|
116
|
+
:on-variant="field.switchVariant"
|
|
117
|
+
:disabled="isDisabled || isReadonly || isPlaintext"
|
|
118
|
+
v-bind="controlPropsWithGuards"
|
|
119
|
+
>
|
|
120
|
+
<DXFieldLabel
|
|
121
|
+
:label="switchText"
|
|
122
|
+
:class="switchHasContextualText ? undefined : 'visually-hidden'"
|
|
123
|
+
/>
|
|
124
|
+
</DXSwitch>
|
|
485
125
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
v-model="
|
|
490
|
-
|
|
491
|
-
:
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
:
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
<template
|
|
504
|
-
<
|
|
126
|
+
<!-- Switch (vertical): toggle with contextual on/off text -->
|
|
127
|
+
<DXSwitch
|
|
128
|
+
v-else-if="field.type === 'switch'"
|
|
129
|
+
v-model="switchModel"
|
|
130
|
+
:on-variant="field.switchVariant"
|
|
131
|
+
:disabled="isDisabled || isReadonly || isPlaintext"
|
|
132
|
+
v-bind="controlPropsWithGuards"
|
|
133
|
+
>
|
|
134
|
+
<DXFieldLabel :label="switchText" :info="resolvedInfo" />
|
|
135
|
+
</DXSwitch>
|
|
136
|
+
|
|
137
|
+
<!-- Switch-list (#160): a list of labelled boolean toggles as config,
|
|
138
|
+
not markup. One real DFormGroup per option row (the ONLY correct
|
|
139
|
+
source of the label/content grid) with the option text in the
|
|
140
|
+
label column and a compact track switch in the content column.
|
|
141
|
+
The model is an array of selected option values, like
|
|
142
|
+
checkbox-group. -->
|
|
143
|
+
<template v-else-if="field.type === 'switch-list'">
|
|
144
|
+
<div
|
|
145
|
+
v-if="!hideLabel && field.label"
|
|
146
|
+
class="dx-switch-list-heading text-muted fw-semibold"
|
|
147
|
+
>
|
|
148
|
+
<DXFieldLabel :label="resolvedLabel" :info="resolvedInfo" />
|
|
149
|
+
</div>
|
|
150
|
+
<div
|
|
151
|
+
v-for="option in resolvedOptions ?? []"
|
|
152
|
+
:key="String(option.value)"
|
|
153
|
+
class="dx-switch-list-row"
|
|
154
|
+
>
|
|
155
|
+
<DFormGroup
|
|
156
|
+
v-bind="switchListRowAttrs"
|
|
157
|
+
class="mb-0 align-items-center"
|
|
158
|
+
>
|
|
159
|
+
<template #label>
|
|
160
|
+
<DXFieldLabel
|
|
161
|
+
:label="option.text"
|
|
162
|
+
:title="option.description"
|
|
163
|
+
/>
|
|
164
|
+
</template>
|
|
165
|
+
<div class="dx-switch-list-control">
|
|
166
|
+
<DFormCheckbox
|
|
167
|
+
switch
|
|
168
|
+
:class="field.switchVariant === 'neutral' ? 'switch-neutral' : undefined"
|
|
169
|
+
:model-value="switchListIsOn(option)"
|
|
170
|
+
:disabled="isDisabled || isReadonly || isPlaintext || option.disabled === true"
|
|
171
|
+
:aria-label="option.text"
|
|
172
|
+
@update:model-value="setSwitchListOn(option, $event === true)"
|
|
173
|
+
/>
|
|
174
|
+
<!--
|
|
175
|
+
@slot Extra per-row content beside a switch-list row's toggle (e.g. a notes input the consumer binds to their own model).
|
|
176
|
+
@binding {FieldOption} option The option this row renders.
|
|
177
|
+
@binding {boolean} on Whether the row's switch is currently on.
|
|
178
|
+
-->
|
|
179
|
+
<slot
|
|
180
|
+
name="switch-list-item"
|
|
181
|
+
:option="option"
|
|
182
|
+
:on="switchListIsOn(option)"
|
|
183
|
+
/>
|
|
184
|
+
</div>
|
|
185
|
+
</DFormGroup>
|
|
186
|
+
</div>
|
|
505
187
|
</template>
|
|
506
|
-
</DInputGroup>
|
|
507
188
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
<
|
|
513
|
-
v-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
/>
|
|
525
|
-
<datalist :id="datalistId">
|
|
526
|
-
<option
|
|
527
|
-
v-for="opt in resolvedOptions"
|
|
528
|
-
:key="String(opt.value)"
|
|
529
|
-
:value="String(opt.value)"
|
|
189
|
+
<!-- Repeater: nested, repeatable sub-form. Renders its own label
|
|
190
|
+
group (a table-layout repeater keeps its label above, so it uses
|
|
191
|
+
repeaterHorizontalAttrs rather than the field's horizontal
|
|
192
|
+
layout). The shell contributes only the trailing block here. -->
|
|
193
|
+
<DFormGroup
|
|
194
|
+
v-else-if="field.type === 'repeater'"
|
|
195
|
+
v-bind="repeaterHorizontalAttrs"
|
|
196
|
+
>
|
|
197
|
+
<template v-if="!hideLabel" #label>
|
|
198
|
+
<DXFieldLabel :label="resolvedLabel" :info="resolvedInfo" />
|
|
199
|
+
</template>
|
|
200
|
+
<DXRepeater
|
|
201
|
+
:form="form"
|
|
202
|
+
:field="field"
|
|
203
|
+
:key-path="keyPath"
|
|
204
|
+
:model="model"
|
|
530
205
|
>
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
206
|
+
<!-- Forward repeater row slot for custom row layouts -->
|
|
207
|
+
<template v-if="$slots['repeater-row']" #row="rowProps">
|
|
208
|
+
<!-- @slot Custom layout for a single repeater row, forwarded to `DXRepeater`'s `row` slot with its row props (row index, item, remove handler, etc.). -->
|
|
209
|
+
<slot name="repeater-row" v-bind="rowProps" />
|
|
210
|
+
</template>
|
|
211
|
+
</DXRepeater>
|
|
212
|
+
</DFormGroup>
|
|
213
|
+
|
|
214
|
+
<!-- Standard labelled field: the control goes inside the shell's
|
|
215
|
+
group (label column + trailing block). -->
|
|
216
|
+
<template v-else>
|
|
217
|
+
<!-- Component escape hatch -->
|
|
218
|
+
<component
|
|
219
|
+
:is="field.component"
|
|
220
|
+
v-if="field.type === 'component' && field.component"
|
|
221
|
+
v-model="fieldValue"
|
|
222
|
+
:field="field"
|
|
223
|
+
:model="model"
|
|
224
|
+
:disabled="isDisabled || isReadonly || isPlaintext"
|
|
225
|
+
v-bind="controlPropsWithGuards"
|
|
226
|
+
/>
|
|
535
227
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
:state="fieldState"
|
|
545
|
-
:disabled="isDisabled"
|
|
546
|
-
:readonly="isReadonly"
|
|
547
|
-
v-bind="passwordInputProps"
|
|
548
|
-
/>
|
|
549
|
-
<template #append>
|
|
550
|
-
<DButton
|
|
551
|
-
variant="outline-secondary"
|
|
552
|
-
:icon="passwordRevealed ? 'eye-slash' : 'eye'"
|
|
553
|
-
:aria-label="passwordRevealed ? 'Hide password' : 'Show password'"
|
|
554
|
-
:aria-pressed="passwordRevealed"
|
|
228
|
+
<!-- Textarea. Error clearing happens in the v-model setter. -->
|
|
229
|
+
<DFormTextarea
|
|
230
|
+
v-else-if="field.type === 'textarea'"
|
|
231
|
+
v-model="fieldValue"
|
|
232
|
+
:required="field.required"
|
|
233
|
+
:placeholder="field.placeholder"
|
|
234
|
+
:rows="field.rows || 3"
|
|
235
|
+
:state="fieldState"
|
|
555
236
|
:disabled="isDisabled"
|
|
556
|
-
|
|
237
|
+
:readonly="isReadonly || isPlaintext"
|
|
238
|
+
:plaintext="isPlaintext"
|
|
239
|
+
v-bind="inputPropsWithGuards"
|
|
557
240
|
/>
|
|
558
|
-
</template>
|
|
559
|
-
</DInputGroup>
|
|
560
|
-
|
|
561
|
-
<!-- Text-based inputs (text/email/password/number/url/tel/date/time/datetime) -->
|
|
562
|
-
<DFormInput
|
|
563
|
-
v-else
|
|
564
|
-
v-model="textFieldModel"
|
|
565
|
-
:type="inputType"
|
|
566
|
-
:required="field.required"
|
|
567
|
-
:placeholder="field.placeholder"
|
|
568
|
-
:step="field.step"
|
|
569
|
-
:min="field.min"
|
|
570
|
-
:max="field.max"
|
|
571
|
-
:state="fieldState"
|
|
572
|
-
:disabled="isDisabled"
|
|
573
|
-
:readonly="isReadonly || isPlaintext"
|
|
574
|
-
:plaintext="isPlaintext"
|
|
575
|
-
v-bind="inputPropsWithGuards"
|
|
576
|
-
/>
|
|
577
241
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
242
|
+
<!-- Choice controls: select (plain or searchable), radio,
|
|
243
|
+
checkbox-group. -->
|
|
244
|
+
<DXChoiceField
|
|
245
|
+
v-else-if="isChoiceType"
|
|
246
|
+
:field="field"
|
|
247
|
+
:model-value="fieldValue"
|
|
248
|
+
:options="resolvedOptions"
|
|
249
|
+
:state="fieldState"
|
|
250
|
+
:disabled="isDisabled || isReadonly || isPlaintext"
|
|
251
|
+
:control-props="controlPropsWithGuards"
|
|
252
|
+
:options-ready="searchableOptionsReady"
|
|
253
|
+
@update:model-value="setValue"
|
|
254
|
+
/>
|
|
582
255
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
256
|
+
<!-- File / image upload -->
|
|
257
|
+
<div v-else-if="field.type === 'image' || field.type === 'file'">
|
|
258
|
+
<DFormInput
|
|
259
|
+
type="file"
|
|
260
|
+
:accept="field.accept || (field.type === 'image' ? 'image/*' : undefined)"
|
|
261
|
+
:required="field.required"
|
|
262
|
+
:state="fieldState"
|
|
263
|
+
:disabled="isDisabled || isReadonly || isPlaintext"
|
|
264
|
+
v-bind="controlPropsWithGuards"
|
|
265
|
+
@change="handleFileChange"
|
|
266
|
+
/>
|
|
267
|
+
<img
|
|
268
|
+
v-if="field.type === 'image' && imagePreview"
|
|
269
|
+
:src="imagePreview"
|
|
270
|
+
alt="Preview"
|
|
271
|
+
class="dx-field-image-preview mt-2"
|
|
272
|
+
/>
|
|
273
|
+
</div>
|
|
590
274
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
275
|
+
<!-- Numeric controls: currency, percentage, number. -->
|
|
276
|
+
<DXNumericField
|
|
277
|
+
v-else-if="isNumericType"
|
|
278
|
+
:field="field"
|
|
279
|
+
:model-value="fieldValue"
|
|
280
|
+
:state="fieldState"
|
|
281
|
+
:is-disabled="isDisabled"
|
|
282
|
+
:is-readonly="isReadonly"
|
|
283
|
+
:is-plaintext="isPlaintext"
|
|
284
|
+
:input-props="inputPropsWithGuards"
|
|
285
|
+
@update:model-value="setValue"
|
|
286
|
+
/>
|
|
601
287
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
288
|
+
<!-- Autocomplete: a free-text input with a datalist of
|
|
289
|
+
suggestions (sync or async options). Unlike select, a value
|
|
290
|
+
not in the list can still be typed and submitted; the
|
|
291
|
+
consumer validates. -->
|
|
292
|
+
<template v-else-if="field.type === 'autocomplete'">
|
|
293
|
+
<DFormInput
|
|
294
|
+
v-model="fieldValue"
|
|
295
|
+
type="text"
|
|
296
|
+
:list="datalistId"
|
|
297
|
+
:required="field.required"
|
|
298
|
+
:placeholder="field.placeholder"
|
|
299
|
+
:state="fieldState"
|
|
300
|
+
:disabled="isDisabled"
|
|
301
|
+
:readonly="isReadonly || isPlaintext"
|
|
302
|
+
:plaintext="isPlaintext"
|
|
303
|
+
autocomplete="off"
|
|
304
|
+
v-bind="inputPropsWithGuards"
|
|
305
|
+
/>
|
|
306
|
+
<datalist :id="datalistId">
|
|
307
|
+
<option
|
|
308
|
+
v-for="opt in resolvedOptions"
|
|
309
|
+
:key="String(opt.value)"
|
|
310
|
+
:value="String(opt.value)"
|
|
311
|
+
>
|
|
312
|
+
{{ opt.text }}
|
|
313
|
+
</option>
|
|
314
|
+
</datalist>
|
|
315
|
+
</template>
|
|
316
|
+
|
|
317
|
+
<!-- Password with a reveal toggle (the default for
|
|
318
|
+
`type: "password"`). Typing a long password blind is the one
|
|
319
|
+
place the eye really earns its keep, so it's opt-out
|
|
320
|
+
(`revealable: false`) rather than opt-in. -->
|
|
321
|
+
<DInputGroup v-else-if="isRevealablePassword">
|
|
322
|
+
<DFormInput
|
|
323
|
+
v-model="textFieldModel"
|
|
324
|
+
:required="field.required"
|
|
325
|
+
:placeholder="field.placeholder"
|
|
326
|
+
:state="fieldState"
|
|
327
|
+
:disabled="isDisabled"
|
|
328
|
+
:readonly="isReadonly"
|
|
329
|
+
v-bind="passwordInputProps"
|
|
330
|
+
/>
|
|
331
|
+
<template #append>
|
|
332
|
+
<DButton
|
|
333
|
+
variant="outline-secondary"
|
|
334
|
+
:icon="passwordRevealed ? 'eye-slash' : 'eye'"
|
|
335
|
+
:aria-label="passwordRevealed ? 'Hide password' : 'Show password'"
|
|
336
|
+
:aria-pressed="passwordRevealed"
|
|
337
|
+
:disabled="isDisabled"
|
|
338
|
+
@click="passwordRevealed = !passwordRevealed"
|
|
339
|
+
/>
|
|
340
|
+
</template>
|
|
341
|
+
</DInputGroup>
|
|
342
|
+
|
|
343
|
+
<!-- Text-based inputs (text/email/password/url/tel/date/time/datetime) -->
|
|
344
|
+
<DFormInput
|
|
345
|
+
v-else
|
|
346
|
+
v-model="textFieldModel"
|
|
347
|
+
:type="inputType"
|
|
348
|
+
:required="field.required"
|
|
349
|
+
:placeholder="field.placeholder"
|
|
350
|
+
:step="field.step"
|
|
351
|
+
:min="field.min"
|
|
352
|
+
:max="field.max"
|
|
353
|
+
:state="fieldState"
|
|
354
|
+
:disabled="isDisabled"
|
|
355
|
+
:readonly="isReadonly || isPlaintext"
|
|
356
|
+
:plaintext="isPlaintext"
|
|
357
|
+
v-bind="inputPropsWithGuards"
|
|
358
|
+
/>
|
|
359
|
+
</template>
|
|
360
|
+
</template>
|
|
361
|
+
</DXFieldShell>
|
|
605
362
|
</template>
|
|
606
363
|
|
|
607
364
|
<script setup lang="ts">
|
|
@@ -609,7 +366,6 @@ import {
|
|
|
609
366
|
computed,
|
|
610
367
|
defineAsyncComponent,
|
|
611
368
|
onBeforeUnmount,
|
|
612
|
-
onMounted,
|
|
613
369
|
provide,
|
|
614
370
|
ref,
|
|
615
371
|
useId,
|
|
@@ -620,18 +376,15 @@ import { dxFieldInfoPopoverKey } from "./dxFieldContext";
|
|
|
620
376
|
import DFormGroup from "../base/DFormGroup.vue";
|
|
621
377
|
import DFormInput from "../base/DFormInput.vue";
|
|
622
378
|
import DFormTextarea from "../base/DFormTextarea.vue";
|
|
623
|
-
import DFormSelect from "../base/DFormSelect.vue";
|
|
624
|
-
import DFormRadioGroup from "../base/DFormRadioGroup.vue";
|
|
625
|
-
import DFormCheckboxGroup from "../base/DFormCheckboxGroup.vue";
|
|
626
379
|
import DFormCheckbox from "../base/DFormCheckbox.vue";
|
|
627
380
|
import DXSwitch from "./DXSwitch.vue";
|
|
628
|
-
import DFormInvalidFeedback from "../base/DFormInvalidFeedback.vue";
|
|
629
|
-
import DFormText from "../base/DFormText.vue";
|
|
630
381
|
import DInputGroup from "../base/DInputGroup.vue";
|
|
631
|
-
import DAutocomplete from "../base/DAutocomplete.vue";
|
|
632
382
|
import DButton from "../base/DButton.vue";
|
|
633
383
|
import DXFieldLabel from "./DXFieldLabel.vue";
|
|
634
|
-
import
|
|
384
|
+
import DXFieldShell from "./DXFieldShell.vue";
|
|
385
|
+
import DXNumericField from "./DXNumericField.vue";
|
|
386
|
+
import DXChoiceField from "./DXChoiceField.vue";
|
|
387
|
+
import { useAsyncOptions } from "../../composables/useAsyncOptions";
|
|
635
388
|
import type { UseFormReturn } from "../../composables/useForm";
|
|
636
389
|
import type { FieldDefinition, FieldOption, FieldType, LabelCols } from "../../types";
|
|
637
390
|
import { getByPath, setByPath } from "../../utils/objectPath";
|
|
@@ -760,6 +513,33 @@ const horizontalAttrs = computed<Record<string, any>>(() =>
|
|
|
760
513
|
// hint below the control.
|
|
761
514
|
const hintInLabel = computed(() => isHorizontal.value && !props.hideLabel);
|
|
762
515
|
|
|
516
|
+
// Which shape DXFieldShell should render: `group` (a DFormGroup with a label
|
|
517
|
+
// column, holding the control + trailing) for the standard field and the
|
|
518
|
+
// horizontal checkbox/switch; `plain` (a div, the control owning its own label
|
|
519
|
+
// or heading) for the vertical checkbox/switch, switch-list and repeater.
|
|
520
|
+
const shellMode = computed<"group" | "plain">(() => {
|
|
521
|
+
const type = props.field.type;
|
|
522
|
+
if (type === "checkbox" || type === "switch") {
|
|
523
|
+
return isHorizontal.value ? "group" : "plain";
|
|
524
|
+
}
|
|
525
|
+
if (type === "switch-list" || type === "repeater") return "plain";
|
|
526
|
+
return "group";
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
// The custom value slot applies to the checkbox/switch/standard branches, but
|
|
530
|
+
// not switch-list or repeater (which never honoured it).
|
|
531
|
+
const showValueSlot = computed(
|
|
532
|
+
() => props.field.type !== "switch-list" && props.field.type !== "repeater",
|
|
533
|
+
);
|
|
534
|
+
|
|
535
|
+
const isChoiceType = computed(() =>
|
|
536
|
+
["select", "radio", "checkbox-group"].includes(props.field.type),
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
const isNumericType = computed(() =>
|
|
540
|
+
["currency", "percentage", "number"].includes(props.field.type),
|
|
541
|
+
);
|
|
542
|
+
|
|
763
543
|
// A `switch` renders visible text inside the control only when the field opts
|
|
764
544
|
// into contextual on/off text (textWhenTrue/textWhenFalse) — e.g. "Product is
|
|
765
545
|
// current". Without it, `switchText` falls back to the row label, which would
|
|
@@ -843,41 +623,6 @@ const textFieldModel = computed({
|
|
|
843
623
|
set: (value: any) => setValue(value),
|
|
844
624
|
});
|
|
845
625
|
|
|
846
|
-
// For `percentage` fields with `asFraction`, the model holds a 0–1 fraction but
|
|
847
|
-
// the input shows/edits a 0–100 percentage. Scale on read/write, rounding away
|
|
848
|
-
// binary-float artefacts (0.2 * 100 = 20.000000000000004). Currency and plain
|
|
849
|
-
// percentage fields pass straight through.
|
|
850
|
-
const isFractionPercentage = computed(
|
|
851
|
-
() => props.field.type === "percentage" && props.field.asFraction === true,
|
|
852
|
-
);
|
|
853
|
-
|
|
854
|
-
const numericInputValue = computed({
|
|
855
|
-
get: () => {
|
|
856
|
-
const value = fieldValue.value;
|
|
857
|
-
if (!isFractionPercentage.value) return value;
|
|
858
|
-
if (value === null || value === undefined || value === "") return value;
|
|
859
|
-
const num = Number(value);
|
|
860
|
-
if (!Number.isFinite(num)) return value;
|
|
861
|
-
return Math.round(num * 100 * 1e6) / 1e6;
|
|
862
|
-
},
|
|
863
|
-
set: (value: any) => {
|
|
864
|
-
if (!isFractionPercentage.value) {
|
|
865
|
-
setValue(value);
|
|
866
|
-
return;
|
|
867
|
-
}
|
|
868
|
-
if (value === null || value === undefined || value === "") {
|
|
869
|
-
setValue(value);
|
|
870
|
-
return;
|
|
871
|
-
}
|
|
872
|
-
const num = Number(value);
|
|
873
|
-
if (!Number.isFinite(num)) {
|
|
874
|
-
setValue(value);
|
|
875
|
-
return;
|
|
876
|
-
}
|
|
877
|
-
setValue(Math.round((num / 100) * 1e9) / 1e9);
|
|
878
|
-
},
|
|
879
|
-
});
|
|
880
|
-
|
|
881
626
|
const NUMERIC_TYPES: ReadonlySet<FieldType> = new Set([
|
|
882
627
|
"number",
|
|
883
628
|
"currency",
|
|
@@ -1081,43 +826,12 @@ const datalistId = useId();
|
|
|
1081
826
|
|
|
1082
827
|
// ————————————————— async options
|
|
1083
828
|
|
|
1084
|
-
const loadedOptions =
|
|
829
|
+
const { loadedOptions } = useAsyncOptions(() => props.field, effectiveModel);
|
|
1085
830
|
|
|
1086
831
|
const resolvedOptions = computed<FieldOption[] | undefined>(
|
|
1087
832
|
() => loadedOptions.value ?? props.field.options,
|
|
1088
833
|
);
|
|
1089
834
|
|
|
1090
|
-
// Monotonic token so out-of-order async responses can't clobber newer ones.
|
|
1091
|
-
let optionsRequestToken = 0;
|
|
1092
|
-
|
|
1093
|
-
async function loadOptions(): Promise<void> {
|
|
1094
|
-
if (!props.field.optionsLoader) return;
|
|
1095
|
-
const token = ++optionsRequestToken;
|
|
1096
|
-
try {
|
|
1097
|
-
const options = await props.field.optionsLoader(effectiveModel.value);
|
|
1098
|
-
// Ignore a stale response superseded by a newer load.
|
|
1099
|
-
if (token === optionsRequestToken) loadedOptions.value = options;
|
|
1100
|
-
} catch (error) {
|
|
1101
|
-
// Swallow loader failures: keep the last successfully loaded options
|
|
1102
|
-
// (or the static `field.options` fallback when none have loaded yet).
|
|
1103
|
-
if (token === optionsRequestToken) {
|
|
1104
|
-
// eslint-disable-next-line no-console
|
|
1105
|
-
console.error(
|
|
1106
|
-
`optionsLoader failed for field "${props.field.key}"`,
|
|
1107
|
-
error,
|
|
1108
|
-
);
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
onMounted(() => {
|
|
1114
|
-
void loadOptions();
|
|
1115
|
-
});
|
|
1116
|
-
|
|
1117
|
-
if (props.field.reloadOptionsOnChange) {
|
|
1118
|
-
watch(effectiveModel, () => void loadOptions(), { deep: true });
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
835
|
// ————————————————— image preview
|
|
1122
836
|
|
|
1123
837
|
const objectUrl = ref<string | null>(null);
|
|
@@ -1179,5 +893,4 @@ onBeforeUnmount(() => {
|
|
|
1179
893
|
border: 1px solid var(--bs-border-color);
|
|
1180
894
|
border-radius: var(--bs-border-radius);
|
|
1181
895
|
}
|
|
1182
|
-
|
|
1183
896
|
</style>
|