@stonecrop/aform 0.2.16 → 0.2.17
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/package.json +11 -11
- package/src/components/form/ADate.vue +8 -2
- package/src/components/form/ADatePicker.vue +3 -2
- package/src/components/form/ADropdown.vue +89 -104
- package/src/components/form/AFieldset.vue +2 -2
- package/src/components/form/ATextInput.vue +22 -54
- package/src/components/utilities/Login.vue +26 -4
- package/src/directives/mask.ts +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stonecrop/aform",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.17",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": {
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"uuid": "^9.0.0",
|
|
35
35
|
"vue": "^3.4.23",
|
|
36
|
-
"@stonecrop/
|
|
37
|
-
"@stonecrop/
|
|
36
|
+
"@stonecrop/utilities": "0.2.17",
|
|
37
|
+
"@stonecrop/themes": "0.2.17"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@histoire/plugin-vue": "^0.17.17",
|
|
@@ -42,23 +42,23 @@
|
|
|
42
42
|
"@typescript-eslint/eslint-plugin": "^7.6.0",
|
|
43
43
|
"@typescript-eslint/parser": "^7.6.0",
|
|
44
44
|
"@vitejs/plugin-vue": "^5.0.4",
|
|
45
|
-
"@vitest/coverage-
|
|
46
|
-
"@vitest/ui": "^1.
|
|
47
|
-
"@vue/test-utils": "^2.
|
|
45
|
+
"@vitest/coverage-istanbul": "^1.6.0",
|
|
46
|
+
"@vitest/ui": "^1.6.0",
|
|
47
|
+
"@vue/test-utils": "^2.4.6",
|
|
48
48
|
"cypress": "^12.11.0",
|
|
49
|
-
"eslint": "^8.40.0",
|
|
50
49
|
"eslint-config-prettier": "^8.8.0",
|
|
51
50
|
"eslint-plugin-vue": "^9.11.1",
|
|
51
|
+
"eslint": "^8.40.0",
|
|
52
52
|
"histoire": "^0.17.17",
|
|
53
|
-
"jsdom": "^
|
|
53
|
+
"jsdom": "^24.0.0",
|
|
54
54
|
"typescript": "^5.4.5",
|
|
55
55
|
"vite": "^5.2.9",
|
|
56
|
-
"vitest": "^1.
|
|
56
|
+
"vitest": "^1.6.0",
|
|
57
57
|
"vue-router": "^4",
|
|
58
|
-
"@stonecrop/atable": "0.2.
|
|
58
|
+
"@stonecrop/atable": "0.2.17"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
|
-
"@stonecrop/atable": "0.2.
|
|
61
|
+
"@stonecrop/atable": "0.2.17"
|
|
62
62
|
},
|
|
63
63
|
"publishConfig": {
|
|
64
64
|
"access": "public"
|
|
@@ -18,13 +18,14 @@ import { ref } from 'vue'
|
|
|
18
18
|
|
|
19
19
|
withDefaults(
|
|
20
20
|
defineProps<{
|
|
21
|
-
label
|
|
21
|
+
label?: string
|
|
22
22
|
required?: boolean
|
|
23
23
|
readonly?: boolean
|
|
24
24
|
uuid?: string
|
|
25
25
|
validation?: Record<string, any>
|
|
26
26
|
}>(),
|
|
27
27
|
{
|
|
28
|
+
label: 'Date',
|
|
28
29
|
validation: () => ({ errorMessage: ' ' }),
|
|
29
30
|
}
|
|
30
31
|
)
|
|
@@ -34,7 +35,12 @@ const dateRef = ref<HTMLInputElement | null>(null)
|
|
|
34
35
|
|
|
35
36
|
const showPicker = () => {
|
|
36
37
|
if (dateRef.value) {
|
|
37
|
-
|
|
38
|
+
if ('showPicker' in HTMLInputElement.prototype) {
|
|
39
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/showPicker
|
|
40
|
+
// TODO: re-check browser support and compatibility; figure out alternative ways
|
|
41
|
+
// to spawn the native datepicker and eventually replace with ADatepicker
|
|
42
|
+
dateRef.value.showPicker()
|
|
43
|
+
}
|
|
38
44
|
}
|
|
39
45
|
}
|
|
40
46
|
</script>
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
<div class="adatepicker" tabindex="0" ref="adatepicker">
|
|
3
3
|
<table>
|
|
4
4
|
<tr>
|
|
5
|
-
<td @click="previousMonth" :tabindex="-1"><</td>
|
|
5
|
+
<td id="previous-month-btn" @click="previousMonth" :tabindex="-1"><</td>
|
|
6
6
|
<th colspan="5" :tabindex="-1">{{ monthAndYear }}</th>
|
|
7
|
-
<td @click="nextMonth" :tabindex="-1">></td>
|
|
7
|
+
<td id="next-month-btn" @click="nextMonth" :tabindex="-1">></td>
|
|
8
8
|
</tr>
|
|
9
9
|
<tr class="days-header">
|
|
10
10
|
<td>M</td>
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
<tr v-for="rowNo in numberOfRows" :key="rowNo">
|
|
19
19
|
<td
|
|
20
20
|
v-for="colNo in numberOfColumns"
|
|
21
|
+
ref="celldate"
|
|
21
22
|
:key="getCurrentCell(rowNo, colNo)"
|
|
22
23
|
:contenteditable="false"
|
|
23
24
|
:spellcheck="false"
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
@keydown.down="onArrowDown"
|
|
11
11
|
@keydown.up="onArrowUp"
|
|
12
12
|
@keydown.enter="onEnter" />
|
|
13
|
+
|
|
13
14
|
<ul id="autocomplete-results" v-show="isOpen" class="autocomplete-results">
|
|
14
15
|
<li class="loading autocomplete-result" v-if="isLoading">Loading results...</li>
|
|
15
16
|
<li
|
|
@@ -27,111 +28,95 @@
|
|
|
27
28
|
</div>
|
|
28
29
|
</template>
|
|
29
30
|
|
|
30
|
-
<script lang="ts">
|
|
31
|
-
import {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
},
|
|
56
|
-
emits: ['update:modelValue', 'filterChanged'],
|
|
57
|
-
data() {
|
|
58
|
-
return {
|
|
59
|
-
results: [],
|
|
60
|
-
search: this.modelValue,
|
|
61
|
-
isLoading: false,
|
|
62
|
-
arrowCounter: 0,
|
|
63
|
-
isOpen: false,
|
|
64
|
-
}
|
|
65
|
-
},
|
|
66
|
-
watch: {
|
|
67
|
-
items: function (value, oldValue) {
|
|
68
|
-
this.isLoading = false
|
|
69
|
-
this.results = value
|
|
70
|
-
},
|
|
71
|
-
},
|
|
72
|
-
mounted() {
|
|
73
|
-
document.addEventListener('click', this.handleClickOutside)
|
|
74
|
-
this.filterResults()
|
|
75
|
-
},
|
|
76
|
-
destroyed() {
|
|
77
|
-
document.removeEventListener('click', this.handleClickOutside)
|
|
78
|
-
},
|
|
79
|
-
methods: {
|
|
80
|
-
setResult(result) {
|
|
81
|
-
this.search = result
|
|
82
|
-
this.closeResults()
|
|
83
|
-
},
|
|
84
|
-
filterResults() {
|
|
85
|
-
this.results = this.items.filter(item => {
|
|
86
|
-
return item.toLowerCase().indexOf(this.search.toLowerCase()) > -1
|
|
87
|
-
})
|
|
88
|
-
},
|
|
89
|
-
onChange() {
|
|
90
|
-
this.isOpen = true
|
|
91
|
-
if (this.isAsync) {
|
|
92
|
-
this.isLoading = true
|
|
93
|
-
this.$emit('filterChanged', this.search)
|
|
94
|
-
} else {
|
|
95
|
-
this.filterResults()
|
|
96
|
-
}
|
|
97
|
-
},
|
|
98
|
-
handleClickOutside(event) {
|
|
99
|
-
if (!this.$el.contains(event.target)) {
|
|
100
|
-
this.closeResults()
|
|
101
|
-
this.arrowCounter = 0
|
|
102
|
-
}
|
|
103
|
-
},
|
|
104
|
-
closeResults() {
|
|
105
|
-
this.isOpen = false
|
|
106
|
-
|
|
107
|
-
if (!this.items.includes(this.search)) {
|
|
108
|
-
this.search = ''
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
this.$emit('update:modelValue', this.search)
|
|
112
|
-
},
|
|
113
|
-
onArrowDown() {
|
|
114
|
-
if (this.arrowCounter < this.results.length) {
|
|
115
|
-
this.arrowCounter = this.arrowCounter + 1
|
|
116
|
-
}
|
|
117
|
-
},
|
|
118
|
-
onArrowUp() {
|
|
119
|
-
if (this.arrowCounter > 0) {
|
|
120
|
-
this.arrowCounter = this.arrowCounter - 1
|
|
121
|
-
}
|
|
122
|
-
},
|
|
123
|
-
onEnter() {
|
|
124
|
-
this.search = this.results[this.arrowCounter]
|
|
125
|
-
this.closeResults()
|
|
126
|
-
this.arrowCounter = 0
|
|
127
|
-
},
|
|
128
|
-
openWithSearch() {
|
|
129
|
-
this.search = ''
|
|
130
|
-
this.onChange()
|
|
131
|
-
this.$refs.mopInput.focus()
|
|
132
|
-
},
|
|
133
|
-
},
|
|
31
|
+
<script setup lang="ts">
|
|
32
|
+
import { onMounted, onUnmounted, ref } from 'vue'
|
|
33
|
+
|
|
34
|
+
const props = defineProps<{
|
|
35
|
+
label: string
|
|
36
|
+
items?: string[]
|
|
37
|
+
isAsync?: boolean
|
|
38
|
+
}>()
|
|
39
|
+
|
|
40
|
+
const emit = defineEmits(['filterChanged'])
|
|
41
|
+
|
|
42
|
+
const results = ref(props.items)
|
|
43
|
+
const search = defineModel<string>()
|
|
44
|
+
const isLoading = ref(false)
|
|
45
|
+
const arrowCounter = ref(0)
|
|
46
|
+
const isOpen = ref(false)
|
|
47
|
+
const mopInput = ref(null)
|
|
48
|
+
|
|
49
|
+
onMounted(() => {
|
|
50
|
+
document.addEventListener('click', handleClickOutside)
|
|
51
|
+
filterResults()
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
onUnmounted(() => {
|
|
55
|
+
document.removeEventListener('click', handleClickOutside)
|
|
134
56
|
})
|
|
57
|
+
|
|
58
|
+
const setResult = result => {
|
|
59
|
+
search.value = result
|
|
60
|
+
closeResults()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const filterResults = () => {
|
|
64
|
+
if (!search.value) {
|
|
65
|
+
results.value = props.items
|
|
66
|
+
} else {
|
|
67
|
+
results.value = props.items.filter(item => {
|
|
68
|
+
return item.toLowerCase().indexOf(search.value.toLowerCase()) > -1
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const onChange = () => {
|
|
74
|
+
isOpen.value = true
|
|
75
|
+
if (props.isAsync) {
|
|
76
|
+
isLoading.value = true
|
|
77
|
+
emit('filterChanged', search.value)
|
|
78
|
+
} else {
|
|
79
|
+
filterResults()
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
84
|
+
closeResults()
|
|
85
|
+
arrowCounter.value = 0
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const closeResults = () => {
|
|
89
|
+
isOpen.value = false
|
|
90
|
+
|
|
91
|
+
// TODO: (test) when would this occur? how should this be tested?
|
|
92
|
+
if (!props.items.includes(search.value)) {
|
|
93
|
+
search.value = ''
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const onArrowDown = () => {
|
|
98
|
+
if (arrowCounter.value < results.value.length) {
|
|
99
|
+
arrowCounter.value = arrowCounter.value + 1
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const onArrowUp = () => {
|
|
104
|
+
if (arrowCounter.value > 0) {
|
|
105
|
+
arrowCounter.value = arrowCounter.value - 1
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const onEnter = () => {
|
|
110
|
+
search.value = results.value[arrowCounter.value]
|
|
111
|
+
closeResults()
|
|
112
|
+
arrowCounter.value = 0
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// const openWithSearch = () => {
|
|
116
|
+
// search.value = ''
|
|
117
|
+
// onChange()
|
|
118
|
+
// mopInput.value.focus()
|
|
119
|
+
// }
|
|
135
120
|
</script>
|
|
136
121
|
|
|
137
122
|
<style>
|
|
@@ -25,8 +25,8 @@ const props = defineProps<{
|
|
|
25
25
|
}>()
|
|
26
26
|
|
|
27
27
|
const formData = ref(props.data || [])
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
const collapsed = ref(false)
|
|
29
|
+
const collapsible = ref(props.collapsible)
|
|
30
30
|
|
|
31
31
|
const formSchema = ref(props.schema)
|
|
32
32
|
function toggleCollapse(event: Event) {
|
|
@@ -12,66 +12,34 @@
|
|
|
12
12
|
</div>
|
|
13
13
|
</template>
|
|
14
14
|
|
|
15
|
-
<script lang="ts">
|
|
16
|
-
import {
|
|
15
|
+
<script setup lang="ts">
|
|
16
|
+
import { inject, ref } from 'vue'
|
|
17
17
|
|
|
18
18
|
import { FormSchema } from 'types'
|
|
19
|
-
import { useStringMask } from '@/directives/mask'
|
|
19
|
+
import { useStringMask as vMask } from '@/directives/mask'
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
type: null as unknown as PropType<string | number>,
|
|
36
|
-
},
|
|
37
|
-
mask: {
|
|
38
|
-
type: String,
|
|
39
|
-
},
|
|
40
|
-
required: {
|
|
41
|
-
type: Boolean,
|
|
42
|
-
},
|
|
43
|
-
readonly: {
|
|
44
|
-
type: Boolean,
|
|
45
|
-
},
|
|
46
|
-
uuid: {
|
|
47
|
-
type: String,
|
|
48
|
-
},
|
|
49
|
-
validation: {
|
|
50
|
-
type: Object,
|
|
51
|
-
default: () => ({ errorMessage: ' ' }),
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
|
-
setup(props, context) {
|
|
55
|
-
const maskFilled = ref(false)
|
|
21
|
+
withDefaults(
|
|
22
|
+
defineProps<{
|
|
23
|
+
schema: FormSchema
|
|
24
|
+
label: string
|
|
25
|
+
mask?: string
|
|
26
|
+
required?: boolean
|
|
27
|
+
readonly?: boolean
|
|
28
|
+
uuid?: string
|
|
29
|
+
validation?: { errorMessage: string }
|
|
30
|
+
}>(),
|
|
31
|
+
{
|
|
32
|
+
validation: () => ({ errorMessage: ' ' }),
|
|
33
|
+
}
|
|
34
|
+
)
|
|
56
35
|
|
|
57
|
-
|
|
58
|
-
|
|
36
|
+
// TODO: setup maskFilled as a computed property
|
|
37
|
+
const maskFilled = ref(true)
|
|
59
38
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return props.modelValue
|
|
63
|
-
},
|
|
64
|
-
set(newValue) {
|
|
65
|
-
context.emit('update:modelValue', newValue)
|
|
66
|
-
},
|
|
67
|
-
})
|
|
39
|
+
// TODO: (state) replace with state management
|
|
40
|
+
// const locale = inject<string>('locale', '')
|
|
68
41
|
|
|
69
|
-
|
|
70
|
-
},
|
|
71
|
-
directives: {
|
|
72
|
-
mask: useStringMask,
|
|
73
|
-
},
|
|
74
|
-
})
|
|
42
|
+
const inputText = defineModel<number | string>()
|
|
75
43
|
</script>
|
|
76
44
|
|
|
77
45
|
<style scoped>
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
name="email"
|
|
18
18
|
placeholder="name@example.com"
|
|
19
19
|
type="email"
|
|
20
|
+
v-model="email"
|
|
20
21
|
auto-capitalize="none"
|
|
21
22
|
auto-complete="email"
|
|
22
23
|
auto-correct="off"
|
|
@@ -25,11 +26,17 @@
|
|
|
25
26
|
|
|
26
27
|
<div class="login-form-password login-form-element">
|
|
27
28
|
<label id="login-password" for="password" class="login-label">Password</label>
|
|
28
|
-
<input
|
|
29
|
+
<input
|
|
30
|
+
id="password"
|
|
31
|
+
class="login-field"
|
|
32
|
+
name="password"
|
|
33
|
+
type="password"
|
|
34
|
+
v-model="password"
|
|
35
|
+
:disabled="isLoading" />
|
|
29
36
|
</div>
|
|
30
37
|
|
|
31
|
-
<button class="btn" :disabled="isLoading">
|
|
32
|
-
<span v-if="isLoading" class="material-symbols-outlined loading-icon">
|
|
38
|
+
<button class="btn" @click="onSubmit" :disabled="isLoading || !email || !password">
|
|
39
|
+
<span v-if="isLoading" class="material-symbols-outlined loading-icon">progress_activity</span>
|
|
33
40
|
<span id="login-form-button">Login</span>
|
|
34
41
|
</button>
|
|
35
42
|
</div>
|
|
@@ -57,13 +64,28 @@ withDefaults(
|
|
|
57
64
|
}
|
|
58
65
|
)
|
|
59
66
|
|
|
67
|
+
const emit = defineEmits(['loginFailed', 'loginSuccess'])
|
|
68
|
+
|
|
69
|
+
const email = ref('')
|
|
70
|
+
const password = ref('')
|
|
71
|
+
|
|
60
72
|
const isLoading = ref(false)
|
|
73
|
+
const loginFailed = ref(false)
|
|
61
74
|
|
|
62
75
|
function onSubmit(event: Event) {
|
|
63
76
|
event.preventDefault()
|
|
64
77
|
isLoading.value = true
|
|
65
78
|
|
|
66
|
-
// TODO: handle submit logic
|
|
79
|
+
// TODO: handle submit logic, handle failure
|
|
80
|
+
|
|
81
|
+
if (loginFailed.value) {
|
|
82
|
+
isLoading.value = false
|
|
83
|
+
emit('loginFailed')
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
isLoading.value = false
|
|
88
|
+
emit('loginSuccess')
|
|
67
89
|
}
|
|
68
90
|
</script>
|
|
69
91
|
|
package/src/directives/mask.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { FormSchema } from 'types'
|
|
2
|
-
import { DirectiveBinding } from 'vue'
|
|
2
|
+
import type { DirectiveBinding } from 'vue'
|
|
3
3
|
|
|
4
4
|
const NAMED_MASKS = {
|
|
5
5
|
date: '##/##/####',
|
|
@@ -35,7 +35,7 @@ function getMask(binding: DirectiveBinding<string>) {
|
|
|
35
35
|
} else {
|
|
36
36
|
// TODO: (state) handle using state management
|
|
37
37
|
const schema: FormSchema = binding.instance['schema']
|
|
38
|
-
const fieldType: string | undefined = schema
|
|
38
|
+
const fieldType: string | undefined = schema?.fieldtype?.toLowerCase()
|
|
39
39
|
if (fieldType && NAMED_MASKS[fieldType]) {
|
|
40
40
|
mask = NAMED_MASKS[fieldType]
|
|
41
41
|
}
|