@stonecrop/aform 0.9.2 → 0.10.1
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/aform.d.ts +20 -29
- package/dist/aform.js +1818 -1743
- package/dist/aform.js.map +1 -1
- package/dist/assets/index.css +1 -1
- package/dist/directives/mask.js +9 -28
- package/dist/src/directives/mask.d.ts.map +1 -1
- package/dist/src/types/index.d.ts +19 -29
- package/dist/src/types/index.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/components/AForm.vue +30 -28
- package/src/components/form/ACheckbox.vue +18 -12
- package/src/components/form/AComboBox.vue +4 -1
- package/src/components/form/ADate.vue +17 -17
- package/src/components/form/ADatePicker.vue +9 -1
- package/src/components/form/ADropdown.vue +30 -16
- package/src/components/form/AFieldset.vue +5 -2
- package/src/components/form/AFileAttach.vue +31 -16
- package/src/components/form/ANumericInput.vue +16 -10
- package/src/components/form/ATextInput.vue +17 -11
- package/src/directives/mask.ts +9 -31
- package/src/types/index.ts +20 -31
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div v-
|
|
2
|
+
<div v-if="mode === 'display'" class="input-wrapper">
|
|
3
|
+
<span class="aform_display-value">{{ search ?? '' }}</span>
|
|
4
|
+
<label>{{ label }}</label>
|
|
5
|
+
</div>
|
|
6
|
+
<div v-else v-on-click-outside="onClickOutside" class="autocomplete" :class="{ isOpen: dropdown.open }">
|
|
3
7
|
<div class="input-wrapper">
|
|
4
8
|
<input
|
|
5
9
|
v-model="search"
|
|
6
10
|
type="text"
|
|
11
|
+
:disabled="mode === 'read'"
|
|
7
12
|
@input="filter"
|
|
8
13
|
@focus="openDropdown"
|
|
9
14
|
@keydown.down="selectNextResult"
|
|
@@ -31,26 +36,33 @@
|
|
|
31
36
|
|
|
32
37
|
<script setup lang="ts">
|
|
33
38
|
import { vOnClickOutside } from '@vueuse/components'
|
|
34
|
-
import { reactive } from 'vue'
|
|
39
|
+
import { reactive, ref } from 'vue'
|
|
40
|
+
|
|
41
|
+
import type { ComponentProps } from '../../types'
|
|
35
42
|
|
|
36
43
|
const {
|
|
37
44
|
label,
|
|
38
|
-
|
|
45
|
+
options = [],
|
|
39
46
|
isAsync = false,
|
|
40
47
|
filterFunction = undefined,
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
mode,
|
|
49
|
+
} = defineProps<
|
|
50
|
+
ComponentProps & {
|
|
51
|
+
options?: string[]
|
|
52
|
+
isAsync?: boolean
|
|
53
|
+
filterFunction?: (search: string) => string[] | Promise<string[]>
|
|
54
|
+
}
|
|
55
|
+
>()
|
|
47
56
|
const search = defineModel<string>()
|
|
48
57
|
|
|
58
|
+
// tracks the last explicitly-committed value so outside-click reverts instead of clears
|
|
59
|
+
const committedValue = ref(search.value ?? '')
|
|
60
|
+
|
|
49
61
|
const dropdown = reactive({
|
|
50
62
|
activeItemIndex: null as number | null,
|
|
51
63
|
open: false,
|
|
52
64
|
loading: false,
|
|
53
|
-
results:
|
|
65
|
+
results: options,
|
|
54
66
|
})
|
|
55
67
|
|
|
56
68
|
const onClickOutside = () => closeDropdown()
|
|
@@ -75,29 +87,31 @@ const filter = async () => {
|
|
|
75
87
|
|
|
76
88
|
const setResult = (result: string) => {
|
|
77
89
|
search.value = result
|
|
90
|
+
committedValue.value = result
|
|
78
91
|
closeDropdown(result)
|
|
79
92
|
}
|
|
80
93
|
|
|
81
94
|
const openDropdown = () => {
|
|
82
|
-
|
|
95
|
+
const idx = options?.indexOf(search.value ?? '') ?? -1
|
|
96
|
+
dropdown.activeItemIndex = isAsync ? null : idx >= 0 ? idx : null
|
|
83
97
|
dropdown.open = true
|
|
84
98
|
// TODO: this should probably call the async function if it's async
|
|
85
|
-
dropdown.results = isAsync ? [] :
|
|
99
|
+
dropdown.results = isAsync ? [] : options
|
|
86
100
|
}
|
|
87
101
|
|
|
88
102
|
const closeDropdown = (result?: string) => {
|
|
89
103
|
dropdown.activeItemIndex = null
|
|
90
104
|
dropdown.open = false
|
|
91
|
-
if (!
|
|
92
|
-
search.value =
|
|
105
|
+
if (!options?.includes(result || search.value || '')) {
|
|
106
|
+
search.value = committedValue.value
|
|
93
107
|
}
|
|
94
108
|
}
|
|
95
109
|
|
|
96
110
|
const filterResults = () => {
|
|
97
111
|
if (!search.value) {
|
|
98
|
-
dropdown.results =
|
|
112
|
+
dropdown.results = options
|
|
99
113
|
} else {
|
|
100
|
-
dropdown.results =
|
|
114
|
+
dropdown.results = options?.filter(item => item.toLowerCase().includes((search.value ?? '').toLowerCase()))
|
|
101
115
|
}
|
|
102
116
|
}
|
|
103
117
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<CollapseButton v-if="collapsible" :collapsed="collapsed" />
|
|
6
6
|
</legend>
|
|
7
7
|
<slot :collapsed="collapsed">
|
|
8
|
-
<AForm v-show="!collapsed" :schema="formSchema"
|
|
8
|
+
<AForm v-show="!collapsed" v-model:data="formData" :schema="formSchema" :mode="mode" />
|
|
9
9
|
</slot>
|
|
10
10
|
</fieldset>
|
|
11
11
|
</template>
|
|
@@ -15,18 +15,21 @@ import { ref } from 'vue'
|
|
|
15
15
|
|
|
16
16
|
import CollapseButton from '../base/CollapseButton.vue'
|
|
17
17
|
import AForm from '../AForm.vue'
|
|
18
|
-
import { SchemaTypes } from '../../types'
|
|
18
|
+
import type { SchemaTypes, FormMode } from '../../types'
|
|
19
19
|
|
|
20
20
|
const {
|
|
21
21
|
schema,
|
|
22
22
|
label,
|
|
23
23
|
collapsible,
|
|
24
24
|
data = {},
|
|
25
|
+
mode = 'edit',
|
|
25
26
|
} = defineProps<{
|
|
26
27
|
schema: SchemaTypes[]
|
|
27
28
|
label: string
|
|
28
29
|
collapsible?: boolean
|
|
29
30
|
data?: Record<string, any>
|
|
31
|
+
/** Rendering mode forwarded to the inner AForm */
|
|
32
|
+
mode?: FormMode
|
|
30
33
|
}>()
|
|
31
34
|
|
|
32
35
|
const collapsed = ref(false)
|
|
@@ -1,20 +1,32 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="aform_form-element aform_file-attach aform__grid--full">
|
|
3
|
-
<template v-if="
|
|
4
|
-
<
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
{{ file.name }}
|
|
10
|
-
</
|
|
11
|
-
</
|
|
3
|
+
<template v-if="mode === 'display'">
|
|
4
|
+
<template v-if="files">
|
|
5
|
+
<div class="aform_file-attach-feedback">
|
|
6
|
+
<p>
|
|
7
|
+
<b>{{ fileLengthText }}</b>
|
|
8
|
+
</p>
|
|
9
|
+
<li v-for="file of files" :key="file.name">{{ file.name }}</li>
|
|
10
|
+
</div>
|
|
11
|
+
</template>
|
|
12
|
+
<span v-else class="aform_display-value">No file selected</span>
|
|
13
|
+
</template>
|
|
14
|
+
<template v-else>
|
|
15
|
+
<template v-if="files">
|
|
16
|
+
<div class="aform_file-attach-feedback">
|
|
17
|
+
<p>
|
|
18
|
+
You have selected: <b>{{ fileLengthText }}</b>
|
|
19
|
+
</p>
|
|
20
|
+
<li v-for="file of files" :key="file.name">
|
|
21
|
+
{{ file.name }}
|
|
22
|
+
</li>
|
|
23
|
+
</div>
|
|
24
|
+
</template>
|
|
25
|
+
<button type="button" class="aform_form-btn" :disabled="mode === 'read'" @click="open()">
|
|
26
|
+
{{ label }}
|
|
27
|
+
</button>
|
|
28
|
+
<button type="button" :disabled="!files || mode === 'read'" class="aform_form-btn" @click="reset()">Reset</button>
|
|
12
29
|
</template>
|
|
13
|
-
|
|
14
|
-
<button type="button" class="aform_form-btn" @click="open()">
|
|
15
|
-
{{ label }}
|
|
16
|
-
</button>
|
|
17
|
-
<button type="button" :disabled="!files" class="aform_form-btn" @click="reset()">Reset</button>
|
|
18
30
|
</div>
|
|
19
31
|
</template>
|
|
20
32
|
|
|
@@ -22,11 +34,14 @@
|
|
|
22
34
|
import { useFileDialog } from '@vueuse/core'
|
|
23
35
|
import { computed } from 'vue'
|
|
24
36
|
|
|
25
|
-
|
|
37
|
+
import type { ComponentProps } from '../../types'
|
|
38
|
+
|
|
39
|
+
const { label, mode } = defineProps<ComponentProps>()
|
|
26
40
|
const { files, open, reset, onChange } = useFileDialog()
|
|
27
41
|
|
|
28
42
|
const fileLengthText = computed(() => {
|
|
29
|
-
|
|
43
|
+
const count = files.value?.length ?? 0
|
|
44
|
+
return `${count} ${count === 1 ? 'file' : 'files'}`
|
|
30
45
|
})
|
|
31
46
|
|
|
32
47
|
onChange(files => files)
|
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="aform_form-element">
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
3
|
+
<template v-if="mode === 'display'">
|
|
4
|
+
<span class="aform_display-value">{{ inputNumber ?? '' }}</span>
|
|
5
|
+
<label class="aform_field-label">{{ label }}</label>
|
|
6
|
+
</template>
|
|
7
|
+
<template v-else>
|
|
8
|
+
<input
|
|
9
|
+
:id="uuid"
|
|
10
|
+
v-model="inputNumber"
|
|
11
|
+
class="aform_input-field"
|
|
12
|
+
type="number"
|
|
13
|
+
:disabled="mode === 'read'"
|
|
14
|
+
:required="required" />
|
|
15
|
+
<label class="aform_field-label" :for="uuid">{{ label }}</label>
|
|
16
|
+
<p v-show="validation.errorMessage" class="aform_error" v-html="validation.errorMessage"></p>
|
|
17
|
+
</template>
|
|
12
18
|
</div>
|
|
13
19
|
</template>
|
|
14
20
|
|
|
15
21
|
<script setup lang="ts">
|
|
16
22
|
import { ComponentProps } from '../../types'
|
|
17
23
|
|
|
18
|
-
const { label, required,
|
|
24
|
+
const { label, required, mode, uuid, validation = { errorMessage: ' ' } } = defineProps<ComponentProps>()
|
|
19
25
|
const inputNumber = defineModel<number>()
|
|
20
26
|
</script>
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="aform_form-element">
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
3
|
+
<template v-if="mode === 'display'">
|
|
4
|
+
<span class="aform_display-value">{{ inputText ?? '' }}</span>
|
|
5
|
+
<label class="aform_field-label">{{ label }}</label>
|
|
6
|
+
</template>
|
|
7
|
+
<template v-else>
|
|
8
|
+
<input
|
|
9
|
+
:id="uuid"
|
|
10
|
+
v-model="inputText"
|
|
11
|
+
v-mask="mask"
|
|
12
|
+
class="aform_input-field"
|
|
13
|
+
:disabled="mode === 'read'"
|
|
14
|
+
:maxlength="mask ? (maskFilled ? mask.length : undefined) : undefined"
|
|
15
|
+
:required="required" />
|
|
16
|
+
<label class="aform_field-label" :for="uuid">{{ label }} </label>
|
|
17
|
+
<p v-show="validation.errorMessage" class="aform_error" v-html="validation.errorMessage"></p>
|
|
18
|
+
</template>
|
|
13
19
|
</div>
|
|
14
20
|
</template>
|
|
15
21
|
|
|
@@ -19,7 +25,7 @@ import { /* inject, */ ref } from 'vue'
|
|
|
19
25
|
import { useStringMask as vMask } from '../../directives/mask'
|
|
20
26
|
import { ComponentProps } from '../../types'
|
|
21
27
|
|
|
22
|
-
const { label, mask, required,
|
|
28
|
+
const { label, mask, required, mode, uuid, validation = { errorMessage: ' ' } } = defineProps<ComponentProps>()
|
|
23
29
|
|
|
24
30
|
// TODO: setup maskFilled as a computed property
|
|
25
31
|
const maskFilled = ref(true)
|
package/src/directives/mask.ts
CHANGED
|
@@ -1,19 +1,5 @@
|
|
|
1
1
|
import type { DirectiveBinding } from 'vue'
|
|
2
2
|
|
|
3
|
-
import type { FormSchema } from '../types'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Named masks for common input types
|
|
7
|
-
*/
|
|
8
|
-
const NAMED_MASKS = {
|
|
9
|
-
date: '##/##/####',
|
|
10
|
-
datetime: '####/##/## ##:##',
|
|
11
|
-
time: '##:##',
|
|
12
|
-
fulltime: '##:##:##',
|
|
13
|
-
phone: '(###) ### - ####',
|
|
14
|
-
card: '#### #### #### ####',
|
|
15
|
-
}
|
|
16
|
-
|
|
17
3
|
/**
|
|
18
4
|
* Extracts a mask function from a stringified function
|
|
19
5
|
* @param mask - Mask string
|
|
@@ -36,23 +22,15 @@ function extractMaskFn(mask: string): ((args: any) => string) | void {
|
|
|
36
22
|
* @returns Mask string
|
|
37
23
|
*/
|
|
38
24
|
function getMask(binding: DirectiveBinding<string>) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
} else {
|
|
50
|
-
// TODO: (state) handle using state management
|
|
51
|
-
const schema = binding.instance?.['schema'] as FormSchema
|
|
52
|
-
const fieldType: string | undefined = schema?.fieldtype?.toLowerCase()
|
|
53
|
-
if (fieldType && NAMED_MASKS[fieldType]) {
|
|
54
|
-
mask = NAMED_MASKS[fieldType]
|
|
55
|
-
}
|
|
25
|
+
const mask = binding.value
|
|
26
|
+
if (!mask) return undefined
|
|
27
|
+
|
|
28
|
+
const maskFn = extractMaskFn(mask)
|
|
29
|
+
if (maskFn) {
|
|
30
|
+
// TODO: (state) replace with state management;
|
|
31
|
+
// pass the entire form/table data to the function
|
|
32
|
+
const locale = binding.instance?.['locale']
|
|
33
|
+
return maskFn(locale) as string
|
|
56
34
|
}
|
|
57
35
|
|
|
58
36
|
return mask
|
package/src/types/index.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { TableColumn, TableConfig, TableRow } from '@stonecrop/atable'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* The rendering mode for AForm components
|
|
5
|
+
* @public
|
|
6
|
+
*/
|
|
7
|
+
export type FormMode = 'edit' | 'read' | 'display'
|
|
8
|
+
|
|
3
9
|
/**
|
|
4
10
|
* Defined props for AForm components
|
|
5
11
|
* @public
|
|
@@ -18,7 +24,10 @@ export type ComponentProps = {
|
|
|
18
24
|
label?: string
|
|
19
25
|
|
|
20
26
|
/**
|
|
21
|
-
* The
|
|
27
|
+
* The mask to apply to inputs inside the component. Accepts either a plain
|
|
28
|
+
* mask string (e.g. `"(###) ###-####"`) or a stringified arrow function that
|
|
29
|
+
* receives `locale` and returns a mask string
|
|
30
|
+
* (e.g. `"(locale) => locale === 'en-US' ? '(###) ###-####' : '####-######'"`).
|
|
22
31
|
* @public
|
|
23
32
|
*/
|
|
24
33
|
mask?: string
|
|
@@ -30,10 +39,10 @@ export type ComponentProps = {
|
|
|
30
39
|
required?: boolean
|
|
31
40
|
|
|
32
41
|
/**
|
|
33
|
-
*
|
|
42
|
+
* The rendering mode for the component
|
|
34
43
|
* @public
|
|
35
44
|
*/
|
|
36
|
-
|
|
45
|
+
mode?: FormMode
|
|
37
46
|
|
|
38
47
|
/**
|
|
39
48
|
* Set a unique identifier for elements inside the component
|
|
@@ -79,10 +88,10 @@ export type BaseSchema = {
|
|
|
79
88
|
component?: string
|
|
80
89
|
|
|
81
90
|
/**
|
|
82
|
-
*
|
|
83
|
-
* @
|
|
91
|
+
* Per-field rendering mode override; takes precedence over the AForm-level `mode` prop
|
|
92
|
+
* @public
|
|
84
93
|
*/
|
|
85
|
-
|
|
94
|
+
mode?: FormMode
|
|
86
95
|
}
|
|
87
96
|
|
|
88
97
|
/**
|
|
@@ -104,17 +113,6 @@ export type FormSchema = BaseSchema & {
|
|
|
104
113
|
|
|
105
114
|
/**
|
|
106
115
|
* The field type for the schema field
|
|
107
|
-
*
|
|
108
|
-
* @remarks
|
|
109
|
-
* This must be a string that represents the field type. A mask string will be automatically
|
|
110
|
-
* applied for the following field types:
|
|
111
|
-
* - Date ('##/##/####')
|
|
112
|
-
* - Datetime ('####/##/## ##:##')
|
|
113
|
-
* - Time ('##:##')
|
|
114
|
-
* - Fulltime ('##:##:##')
|
|
115
|
-
* - Phone ('(###) ### - ####')
|
|
116
|
-
* - Card ('#### #### #### ####')
|
|
117
|
-
*
|
|
118
116
|
* @public
|
|
119
117
|
*/
|
|
120
118
|
fieldtype?: string
|
|
@@ -138,8 +136,11 @@ export type FormSchema = BaseSchema & {
|
|
|
138
136
|
width?: string
|
|
139
137
|
|
|
140
138
|
/**
|
|
141
|
-
* The mask
|
|
142
|
-
*
|
|
139
|
+
* The mask to apply to the field. Accepts either a plain mask string
|
|
140
|
+
* (e.g. `"##/##/####"`) or a stringified arrow function that receives `locale`
|
|
141
|
+
* and returns a mask string
|
|
142
|
+
* (e.g. `"(locale) => locale === 'en-US' ? '(###) ###-####' : '####-######'"`).
|
|
143
|
+
* @public
|
|
143
144
|
*/
|
|
144
145
|
mask?: string
|
|
145
146
|
}
|
|
@@ -232,12 +233,6 @@ export type DoctypeSchema = BaseSchema & {
|
|
|
232
233
|
* @public
|
|
233
234
|
*/
|
|
234
235
|
schema?: SchemaTypes[]
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Indicate whether the nested form is read-only
|
|
238
|
-
* @public
|
|
239
|
-
*/
|
|
240
|
-
readOnly?: boolean
|
|
241
236
|
}
|
|
242
237
|
|
|
243
238
|
/**
|
|
@@ -291,12 +286,6 @@ export type TableDoctypeSchema = BaseSchema & {
|
|
|
291
286
|
* @public
|
|
292
287
|
*/
|
|
293
288
|
rows?: TableRow[]
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Indicate whether the table is read-only
|
|
297
|
-
* @public
|
|
298
|
-
*/
|
|
299
|
-
readOnly?: boolean
|
|
300
289
|
}
|
|
301
290
|
|
|
302
291
|
/**
|