@stonecrop/aform 0.2.5

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.
@@ -0,0 +1,123 @@
1
+ <template>
2
+ <div>
3
+ <label id="checkbox-container">
4
+ <input v-model="checkbox" type="checkbox" :id="uuid" class="checkbox" :readonly="readOnly" :required="required" />
5
+ <span id="custom-checkbox">{{ checkbox }}</span>
6
+ </label>
7
+ <label :for="uuid" id="checkbox-label">{{ label }}</label>
8
+ <p v-show="validation.errorMessage" v-html="validation.errorMessage"></p>
9
+ </div>
10
+ </template>
11
+
12
+ <script setup lang="ts">
13
+ import { computed, InputHTMLAttributes } from 'vue'
14
+
15
+ const props = withDefaults(
16
+ defineProps<{
17
+ label?: string
18
+ value?: InputHTMLAttributes['checked']
19
+ required?: boolean
20
+ readOnly?: boolean
21
+ uuid?: string
22
+ validation?: Record<string, any>
23
+ }>(),
24
+ {
25
+ validation: () => ({ errorMessage: '&nbsp;' }),
26
+ }
27
+ )
28
+
29
+ const emit = defineEmits<{
30
+ (e: 'update:value', value: InputHTMLAttributes['checked']): void
31
+ }>()
32
+
33
+ const checkbox = computed({
34
+ get() {
35
+ return props.value
36
+ },
37
+ set(value) {
38
+ emit('update:value', value)
39
+ },
40
+ })
41
+ </script>
42
+
43
+ <style scoped>
44
+ div {
45
+ display: inline-block;
46
+ min-width: 40ch;
47
+ border: 1px solid transparent;
48
+ padding: 0rem;
49
+ margin: 0rem;
50
+ margin-right: 1ch;
51
+ }
52
+
53
+ p,
54
+ label {
55
+ color: var(--input-label-color);
56
+ display: block;
57
+ min-height: 1.15rem;
58
+ padding: 0rem;
59
+ margin: 0rem;
60
+ border: 1px solid transparent;
61
+ margin-bottom: 0.25rem;
62
+ }
63
+
64
+ p {
65
+ width: 100%;
66
+ color: red;
67
+ font-size: 85%;
68
+ }
69
+
70
+ .checkbox {
71
+ visibility: hidden;
72
+ }
73
+
74
+ .checkbox + #custom-checkbox:after {
75
+ content: '⬡';
76
+ padding: 1ch 0 0.5ch 0;
77
+ font-size: 120%;
78
+ cursor: pointer;
79
+ position: relative;
80
+ left: -18px;
81
+ }
82
+
83
+ .checkbox:checked + #custom-checkbox:after {
84
+ content: '⬣';
85
+ padding: 1ch 0 0.5ch 0;
86
+ font-size: 120%;
87
+ cursor: pointer;
88
+ position: relative;
89
+ left: -18px;
90
+ }
91
+
92
+ #custom-checkbox {
93
+ display: inline-block;
94
+ }
95
+
96
+ #checkbox-container {
97
+ display: inline-block;
98
+ min-width: calc(100% - 1ch);
99
+ outline: 1px solid transparent;
100
+ border: 1px solid var(--input-border-color);
101
+ padding: 1ch 0.5ch 0.5ch 1ch;
102
+ margin: calc(1.15rem / 2) 0 0 0;
103
+ height: 1.15rem;
104
+ border-radius: 0.25rem;
105
+ }
106
+
107
+ #checkbox-container:hover {
108
+ border: 1px solid var(--input-active-border-color);
109
+ }
110
+
111
+ #checkbox-container:hover + label {
112
+ color: var(--input-active-label-color);
113
+ }
114
+
115
+ #checkbox-label {
116
+ z-index: 2;
117
+ font-size: 80%;
118
+ position: absolute;
119
+ background: white;
120
+ margin: calc(-1.5rem - calc(2.15rem / 2)) 0 0 1ch;
121
+ padding: 0 0.25ch 0 0.25ch;
122
+ }
123
+ </style>
@@ -0,0 +1,13 @@
1
+ <template>
2
+ <ATableModal :event="event" :cellData="cellData" class="amodal">
3
+ <div>
4
+ <input type="text" />
5
+ <input type="text" />
6
+ <input type="text" />
7
+ </div>
8
+ </ATableModal>
9
+ </template>
10
+
11
+ <script setup lang="ts">
12
+ defineProps(['event', 'cellData', 'tableID'])
13
+ </script>
@@ -0,0 +1,277 @@
1
+ <template>
2
+ <div
3
+ v-if="!readonly"
4
+ :event="event"
5
+ :colIndex="colIndex"
6
+ :rowIndex="rowIndex"
7
+ :tableid="tableid"
8
+ class="adate"
9
+ tabindex="0"
10
+ ref="adatepicker">
11
+ <table>
12
+ <tr>
13
+ <td @click="previousMonth" :tabindex="-1">&lt;</td>
14
+ <th colspan="5">{{ monthAndYear }}</th>
15
+ <td @click="nextMonth" :tabindex="-1">&gt;</td>
16
+ </tr>
17
+ <tr v-for="rowNo in numberOfRows" :key="rowNo">
18
+ <!-- TODO: (style) remove inline styling and replace with theme package -->
19
+ <td
20
+ v-for="colNo in numberOfColumns"
21
+ :key="(rowNo - 1) * numberOfColumns + colNo"
22
+ :contenteditable="false"
23
+ :spellcheck="false"
24
+ :tabindex="0"
25
+ :style="{
26
+ border: isSelectedDate(currentDates[(rowNo - 1) * numberOfColumns + colNo])
27
+ ? '2px solid var(--focus-cell-outline)'
28
+ : 'none',
29
+ borderBottomColor: isTodaysDate(currentDates[(rowNo - 1) * numberOfColumns + colNo])
30
+ ? 'var(--focus-cell-outline)'
31
+ : 'none',
32
+ }"
33
+ @click.prevent.stop="selectDate($event, (rowNo - 1) * numberOfColumns + colNo)"
34
+ :class="{
35
+ todaysdate: isTodaysDate(currentDates[(rowNo - 1) * numberOfColumns + colNo]),
36
+ selecteddate: isSelectedDate(currentDates[(rowNo - 1) * numberOfColumns + colNo]),
37
+ }">
38
+ {{ new Date(currentDates[(rowNo - 1) * numberOfColumns + colNo]).getDate() }}
39
+ </td>
40
+ </tr>
41
+ </table>
42
+ </div>
43
+ </template>
44
+
45
+ <script setup lang="ts">
46
+ import { computed, inject, nextTick, onMounted, ref, watch } from 'vue'
47
+
48
+ import { TableDataStore } from '@stonecrop/atable'
49
+ import { defaultKeypressHandlers, useKeyboardNav } from '@stonecrop/utilities'
50
+
51
+ const props = defineProps<{
52
+ colIndex?: number
53
+ rowIndex?: number
54
+ tableid?: string
55
+ event?: Event
56
+ indent?: number
57
+ readonly?: boolean
58
+ }>()
59
+
60
+ const tableData = inject<TableDataStore>(props.tableid)
61
+
62
+ const numberOfRows = 6
63
+ const numberOfColumns = 7
64
+ const todaysDate = new Date()
65
+
66
+ const selectedDate = ref<Date>()
67
+ const currentMonth = ref<number>()
68
+ const currentYear = ref<number>()
69
+ const currentDates = ref<number[]>([])
70
+ // const width = ref('')
71
+
72
+ onMounted(async () => {
73
+ let cellDate = tableData.cellData<string | number | Date>(props.colIndex, props.rowIndex)
74
+ if (cellDate) {
75
+ if (!(cellDate instanceof Date)) {
76
+ cellDate = new Date(cellDate)
77
+ }
78
+
79
+ selectedDate.value = cellDate
80
+ currentMonth.value = selectedDate.value.getMonth()
81
+ currentYear.value = selectedDate.value.getFullYear()
82
+ } else {
83
+ currentMonth.value = todaysDate.getMonth()
84
+ currentYear.value = todaysDate.getFullYear()
85
+ }
86
+
87
+ renderMonth()
88
+ await nextTick()
89
+
90
+ const $selectedDate = document.getElementsByClassName('selecteddate')
91
+ if ($selectedDate.length > 0) {
92
+ ;($selectedDate[0] as HTMLElement).focus()
93
+ } else {
94
+ const $todaysDate = document.getElementsByClassName('todaysdate')
95
+ if ($todaysDate.length > 0) {
96
+ ;($todaysDate[0] as HTMLElement).focus()
97
+ }
98
+ }
99
+ })
100
+
101
+ watch([currentMonth, currentYear], () => {
102
+ renderMonth()
103
+ })
104
+
105
+ const renderMonth = () => {
106
+ currentDates.value = []
107
+ const firstOfMonth = new Date(currentYear.value, currentMonth.value, 1)
108
+ const monthStartWeekday = firstOfMonth.getDay()
109
+ const calendarStartDay = firstOfMonth.setDate(firstOfMonth.getDate() - monthStartWeekday)
110
+ for (let dayIndex of Array(43).keys()) {
111
+ currentDates.value.push(calendarStartDay + dayIndex * 86400000)
112
+ }
113
+ }
114
+
115
+ const previousYear = () => {
116
+ currentYear.value -= 1
117
+ }
118
+
119
+ const nextYear = () => {
120
+ currentYear.value += 1
121
+ }
122
+
123
+ const previousMonth = () => {
124
+ if (currentMonth.value == 0) {
125
+ currentMonth.value = 11
126
+ previousYear()
127
+ } else {
128
+ currentMonth.value -= 1
129
+ }
130
+ }
131
+
132
+ const nextMonth = () => {
133
+ if (currentMonth.value == 11) {
134
+ currentMonth.value = 0
135
+ nextYear()
136
+ } else {
137
+ currentMonth.value += 1
138
+ }
139
+ }
140
+
141
+ const isTodaysDate = (day: string | number | Date) => {
142
+ if (currentMonth.value !== todaysDate.getMonth()) {
143
+ return
144
+ }
145
+ return todaysDate.toDateString() === new Date(day).toDateString()
146
+ }
147
+
148
+ const isSelectedDate = (day: string | number | Date) => {
149
+ return new Date(day).toDateString() === new Date(selectedDate.value).toDateString()
150
+ }
151
+
152
+ const selectDate = (event: Event, currentIndex: number) => {
153
+ selectedDate.value = new Date(currentDates.value[currentIndex])
154
+ updateData()
155
+ // TODO: (typing) figure out a way to close datepicker
156
+ // context.refs.adatepicker.destroy()
157
+ }
158
+
159
+ const updateData = () => {
160
+ // TODO: check proper date format to feed back (assuming number for now)
161
+ tableData.setCellData(props.rowIndex, props.colIndex, selectedDate.value.getTime())
162
+ }
163
+
164
+ // const dayWidth = computed(() => {
165
+ // const widthValue = Number(width.value.replace('px', ''))
166
+ // return `${widthValue / (numberOfColumns - 1)}px`
167
+ // })
168
+
169
+ const monthAndYear = computed(() => {
170
+ return new Date(currentYear.value, currentMonth.value, 1).toLocaleDateString(undefined, {
171
+ year: 'numeric',
172
+ month: 'long',
173
+ })
174
+ })
175
+
176
+ // setup keyboard navigation
177
+ useKeyboardNav([
178
+ {
179
+ parent: 'table.adate',
180
+ selectors: 'td',
181
+ handlers: {
182
+ ...defaultKeypressHandlers,
183
+ ...{
184
+ 'keydown.pageup': previousMonth,
185
+ 'keydown.shift.pageup': previousYear,
186
+ 'keydown.pagedown': nextMonth,
187
+ 'keydown.shift.pagedown': nextYear,
188
+ },
189
+ },
190
+ },
191
+ ])
192
+ </script>
193
+
194
+ <style scoped>
195
+ @import '@/theme/aform.css';
196
+
197
+ .adate {
198
+ border: 2px solid var(--focus-cell-outline);
199
+ position: absolute;
200
+ z-index: 100;
201
+ font-size: var(--table-font-size);
202
+ display: inline-table;
203
+ background-color: var(--row-color-zebra-light);
204
+ color: var(--cell-text-color);
205
+ outline: none;
206
+ width: calc(100% - 4px);
207
+ }
208
+
209
+ .adate tr {
210
+ height: 1.15rem;
211
+ text-align: center;
212
+ vertical-align: middle;
213
+ }
214
+
215
+ .adate td {
216
+ border: 2px solid transparent;
217
+ min-width: 2.25ch; /* this doesn't zoom correctly */
218
+ }
219
+
220
+ .adate td:hover {
221
+ border: 2px solid var(--focus-cell-outline);
222
+ }
223
+
224
+ .adate td {
225
+ border: 1px;
226
+ border-style: solid;
227
+ border-color: var(--cell-border-color);
228
+ border-radius: 0px;
229
+ box-sizing: border-box;
230
+ margin: 0px;
231
+ outline: none;
232
+ box-shadow: none;
233
+ color: var(--cell-text-color);
234
+ text-overflow: ellipsis;
235
+ overflow: hidden;
236
+ padding-left: 0.5ch;
237
+ padding-right: 0.5ch;
238
+ }
239
+
240
+ .adate td:focus,
241
+ .adate td:focus-within {
242
+ background-color: var(--focus-cell-background);
243
+ outline-width: 2px;
244
+ outline-style: solid;
245
+ outline-color: var(--focus-cell-outline);
246
+ box-shadow: none;
247
+ overflow: hidden;
248
+ min-height: 1.15em;
249
+ max-height: 1.15em;
250
+ overflow: hidden;
251
+ }
252
+
253
+ button {
254
+ background-color: var(--row-color-zebra-light);
255
+ border: none;
256
+ padding: 0px;
257
+ margin: 0px;
258
+ color: var(--cell-text-color);
259
+ outline: none;
260
+ font-size: var(--table-font-size);
261
+ }
262
+
263
+ .dateheader {
264
+ font-weight: 700;
265
+ display: flex;
266
+ align-items: center;
267
+ justify-content: space-between;
268
+ }
269
+
270
+ .adate .todaysdate {
271
+ border-bottom-color: var(--focus-cell-outline);
272
+ }
273
+
274
+ .adate .selecteddate {
275
+ border: 2px solid var(--focus-cell-outline);
276
+ }
277
+ </style>
@@ -0,0 +1,206 @@
1
+ <template>
2
+ <div class="autocomplete" :class="{ isOpen: isOpen }">
3
+ <div class="input-wrapper">
4
+ <input
5
+ ref="mopInput"
6
+ type="text"
7
+ @input="onChange"
8
+ @focus="onChange"
9
+ v-model="search"
10
+ @keydown.down="onArrowDown"
11
+ @keydown.up="onArrowUp"
12
+ @keydown.enter="onEnter" />
13
+ <ul id="autocomplete-results" v-show="isOpen" class="autocomplete-results">
14
+ <li class="loading autocomplete-result" v-if="isLoading">Loading results...</li>
15
+ <li
16
+ v-else
17
+ v-for="(result, i) in results"
18
+ :key="i"
19
+ @click="setResult(result)"
20
+ class="autocomplete-result"
21
+ :class="{ 'is-active': i === arrowCounter }">
22
+ {{ result }}
23
+ </li>
24
+ </ul>
25
+ <label>{{ label }}</label>
26
+ </div>
27
+ </div>
28
+ </template>
29
+
30
+ <script lang="ts">
31
+ import { defineComponent } from 'vue'
32
+ export default defineComponent({
33
+ name: 'ADropdown',
34
+ props: {
35
+ modelValue: {
36
+ type: String,
37
+ required: false,
38
+ default: '',
39
+ },
40
+ label: {
41
+ type: String,
42
+ required: true,
43
+ },
44
+ value: String,
45
+ items: {
46
+ type: Array,
47
+ required: false,
48
+ default: () => [],
49
+ },
50
+ isAsync: {
51
+ type: Boolean,
52
+ required: false,
53
+ default: false,
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
+ },
134
+ })
135
+ </script>
136
+
137
+ <style>
138
+ /* variables taken from here: https://github.com/frappe/frappe/blob/version-13/frappe/public/scss/common/awesomeplete.scss */
139
+ .autocomplete {
140
+ position: relative;
141
+ }
142
+
143
+ .input-wrapper {
144
+ min-width: 40ch;
145
+ border: 1px solid transparent;
146
+ padding: 0rem;
147
+ margin: 0rem;
148
+ margin-right: 1ch;
149
+ }
150
+
151
+ input {
152
+ width: calc(100% - 1ch);
153
+ outline: 1px solid transparent;
154
+ border: 1px solid var(--input-border-color);
155
+ padding: 1ch 0.5ch 0.5ch 1ch;
156
+ margin: calc(1.15rem / 2) 0 0 0;
157
+ min-height: 1.15rem;
158
+ border-radius: 0.25rem;
159
+ }
160
+
161
+ input:focus {
162
+ border: 1px solid var(--input-active-border-color);
163
+ border-radius: 0.25rem 0.25rem 0 0;
164
+ border-bottom: none;
165
+ }
166
+
167
+ label {
168
+ display: block;
169
+ min-height: 1.15rem;
170
+ padding: 0rem;
171
+ margin: 0rem;
172
+ border: 1px solid transparent;
173
+ margin-bottom: 0.25rem;
174
+ z-index: 2;
175
+ font-size: 80%;
176
+ position: absolute;
177
+ background: white;
178
+ margin: calc(-1.5rem - calc(2.15rem / 2)) 0 0 1ch;
179
+ padding: 0 0.25ch 0 0.25ch;
180
+ }
181
+
182
+ .autocomplete-results {
183
+ position: absolute;
184
+ width: calc(100% - 1ch + 1.5px);
185
+ z-index: 1;
186
+ padding: 0;
187
+ margin: 0;
188
+ color: #000000;
189
+ border: 1px solid var(--input-active-border-color);
190
+ border-radius: 0 0 0.25rem 0.25rem;
191
+ border-top: none;
192
+ }
193
+
194
+ .autocomplete-result {
195
+ list-style: none;
196
+ text-align: left;
197
+ padding: 4px 6px;
198
+ cursor: pointer;
199
+ }
200
+
201
+ .autocomplete-result.is-active,
202
+ .autocomplete-result:hover {
203
+ background-color: var(--row-color-zebra-light);
204
+ color: #000000;
205
+ }
206
+ </style>
@@ -0,0 +1,63 @@
1
+ <template>
2
+ <fieldset>
3
+ <legend @click="toggleCollapse" @submit="toggleCollapse">
4
+ {{ label }}
5
+ <CollapseButton v-if="collapsible" :collapsed="collapsed" />
6
+ </legend>
7
+ <slot :collapsed="collapsed">
8
+ <AForm v-show="!collapsed" v-model="formSchema" :data="formData" />
9
+ </slot>
10
+ </fieldset>
11
+ </template>
12
+
13
+ <script setup lang="ts">
14
+ import { ref } from 'vue'
15
+
16
+ import CollapseButton from '@/components/base/CollapseButton.vue'
17
+ import AForm from '@/components/AForm.vue'
18
+ import { SchemaTypes } from 'types/index'
19
+
20
+ const props = defineProps<{
21
+ schema: SchemaTypes[]
22
+ label: string
23
+ collapsible?: boolean
24
+ data?: any
25
+ }>()
26
+
27
+ const formData = ref(props.data || [])
28
+ let collapsed = ref(false)
29
+ let collapsible = ref(props.collapsible)
30
+
31
+ const formSchema = ref(props.schema)
32
+ function toggleCollapse(event: Event) {
33
+ event.preventDefault()
34
+ if (!collapsible.value) {
35
+ return
36
+ }
37
+ collapsed.value = !collapsed.value
38
+ }
39
+ </script>
40
+
41
+ <style scoped>
42
+ fieldset {
43
+ max-width: 100%;
44
+ width: 100%;
45
+ margin-right: 2ch;
46
+ border: 1px solid transparent;
47
+ border-bottom: 1px solid var(--gray-50);
48
+ }
49
+
50
+ legend {
51
+ width: 100%;
52
+ height: 1.15rem;
53
+ border: 1px solid transparent;
54
+ padding-bottom: 0.5rem;
55
+ font-size: 110%;
56
+ font-weight: 600;
57
+ user-select: none;
58
+ }
59
+
60
+ .collapse-button {
61
+ float: right;
62
+ }
63
+ </style>