@stonecrop/aform 0.31.0 → 0.32.0

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.
Files changed (92) hide show
  1. package/dist/aform.js +3024 -2256
  2. package/dist/aform.js.map +1 -1
  3. package/dist/assets/index.css +1170 -1
  4. package/dist/src/components/AForm.vue.d.ts +20 -0
  5. package/dist/src/components/AForm.vue.d.ts.map +1 -0
  6. package/dist/src/components/AFormLoading.vue.d.ts +14 -0
  7. package/dist/src/components/AFormLoading.vue.d.ts.map +1 -0
  8. package/dist/src/components/base/CollapseButton.vue.d.ts +7 -0
  9. package/dist/src/components/base/CollapseButton.vue.d.ts.map +1 -0
  10. package/dist/src/components/base/ExpandButton.vue.d.ts +7 -0
  11. package/dist/src/components/base/ExpandButton.vue.d.ts.map +1 -0
  12. package/dist/src/components/form/ABadge.vue.d.ts +14 -0
  13. package/dist/src/components/form/ABadge.vue.d.ts.map +1 -0
  14. package/dist/src/components/form/ACheckbox.vue.d.ts +15 -0
  15. package/dist/src/components/form/ACheckbox.vue.d.ts.map +1 -0
  16. package/dist/src/components/form/ACurrencyInput.vue.d.ts +20 -0
  17. package/dist/src/components/form/ACurrencyInput.vue.d.ts.map +1 -0
  18. package/dist/src/components/form/ADate.vue.d.ts +14 -0
  19. package/dist/src/components/form/ADate.vue.d.ts.map +1 -0
  20. package/dist/src/components/form/ADatePicker.vue.d.ts +28 -0
  21. package/dist/src/components/form/ADatePicker.vue.d.ts.map +1 -0
  22. package/dist/src/components/form/ADateRange.vue.d.ts +18 -0
  23. package/dist/src/components/form/ADateRange.vue.d.ts.map +1 -0
  24. package/dist/src/components/form/ADateSelection.vue.d.ts +52 -0
  25. package/dist/src/components/form/ADateSelection.vue.d.ts.map +1 -0
  26. package/dist/src/components/form/ADateTime.vue.d.ts +17 -0
  27. package/dist/src/components/form/ADateTime.vue.d.ts.map +1 -0
  28. package/dist/src/components/form/ADateTimeInput.vue.d.ts +30 -0
  29. package/dist/src/components/form/ADateTimeInput.vue.d.ts.map +1 -0
  30. package/dist/src/components/form/ADropdown.vue.d.ts +20 -0
  31. package/dist/src/components/form/ADropdown.vue.d.ts.map +1 -0
  32. package/dist/src/components/form/ADuration.vue.d.ts +18 -0
  33. package/dist/src/components/form/ADuration.vue.d.ts.map +1 -0
  34. package/dist/src/components/form/AFieldset.vue.d.ts +28 -0
  35. package/dist/src/components/form/AFieldset.vue.d.ts.map +1 -0
  36. package/dist/src/components/form/AFileAttach.vue.d.ts +5 -0
  37. package/dist/src/components/form/AFileAttach.vue.d.ts.map +1 -0
  38. package/dist/src/components/form/AFormLink.vue.d.ts +40 -0
  39. package/dist/src/components/form/AFormLink.vue.d.ts.map +1 -0
  40. package/dist/src/components/form/ANumericInput.vue.d.ts +14 -0
  41. package/dist/src/components/form/ANumericInput.vue.d.ts.map +1 -0
  42. package/dist/src/components/form/AQuantityInput.vue.d.ts +20 -0
  43. package/dist/src/components/form/AQuantityInput.vue.d.ts.map +1 -0
  44. package/dist/src/components/form/ASegmentedControl.vue.d.ts +23 -0
  45. package/dist/src/components/form/ASegmentedControl.vue.d.ts.map +1 -0
  46. package/dist/src/components/form/ATextInput.vue.d.ts +14 -0
  47. package/dist/src/components/form/ATextInput.vue.d.ts.map +1 -0
  48. package/dist/src/components/form/ATextboxInput.vue.d.ts +21 -0
  49. package/dist/src/components/form/ATextboxInput.vue.d.ts.map +1 -0
  50. package/dist/src/components/utilities/Login.vue.d.ts +14 -0
  51. package/dist/src/components/utilities/Login.vue.d.ts.map +1 -0
  52. package/dist/{tsdoc-metadata.json → src/tsdoc-metadata.json} +1 -1
  53. package/package.json +35 -30
  54. package/dist/aform.d.ts +0 -441
  55. package/dist/aform.tsbuildinfo +0 -1
  56. package/dist/src/directives/mask.js +0 -107
  57. package/dist/src/index.js +0 -58
  58. package/dist/src/types/index.js +0 -0
  59. package/dist/src/utils/badge.js +0 -71
  60. package/dist/src/utils/columns.js +0 -33
  61. package/dist/src/utils/deserialize.js +0 -20
  62. package/src/components/AForm.vue +0 -314
  63. package/src/components/AFormLoading.vue +0 -76
  64. package/src/components/base/CollapseButton.vue +0 -36
  65. package/src/components/base/ExpandButton.vue +0 -34
  66. package/src/components/form/ABadge.vue +0 -135
  67. package/src/components/form/ACheckbox.vue +0 -62
  68. package/src/components/form/ACurrencyInput.vue +0 -357
  69. package/src/components/form/ADate.vue +0 -93
  70. package/src/components/form/ADatePicker.vue +0 -470
  71. package/src/components/form/ADateRange.vue +0 -133
  72. package/src/components/form/ADateSelection.vue +0 -174
  73. package/src/components/form/ADateTime.vue +0 -149
  74. package/src/components/form/ADateTimeInput.vue +0 -299
  75. package/src/components/form/ADropdown.vue +0 -222
  76. package/src/components/form/ADuration.vue +0 -124
  77. package/src/components/form/AFieldset.vue +0 -72
  78. package/src/components/form/AFileAttach.vue +0 -97
  79. package/src/components/form/AFormLink.vue +0 -368
  80. package/src/components/form/ANumericInput.vue +0 -32
  81. package/src/components/form/AQuantityInput.vue +0 -349
  82. package/src/components/form/ASegmentedControl.vue +0 -342
  83. package/src/components/form/ATextInput.vue +0 -40
  84. package/src/components/form/ATextboxInput.vue +0 -66
  85. package/src/components/utilities/Login.vue +0 -189
  86. package/src/directives/mask.ts +0 -122
  87. package/src/index.ts +0 -92
  88. package/src/shims-vue.d.ts +0 -5
  89. package/src/types/index.ts +0 -333
  90. package/src/utils/badge.ts +0 -94
  91. package/src/utils/columns.ts +0 -38
  92. package/src/utils/deserialize.ts +0 -20
@@ -1,368 +0,0 @@
1
- <template>
2
- <div :class="['aform_form-element', { 'aform_form-element--embedded': embedded }]">
3
- <span v-if="mode === 'display'" class="aform_display-value">{{ displayedText }}</span>
4
- <template v-else>
5
- <div v-on-click-outside="onClickOutside" class="aform_form-link-wrapper">
6
- <div class="input-group">
7
- <input
8
- v-model="searchText"
9
- type="text"
10
- role="combobox"
11
- autocomplete="off"
12
- aria-autocomplete="list"
13
- :class="['aform_input-field', { 'aform_input-field--embedded': embedded }]"
14
- :placeholder="placeholder"
15
- :aria-label="ariaLabel"
16
- :required="required"
17
- :aria-expanded="dropdownOpen"
18
- :aria-controls="dropdownOpen ? listboxId : undefined"
19
- :aria-activedescendant="activeIndex === null ? undefined : `${listboxId}-opt-${activeIndex}`"
20
- :disabled="disabled || mode === 'read'"
21
- @input="onInput"
22
- @focus="onFocus"
23
- @keydown.down.prevent="selectNext"
24
- @keydown.up.prevent="selectPrev"
25
- @keydown.enter.prevent="selectCurrent"
26
- @keydown.esc="closeDropdown"
27
- @keydown.tab="closeDropdown" />
28
- <button
29
- v-if="hasValidId && !disabled && !embedded"
30
- type="button"
31
- class="aform_form-btn"
32
- @click="handleNavigate"
33
- @keydown.enter.prevent="handleNavigate">
34
- <span>{{ icon === 'chevron-right' ? '›' : '→' }}</span>
35
- </button>
36
- </div>
37
- <ul v-if="dropdownOpen" :id="listboxId" class="autocomplete-results" role="listbox" :aria-label="ariaLabel">
38
- <li v-if="loading" class="autocomplete-result loading">Loading…</li>
39
- <li
40
- v-for="(option, i) in dropdownResults"
41
- v-else
42
- :id="`${listboxId}-opt-${i}`"
43
- :key="String(option.id)"
44
- role="option"
45
- :aria-selected="i === activeIndex"
46
- class="autocomplete-result"
47
- :class="{ 'is-active': i === activeIndex }"
48
- @mousedown.prevent="selectOption(option)">
49
- <slot name="option" :option="option">{{ option.displayText ?? String(option.id) }}</slot>
50
- </li>
51
- </ul>
52
- </div>
53
- <label v-if="label && !embedded" class="aform_field-label">{{ label }}</label>
54
- </template>
55
- </div>
56
- </template>
57
-
58
- <script setup lang="ts">
59
- import { vOnClickOutside } from '@vueuse/components'
60
- import { computed, inject, ref, useId, watch } from 'vue'
61
-
62
- import type { AFormLinkModelValue, AFormLinkNavigator, AFormLinkValue, ComponentProps } from '../../types'
63
- import { deserializeFunction } from '../../utils/deserialize'
64
-
65
- const {
66
- label,
67
- mode,
68
- uuid,
69
- required,
70
- doctype = undefined,
71
- formatter = undefined,
72
- icon = 'arrow-right',
73
- disabled = false,
74
- filterFunction = undefined,
75
- isAsync = false,
76
- embedded = false,
77
- placeholder = undefined,
78
- ariaLabel = undefined,
79
- } = defineProps<
80
- ComponentProps & {
81
- doctype?: string
82
- formatter?: (value: AFormLinkValue) => string
83
- icon?: 'arrow-right' | 'chevron-right'
84
- disabled?: boolean
85
- filterFunction?: string | ((search: string) => AFormLinkValue[] | Promise<AFormLinkValue[]>)
86
- isAsync?: boolean
87
- // Bare rendering for compositing into another component's own bordered container
88
- // (e.g. ACurrencyInput's merged amount+currency group): suppresses this component's
89
- // own outline/border and floating label so the parent supplies both exactly once.
90
- embedded?: boolean
91
- placeholder?: string
92
- // Accessible name for the search input. Needed in `embedded` mode, where the visible
93
- // <label> is suppressed and a placeholder is not a dependable accessible name — it is
94
- // not exposed consistently across screen readers and vanishes once a value is picked.
95
- ariaLabel?: string
96
- }
97
- >()
98
-
99
- // Ties the input to its listbox and to the active option (aria-controls / aria-activedescendant).
100
- // `uuid` is per-field when AForm supplies it; the fallback keeps the ids unique for a standalone
101
- // mount so two pickers on one page can't cross-wire their ARIA relationships.
102
- const listboxId = `${uuid ?? `aform-link-${useId()}`}-listbox`
103
-
104
- const modelValue = defineModel<AFormLinkModelValue>({ default: () => ({ id: '', displayText: '' }) })
105
-
106
- const hasValidId = computed(() => linkId(modelValue.value) !== undefined)
107
-
108
- const displayedText = computed(() => {
109
- const id = linkId(modelValue.value)
110
- if (id === undefined) return '—'
111
- if (formatter) return formatter(asLinkValue(modelValue.value))
112
- return linkDisplayText(modelValue.value) ?? id
113
- })
114
-
115
- const searchText = ref(hasValidId.value ? displayedText.value : '')
116
- const dropdownOpen = ref(false)
117
- const loading = ref(false)
118
- const dropdownResults = ref<AFormLinkValue[]>([])
119
- const activeIndex = ref<number | null>(null)
120
-
121
- const navigator = inject<AFormLinkNavigator | null>('aformLinkNavigator', null)
122
-
123
- type FilterFn = (search: string) => AFormLinkValue[] | Promise<AFormLinkValue[]>
124
- type ResolverFn = (doctype: string, id: string) => string | undefined | Promise<string | undefined>
125
- const resolver = inject<ResolverFn | null>('aformLinkResolver', null)
126
-
127
- // A link's value arrives as a bare scalar as often as it does as an AFormLinkValue: a record
128
- // loaded from the DB carries the raw FK column, and a parent bound to that column coerces every
129
- // update it receives back to a scalar. So read the value through these three helpers rather than
130
- // reaching into `modelValue.value` directly.
131
- //
132
- // Rejected: normalizing on write — an immediate watch that replaced a scalar model with `{ id }`.
133
- // It survives only until the parent writes its scalar back, so it fixed a scalar passed once at
134
- // mount and nothing else, and it emitted an `update:modelValue` the user never made on every
135
- // mount of a scalar-valued link field.
136
- function linkId(value: AFormLinkModelValue | null | undefined): string | undefined {
137
- if (value == null) return undefined
138
- if (typeof value === 'string') return value === '' ? undefined : value
139
- if (typeof value === 'number') return String(value)
140
- const id = value.id
141
- if (id === null || id === undefined || id === '') return undefined
142
- return String(id)
143
- }
144
-
145
- function linkDisplayText(value: AFormLinkModelValue | null | undefined): string | undefined {
146
- if (value == null || typeof value !== 'object') return undefined
147
- const text = value.displayText
148
- return typeof text === 'string' || typeof text === 'number' ? String(text) : undefined
149
- }
150
-
151
- function asLinkValue(value: AFormLinkModelValue | null | undefined): AFormLinkValue {
152
- if (value != null && typeof value === 'object') return value
153
- const id = linkId(value)
154
- return id !== undefined ? { id } : { id: '' }
155
- }
156
-
157
- // When the id changes (including on first render), resolve its display text.
158
- // Tries filterFunction first (returns a list of candidates to search); falls back to the
159
- // injected resolver (a direct doctype+id lookup). Skips if displayText is already set.
160
- watch(
161
- () => linkId(modelValue.value),
162
- async id => {
163
- // Already carries its text: nothing to resolve, but the input still has to show it.
164
- // `searchText` is assigned at setup and then only by the branches below, by `selectOption`
165
- // and by `closeDropdown` — and at setup a record's value has not arrived yet. So a link the
166
- // server resolved for us (the middleware returns `{ id, displayText }` for an inline link)
167
- // rendered blank, while a bare id rendered fine because it took the resolve branch.
168
- if (id && linkDisplayText(modelValue.value)) {
169
- // Through `displayedText` rather than the raw text, for the reason the resolve branch
170
- // formats too: the same value must not render differently depending on how it arrived.
171
- searchText.value = displayedText.value
172
- return
173
- }
174
- if (!id) return
175
- try {
176
- let match: AFormLinkValue | undefined
177
- let displayText: string | undefined
178
- if (filterFunction) {
179
- const fn: FilterFn =
180
- typeof filterFunction === 'string' ? deserializeFunction<FilterFn>(filterFunction) : filterFunction
181
- const results = await fn(String(id))
182
- match = results.find(r => String(r.id) === String(id))
183
- displayText = match?.displayText
184
- if (displayText === undefined) {
185
- console.warn(
186
- `[AFormLink] filterFunction returned no matching result for id "${id}". ` +
187
- `The function must return an item whose \`id\` exactly matches the search string.`
188
- )
189
- }
190
- } else if (resolver && doctype) {
191
- displayText = (await resolver(doctype, id.toString())) ?? undefined
192
- }
193
- if (displayText) {
194
- // Merge the whole matched record, not just its display text — the extra properties
195
- // an AFormLinkValue carries are part of the value (ACurrencyInput's `symbol`, which
196
- // its formatter renders, is one). `id` is pinned to the value we already hold so a
197
- // loosely-typed match (1 vs '1') can't change the FK's type underneath the record.
198
- const resolved: AFormLinkValue = { ...asLinkValue(modelValue.value), ...match, id, displayText }
199
- // Format for the same reason selectOption does: the input shows `formatter`'s output
200
- // everywhere else (initial render, blur), so assigning the raw displayText here would
201
- // make a value that arrived as a bare id render differently from the identical value
202
- // picked by hand — e.g. "Euro" instead of "€".
203
- searchText.value = formatter ? formatter(resolved) : displayText
204
- modelValue.value = resolved
205
- }
206
- } catch {
207
- // silent — fall back to showing the raw id
208
- }
209
- },
210
- { immediate: true }
211
- )
212
-
213
- const handleNavigate = () => {
214
- const id = linkId(modelValue.value)
215
- if (navigator && doctype && id !== undefined) {
216
- navigator.navigate(doctype, id)
217
- } else if (navigator && !doctype) {
218
- console.warn(
219
- `[AFormLink] Navigation requested but \`doctype\` prop is missing. ` +
220
- `Add \`options\` (or \`doctype\`) to the Link field definition to enable navigation.`
221
- )
222
- }
223
- }
224
-
225
- const openDropdown = async (text: string) => {
226
- if (!filterFunction || mode === 'read') return
227
- activeIndex.value = null
228
- dropdownOpen.value = true
229
- if (isAsync) loading.value = true
230
- try {
231
- const fn: FilterFn =
232
- typeof filterFunction === 'string' ? deserializeFunction<FilterFn>(filterFunction) : filterFunction
233
- dropdownResults.value = (await fn(text)) ?? []
234
- } catch {
235
- dropdownResults.value = []
236
- } finally {
237
- loading.value = false
238
- }
239
- }
240
-
241
- const closeDropdown = () => {
242
- dropdownOpen.value = false
243
- activeIndex.value = null
244
- searchText.value = hasValidId.value ? displayedText.value : ''
245
- }
246
-
247
- const onClickOutside = () => {
248
- if (dropdownOpen.value) closeDropdown()
249
- }
250
-
251
- const onFocus = () => openDropdown(searchText.value)
252
-
253
- const onInput = () => openDropdown(searchText.value)
254
-
255
- const selectOption = (option: AFormLinkValue) => {
256
- modelValue.value = option
257
- // Format `option` directly rather than reading back displayedText/modelValue: under a real
258
- // two-way v-model (e.g. ACurrencyInput binding to a computed with a side-effecting setter),
259
- // modelValue.value still reflects the *prop* until the parent's update round-trips back,
260
- // so reading it synchronously here would show stale (pre-selection) text for a tick.
261
- searchText.value = formatter ? formatter(option) : (option.displayText ?? String(option.id))
262
- dropdownOpen.value = false
263
- activeIndex.value = null
264
- }
265
-
266
- const selectNext = () => {
267
- const len = dropdownResults.value.length
268
- if (!len) return
269
- activeIndex.value = activeIndex.value === null ? 0 : (activeIndex.value + 1) % len
270
- }
271
-
272
- const selectPrev = () => {
273
- const len = dropdownResults.value.length
274
- if (!len) return
275
- if (activeIndex.value === null || activeIndex.value === 0) {
276
- activeIndex.value = null
277
- } else {
278
- activeIndex.value -= 1
279
- }
280
- }
281
-
282
- const selectCurrent = () => {
283
- if (activeIndex.value !== null && dropdownResults.value[activeIndex.value]) {
284
- selectOption(dropdownResults.value[activeIndex.value])
285
- }
286
- }
287
- </script>
288
-
289
- <style scoped>
290
- .aform_form-link-wrapper {
291
- position: relative;
292
- }
293
-
294
- .input-group {
295
- display: flex;
296
- align-items: stretch;
297
- }
298
-
299
- .aform_input-field {
300
- flex: 1;
301
- min-width: 0;
302
- }
303
-
304
- .aform_form-element--embedded {
305
- min-width: 0;
306
- flex-grow: 0;
307
- }
308
-
309
- /* Embedded mode: the host component's own container supplies the border, so this input
310
- goes borderless and inherits the host's padding scale instead of the standalone default. */
311
- .aform_input-field--embedded {
312
- outline: none;
313
- background: transparent;
314
- padding: 0.5ch 1ch;
315
- }
316
-
317
- /* Only in embedded mode is the trigger deliberately narrower than its options (e.g. a currency
318
- symbol box): let the dropdown grow past it rather than truncate. Standalone pickers keep the
319
- dropdown flush with the input, so a long display text can't overhang a narrow form column. */
320
- .aform_form-element--embedded .autocomplete-results {
321
- min-width: 100%;
322
- width: max-content;
323
- }
324
-
325
- /* Give the button the same outline as the input, then slide it 2px left so the outlines
326
- overlap exactly at the join: input right outline sits at (input_right - 1px),
327
- button left outline also sits at (button_left + 1px) = (input_right - 2px + 1px) = same pixel. */
328
- .aform_form-btn {
329
- appearance: none;
330
- border: none;
331
- outline: 1px solid var(--sc-input-border-color);
332
- outline-offset: -1px;
333
- margin-left: -2px;
334
- background: var(--sc-input-field-background);
335
- padding: 0 0.75rem;
336
- cursor: pointer;
337
- }
338
-
339
- .aform_form-btn:focus,
340
- .input-group:focus-within .aform_form-btn {
341
- outline-color: var(--sc-input-active-border-color);
342
- }
343
-
344
- .autocomplete-results {
345
- position: absolute;
346
- width: 100%;
347
- z-index: 100;
348
- padding: 0;
349
- margin: 0;
350
- list-style: none;
351
- border: 1px solid var(--sc-input-active-border-color);
352
- border-top: none;
353
- border-radius: 0 0 0.25rem 0.25rem;
354
- background: #fff;
355
- }
356
-
357
- .autocomplete-result {
358
- padding: 4px 6px;
359
- cursor: pointer;
360
- border-bottom: 0.5px solid lightgray;
361
- }
362
-
363
- .autocomplete-result.is-active,
364
- .autocomplete-result:hover {
365
- background-color: var(--sc-row-color-zebra-light);
366
- color: var(--sc-input-active-border-color);
367
- }
368
- </style>
@@ -1,32 +0,0 @@
1
- <template>
2
- <div class="aform_form-element">
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="errorText" class="aform_error" v-html="errorText"></p>
17
- </template>
18
- </div>
19
- </template>
20
-
21
- <script setup lang="ts">
22
- import { computed } from 'vue'
23
-
24
- import { ComponentProps } from '../../types'
25
-
26
- const { label, required, mode, uuid, errors, validation = { errorMessage: '' } } = defineProps<ComponentProps>()
27
-
28
- // Dynamic trigger errors take precedence over a static schema errorMessage; empty means the slot hides.
29
- const errorText = computed(() => (errors?.length ? errors.join('; ') : (validation.errorMessage ?? '')))
30
-
31
- const inputNumber = defineModel<number>()
32
- </script>