@bagelink/vue 1.15.366 → 1.15.371
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/core.css +1 -1
- package/dist/form-flow/FormFlowPlayer.vue.d.ts +3 -0
- package/dist/form-flow/FormFlowPlayer.vue.d.ts.map +1 -1
- package/dist/form-flow/form-flow.d.ts +16 -1
- package/dist/form-flow/form-flow.d.ts.map +1 -1
- package/dist/index.cjs +72 -72
- package/dist/index.css +1 -1
- package/dist/index.mjs +7045 -7006
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/form/inputs/ToggleInput.vue +3 -3
- package/src/form-flow/FormFlowPlayer.vue +8 -1
- package/src/form-flow/form-flow.ts +109 -7
package/package.json
CHANGED
|
@@ -128,14 +128,14 @@ outline-color: transparent;
|
|
|
128
128
|
outline: -webkit-focus-ring-color auto 1px;
|
|
129
129
|
outline-offset: 2px
|
|
130
130
|
}
|
|
131
|
-
.bgl-toggle :checked{
|
|
132
|
-
background: var(--bgl-input-label-active-color
|
|
131
|
+
.bgl-toggle input:checked{
|
|
132
|
+
background: var(--bgl-input-label-active-color) !important;
|
|
133
133
|
}
|
|
134
134
|
.bgl-toggle :checked::before{
|
|
135
135
|
margin-inline-start:calc(var(--bgl-input-height) / 2 - 2px) ;
|
|
136
136
|
}
|
|
137
137
|
.bgl-toggle input:checked + label {
|
|
138
|
-
color: var(--bgl-input-label-active-color
|
|
138
|
+
color: var(--bgl-input-label-active-color) !important;
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
/* ── bgl-outline variant ── */
|
|
@@ -20,6 +20,7 @@ import type { Slots } from 'vue'
|
|
|
20
20
|
import { computed, ref, useSlots, watch } from 'vue'
|
|
21
21
|
import { useI18n } from '../i18n'
|
|
22
22
|
import { fromJSONSchema } from './form-flow'
|
|
23
|
+
import type { JSONSchemaOptionsSourceLoader } from './form-flow'
|
|
23
24
|
import Btn from '../components/Btn.vue'
|
|
24
25
|
import FormFlow from './FormFlow.vue'
|
|
25
26
|
import type { FormStepEventPayload } from './structure'
|
|
@@ -52,6 +53,8 @@ export interface Props {
|
|
|
52
53
|
hideCounter?: boolean
|
|
53
54
|
/** Optional async submit handler; awaited before `complete` is emitted. */
|
|
54
55
|
onComplete?: (data: Record<string, any>) => Promise<void> | void
|
|
56
|
+
/** Resolve declarative x-options-source fields in stored JSON Schemas. */
|
|
57
|
+
loadOptionsSource?: JSONSchemaOptionsSourceLoader
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -142,7 +145,11 @@ watch(currentKey, (key, prev) => {
|
|
|
142
145
|
// Convert the current step's stored JSON Schema into a renderable schema.
|
|
143
146
|
const currentSchema = computed(() => {
|
|
144
147
|
const step = currentStep.value
|
|
145
|
-
return step
|
|
148
|
+
return step
|
|
149
|
+
? fromJSONSchema(step.schema as any, {
|
|
150
|
+
loadOptionsSource: props.loadOptionsSource,
|
|
151
|
+
})
|
|
152
|
+
: null
|
|
146
153
|
})
|
|
147
154
|
|
|
148
155
|
// The *next* step given current answers (reactive: recomputes as fields change).
|
|
@@ -774,6 +774,7 @@ interface JSONSchemaProperty {
|
|
|
774
774
|
'additionalProperties'?: boolean | JSONSchemaProperty
|
|
775
775
|
'x-ui'?: {
|
|
776
776
|
fieldType?: string
|
|
777
|
+
required?: boolean
|
|
777
778
|
placeholder?: string
|
|
778
779
|
disabled?: boolean
|
|
779
780
|
readonly?: boolean
|
|
@@ -795,6 +796,7 @@ interface JSONSchemaProperty {
|
|
|
795
796
|
// designer-built schema renders identically in FormFlow/FormFlowPlayer.
|
|
796
797
|
'x-field-type'?: string
|
|
797
798
|
'x-options'?: { label: string, value: any }[]
|
|
799
|
+
'x-options-source'?: JSONSchemaOptionsSource
|
|
798
800
|
'x-placeholder'?: string
|
|
799
801
|
'x-select-multiselect'?: boolean
|
|
800
802
|
'x-radio-inline'?: boolean
|
|
@@ -802,6 +804,51 @@ interface JSONSchemaProperty {
|
|
|
802
804
|
'x-radio-text-align'?: 'left' | 'center' | 'right'
|
|
803
805
|
}
|
|
804
806
|
|
|
807
|
+
export interface JSONSchemaOptionsSource {
|
|
808
|
+
endpoint: string
|
|
809
|
+
itemsPath?: string
|
|
810
|
+
valuePath?: string
|
|
811
|
+
labelPath?: string
|
|
812
|
+
subtitlePath?: string
|
|
813
|
+
searchParam?: string
|
|
814
|
+
limitParam?: string
|
|
815
|
+
limit?: number
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
export type JSONSchemaOptionsSourceLoader = (
|
|
819
|
+
source: JSONSchemaOptionsSource,
|
|
820
|
+
query: string,
|
|
821
|
+
) => Promise<unknown>
|
|
822
|
+
|
|
823
|
+
function valueAtPath(value: unknown, path?: string): unknown {
|
|
824
|
+
if (!path) return value
|
|
825
|
+
return path.split('.').reduce<unknown>((current, key) => {
|
|
826
|
+
if (current == null || typeof current !== 'object') return undefined
|
|
827
|
+
return (current as Record<string, unknown>)[key]
|
|
828
|
+
}, value)
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function normalizeOptionsSourceResponse(
|
|
832
|
+
response: unknown,
|
|
833
|
+
source: JSONSchemaOptionsSource,
|
|
834
|
+
): { label: string, value: string, subtitle?: string }[] {
|
|
835
|
+
const items = valueAtPath(response, source.itemsPath)
|
|
836
|
+
if (!Array.isArray(items)) return []
|
|
837
|
+
return items.flatMap((item) => {
|
|
838
|
+
const value = valueAtPath(item, source.valuePath ?? 'id')
|
|
839
|
+
const label = valueAtPath(item, source.labelPath ?? 'name')
|
|
840
|
+
if (value == null || label == null) return []
|
|
841
|
+
const subtitle = source.subtitlePath
|
|
842
|
+
? valueAtPath(item, source.subtitlePath)
|
|
843
|
+
: undefined
|
|
844
|
+
return [{
|
|
845
|
+
label: String(label),
|
|
846
|
+
value: String(value),
|
|
847
|
+
...(subtitle == null ? {} : { subtitle: String(subtitle) }),
|
|
848
|
+
}]
|
|
849
|
+
})
|
|
850
|
+
}
|
|
851
|
+
|
|
805
852
|
interface JSONSchemaObject {
|
|
806
853
|
'type'?: string
|
|
807
854
|
'title'?: string
|
|
@@ -832,13 +879,22 @@ interface JSONSchemaObject {
|
|
|
832
879
|
* // Use in component
|
|
833
880
|
* <FormFlow v-model="data" :schema="loadedSchema" />
|
|
834
881
|
*/
|
|
835
|
-
export function fromJSONSchema(
|
|
882
|
+
export function fromJSONSchema(
|
|
883
|
+
jsonSchema: JSONSchemaObject,
|
|
884
|
+
options?: { loadOptionsSource?: JSONSchemaOptionsSourceLoader },
|
|
885
|
+
): SchemaDefinition {
|
|
836
886
|
const fields: Record<string, FieldBuilder> = {}
|
|
837
887
|
const requiredSet = new Set(jsonSchema.required || [])
|
|
838
888
|
|
|
839
889
|
if (jsonSchema.properties) {
|
|
840
890
|
for (const [key, prop] of Object.entries(jsonSchema.properties)) {
|
|
841
|
-
fields[key] = createFieldFromProperty(
|
|
891
|
+
fields[key] = createFieldFromProperty(
|
|
892
|
+
key,
|
|
893
|
+
prop,
|
|
894
|
+
requiredSet.has(key),
|
|
895
|
+
jsonSchema,
|
|
896
|
+
options?.loadOptionsSource,
|
|
897
|
+
)
|
|
842
898
|
}
|
|
843
899
|
}
|
|
844
900
|
|
|
@@ -916,7 +972,8 @@ function createFieldFromProperty(
|
|
|
916
972
|
key: string,
|
|
917
973
|
prop: JSONSchemaProperty,
|
|
918
974
|
isRequired: boolean,
|
|
919
|
-
rootSchema: JSONSchemaObject
|
|
975
|
+
rootSchema: JSONSchemaObject,
|
|
976
|
+
loadOptionsSource?: JSONSchemaOptionsSourceLoader,
|
|
920
977
|
): FieldBuilder {
|
|
921
978
|
// Resolve $ref if present
|
|
922
979
|
let resolvedProp = prop
|
|
@@ -958,7 +1015,7 @@ function createFieldFromProperty(
|
|
|
958
1015
|
if (resolvedProp.title) config.label = resolvedProp.title
|
|
959
1016
|
if (resolvedProp.description) config.helpText = resolvedProp.description
|
|
960
1017
|
if (resolvedProp.default !== undefined) config.default = resolvedProp.default
|
|
961
|
-
if (isRequired) config.required = true
|
|
1018
|
+
if (isRequired || xUi.required === true) config.required = true
|
|
962
1019
|
|
|
963
1020
|
// UI hints from x-ui / flat x-* (designer)
|
|
964
1021
|
if (xUi.placeholder) config.placeholder = xUi.placeholder
|
|
@@ -969,6 +1026,31 @@ function createFieldFromProperty(
|
|
|
969
1026
|
// designer renders with the same options in the player. Only the props each
|
|
970
1027
|
// input actually reads are forwarded.
|
|
971
1028
|
applyDesignerConfig(config, resolvedProp)
|
|
1029
|
+
if (resolvedProp['x-options-source']) {
|
|
1030
|
+
if (!loadOptionsSource) {
|
|
1031
|
+
throw new Error(`Field "${key}" requires an x-options-source loader`)
|
|
1032
|
+
}
|
|
1033
|
+
const source = resolvedProp['x-options-source']
|
|
1034
|
+
let cachedOptions: { label: string, value: string, subtitle?: string }[] | undefined
|
|
1035
|
+
config.options = async (query: string) => {
|
|
1036
|
+
const options = source.searchParam
|
|
1037
|
+
? normalizeOptionsSourceResponse(
|
|
1038
|
+
await loadOptionsSource(source, query),
|
|
1039
|
+
source,
|
|
1040
|
+
)
|
|
1041
|
+
: cachedOptions ??= normalizeOptionsSourceResponse(
|
|
1042
|
+
await loadOptionsSource(source, source.searchParam ? query : ''),
|
|
1043
|
+
source,
|
|
1044
|
+
)
|
|
1045
|
+
if (source.searchParam || !query.trim()) return options
|
|
1046
|
+
const term = query.trim().toLocaleLowerCase()
|
|
1047
|
+
return options.filter(option =>
|
|
1048
|
+
[option.label, option.subtitle, option.value]
|
|
1049
|
+
.some(value => value?.toLocaleLowerCase().includes(term)),
|
|
1050
|
+
)
|
|
1051
|
+
}
|
|
1052
|
+
config.searchable = true
|
|
1053
|
+
}
|
|
972
1054
|
if (xUi.radioAlign !== undefined) config.align = xUi.radioAlign
|
|
973
1055
|
if (xUi.radioTextAlign !== undefined) config.textAlign = xUi.radioTextAlign
|
|
974
1056
|
if (xUi.disabled) config.disabled = xUi.disabled
|
|
@@ -982,6 +1064,9 @@ function createFieldFromProperty(
|
|
|
982
1064
|
if (resolvedProp.maximum !== undefined) config.max = resolvedProp.maximum
|
|
983
1065
|
if (resolvedProp.minLength !== undefined) config.min = resolvedProp.minLength
|
|
984
1066
|
if (resolvedProp.maxLength !== undefined) config.max = resolvedProp.maxLength
|
|
1067
|
+
if ((resolvedProp as Record<string, any>).pattern !== undefined) {
|
|
1068
|
+
config.pattern = (resolvedProp as Record<string, any>).pattern
|
|
1069
|
+
}
|
|
985
1070
|
|
|
986
1071
|
// Create the appropriate field builder
|
|
987
1072
|
let field: FieldBuilder
|
|
@@ -1031,10 +1116,10 @@ function createFieldFromProperty(
|
|
|
1031
1116
|
field = $.radio(xOptions ?? resolvedProp.enum ?? [], config)
|
|
1032
1117
|
break
|
|
1033
1118
|
case 'select':
|
|
1034
|
-
field = $.select(xOptions ?? resolvedProp.enum ?? [], config)
|
|
1119
|
+
field = $.select(config.options ?? xOptions ?? resolvedProp.enum ?? [], config)
|
|
1035
1120
|
break
|
|
1036
1121
|
case 'multiselect':
|
|
1037
|
-
field = $.multiselect(xOptions ?? resolvedProp.enum ?? [], config)
|
|
1122
|
+
field = $.multiselect(config.options ?? xOptions ?? resolvedProp.enum ?? [], config)
|
|
1038
1123
|
break
|
|
1039
1124
|
case 'icon':
|
|
1040
1125
|
field = $.icon(config)
|
|
@@ -1086,6 +1171,14 @@ function createFieldFromProperty(
|
|
|
1086
1171
|
: config.requiredMessage || 'form.required'
|
|
1087
1172
|
)
|
|
1088
1173
|
}
|
|
1174
|
+
if ((resolvedProp as Record<string, any>).pattern) {
|
|
1175
|
+
const pattern = new RegExp((resolvedProp as Record<string, any>).pattern)
|
|
1176
|
+
field.validate(value =>
|
|
1177
|
+
value == null || value === '' || pattern.test(String(value))
|
|
1178
|
+
? true
|
|
1179
|
+
: 'form.pattern',
|
|
1180
|
+
)
|
|
1181
|
+
}
|
|
1089
1182
|
|
|
1090
1183
|
// Apply class
|
|
1091
1184
|
if (xUi.class) {
|
|
@@ -1131,7 +1224,16 @@ function applyDesignerConfig(config: Record<string, any>, prop: JSONSchemaProper
|
|
|
1131
1224
|
set('layout', 'x-number-layout')
|
|
1132
1225
|
if (p['x-number-spinner'] === false) config.spinner = false
|
|
1133
1226
|
// Phone
|
|
1134
|
-
|
|
1227
|
+
if (typeof p['x-phone-only-countries'] === 'string') {
|
|
1228
|
+
config.onlyCountries = p['x-phone-only-countries']
|
|
1229
|
+
.split(',')
|
|
1230
|
+
.map((country: string) => country.trim())
|
|
1231
|
+
.filter(Boolean)
|
|
1232
|
+
}
|
|
1233
|
+
else if (Array.isArray(p['x-phone-only-countries'])) {
|
|
1234
|
+
config.onlyCountries = p['x-phone-only-countries']
|
|
1235
|
+
}
|
|
1236
|
+
set('defaultCountry', 'x-phone-default-country')
|
|
1135
1237
|
// Range
|
|
1136
1238
|
if (p['x-range-multi']) config.multiRange = true
|
|
1137
1239
|
// File / upload
|