@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.
- package/dist/aform.js +3024 -2256
- package/dist/aform.js.map +1 -1
- package/dist/assets/index.css +1170 -1
- package/dist/src/components/AForm.vue.d.ts +20 -0
- package/dist/src/components/AForm.vue.d.ts.map +1 -0
- package/dist/src/components/AFormLoading.vue.d.ts +14 -0
- package/dist/src/components/AFormLoading.vue.d.ts.map +1 -0
- package/dist/src/components/base/CollapseButton.vue.d.ts +7 -0
- package/dist/src/components/base/CollapseButton.vue.d.ts.map +1 -0
- package/dist/src/components/base/ExpandButton.vue.d.ts +7 -0
- package/dist/src/components/base/ExpandButton.vue.d.ts.map +1 -0
- package/dist/src/components/form/ABadge.vue.d.ts +14 -0
- package/dist/src/components/form/ABadge.vue.d.ts.map +1 -0
- package/dist/src/components/form/ACheckbox.vue.d.ts +15 -0
- package/dist/src/components/form/ACheckbox.vue.d.ts.map +1 -0
- package/dist/src/components/form/ACurrencyInput.vue.d.ts +20 -0
- package/dist/src/components/form/ACurrencyInput.vue.d.ts.map +1 -0
- package/dist/src/components/form/ADate.vue.d.ts +14 -0
- package/dist/src/components/form/ADate.vue.d.ts.map +1 -0
- package/dist/src/components/form/ADatePicker.vue.d.ts +28 -0
- package/dist/src/components/form/ADatePicker.vue.d.ts.map +1 -0
- package/dist/src/components/form/ADateRange.vue.d.ts +18 -0
- package/dist/src/components/form/ADateRange.vue.d.ts.map +1 -0
- package/dist/src/components/form/ADateSelection.vue.d.ts +52 -0
- package/dist/src/components/form/ADateSelection.vue.d.ts.map +1 -0
- package/dist/src/components/form/ADateTime.vue.d.ts +17 -0
- package/dist/src/components/form/ADateTime.vue.d.ts.map +1 -0
- package/dist/src/components/form/ADateTimeInput.vue.d.ts +30 -0
- package/dist/src/components/form/ADateTimeInput.vue.d.ts.map +1 -0
- package/dist/src/components/form/ADropdown.vue.d.ts +20 -0
- package/dist/src/components/form/ADropdown.vue.d.ts.map +1 -0
- package/dist/src/components/form/ADuration.vue.d.ts +18 -0
- package/dist/src/components/form/ADuration.vue.d.ts.map +1 -0
- package/dist/src/components/form/AFieldset.vue.d.ts +28 -0
- package/dist/src/components/form/AFieldset.vue.d.ts.map +1 -0
- package/dist/src/components/form/AFileAttach.vue.d.ts +5 -0
- package/dist/src/components/form/AFileAttach.vue.d.ts.map +1 -0
- package/dist/src/components/form/AFormLink.vue.d.ts +40 -0
- package/dist/src/components/form/AFormLink.vue.d.ts.map +1 -0
- package/dist/src/components/form/ANumericInput.vue.d.ts +14 -0
- package/dist/src/components/form/ANumericInput.vue.d.ts.map +1 -0
- package/dist/src/components/form/AQuantityInput.vue.d.ts +20 -0
- package/dist/src/components/form/AQuantityInput.vue.d.ts.map +1 -0
- package/dist/src/components/form/ASegmentedControl.vue.d.ts +23 -0
- package/dist/src/components/form/ASegmentedControl.vue.d.ts.map +1 -0
- package/dist/src/components/form/ATextInput.vue.d.ts +14 -0
- package/dist/src/components/form/ATextInput.vue.d.ts.map +1 -0
- package/dist/src/components/form/ATextboxInput.vue.d.ts +21 -0
- package/dist/src/components/form/ATextboxInput.vue.d.ts.map +1 -0
- package/dist/src/components/utilities/Login.vue.d.ts +14 -0
- package/dist/src/components/utilities/Login.vue.d.ts.map +1 -0
- package/dist/{tsdoc-metadata.json → src/tsdoc-metadata.json} +1 -1
- package/package.json +35 -30
- package/dist/aform.d.ts +0 -441
- package/dist/aform.tsbuildinfo +0 -1
- package/dist/src/directives/mask.js +0 -107
- package/dist/src/index.js +0 -58
- package/dist/src/types/index.js +0 -0
- package/dist/src/utils/badge.js +0 -71
- package/dist/src/utils/columns.js +0 -33
- package/dist/src/utils/deserialize.js +0 -20
- package/src/components/AForm.vue +0 -314
- package/src/components/AFormLoading.vue +0 -76
- package/src/components/base/CollapseButton.vue +0 -36
- package/src/components/base/ExpandButton.vue +0 -34
- package/src/components/form/ABadge.vue +0 -135
- package/src/components/form/ACheckbox.vue +0 -62
- package/src/components/form/ACurrencyInput.vue +0 -357
- package/src/components/form/ADate.vue +0 -93
- package/src/components/form/ADatePicker.vue +0 -470
- package/src/components/form/ADateRange.vue +0 -133
- package/src/components/form/ADateSelection.vue +0 -174
- package/src/components/form/ADateTime.vue +0 -149
- package/src/components/form/ADateTimeInput.vue +0 -299
- package/src/components/form/ADropdown.vue +0 -222
- package/src/components/form/ADuration.vue +0 -124
- package/src/components/form/AFieldset.vue +0 -72
- package/src/components/form/AFileAttach.vue +0 -97
- package/src/components/form/AFormLink.vue +0 -368
- package/src/components/form/ANumericInput.vue +0 -32
- package/src/components/form/AQuantityInput.vue +0 -349
- package/src/components/form/ASegmentedControl.vue +0 -342
- package/src/components/form/ATextInput.vue +0 -40
- package/src/components/form/ATextboxInput.vue +0 -66
- package/src/components/utilities/Login.vue +0 -189
- package/src/directives/mask.ts +0 -122
- package/src/index.ts +0 -92
- package/src/shims-vue.d.ts +0 -5
- package/src/types/index.ts +0 -333
- package/src/utils/badge.ts +0 -94
- package/src/utils/columns.ts +0 -38
- package/src/utils/deserialize.ts +0 -20
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<span v-if="displayLabel" :class="classes" :style="styleVars">{{ displayLabel }}</span>
|
|
3
|
-
</template>
|
|
4
|
-
|
|
5
|
-
<script setup lang="ts">
|
|
6
|
-
import type { BadgeDescriptor, BadgePresentation, BadgeVariant, FieldOptions } from '@stonecrop/schema'
|
|
7
|
-
import { lookupBadge } from '@stonecrop/schema'
|
|
8
|
-
import { computed, type CSSProperties } from 'vue'
|
|
9
|
-
|
|
10
|
-
const props = defineProps<
|
|
11
|
-
Partial<BadgeDescriptor> & {
|
|
12
|
-
/** Defaults to `cell-fill`: every call site fully determines this, so omitting it should
|
|
13
|
-
* degrade to the common case rather than rendering an unstyled badge. */
|
|
14
|
-
presentation?: BadgePresentation
|
|
15
|
-
/** Stored field value — used with `options` when `label` is omitted. */
|
|
16
|
-
value?: unknown
|
|
17
|
-
options?: FieldOptions
|
|
18
|
-
}
|
|
19
|
-
>()
|
|
20
|
-
|
|
21
|
-
const resolved = computed((): BadgeDescriptor | undefined => {
|
|
22
|
-
if (props.label?.trim()) {
|
|
23
|
-
return {
|
|
24
|
-
label: props.label.trim(),
|
|
25
|
-
variant: props.variant,
|
|
26
|
-
color: props.color,
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
if (props.value === null || props.value === undefined || props.value === '') return undefined
|
|
30
|
-
const key = typeof props.value === 'string' ? props.value : String(props.value)
|
|
31
|
-
return lookupBadge(props.options, key)
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
const displayLabel = computed(() => resolved.value?.label?.trim() ?? '')
|
|
35
|
-
|
|
36
|
-
const activeVariant = computed((): BadgeVariant => resolved.value?.variant ?? props.variant ?? 'neutral')
|
|
37
|
-
|
|
38
|
-
const activeColor = computed(() => resolved.value?.color ?? props.color)
|
|
39
|
-
|
|
40
|
-
const classes = computed(() => [
|
|
41
|
-
'abadge',
|
|
42
|
-
`abadge--${props.presentation ?? 'cell-fill'}`,
|
|
43
|
-
`abadge--${activeVariant.value}`,
|
|
44
|
-
])
|
|
45
|
-
|
|
46
|
-
const styleVars = computed((): CSSProperties => {
|
|
47
|
-
const color = activeColor.value
|
|
48
|
-
if (!color) return {}
|
|
49
|
-
return {
|
|
50
|
-
'--sc-badge-bg': color,
|
|
51
|
-
'--sc-badge-text': color,
|
|
52
|
-
'--sc-badge-accent': color,
|
|
53
|
-
} as CSSProperties
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
export type { BadgePresentation, BadgeVariant }
|
|
57
|
-
</script>
|
|
58
|
-
|
|
59
|
-
<style scoped>
|
|
60
|
-
.abadge {
|
|
61
|
-
display: block;
|
|
62
|
-
box-sizing: border-box;
|
|
63
|
-
font: inherit;
|
|
64
|
-
line-height: inherit;
|
|
65
|
-
white-space: nowrap;
|
|
66
|
-
overflow: hidden;
|
|
67
|
-
text-overflow: ellipsis;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
.abadge--cell-fill {
|
|
71
|
-
display: block;
|
|
72
|
-
box-sizing: border-box;
|
|
73
|
-
width: 100%;
|
|
74
|
-
min-height: calc(var(--sc-atable-row-height) + 2 * var(--sc-atable-row-padding));
|
|
75
|
-
line-height: calc(var(--sc-atable-row-height) + 2 * var(--sc-atable-row-padding));
|
|
76
|
-
margin: 0;
|
|
77
|
-
padding: 0 0.5ch;
|
|
78
|
-
border-radius: 0;
|
|
79
|
-
text-align: inherit;
|
|
80
|
-
background: var(--sc-badge-bg);
|
|
81
|
-
color: var(--sc-badge-text);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
.abadge--cell-fill.abadge--neutral {
|
|
85
|
-
--sc-badge-bg: var(--sc-badge-neutral-bg);
|
|
86
|
-
--sc-badge-text: var(--sc-badge-neutral-text);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
.abadge--cell-fill.abadge--success {
|
|
90
|
-
--sc-badge-bg: var(--sc-badge-success-bg);
|
|
91
|
-
--sc-badge-text: var(--sc-badge-success-text);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
.abadge--cell-fill.abadge--warning {
|
|
95
|
-
--sc-badge-bg: var(--sc-badge-warning-bg);
|
|
96
|
-
--sc-badge-text: var(--sc-badge-warning-text);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
.abadge--cell-fill.abadge--danger {
|
|
100
|
-
--sc-badge-bg: var(--sc-badge-danger-bg);
|
|
101
|
-
--sc-badge-text: var(--sc-badge-danger-text);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
.abadge--cell-fill.abadge--brand {
|
|
105
|
-
--sc-badge-bg: var(--sc-badge-brand-bg);
|
|
106
|
-
--sc-badge-text: var(--sc-badge-brand-text);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
.abadge--input-accent {
|
|
110
|
-
background: var(--sc-input-field-background);
|
|
111
|
-
color: inherit;
|
|
112
|
-
border-left: 4px solid var(--sc-badge-accent);
|
|
113
|
-
padding-left: calc(1ch - 4px);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
.abadge--input-accent.abadge--neutral {
|
|
117
|
-
--sc-badge-accent: var(--sc-badge-neutral-accent);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
.abadge--input-accent.abadge--success {
|
|
121
|
-
--sc-badge-accent: var(--sc-badge-success-accent);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
.abadge--input-accent.abadge--warning {
|
|
125
|
-
--sc-badge-accent: var(--sc-badge-warning-accent);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
.abadge--input-accent.abadge--danger {
|
|
129
|
-
--sc-badge-accent: var(--sc-badge-danger-accent);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
.abadge--input-accent.abadge--brand {
|
|
133
|
-
--sc-badge-accent: var(--sc-badge-brand-accent);
|
|
134
|
-
}
|
|
135
|
-
</style>
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="aform_form-element">
|
|
3
|
-
<template v-if="mode === 'display'">
|
|
4
|
-
<label class="aform_field-label">{{ label }}</label>
|
|
5
|
-
<span class="aform_display-value">{{ checkbox ? '✓' : '✗' }}</span>
|
|
6
|
-
</template>
|
|
7
|
-
<template v-else>
|
|
8
|
-
<label class="aform_field-label" :for="uuid">{{ label }}</label>
|
|
9
|
-
<span class="aform_checkbox-container aform_input-field">
|
|
10
|
-
<input
|
|
11
|
-
:id="uuid"
|
|
12
|
-
v-model="checkbox"
|
|
13
|
-
type="checkbox"
|
|
14
|
-
class="aform_checkbox"
|
|
15
|
-
:disabled="mode === 'read'"
|
|
16
|
-
:required="required" />
|
|
17
|
-
</span>
|
|
18
|
-
<p v-show="errorText" class="aform_error" v-html="errorText"></p>
|
|
19
|
-
</template>
|
|
20
|
-
</div>
|
|
21
|
-
</template>
|
|
22
|
-
|
|
23
|
-
<script setup lang="ts">
|
|
24
|
-
import { computed, InputHTMLAttributes } from 'vue'
|
|
25
|
-
|
|
26
|
-
import { ComponentProps } from '../../types'
|
|
27
|
-
|
|
28
|
-
const { label, required, mode, uuid, errors, validation = { errorMessage: '' } } = defineProps<ComponentProps>()
|
|
29
|
-
|
|
30
|
-
// Dynamic trigger errors take precedence over a static schema errorMessage; empty means the slot hides.
|
|
31
|
-
const errorText = computed(() => (errors?.length ? errors.join('; ') : (validation.errorMessage ?? '')))
|
|
32
|
-
|
|
33
|
-
const checkbox = defineModel<InputHTMLAttributes['checked']>()
|
|
34
|
-
</script>
|
|
35
|
-
|
|
36
|
-
<style scoped>
|
|
37
|
-
.aform_checkbox {
|
|
38
|
-
cursor: pointer;
|
|
39
|
-
width: auto;
|
|
40
|
-
margin-top: 0;
|
|
41
|
-
display: block;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
.aform_checkbox:checked {
|
|
45
|
-
accent-color: var(--sc-primary-color);
|
|
46
|
-
border: 1px solid black;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
.aform_checkbox-container {
|
|
50
|
-
width: 100%;
|
|
51
|
-
display: inline-block;
|
|
52
|
-
text-align: left;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
.aform_checkbox-container input {
|
|
56
|
-
width: auto;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
.aform_checkbox-container:hover + .aform_field-label {
|
|
60
|
-
color: var(--sc-input-active-label-color);
|
|
61
|
-
}
|
|
62
|
-
</style>
|
|
@@ -1,357 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="aform_form-element acurrency">
|
|
3
|
-
<template v-if="mode === 'display'">
|
|
4
|
-
<span class="aform_display-value">{{ displayText }}</span>
|
|
5
|
-
<label class="aform_field-label">{{ label }}</label>
|
|
6
|
-
</template>
|
|
7
|
-
<template v-else>
|
|
8
|
-
<div class="acurrency__row">
|
|
9
|
-
<div class="acurrency__field acurrency__field--amount">
|
|
10
|
-
<div class="acurrency__group">
|
|
11
|
-
<div class="acurrency__currency">
|
|
12
|
-
<AFormLink
|
|
13
|
-
v-model="currency"
|
|
14
|
-
:mode="mode"
|
|
15
|
-
embedded
|
|
16
|
-
:placeholder="currencyLabel"
|
|
17
|
-
:aria-label="currencyLabel"
|
|
18
|
-
:required="required"
|
|
19
|
-
:formatter="currencySymbol"
|
|
20
|
-
:doctype="options.doctype"
|
|
21
|
-
:filter-function="options.filterFunction"
|
|
22
|
-
:is-async="options.isAsync">
|
|
23
|
-
<template #option="{ option }"
|
|
24
|
-
>{{ option.symbol ? `${option.symbol} — ` : '' }}{{ option.displayText ?? option.id }}</template
|
|
25
|
-
>
|
|
26
|
-
</AFormLink>
|
|
27
|
-
</div>
|
|
28
|
-
<div class="acurrency__amount-wrap">
|
|
29
|
-
<input
|
|
30
|
-
:id="uuid"
|
|
31
|
-
v-model.number="amount"
|
|
32
|
-
class="acurrency__amount"
|
|
33
|
-
type="number"
|
|
34
|
-
:disabled="mode === 'read'"
|
|
35
|
-
:required="required"
|
|
36
|
-
@keydown="onAmountKeydown"
|
|
37
|
-
@paste="onAmountPaste" />
|
|
38
|
-
<label class="aform_field-label" :for="uuid">{{ label }}</label>
|
|
39
|
-
</div>
|
|
40
|
-
</div>
|
|
41
|
-
</div>
|
|
42
|
-
</div>
|
|
43
|
-
<div class="acurrency__row acurrency__row--base">
|
|
44
|
-
<div class="acurrency__field acurrency__field--base-currency">
|
|
45
|
-
<input :value="baseCurrencyText" class="aform_input-field acurrency__base-field" type="text" disabled />
|
|
46
|
-
<label class="aform_field-label">{{ baseCurrencyLabel }}</label>
|
|
47
|
-
</div>
|
|
48
|
-
<div class="acurrency__field acurrency__field--base-amount">
|
|
49
|
-
<input
|
|
50
|
-
:value="modelValue.baseAmount"
|
|
51
|
-
class="aform_input-field acurrency__base-field"
|
|
52
|
-
type="number"
|
|
53
|
-
disabled />
|
|
54
|
-
<label class="aform_field-label">{{ baseAmountLabel }}</label>
|
|
55
|
-
</div>
|
|
56
|
-
<div class="acurrency__field acurrency__field--exchange-rate">
|
|
57
|
-
<input
|
|
58
|
-
:value="modelValue.exchangeRate"
|
|
59
|
-
class="aform_input-field acurrency__base-field"
|
|
60
|
-
type="number"
|
|
61
|
-
disabled />
|
|
62
|
-
<label class="aform_field-label">{{ exchangeRateLabel }}</label>
|
|
63
|
-
</div>
|
|
64
|
-
</div>
|
|
65
|
-
<p v-show="validation.errorMessage" class="aform_error" v-html="validation.errorMessage"></p>
|
|
66
|
-
</template>
|
|
67
|
-
</div>
|
|
68
|
-
</template>
|
|
69
|
-
|
|
70
|
-
<script setup lang="ts">
|
|
71
|
-
import { computed, inject, ref, watch } from 'vue'
|
|
72
|
-
|
|
73
|
-
import type { AFormLinkValue, ComponentProps, CurrencyOptions, CurrencyValue } from '../../types'
|
|
74
|
-
import AFormLink from './AFormLink.vue'
|
|
75
|
-
|
|
76
|
-
const {
|
|
77
|
-
label,
|
|
78
|
-
required,
|
|
79
|
-
mode,
|
|
80
|
-
uuid,
|
|
81
|
-
validation = { errorMessage: ' ' },
|
|
82
|
-
options = {},
|
|
83
|
-
currencyLabel = 'Currency',
|
|
84
|
-
baseCurrencyLabel = 'Base Currency',
|
|
85
|
-
baseAmountLabel = 'Base Amount',
|
|
86
|
-
exchangeRateLabel = 'Exchange Rate',
|
|
87
|
-
} = defineProps<
|
|
88
|
-
ComponentProps & {
|
|
89
|
-
options?: CurrencyOptions
|
|
90
|
-
currencyLabel?: string
|
|
91
|
-
baseCurrencyLabel?: string
|
|
92
|
-
baseAmountLabel?: string
|
|
93
|
-
exchangeRateLabel?: string
|
|
94
|
-
}
|
|
95
|
-
>()
|
|
96
|
-
|
|
97
|
-
// The merged currency prefix is compact by design, so it shows the symbol rather than the
|
|
98
|
-
// full currency name once a value is picked — falls back gracefully when a currency record
|
|
99
|
-
// (or the story/app data behind it) doesn't carry a `symbol`.
|
|
100
|
-
const currencySymbol = (value: AFormLinkValue): string => value.symbol ?? value.displayText ?? String(value.id)
|
|
101
|
-
|
|
102
|
-
const modelValue = defineModel<CurrencyValue>({
|
|
103
|
-
default: () => ({
|
|
104
|
-
amount: 0,
|
|
105
|
-
currency: { id: '' },
|
|
106
|
-
baseAmount: 0,
|
|
107
|
-
baseCurrency: { id: '' },
|
|
108
|
-
exchangeRate: 1,
|
|
109
|
-
}),
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
// The base currency is fixed configuration, not user-editable. It may be supplied as a bare id
|
|
113
|
-
// (resolved to displayText below via the same `aformLinkResolver` injection AFormLink uses) or
|
|
114
|
-
// as a full AFormLinkValue that already carries displayText.
|
|
115
|
-
const normalizedBaseCurrency = computed<AFormLinkValue>(() => {
|
|
116
|
-
const base = options.baseCurrency ?? modelValue.value?.baseCurrency
|
|
117
|
-
if (!base) return { id: '' }
|
|
118
|
-
return typeof base === 'string' ? { id: base } : base
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
type ResolverFn = (doctype: string, id: string) => string | undefined | Promise<string | undefined>
|
|
122
|
-
const resolver = inject<ResolverFn | null>('aformLinkResolver', null)
|
|
123
|
-
|
|
124
|
-
const resolvedBaseCurrency = ref<AFormLinkValue>(normalizedBaseCurrency.value)
|
|
125
|
-
|
|
126
|
-
watch(
|
|
127
|
-
normalizedBaseCurrency,
|
|
128
|
-
async base => {
|
|
129
|
-
if (!base.id || base.displayText || !resolver || !options.doctype) {
|
|
130
|
-
resolvedBaseCurrency.value = base
|
|
131
|
-
return
|
|
132
|
-
}
|
|
133
|
-
try {
|
|
134
|
-
const displayText = await resolver(options.doctype, String(base.id))
|
|
135
|
-
resolvedBaseCurrency.value = displayText ? { ...base, displayText } : base
|
|
136
|
-
} catch {
|
|
137
|
-
resolvedBaseCurrency.value = base
|
|
138
|
-
}
|
|
139
|
-
},
|
|
140
|
-
{ immediate: true }
|
|
141
|
-
)
|
|
142
|
-
|
|
143
|
-
const baseCurrencyText = computed(() => {
|
|
144
|
-
const base = resolvedBaseCurrency.value
|
|
145
|
-
if (!base?.id) return ''
|
|
146
|
-
return base.displayText ?? String(base.id)
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
const resolveExchangeRate = (currencyId: string | number | undefined): number => {
|
|
150
|
-
const baseId = resolvedBaseCurrency.value.id
|
|
151
|
-
if (!currencyId || String(currencyId) === String(baseId)) return 1
|
|
152
|
-
// The rate the value was booked at wins for as long as the currency is unchanged. Rates are
|
|
153
|
-
// time-varying in a way conversion factors are not (see AQuantityInput), so `exchangeRates`
|
|
154
|
-
// carries *today's* rates: preferring it here would silently re-rate a stored line to the
|
|
155
|
-
// current rate on any touch — including the write-back AFormLink does when it resolves the
|
|
156
|
-
// currency's display text, i.e. on mere render.
|
|
157
|
-
if (String(currencyId) === String(modelValue.value?.currency?.id)) {
|
|
158
|
-
return modelValue.value?.exchangeRate ?? options.exchangeRates?.[String(currencyId)] ?? 1
|
|
159
|
-
}
|
|
160
|
-
// Switching to a currency absent from the rate map resets to 1 rather than silently reusing
|
|
161
|
-
// the outgoing currency's rate.
|
|
162
|
-
return options.exchangeRates?.[String(currencyId)] ?? 1
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Enough decimal places to shed floating-point noise from the multiplication (e.g. 4 * 1.1 → 4.4
|
|
166
|
-
// rather than 4.4000000000000004) without discarding a digit the rate actually produced. Matches
|
|
167
|
-
// AQuantityInput's roundQty.
|
|
168
|
-
const FLOAT_NOISE_DECIMALS = 6
|
|
169
|
-
|
|
170
|
-
// How far to round the base amount is the *base currency's* business, and only the app knows what
|
|
171
|
-
// that is — so it says so via `precision` (JPY carries 0 decimals, most currencies 2, KWD 3).
|
|
172
|
-
// Unset stays deliberately loose rather than defaulting to 2: hard-rounding every currency to
|
|
173
|
-
// cents destroys value outright, e.g. 50 IDR at 0.000063 rounds to a base amount of 0. A garbage
|
|
174
|
-
// precision (non-integer, negative, or past toFixed's 100 ceiling) falls back rather than throwing
|
|
175
|
-
// inside the setter and breaking the field.
|
|
176
|
-
const baseDecimals = computed(() => {
|
|
177
|
-
const { precision } = options
|
|
178
|
-
if (precision === undefined) return FLOAT_NOISE_DECIMALS
|
|
179
|
-
return Number.isInteger(precision) && precision >= 0 && precision <= 100 ? precision : FLOAT_NOISE_DECIMALS
|
|
180
|
-
})
|
|
181
|
-
|
|
182
|
-
const roundAmount = (value: number): number => Number(value.toFixed(baseDecimals.value))
|
|
183
|
-
|
|
184
|
-
const recompute = (amount: number, currencyValue: AFormLinkValue) => {
|
|
185
|
-
const exchangeRate = resolveExchangeRate(currencyValue.id)
|
|
186
|
-
modelValue.value = {
|
|
187
|
-
amount,
|
|
188
|
-
currency: currencyValue,
|
|
189
|
-
exchangeRate,
|
|
190
|
-
baseCurrency: resolvedBaseCurrency.value,
|
|
191
|
-
baseAmount: roundAmount(amount * exchangeRate),
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const amount = computed({
|
|
196
|
-
get: () => modelValue.value?.amount ?? 0,
|
|
197
|
-
set: (value: number) => recompute(value, modelValue.value?.currency ?? { id: '' }),
|
|
198
|
-
})
|
|
199
|
-
|
|
200
|
-
const currency = computed<AFormLinkValue>({
|
|
201
|
-
get: () => modelValue.value?.currency ?? { id: '' },
|
|
202
|
-
set: (value: AFormLinkValue) => recompute(modelValue.value?.amount ?? 0, value),
|
|
203
|
-
})
|
|
204
|
-
|
|
205
|
-
const amountNavigationKeys = new Set([
|
|
206
|
-
'Backspace',
|
|
207
|
-
'Delete',
|
|
208
|
-
'Tab',
|
|
209
|
-
'Escape',
|
|
210
|
-
'Enter',
|
|
211
|
-
'ArrowLeft',
|
|
212
|
-
'ArrowRight',
|
|
213
|
-
'ArrowUp',
|
|
214
|
-
'ArrowDown',
|
|
215
|
-
'Home',
|
|
216
|
-
'End',
|
|
217
|
-
])
|
|
218
|
-
|
|
219
|
-
const onAmountKeydown = (event: KeyboardEvent) => {
|
|
220
|
-
if (event.ctrlKey || event.metaKey || event.altKey) return
|
|
221
|
-
if (amountNavigationKeys.has(event.key)) return
|
|
222
|
-
if (/^[0-9]$/.test(event.key)) return
|
|
223
|
-
// A currency amount is signed — credit notes, refunds and adjustments are negative.
|
|
224
|
-
//
|
|
225
|
-
// Both dedup checks below are best-effort: a `type="number"` input reports `value` as `''`
|
|
226
|
-
// whenever its content isn't a valid number, so mid-entry states like "1." or "-" read as
|
|
227
|
-
// empty and a second separator can slip through. The browser rejects the resulting value
|
|
228
|
-
// anyway; this guard exists to stop the obviously-wrong keys (letters, 'e', '+'), not to be
|
|
229
|
-
// the sole arbiter of well-formedness.
|
|
230
|
-
const input = event.target as HTMLInputElement
|
|
231
|
-
if (event.key === '.' && !input.value.includes('.')) return
|
|
232
|
-
if (event.key === '-' && !input.value.includes('-')) return
|
|
233
|
-
event.preventDefault()
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const onAmountPaste = (event: ClipboardEvent) => {
|
|
237
|
-
const pasted = event.clipboardData?.getData('text') ?? ''
|
|
238
|
-
if (!/^-?\d*\.?\d*$/.test(pasted)) event.preventDefault()
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const showBase = computed(() => {
|
|
242
|
-
const v = modelValue.value
|
|
243
|
-
return !!v?.baseCurrency?.id && (v.currency?.id !== v.baseCurrency?.id || v.amount !== v.baseAmount)
|
|
244
|
-
})
|
|
245
|
-
|
|
246
|
-
const displayText = computed(() => {
|
|
247
|
-
const v = modelValue.value
|
|
248
|
-
if (!v || !v.currency?.id) return '—'
|
|
249
|
-
const currencyText = v.currency.displayText ?? String(v.currency.id)
|
|
250
|
-
const base = `${v.amount} ${currencyText}`
|
|
251
|
-
if (!showBase.value) return base
|
|
252
|
-
const baseText = v.baseCurrency.displayText ?? String(v.baseCurrency.id)
|
|
253
|
-
return `${base} (${v.baseAmount} ${baseText})`
|
|
254
|
-
})
|
|
255
|
-
</script>
|
|
256
|
-
|
|
257
|
-
<style scoped>
|
|
258
|
-
.acurrency__row {
|
|
259
|
-
display: flex;
|
|
260
|
-
gap: 1ch;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
.acurrency__row--base {
|
|
264
|
-
margin-top: 1.5rem;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
.acurrency__field {
|
|
268
|
-
position: relative;
|
|
269
|
-
flex: 1;
|
|
270
|
-
min-width: 0;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
.acurrency__group {
|
|
274
|
-
display: flex;
|
|
275
|
-
align-items: stretch;
|
|
276
|
-
width: 100%;
|
|
277
|
-
border: 1px solid var(--sc-input-border-color);
|
|
278
|
-
border-radius: 0.25rem;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
.acurrency__group:focus-within {
|
|
282
|
-
border-color: var(--sc-input-active-border-color);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/* Wraps just the amount input so its floating label (`left: 10px` of the nearest
|
|
286
|
-
`position: relative` ancestor, per .aform_field-label) anchors above the amount box
|
|
287
|
-
specifically, not the group's outer left edge — which is now the currency prefix. */
|
|
288
|
-
.acurrency__amount-wrap {
|
|
289
|
-
position: relative;
|
|
290
|
-
flex: 1;
|
|
291
|
-
min-width: 0;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/* The amount label ("Total") is the primary label for the whole merged group — bumped up
|
|
295
|
-
slightly from the shared .aform_field-label size so it reads as the group's main label. */
|
|
296
|
-
.acurrency__amount-wrap .aform_field-label {
|
|
297
|
-
font-size: 0.85rem;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
.acurrency__amount {
|
|
301
|
-
width: 100%;
|
|
302
|
-
box-sizing: border-box;
|
|
303
|
-
border: none;
|
|
304
|
-
outline: none;
|
|
305
|
-
padding: 0.5ch 1ch;
|
|
306
|
-
background: transparent;
|
|
307
|
-
border-radius: 0 0.25rem 0.25rem 0;
|
|
308
|
-
text-align: right;
|
|
309
|
-
appearance: textfield;
|
|
310
|
-
-moz-appearance: textfield;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
.acurrency__amount::-webkit-outer-spin-button,
|
|
314
|
-
.acurrency__amount::-webkit-inner-spin-button {
|
|
315
|
-
appearance: none;
|
|
316
|
-
-webkit-appearance: none;
|
|
317
|
-
margin: 0;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/* The currency picker reads as a simple prefix addon (like Bootstrap's "$" prepend) rather
|
|
321
|
-
than an equal partner to the amount box: compact width (it shows a symbol once a value is
|
|
322
|
-
picked, not the full name), tinted background, left-rounded to match the group's own
|
|
323
|
-
corner so the tint doesn't overhang the border. The dropdown itself isn't bound by this
|
|
324
|
-
width — see AFormLink's .autocomplete-results — so search results still show full names. */
|
|
325
|
-
.acurrency__currency {
|
|
326
|
-
position: relative;
|
|
327
|
-
flex: 0 0 auto;
|
|
328
|
-
min-width: 4.5rem;
|
|
329
|
-
background: var(--sc-gray-5);
|
|
330
|
-
border-right: 1px solid var(--sc-input-border-color);
|
|
331
|
-
border-radius: 0.25rem 0 0 0.25rem;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
.acurrency__base-field {
|
|
335
|
-
width: 100%;
|
|
336
|
-
box-sizing: border-box;
|
|
337
|
-
font-size: 1rem;
|
|
338
|
-
padding: 0.5ch 1ch;
|
|
339
|
-
border: 1px solid var(--sc-input-border-color);
|
|
340
|
-
border-radius: 0.25rem;
|
|
341
|
-
outline: none;
|
|
342
|
-
appearance: textfield;
|
|
343
|
-
-moz-appearance: textfield;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
.acurrency__base-field:disabled {
|
|
347
|
-
color: var(--sc-gray-50);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
/* Base Amount and Exchange Rate are read-only — the number spinner offers nothing here. */
|
|
351
|
-
.acurrency__base-field::-webkit-outer-spin-button,
|
|
352
|
-
.acurrency__base-field::-webkit-inner-spin-button {
|
|
353
|
-
appearance: none;
|
|
354
|
-
-webkit-appearance: none;
|
|
355
|
-
margin: 0;
|
|
356
|
-
}
|
|
357
|
-
</style>
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="aform_form-element">
|
|
3
|
-
<template v-if="mode === 'display'">
|
|
4
|
-
<span class="aform_display-value">{{ modelValue ? new Date(inputDate).toLocaleDateString() : '' }}</span>
|
|
5
|
-
<label class="aform_field-label">{{ label }}</label>
|
|
6
|
-
</template>
|
|
7
|
-
<template v-else>
|
|
8
|
-
<input
|
|
9
|
-
:id="uuid"
|
|
10
|
-
ref="date"
|
|
11
|
-
v-model="inputDate"
|
|
12
|
-
class="aform_input-field"
|
|
13
|
-
type="date"
|
|
14
|
-
:disabled="mode === 'read'"
|
|
15
|
-
:required="required"
|
|
16
|
-
@click="openPicker" />
|
|
17
|
-
<label class="aform_field-label" :for="uuid">{{ label }}</label>
|
|
18
|
-
<p v-show="errorText" class="aform_error" v-html="errorText"></p>
|
|
19
|
-
<ADateSelection
|
|
20
|
-
v-if="showPicker"
|
|
21
|
-
ref="picker"
|
|
22
|
-
class="adate-picker"
|
|
23
|
-
:select-range="false"
|
|
24
|
-
:show-time="false"
|
|
25
|
-
@get-date="handleDate" />
|
|
26
|
-
</template>
|
|
27
|
-
</div>
|
|
28
|
-
</template>
|
|
29
|
-
|
|
30
|
-
<script setup lang="ts">
|
|
31
|
-
import { useTemplateRef, ref, computed, watch } from 'vue'
|
|
32
|
-
import { onClickOutside } from '@vueuse/core'
|
|
33
|
-
|
|
34
|
-
import ADateSelection from './ADateSelection.vue'
|
|
35
|
-
import type { ComponentProps } from '../../types'
|
|
36
|
-
|
|
37
|
-
const {
|
|
38
|
-
label = 'Date',
|
|
39
|
-
required,
|
|
40
|
-
mode,
|
|
41
|
-
uuid,
|
|
42
|
-
errors,
|
|
43
|
-
validation = { errorMessage: '' },
|
|
44
|
-
} = defineProps<ComponentProps>()
|
|
45
|
-
|
|
46
|
-
// Dynamic trigger errors take precedence over a static schema errorMessage; empty means the slot hides.
|
|
47
|
-
const errorText = computed(() => (errors?.length ? errors.join('; ') : (validation.errorMessage ?? '')))
|
|
48
|
-
|
|
49
|
-
const modelValue = defineModel<string | Date>()
|
|
50
|
-
|
|
51
|
-
const currentDate = ref(modelValue.value ? new Date(modelValue.value) : new Date())
|
|
52
|
-
const inputDate = computed({
|
|
53
|
-
get: () => currentDate.value.toISOString().split('T')[0],
|
|
54
|
-
set: (value: string) => {
|
|
55
|
-
currentDate.value = new Date(value)
|
|
56
|
-
modelValue.value = value
|
|
57
|
-
},
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
const pickerRef = useTemplateRef<HTMLDivElement>('picker')
|
|
61
|
-
const showPicker = ref(false)
|
|
62
|
-
|
|
63
|
-
onClickOutside(pickerRef, () => (showPicker.value = false))
|
|
64
|
-
|
|
65
|
-
const openPicker = () => {
|
|
66
|
-
if (mode !== 'read') showPicker.value = !showPicker.value
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
watch(
|
|
70
|
-
() => modelValue.value,
|
|
71
|
-
newValue => {
|
|
72
|
-
if (newValue) {
|
|
73
|
-
currentDate.value = new Date(newValue)
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
const handleDate = (data: { selected: Date }) => {
|
|
79
|
-
currentDate.value = data.selected
|
|
80
|
-
modelValue.value = inputDate.value
|
|
81
|
-
showPicker.value = false
|
|
82
|
-
}
|
|
83
|
-
</script>
|
|
84
|
-
|
|
85
|
-
<style scoped>
|
|
86
|
-
.adate-picker {
|
|
87
|
-
position: absolute;
|
|
88
|
-
top: 100%;
|
|
89
|
-
left: 0;
|
|
90
|
-
z-index: 1000;
|
|
91
|
-
margin-top: 0.25rem;
|
|
92
|
-
}
|
|
93
|
-
</style>
|