@rokkit/forms 1.0.0-next.122
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/src/forms-old/input/types.d.ts +7 -0
- package/dist/src/forms-old/lib/form.d.ts +95 -0
- package/dist/src/forms-old/lib/index.d.ts +1 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/input/index.d.ts +18 -0
- package/dist/src/lib/builder.svelte.d.ts +140 -0
- package/dist/src/lib/deprecated/nested.d.ts +48 -0
- package/dist/src/lib/deprecated/nested.spec.d.ts +1 -0
- package/dist/src/lib/deprecated/validator.d.ts +30 -0
- package/dist/src/lib/deprecated/validator.spec.d.ts +1 -0
- package/dist/src/lib/fields.d.ts +16 -0
- package/dist/src/lib/fields.spec.d.ts +1 -0
- package/dist/src/lib/index.d.ts +7 -0
- package/dist/src/lib/layout.d.ts +7 -0
- package/dist/src/lib/schema.d.ts +7 -0
- package/dist/src/lib/validation.d.ts +41 -0
- package/dist/src/types.d.ts +5 -0
- package/package.json +38 -0
- package/src/DataEditor.svelte +30 -0
- package/src/FieldLayout.svelte +48 -0
- package/src/FormRenderer.svelte +118 -0
- package/src/Input.svelte +75 -0
- package/src/InputField.svelte +55 -0
- package/src/ListEditor.svelte +44 -0
- package/src/NestedEditor.svelte +85 -0
- package/src/forms-old/CheckBox.svelte +56 -0
- package/src/forms-old/DataEditor.svelte +30 -0
- package/src/forms-old/FieldLayout.svelte +48 -0
- package/src/forms-old/Form.svelte +17 -0
- package/src/forms-old/Icon.svelte +76 -0
- package/src/forms-old/Item.svelte +25 -0
- package/src/forms-old/ListEditor.svelte +44 -0
- package/src/forms-old/Tabs.svelte +57 -0
- package/src/forms-old/Wrapper.svelte +12 -0
- package/src/forms-old/input/Input.svelte +17 -0
- package/src/forms-old/input/InputField.svelte +70 -0
- package/src/forms-old/input/InputSelect.svelte +23 -0
- package/src/forms-old/input/InputSwitch.svelte +19 -0
- package/src/forms-old/input/types.js +29 -0
- package/src/forms-old/lib/form.js +72 -0
- package/src/forms-old/lib/index.js +12 -0
- package/src/forms-old/mocks/CustomField.svelte +7 -0
- package/src/forms-old/mocks/CustomWrapper.svelte +8 -0
- package/src/forms-old/mocks/Register.svelte +25 -0
- package/src/index.js +7 -0
- package/src/inp/Input.svelte +17 -0
- package/src/inp/InputField.svelte +69 -0
- package/src/inp/InputSelect.svelte +23 -0
- package/src/inp/InputSwitch.svelte +19 -0
- package/src/input/InputCheckbox.svelte +74 -0
- package/src/input/InputColor.svelte +42 -0
- package/src/input/InputDate.svelte +54 -0
- package/src/input/InputDateTime.svelte +54 -0
- package/src/input/InputEmail.svelte +63 -0
- package/src/input/InputFile.svelte +45 -0
- package/src/input/InputMonth.svelte +54 -0
- package/src/input/InputNumber.svelte +57 -0
- package/src/input/InputPassword.svelte +60 -0
- package/src/input/InputRadio.svelte +60 -0
- package/src/input/InputRange.svelte +51 -0
- package/src/input/InputSelect.svelte +71 -0
- package/src/input/InputSwitch.svelte +29 -0
- package/src/input/InputTel.svelte +60 -0
- package/src/input/InputText.svelte +60 -0
- package/src/input/InputTextArea.svelte +59 -0
- package/src/input/InputTime.svelte +54 -0
- package/src/input/InputUrl.svelte +60 -0
- package/src/input/InputWeek.svelte +54 -0
- package/src/input/index.js +23 -0
- package/src/lib/Input.svelte +291 -0
- package/src/lib/builder.svelte.js +359 -0
- package/src/lib/deprecated/Form.svelte +17 -0
- package/src/lib/deprecated/FormRenderer.svelte +121 -0
- package/src/lib/deprecated/nested.js +192 -0
- package/src/lib/deprecated/nested.spec.js +512 -0
- package/src/lib/deprecated/validator.js +137 -0
- package/src/lib/deprecated/validator.spec.js +348 -0
- package/src/lib/fields.js +119 -0
- package/src/lib/fields.spec.js +250 -0
- package/src/lib/index.js +7 -0
- package/src/lib/layout.js +63 -0
- package/src/lib/schema.js +32 -0
- package/src/lib/validation.js +273 -0
- package/src/types.js +29 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { deriveSchemaFromValue } from './schema.js'
|
|
2
|
+
import { deriveLayoutFromValue } from './layout.js'
|
|
3
|
+
import { getSchemaWithLayout } from './fields.js'
|
|
4
|
+
import { omit } from 'ramda'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {Object} FormElement
|
|
8
|
+
* @property {string} scope - JSON Pointer path (e.g., '#/email', '#/user/name')
|
|
9
|
+
* @property {string} type - Input type (text, number, range, checkbox, select, etc.)
|
|
10
|
+
* @property {any} value - Current value from data
|
|
11
|
+
* @property {boolean} override - Whether to use custom child snippet (from layout)
|
|
12
|
+
* @property {Object} props - Merged properties from schema + layout + validation
|
|
13
|
+
* @property {string} [props.label] - Display label (from layout)
|
|
14
|
+
* @property {string} [props.description] - Help text (from layout)
|
|
15
|
+
* @property {string} [props.placeholder] - Placeholder text (from layout)
|
|
16
|
+
* @property {boolean} [props.required] - Required flag (from schema)
|
|
17
|
+
* @property {number} [props.min] - Minimum value (from schema)
|
|
18
|
+
* @property {number} [props.max] - Maximum value (from schema)
|
|
19
|
+
* @property {Object} [props.message] - Validation message object
|
|
20
|
+
* @property {string} [props.message.state] - Message state: 'error', 'warning', 'info', 'success'
|
|
21
|
+
* @property {string} [props.message.text] - Message text content
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* FormBuilder class for dynamically generating forms from data structures
|
|
26
|
+
*/
|
|
27
|
+
export class FormBuilder {
|
|
28
|
+
/** @type {Object} */
|
|
29
|
+
#data = $state({})
|
|
30
|
+
|
|
31
|
+
/** @type {Object} */
|
|
32
|
+
#schema = $state({})
|
|
33
|
+
|
|
34
|
+
/** @type {Object} */
|
|
35
|
+
#layout = $state({})
|
|
36
|
+
|
|
37
|
+
/** @type {Object} */
|
|
38
|
+
#validation = $state({})
|
|
39
|
+
|
|
40
|
+
/** @type {FormElement[]} */
|
|
41
|
+
elements = $derived(this.#buildElements())
|
|
42
|
+
combined = $derived(getSchemaWithLayout(this.schema, this.layout))
|
|
43
|
+
/**
|
|
44
|
+
* Get the current data
|
|
45
|
+
* @returns {Object} Current data object
|
|
46
|
+
*/
|
|
47
|
+
get data() {
|
|
48
|
+
return this.#data
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Set the data
|
|
53
|
+
* @param {Object} value - New data object
|
|
54
|
+
*/
|
|
55
|
+
set data(value) {
|
|
56
|
+
this.#data = value
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get the current schema
|
|
61
|
+
* @returns {Object} Current schema object
|
|
62
|
+
*/
|
|
63
|
+
get schema() {
|
|
64
|
+
return this.#schema
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Set the schema
|
|
69
|
+
* @param {Object} value - New schema object
|
|
70
|
+
*/
|
|
71
|
+
set schema(value) {
|
|
72
|
+
this.#schema = value ?? deriveSchemaFromValue(this.#data)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get the current layout
|
|
77
|
+
* @returns {Object} Current layout object
|
|
78
|
+
*/
|
|
79
|
+
get layout() {
|
|
80
|
+
return this.#layout
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Set the layout
|
|
85
|
+
* @param {Object} value - New layout object
|
|
86
|
+
*/
|
|
87
|
+
set layout(value) {
|
|
88
|
+
this.#layout = value ?? deriveLayoutFromValue(this.#data)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get the current validation state
|
|
93
|
+
* @returns {Object} Current validation object
|
|
94
|
+
*/
|
|
95
|
+
get validation() {
|
|
96
|
+
return this.#validation
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Set validation messages for fields
|
|
101
|
+
* @param {Object} value - Validation object with field paths as keys
|
|
102
|
+
*/
|
|
103
|
+
set validation(value) {
|
|
104
|
+
this.#validation = value
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Create a new FormBuilder instance
|
|
109
|
+
* @param {Object} [data={}] - Initial data object
|
|
110
|
+
* @param {Object|null} [schema=null] - Optional schema override
|
|
111
|
+
* @param {Object|null} [layout=null] - Optional layout override
|
|
112
|
+
*/
|
|
113
|
+
constructor(data = {}, schema = null, layout = null) {
|
|
114
|
+
this.#data = data
|
|
115
|
+
this.schema = schema
|
|
116
|
+
this.layout = layout
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Update a specific field value
|
|
121
|
+
* @param {string} path - Field path (e.g., 'count', 'settings/distance')
|
|
122
|
+
* @param {any} value - New value
|
|
123
|
+
*/
|
|
124
|
+
updateField(path, value) {
|
|
125
|
+
// Simple path handling for now - can be enhanced for nested objects
|
|
126
|
+
const keys = path.split('/')
|
|
127
|
+
if (keys.length === 1) {
|
|
128
|
+
this.#data = { ...this.#data, [keys[0]]: value }
|
|
129
|
+
} else {
|
|
130
|
+
// Handle nested paths if needed
|
|
131
|
+
const updatedData = { ...this.#data }
|
|
132
|
+
let current = updatedData
|
|
133
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
134
|
+
current[keys[i]] = { ...current[keys[i]] }
|
|
135
|
+
current = current[keys[i]]
|
|
136
|
+
}
|
|
137
|
+
current[keys[keys.length - 1]] = value
|
|
138
|
+
this.#data = updatedData
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Get a field value by path
|
|
144
|
+
* @param {string} path - Field path
|
|
145
|
+
* @returns {any} Field value
|
|
146
|
+
*/
|
|
147
|
+
getValue(path) {
|
|
148
|
+
if (!path) return undefined
|
|
149
|
+
|
|
150
|
+
const keys = path.split('/')
|
|
151
|
+
let current = this.#data
|
|
152
|
+
for (const key of keys) {
|
|
153
|
+
if (current && typeof current === 'object') {
|
|
154
|
+
current = current[key]
|
|
155
|
+
} else {
|
|
156
|
+
return undefined
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return current
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Build form elements from schema and layout using getSchemaWithLayout
|
|
164
|
+
* @private
|
|
165
|
+
* @returns {FormElement[]} Array of form elements
|
|
166
|
+
*/
|
|
167
|
+
#buildElements() {
|
|
168
|
+
// if (!this.#schema || !this.#layout || !this.#layout.elements) {
|
|
169
|
+
// return []
|
|
170
|
+
// }
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
// Use getSchemaWithLayout to combine schema and layout
|
|
174
|
+
// const combined = getSchemaWithLayout(this.#schema, this.#layout)
|
|
175
|
+
// Convert combined elements to FormElement format
|
|
176
|
+
return this.combined.elements
|
|
177
|
+
.map((element) => this.#convertToFormElement(element))
|
|
178
|
+
.filter((element) => element !== null)
|
|
179
|
+
} catch (error) {
|
|
180
|
+
// If getSchemaWithLayout fails, fall back to basic element creation
|
|
181
|
+
console.warn('Failed to build elements:', error)
|
|
182
|
+
return this.#buildBasicElements()
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Build basic form elements when getSchemaWithLayout fails
|
|
188
|
+
* @private
|
|
189
|
+
* @returns {FormElement[]} Array of form elements
|
|
190
|
+
*/
|
|
191
|
+
#buildBasicElements() {
|
|
192
|
+
const elements = []
|
|
193
|
+
|
|
194
|
+
if (this.#layout.elements) {
|
|
195
|
+
for (const layoutElement of this.#layout.elements) {
|
|
196
|
+
const formElement = this.#buildBasicElement(layoutElement)
|
|
197
|
+
if (formElement) {
|
|
198
|
+
elements.push(formElement)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return elements
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Build a basic form element from layout only
|
|
208
|
+
* @private
|
|
209
|
+
* @param {Object} layoutElement - Layout element definition
|
|
210
|
+
* @returns {FormElement|null} Form element or null
|
|
211
|
+
*/
|
|
212
|
+
#buildBasicElement(layoutElement) {
|
|
213
|
+
const { scope, label, override = false, ...layoutProps } = layoutElement
|
|
214
|
+
|
|
215
|
+
if (!scope) return null
|
|
216
|
+
|
|
217
|
+
// Extract field name from scope (remove leading '#/')
|
|
218
|
+
const fieldPath = scope.replace(/^#\//, '')
|
|
219
|
+
const value = this.getValue(fieldPath)
|
|
220
|
+
|
|
221
|
+
// Default type is text when no schema is available
|
|
222
|
+
const type = 'text'
|
|
223
|
+
|
|
224
|
+
// Basic props
|
|
225
|
+
const props = {
|
|
226
|
+
label: label || fieldPath,
|
|
227
|
+
...layoutProps,
|
|
228
|
+
message: this.#validation[fieldPath] || null,
|
|
229
|
+
type
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
scope,
|
|
234
|
+
// type,
|
|
235
|
+
value,
|
|
236
|
+
override,
|
|
237
|
+
props
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Convert a combined schema/layout element to FormElement format
|
|
243
|
+
* @private
|
|
244
|
+
* @param {Object} element - Combined element from getSchemaWithLayout
|
|
245
|
+
* @param {string} parentPath - Parent path for nested elements
|
|
246
|
+
* @returns {FormElement} Form element
|
|
247
|
+
*/
|
|
248
|
+
#convertToFormElement(element, parentPath = '') {
|
|
249
|
+
const { key, props } = element
|
|
250
|
+
|
|
251
|
+
// Skip elements without a key
|
|
252
|
+
if (!key) {
|
|
253
|
+
return null
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Create scope in JSON Pointer format
|
|
257
|
+
const fieldPath = parentPath ? `${parentPath}/${key}` : key
|
|
258
|
+
const scope = `#/${fieldPath}`
|
|
259
|
+
const value = this.getValue(fieldPath)
|
|
260
|
+
|
|
261
|
+
// Handle nested elements (arrays and objects)
|
|
262
|
+
if (element.elements) {
|
|
263
|
+
// This is a nested structure, process children
|
|
264
|
+
const nestedElements = element.elements.map((child) =>
|
|
265
|
+
this.#convertToFormElement(child, fieldPath)
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
scope,
|
|
270
|
+
type: 'group',
|
|
271
|
+
value,
|
|
272
|
+
override: element.override || false,
|
|
273
|
+
props: {
|
|
274
|
+
...props,
|
|
275
|
+
elements: nestedElements,
|
|
276
|
+
message: this.#validation[fieldPath] || null
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Determine input type based on schema type
|
|
282
|
+
let type = 'text'
|
|
283
|
+
if (props.type) {
|
|
284
|
+
switch (props.type) {
|
|
285
|
+
case 'number':
|
|
286
|
+
case 'integer':
|
|
287
|
+
type = props.min !== undefined && props.max !== undefined ? 'range' : 'number'
|
|
288
|
+
break
|
|
289
|
+
case 'boolean':
|
|
290
|
+
type = 'checkbox'
|
|
291
|
+
break
|
|
292
|
+
case 'string':
|
|
293
|
+
if (props.enum) {
|
|
294
|
+
type = 'select'
|
|
295
|
+
// Map enum values to options format expected by select inputs
|
|
296
|
+
if (Array.isArray(props.enum)) {
|
|
297
|
+
props.options = props.enum //.map((val) => ({ value: val, label: val }))
|
|
298
|
+
}
|
|
299
|
+
} else {
|
|
300
|
+
type = 'text'
|
|
301
|
+
}
|
|
302
|
+
break
|
|
303
|
+
case 'array':
|
|
304
|
+
type = 'array'
|
|
305
|
+
break
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Add validation message if exists
|
|
310
|
+
const validationMessage = this.#validation[fieldPath] || null
|
|
311
|
+
|
|
312
|
+
// Compose final props
|
|
313
|
+
const finalProps = {
|
|
314
|
+
...props,
|
|
315
|
+
type,
|
|
316
|
+
message: validationMessage
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
scope,
|
|
321
|
+
// type,
|
|
322
|
+
value,
|
|
323
|
+
override: element.override || false,
|
|
324
|
+
props: finalProps
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Set validation message for a specific field
|
|
330
|
+
* @param {string} fieldPath - Field path (without '#/' prefix)
|
|
331
|
+
* @param {Object|null} message - Validation message object or null to clear
|
|
332
|
+
* @param {string} message.state - Message state: 'error', 'warning', 'info', 'success'
|
|
333
|
+
* @param {string} message.text - Message text content
|
|
334
|
+
*/
|
|
335
|
+
setFieldValidation(fieldPath, message) {
|
|
336
|
+
if (message) {
|
|
337
|
+
this.#validation = { ...this.#validation, [fieldPath]: message }
|
|
338
|
+
} else {
|
|
339
|
+
this.#validation = omit([fieldPath], this.#validation)
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Clear all validation messages
|
|
345
|
+
*/
|
|
346
|
+
clearValidation() {
|
|
347
|
+
this.#validation = {}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Reset form to initial state
|
|
352
|
+
*/
|
|
353
|
+
reset() {
|
|
354
|
+
this.#data = {}
|
|
355
|
+
// this.#schema = {}
|
|
356
|
+
// this.#layout = {}
|
|
357
|
+
this.#validation = {}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import DataEditor from './DataEditor.svelte'
|
|
3
|
+
|
|
4
|
+
export let value
|
|
5
|
+
export let schema = null
|
|
6
|
+
export let layout = null
|
|
7
|
+
export let using = {}
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<form on:submit>
|
|
11
|
+
<DataEditor bind:value {schema} {layout} {using} />
|
|
12
|
+
<span>
|
|
13
|
+
<slot>
|
|
14
|
+
<button type="submit">Submit</button>
|
|
15
|
+
</slot>
|
|
16
|
+
</span>
|
|
17
|
+
</form>
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
// Props using Svelte 5 runes
|
|
3
|
+
let { elements = [], onUpdate = null } = $props()
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Handle field value changes
|
|
7
|
+
* @param {string} scope - Field scope/path
|
|
8
|
+
* @param {any} value - New value
|
|
9
|
+
*/
|
|
10
|
+
function handleChange(scope, value) {
|
|
11
|
+
onUpdate?.(scope, value)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Handle range input changes (convert to number)
|
|
16
|
+
* @param {string} scope - Field scope/path
|
|
17
|
+
* @param {Event} event - Input event
|
|
18
|
+
*/
|
|
19
|
+
function handleRangeChange(scope, event) {
|
|
20
|
+
const value = Number(event.target.value)
|
|
21
|
+
handleChange(scope, value)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Handle number input changes (convert to number)
|
|
26
|
+
* @param {string} scope - Field scope/path
|
|
27
|
+
* @param {Event} event - Input event
|
|
28
|
+
*/
|
|
29
|
+
function handleNumberChange(scope, event) {
|
|
30
|
+
const value = Number(event.target.value)
|
|
31
|
+
handleChange(scope, value)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Handle checkbox changes
|
|
36
|
+
* @param {string} scope - Field scope/path
|
|
37
|
+
* @param {Event} event - Input event
|
|
38
|
+
*/
|
|
39
|
+
function handleCheckboxChange(scope, event) {
|
|
40
|
+
const value = event.target.checked
|
|
41
|
+
handleChange(scope, value)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Handle text input changes
|
|
46
|
+
* @param {string} scope - Field scope/path
|
|
47
|
+
* @param {Event} event - Input event
|
|
48
|
+
*/
|
|
49
|
+
function handleTextChange(scope, event) {
|
|
50
|
+
const value = event.target.value
|
|
51
|
+
handleChange(scope, value)
|
|
52
|
+
}
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<div class="space-y-4">
|
|
56
|
+
{#each elements as element (element.scope)}
|
|
57
|
+
<div class="form-element">
|
|
58
|
+
{#if element.type === 'range'}
|
|
59
|
+
<label class="block text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
|
60
|
+
{element.label}: {element.value}
|
|
61
|
+
<input
|
|
62
|
+
type="range"
|
|
63
|
+
value={element.value}
|
|
64
|
+
min={element.constraints?.min}
|
|
65
|
+
max={element.constraints?.max}
|
|
66
|
+
step={element.constraints?.step}
|
|
67
|
+
oninput={(e) => handleRangeChange(element.scope, e)}
|
|
68
|
+
class="mt-1 h-2 w-full cursor-pointer appearance-none rounded-lg bg-neutral-200 dark:bg-neutral-700"
|
|
69
|
+
/>
|
|
70
|
+
</label>
|
|
71
|
+
{:else if element.type === 'number'}
|
|
72
|
+
<label class="block text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
|
73
|
+
{element.label}
|
|
74
|
+
<input
|
|
75
|
+
type="number"
|
|
76
|
+
value={element.value}
|
|
77
|
+
min={element.constraints?.min}
|
|
78
|
+
max={element.constraints?.max}
|
|
79
|
+
step={element.constraints?.step}
|
|
80
|
+
oninput={(e) => handleNumberChange(element.scope, e)}
|
|
81
|
+
class="focus:border-primary-500 focus:ring-primary-500 mt-1 block w-full rounded-md border border-neutral-300 px-3 py-2 shadow-sm focus:outline-none dark:border-neutral-600 dark:bg-neutral-700 dark:text-white"
|
|
82
|
+
/>
|
|
83
|
+
</label>
|
|
84
|
+
{:else if element.type === 'checkbox'}
|
|
85
|
+
<label class="flex items-center text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
|
86
|
+
<input
|
|
87
|
+
type="checkbox"
|
|
88
|
+
checked={element.value}
|
|
89
|
+
onchange={(e) => handleCheckboxChange(element.scope, e)}
|
|
90
|
+
class="text-primary-600 focus:ring-primary-500 mr-2 h-4 w-4 rounded border-neutral-300 dark:border-neutral-600"
|
|
91
|
+
/>
|
|
92
|
+
{element.label}
|
|
93
|
+
</label>
|
|
94
|
+
{:else if element.type === 'select'}
|
|
95
|
+
<label class="block text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
|
96
|
+
{element.label}
|
|
97
|
+
<select
|
|
98
|
+
value={element.value}
|
|
99
|
+
onchange={(e) => handleTextChange(element.scope, e)}
|
|
100
|
+
class="focus:border-primary-500 focus:ring-primary-500 mt-1 block w-full rounded-md border border-neutral-300 px-3 py-2 shadow-sm focus:outline-none dark:border-neutral-600 dark:bg-neutral-700 dark:text-white"
|
|
101
|
+
>
|
|
102
|
+
{#each element.constraints?.options || [] as option, index (index)}
|
|
103
|
+
<option value={option}>{option}</option>
|
|
104
|
+
{/each}
|
|
105
|
+
</select>
|
|
106
|
+
</label>
|
|
107
|
+
{:else}
|
|
108
|
+
<!-- Default text input -->
|
|
109
|
+
<label class="block text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
|
110
|
+
{element.label}
|
|
111
|
+
<input
|
|
112
|
+
type="text"
|
|
113
|
+
value={element.value || ''}
|
|
114
|
+
oninput={(e) => handleTextChange(element.scope, e)}
|
|
115
|
+
class="focus:border-primary-500 focus:ring-primary-500 mt-1 block w-full rounded-md border border-neutral-300 px-3 py-2 shadow-sm focus:outline-none dark:border-neutral-600 dark:bg-neutral-700 dark:text-white"
|
|
116
|
+
/>
|
|
117
|
+
</label>
|
|
118
|
+
{/if}
|
|
119
|
+
</div>
|
|
120
|
+
{/each}
|
|
121
|
+
</div>
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { omit, pick } from 'ramda'
|
|
2
|
+
import { isObject } from '@rokkit/core'
|
|
3
|
+
import { typeOf } from '@rokkit/data'
|
|
4
|
+
import { deriveSchemaFromValue } from '../schema'
|
|
5
|
+
import { deriveLayoutFromValue } from '../layout'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Flattens an object into a flat object
|
|
9
|
+
*
|
|
10
|
+
* @param {Object} input - The object to flatten
|
|
11
|
+
* @param {String} scope - The scope of the object
|
|
12
|
+
*/
|
|
13
|
+
export function flattenObject(input, scope = '#') {
|
|
14
|
+
// eslint-disable-next-line no-use-before-define
|
|
15
|
+
return flattenAttributes(input, scope).reduce(
|
|
16
|
+
// eslint-disable-next-line no-use-before-define
|
|
17
|
+
(acc, item) => ({ ...acc, ...flattenElement(item) }),
|
|
18
|
+
{
|
|
19
|
+
[scope]: {
|
|
20
|
+
type: 'object',
|
|
21
|
+
value: input,
|
|
22
|
+
scope,
|
|
23
|
+
key: scope.split('/').slice(-1)[0]
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Flattens an object into an array of key-value pairs
|
|
30
|
+
*
|
|
31
|
+
* @param {Object} input - The object to flatten
|
|
32
|
+
* @param {String} scope - The scope of the object
|
|
33
|
+
*/
|
|
34
|
+
export function flattenAttributes(input, scope = '#') {
|
|
35
|
+
return Object.entries(input).map(([key, value]) => ({
|
|
36
|
+
key,
|
|
37
|
+
value,
|
|
38
|
+
type: typeOf(value),
|
|
39
|
+
scope: [scope, key].join('/')
|
|
40
|
+
}))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Derives a nested schema from an object
|
|
45
|
+
*
|
|
46
|
+
* @param {Object} input - The object to derive the schema from
|
|
47
|
+
* @param {String} scope - The scope of the object
|
|
48
|
+
* @returns {Object} The derived schema
|
|
49
|
+
*/
|
|
50
|
+
export function deriveNestedSchema(input, scope = '#') {
|
|
51
|
+
const elements = flattenAttributes(input)
|
|
52
|
+
const atoms = elements.filter(({ type }) => !['object', 'array'].includes(type))
|
|
53
|
+
|
|
54
|
+
const schema = {
|
|
55
|
+
type: 'object'
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (atoms.length > 0) {
|
|
59
|
+
schema.properties = atoms.reduce(
|
|
60
|
+
(acc, { key, type, value }) => ({
|
|
61
|
+
...acc,
|
|
62
|
+
[key]: {
|
|
63
|
+
type,
|
|
64
|
+
default: value
|
|
65
|
+
}
|
|
66
|
+
}),
|
|
67
|
+
{}
|
|
68
|
+
)
|
|
69
|
+
schema.layout = {
|
|
70
|
+
type: 'vertical',
|
|
71
|
+
elements: atoms.map((el) => ({ label: el.key, scope: el.scope }))
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (atoms.length < elements.length) {
|
|
76
|
+
// eslint-disable-next-line no-use-before-define
|
|
77
|
+
schema.children = deriveSchemaForChildren(elements, scope)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (scope !== '#') return schema
|
|
81
|
+
return schema.properties ? [schema] : schema.children
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Derives the children of an object
|
|
86
|
+
*
|
|
87
|
+
* @param {Array} elements - The elements to derive children from
|
|
88
|
+
* @param {String} scope - The scope of the object
|
|
89
|
+
* @returns {Array} The derived children
|
|
90
|
+
*/
|
|
91
|
+
function deriveSchemaForChildren(elements, scope) {
|
|
92
|
+
return [
|
|
93
|
+
...elements
|
|
94
|
+
.filter(({ type }) => type === 'object')
|
|
95
|
+
.map((item) => ({
|
|
96
|
+
...omit(['value', 'scope'], item),
|
|
97
|
+
scope: [scope, item.key].join('/'),
|
|
98
|
+
...deriveNestedSchema(item.value, [scope, item.key].join('/'))
|
|
99
|
+
})),
|
|
100
|
+
...elements
|
|
101
|
+
.filter(({ type }) => type === 'array')
|
|
102
|
+
.map((item) => ({
|
|
103
|
+
...omit(['value'], item),
|
|
104
|
+
default: [],
|
|
105
|
+
scope: [scope, item.key].join('/'),
|
|
106
|
+
items: deriveSchemaFromValue(item.value.length ? item.value[0] : null),
|
|
107
|
+
layout: deriveLayoutFromValue(item.value.length ? item.value[0] : null)
|
|
108
|
+
}))
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Flattens an element into a flat object
|
|
114
|
+
*
|
|
115
|
+
* @param {Object} element - The element to flatten
|
|
116
|
+
*/
|
|
117
|
+
export function flattenElement(element) {
|
|
118
|
+
if (element.type === 'object') {
|
|
119
|
+
return flattenObject(element.value, element.scope)
|
|
120
|
+
} else if (element.type === 'array') {
|
|
121
|
+
return element.value
|
|
122
|
+
.map((item, index) => ({
|
|
123
|
+
value: item,
|
|
124
|
+
scope: [element.scope, `[${index}]`].join('/'),
|
|
125
|
+
key: `[${index}]`,
|
|
126
|
+
type: typeOf(item)
|
|
127
|
+
}))
|
|
128
|
+
.reduce((acc, item) => ({ ...acc, ...flattenElement(item) }), {
|
|
129
|
+
[element.scope]: pick(['key', 'type', 'scope', 'value'], element)
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
return { [element.scope]: element }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Generates an index array referencing the input data
|
|
137
|
+
*
|
|
138
|
+
* @param {Object} data - The flat object to index
|
|
139
|
+
* @param {String} key - The key to use as index
|
|
140
|
+
*/
|
|
141
|
+
export function generateIndex(data, key = 'scope') {
|
|
142
|
+
const index = data
|
|
143
|
+
.map((item) => ({
|
|
144
|
+
...item,
|
|
145
|
+
_path: item[key],
|
|
146
|
+
_isParent: false,
|
|
147
|
+
_isExpanded: true,
|
|
148
|
+
_levels: []
|
|
149
|
+
}))
|
|
150
|
+
.sort((a, b) => a[key].localeCompare(b[key]))
|
|
151
|
+
.filter((item) => item[key] !== '#')
|
|
152
|
+
|
|
153
|
+
let levels = [0]
|
|
154
|
+
let current = 0
|
|
155
|
+
|
|
156
|
+
index.forEach((item, row) => {
|
|
157
|
+
const path = item._path.split('/').slice(1)
|
|
158
|
+
item._depth = path.length - 1
|
|
159
|
+
if (row === 0) {
|
|
160
|
+
item._levels = [0]
|
|
161
|
+
} else if (path.length > levels.length) {
|
|
162
|
+
index[row - 1]._isParent = true
|
|
163
|
+
item._levels = [...levels, 0]
|
|
164
|
+
} else {
|
|
165
|
+
current = levels[path.length - 1] + 1
|
|
166
|
+
item._levels = [...levels.slice(0, path.length - 1), current]
|
|
167
|
+
}
|
|
168
|
+
levels = item._levels
|
|
169
|
+
})
|
|
170
|
+
return index
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Generates a tree table from the input data
|
|
175
|
+
*
|
|
176
|
+
* @param {Object} data - The data to generate the tree table from
|
|
177
|
+
* @param {String} key - The key to use as index
|
|
178
|
+
* @param {Boolean} ellipsis - Whether to truncate the value
|
|
179
|
+
*/
|
|
180
|
+
export function generateTreeTable(data, key = 'scope', ellipsis = false) {
|
|
181
|
+
let result = []
|
|
182
|
+
if (Array.isArray(data)) result = generateIndex(data, key)
|
|
183
|
+
if (isObject(data)) result = generateIndex(Object.values(flattenObject(data)), key)
|
|
184
|
+
|
|
185
|
+
if (ellipsis) {
|
|
186
|
+
result = result.map((item) => ({
|
|
187
|
+
...omit(['value'], item),
|
|
188
|
+
value: ['array', 'object'].includes(item.type) ? '...' : item.value
|
|
189
|
+
}))
|
|
190
|
+
}
|
|
191
|
+
return result
|
|
192
|
+
}
|