@leavepulse/ui 0.78.1 → 0.78.3
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/chunks/{DropzoneLinkField-CLLhd3WB.js → DropzoneLinkField-CyE7TJ5f.js} +1 -1
- package/dist/chunks/LpAddressInput-ft8fQOGx.js +401 -0
- package/dist/chunks/{LpAppShell-BKbYCHkx.js → LpAppShell-DRHoQtr-.js} +34 -17
- package/dist/chunks/LpAutocomplete-CrZbd0uv.js +310 -0
- package/dist/chunks/LpCheckbox-CCWCwD0y.js +129 -0
- package/dist/chunks/{LpCodeDisplay-D3BUAhV1.js → LpCodeDisplay-D59uPwRb.js} +1 -1
- package/dist/chunks/LpColorPicker-BONURBFN.js +574 -0
- package/dist/chunks/LpDatePicker-C2AOtxgF.js +171 -0
- package/dist/chunks/{LpDateRangePicker-CNQXdTVv.js → LpDateRangePicker-CiXdIAyh.js} +43 -35
- package/dist/chunks/{LpFileTree-C-rJKzEQ.js → LpFileTree-CpNurwZp.js} +2 -2
- package/dist/chunks/{LpFileTreeNode-BgYn4U1J.js → LpFileTreeNode-DdF8Cq-s.js} +1 -1
- package/dist/chunks/{LpInput-D0O6dpuA.js → LpInput-CbI7z5fQ.js} +3 -4
- package/dist/chunks/{LpLogInspector-BdQlX6S-.js → LpLogInspector-CcSJSGan.js} +1 -1
- package/dist/chunks/{LpLogViewer-umCZBapN.js → LpLogViewer-CWu49Dko.js} +2 -2
- package/dist/chunks/{LpModalNav-gOi1Giz1.js → LpModalNav-DI3BW5ff.js} +1 -1
- package/dist/chunks/LpNumberField-C8LfUz_R.js +154 -0
- package/dist/chunks/LpOtpInput-B1X9l9no.js +140 -0
- package/dist/chunks/{LpPaletteTheme-BIXpZLGe.js → LpPaletteTheme-B-xuZiKK.js} +2 -2
- package/dist/chunks/{LpPasswordInput-D4HPhqfV.js → LpPasswordInput-Duc_3wPv.js} +1 -1
- package/dist/chunks/{LpPhoneInput-BSL7NX4z.js → LpPhoneInput-CctcRK9K.js} +124 -118
- package/dist/chunks/LpRadioGroup-DTM0nd9G.js +103 -0
- package/dist/chunks/LpRangeSlider-DJsEZXad.js +97 -0
- package/dist/chunks/LpRepeater-Czib3rmv.js +193 -0
- package/dist/chunks/{LpSecretField-B2roc4Iz.js → LpSecretField-BtlCk3IH.js} +1 -1
- package/dist/chunks/LpSegmented-sskrJKJJ.js +128 -0
- package/dist/chunks/LpSelect-JFZehOkd.js +515 -0
- package/dist/chunks/{LpSidebar-DDNJzb69.js → LpSidebar-CvRvVBTt.js} +1 -1
- package/dist/chunks/LpSlider-C4ilCtZX.js +76 -0
- package/dist/chunks/LpSwitch-DyAYTchz.js +122 -0
- package/dist/chunks/{LpTableFilter-B_XMOeAh.js → LpTableFilter-CJg5eIcV.js} +2 -2
- package/dist/chunks/{LpTagsInput-Dt0mteSj.js → LpTagsInput-CkNxpNYa.js} +4 -3
- package/dist/chunks/{LpTextarea-Co0b9PVx.js → LpTextarea-CFK_5QB8.js} +3 -2
- package/dist/chunks/{LpThemeStudio-ChU0_Ga8.js → LpThemeStudio-R-wohbCY.js} +2 -2
- package/dist/chunks/{LpThemeSwitcher-DmCZ21P_.js → LpThemeSwitcher-Bzy15MCP.js} +1 -1
- package/dist/chunks/LpTimeField-B5n0m8m0.js +115 -0
- package/dist/chunks/{LpTopologyCanvas-jqz1KCq2.js → LpTopologyCanvas-BNgw9RyZ.js} +2 -2
- package/dist/chunks/{LpWorkspaceSwitcher-BeNHCLd5.js → LpWorkspaceSwitcher-ugv-fyig.js} +1 -1
- package/dist/components/DropzoneLinkField.vue.js +1 -1
- package/dist/components/LpAddressInput.vue.d.ts +4 -0
- package/dist/components/LpAddressInput.vue.js +1 -1
- package/dist/components/LpAlert.vue.d.ts +3 -3
- package/dist/components/LpAppShell.vue.d.ts +19 -1
- package/dist/components/LpAppShell.vue.js +1 -1
- package/dist/components/LpAutocomplete.vue.d.ts +4 -0
- package/dist/components/LpAutocomplete.vue.js +1 -1
- package/dist/components/LpBadge.vue.d.ts +2 -2
- package/dist/components/LpChart.vue.d.ts +1 -1
- package/dist/components/LpCheckbox.vue.d.ts +4 -0
- package/dist/components/LpCheckbox.vue.js +1 -1
- package/dist/components/LpCodeDisplay.vue.js +1 -1
- package/dist/components/LpCollapse.vue.d.ts +1 -1
- package/dist/components/LpColorPicker.vue.d.ts +5 -3
- package/dist/components/LpColorPicker.vue.js +1 -1
- package/dist/components/LpDatePicker.vue.js +1 -1
- package/dist/components/LpDateRangePicker.vue.js +1 -1
- package/dist/components/LpDropzone.vue.js +1 -1
- package/dist/components/LpFileTree.vue.js +1 -1
- package/dist/components/LpFileTreeNode.vue.js +1 -1
- package/dist/components/LpImageEditor.vue.d.ts +1 -1
- package/dist/components/LpInput.vue.js +1 -1
- package/dist/components/LpLogInspector.vue.js +1 -1
- package/dist/components/LpLogViewer.vue.js +1 -1
- package/dist/components/LpModalNav.vue.js +1 -1
- package/dist/components/LpNumberField.vue.d.ts +4 -0
- package/dist/components/LpNumberField.vue.js +1 -1
- package/dist/components/LpOtpInput.vue.d.ts +4 -0
- package/dist/components/LpOtpInput.vue.js +1 -1
- package/dist/components/LpPaletteTheme.vue.js +1 -1
- package/dist/components/LpPasswordInput.vue.js +1 -1
- package/dist/components/LpPhoneInput.vue.d.ts +4 -0
- package/dist/components/LpPhoneInput.vue.js +1 -1
- package/dist/components/LpRadioGroup.vue.d.ts +4 -0
- package/dist/components/LpRadioGroup.vue.js +1 -1
- package/dist/components/LpRangeSlider.vue.d.ts +4 -0
- package/dist/components/LpRangeSlider.vue.js +1 -1
- package/dist/components/LpRepeater.vue.d.ts +2 -0
- package/dist/components/LpRepeater.vue.js +1 -1
- package/dist/components/LpScrollArea.vue.d.ts +1 -1
- package/dist/components/LpSecretField.vue.js +1 -1
- package/dist/components/LpSegmented.vue.d.ts +4 -0
- package/dist/components/LpSegmented.vue.js +1 -1
- package/dist/components/LpSelect.vue.d.ts +2 -0
- package/dist/components/LpSelect.vue.js +1 -1
- package/dist/components/LpShinyText.vue.d.ts +1 -1
- package/dist/components/LpSidebar.vue.d.ts +1 -1
- package/dist/components/LpSidebar.vue.js +1 -1
- package/dist/components/LpSlider.vue.d.ts +4 -0
- package/dist/components/LpSlider.vue.js +1 -1
- package/dist/components/LpSwitch.vue.d.ts +4 -0
- package/dist/components/LpSwitch.vue.js +1 -1
- package/dist/components/LpTable.vue.js +2 -2
- package/dist/components/LpTableFilter.vue.js +1 -1
- package/dist/components/LpTabs.vue.d.ts +1 -1
- package/dist/components/LpTagsInput.vue.js +1 -1
- package/dist/components/LpTextarea.vue.js +1 -1
- package/dist/components/LpThemeStudio.vue.js +1 -1
- package/dist/components/LpThemeSwitcher.vue.js +1 -1
- package/dist/components/LpTimeField.vue.d.ts +4 -0
- package/dist/components/LpTimeField.vue.js +1 -1
- package/dist/components/LpTopologyCanvas.vue.js +1 -1
- package/dist/components/LpWorkspaceSwitcher.vue.js +1 -1
- package/dist/components/calendar.d.ts +2 -0
- package/dist/composables/useField.d.ts +50 -0
- package/dist/composables/useField.js +31 -2
- package/dist/composables/useMergedClass.d.ts +6 -2
- package/dist/composables/useMergedClass.js +6 -6
- package/dist/index.js +34 -34
- package/package.json +1 -1
- package/src/components/LpAddressInput.vue +8 -4
- package/src/components/LpAppShell.vue +56 -18
- package/src/components/LpAutocomplete.vue +11 -5
- package/src/components/LpCheckbox.vue +8 -6
- package/src/components/LpColorPicker.vue +7 -4
- package/src/components/LpDatePicker.vue +8 -4
- package/src/components/LpDateRangePicker.vue +8 -4
- package/src/components/LpInput.vue +3 -4
- package/src/components/LpNumberField.vue +11 -6
- package/src/components/LpOtpInput.vue +8 -6
- package/src/components/LpPhoneInput.vue +11 -6
- package/src/components/LpRadioGroup.vue +9 -5
- package/src/components/LpRangeSlider.vue +8 -4
- package/src/components/LpRepeater.vue +16 -4
- package/src/components/LpSegmented.vue +8 -4
- package/src/components/LpSelect.vue +8 -7
- package/src/components/LpSlider.vue +8 -4
- package/src/components/LpSwitch.vue +8 -5
- package/src/components/LpTagsInput.vue +4 -3
- package/src/components/LpTextarea.vue +3 -2
- package/src/components/LpTimeField.vue +8 -6
- package/src/components/calendar.ts +2 -0
- package/src/composables/useField.ts +52 -0
- package/src/composables/useMergedClass.ts +11 -5
- package/dist/chunks/LpAddressInput-BSS49jpm.js +0 -393
- package/dist/chunks/LpAutocomplete-DkY6CSyD.js +0 -302
- package/dist/chunks/LpCheckbox-D6RuDW4X.js +0 -124
- package/dist/chunks/LpColorPicker-COHzV68R.js +0 -567
- package/dist/chunks/LpDatePicker-DwSDY0mt.js +0 -163
- package/dist/chunks/LpNumberField-Csg-1Zh_.js +0 -148
- package/dist/chunks/LpOtpInput-BRpLBbOD.js +0 -132
- package/dist/chunks/LpRadioGroup-BrazjLY_.js +0 -95
- package/dist/chunks/LpRangeSlider-fz61tzc-.js +0 -89
- package/dist/chunks/LpRepeater-BkJHSPI1.js +0 -180
- package/dist/chunks/LpSegmented-DbEJhbv-.js +0 -123
- package/dist/chunks/LpSelect-Y-iuSymW.js +0 -512
- package/dist/chunks/LpSlider-A4ixovgS.js +0 -68
- package/dist/chunks/LpSwitch-DnC2-0aV.js +0 -115
- package/dist/chunks/LpTimeField-DeOcKWJq.js +0 -110
|
@@ -6,6 +6,7 @@ export default { inheritAttrs: false }
|
|
|
6
6
|
</script>
|
|
7
7
|
|
|
8
8
|
<script setup lang="ts">
|
|
9
|
+
import FieldWrapper from "./FieldWrapper.vue"
|
|
9
10
|
import { CONTROL_SIZE } from "./controlSize"
|
|
10
11
|
import { usePaletteControl, type PaletteControlOptions } from "../composables/usePaletteCommands"
|
|
11
12
|
/*
|
|
@@ -22,7 +23,7 @@ import { usePaletteControl, type PaletteControlOptions } from "../composables/us
|
|
|
22
23
|
import { Time } from "@internationalized/date"
|
|
23
24
|
import { TimeFieldInput, TimeFieldRoot } from "reka-ui"
|
|
24
25
|
import { computed } from "vue"
|
|
25
|
-
import { focusLeft,
|
|
26
|
+
import { focusLeft, useOwnField, mergeDescribedBy } from "../composables/useField"
|
|
26
27
|
import { useMergedAttrs } from "../composables/useMergedClass"
|
|
27
28
|
import LpIcon from "./LpIcon.vue"
|
|
28
29
|
|
|
@@ -34,6 +35,8 @@ const props = withDefaults(
|
|
|
34
35
|
modelValue?: string
|
|
35
36
|
disabled?: boolean
|
|
36
37
|
invalid?: boolean
|
|
38
|
+
/** A message under the control, as on LpInput: it marks it invalid and shows under it (what `form.field()` hands it). */
|
|
39
|
+
error?: string
|
|
37
40
|
size?: "sm" | "md" | "lg"
|
|
38
41
|
/** 24 or 12-hour clock, independent of the browser locale. */
|
|
39
42
|
hourCycle?: 12 | 24
|
|
@@ -68,9 +71,7 @@ function onUpdate(next: unknown) {
|
|
|
68
71
|
emit("update:modelValue", `${pad(next.hour)}:${pad(next.minute)}`)
|
|
69
72
|
}
|
|
70
73
|
|
|
71
|
-
const field =
|
|
72
|
-
wrapped: false,
|
|
73
|
-
invalid: props.invalid,
|
|
74
|
+
const { field, wrapper, ownAttrs } = useOwnField(() => ({ error: props.error, invalid: props.invalid,
|
|
74
75
|
disabled: props.disabled,
|
|
75
76
|
}))
|
|
76
77
|
|
|
@@ -82,13 +83,13 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
|
|
|
82
83
|
"flex w-full items-center gap-0.5 rounded-control border bg-surface-soft px-3 text-ink transition-colors duration-[var(--duration-fast)] focus-within:ring-2 focus-within:ring-ring data-[disabled]:cursor-not-allowed data-[disabled]:opacity-55",
|
|
83
84
|
shellSize[props.size],
|
|
84
85
|
field.value.invalid ? "border-danger focus-within:ring-danger-soft" : "border-line focus-within:border-brand",
|
|
85
|
-
].join(" "),
|
|
86
|
-
)
|
|
86
|
+
].join(" "), () => ownAttrs())
|
|
87
87
|
|
|
88
88
|
usePaletteControl({ disabled: () => field.value.disabled, invalid: () => field.value.invalid })
|
|
89
89
|
</script>
|
|
90
90
|
|
|
91
91
|
<template>
|
|
92
|
+
<FieldWrapper v-bind="wrapper">
|
|
92
93
|
<TimeFieldRoot
|
|
93
94
|
v-slot="{ segments }"
|
|
94
95
|
:model-value="value"
|
|
@@ -121,4 +122,5 @@ usePaletteControl({ disabled: () => field.value.disabled, invalid: () => field.v
|
|
|
121
122
|
</TimeFieldInput>
|
|
122
123
|
</template>
|
|
123
124
|
</TimeFieldRoot>
|
|
125
|
+
</FieldWrapper>
|
|
124
126
|
</template>
|
|
@@ -52,6 +52,8 @@ export interface PickerFieldProps {
|
|
|
52
52
|
clearable?: boolean
|
|
53
53
|
disabled?: boolean
|
|
54
54
|
invalid?: boolean
|
|
55
|
+
/** A message under the picker, as on LpInput: it marks it invalid and shows under it (what `form.field()` hands it). */
|
|
56
|
+
error?: string
|
|
55
57
|
size?: "sm" | "md" | "lg"
|
|
56
58
|
/** Locale of the shown date and the calendar; defaults to reka's ConfigProvider, then the browser. */
|
|
57
59
|
locale?: string
|
|
@@ -154,6 +154,58 @@ export function fieldWrapperBinds(
|
|
|
154
154
|
return { wrapped, id: field.id, label, hint, error, required, class: attrs.class, style: attrs.style }
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Whether a control with these props draws its own LpFormField. A label or a
|
|
159
|
+
* hint is its own; an error alone is, except inside an outer LpFormField, which
|
|
160
|
+
* owns the label (its `for` points at the control) and the message line: a
|
|
161
|
+
* second field in it showed the message twice and took the label's control id.
|
|
162
|
+
* There the error only marks the control invalid.
|
|
163
|
+
*/
|
|
164
|
+
export function wrapsItself(
|
|
165
|
+
own: { label?: string; hint?: string; error?: string },
|
|
166
|
+
outer: ComputedRef<FieldContext> | null,
|
|
167
|
+
): boolean {
|
|
168
|
+
if (own.label || own.hint) return true
|
|
169
|
+
return !!own.error && !outer?.value
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* A control that takes `label`, `hint` and `error` the way LpInput does: given
|
|
174
|
+
* any of them it wraps itself in LpFormField (FieldWrapper, bound to
|
|
175
|
+
* `wrapper`), an error marks it invalid, and the consumer's class and style lay
|
|
176
|
+
* out that field instead of the control (`ownAttrs` says whether the control's
|
|
177
|
+
* root still takes them). Without them it is the bare control, as before. So
|
|
178
|
+
* `v-bind="form.field('x')"`, which hands every control `error`, shows the
|
|
179
|
+
* message on any of them.
|
|
180
|
+
* Inside an outer LpFormField an error alone does not wrap (wrapsItself).
|
|
181
|
+
*/
|
|
182
|
+
export function useOwnField(
|
|
183
|
+
state: () => { label?: string; hint?: string; error?: string; required?: boolean; invalid?: boolean; disabled?: boolean; id?: string },
|
|
184
|
+
) {
|
|
185
|
+
const attrs = useAttrs()
|
|
186
|
+
const outer = useField()
|
|
187
|
+
const wrapped = computed(() => wrapsItself(state(), outer))
|
|
188
|
+
const field = useFieldControl(() => {
|
|
189
|
+
const own = state()
|
|
190
|
+
return {
|
|
191
|
+
wrapped: wrapped.value,
|
|
192
|
+
hasMessage: !!own.error || !!own.hint,
|
|
193
|
+
id: own.id ?? (typeof attrs.id === "string" ? attrs.id : undefined),
|
|
194
|
+
invalid: !!own.invalid || !!own.error,
|
|
195
|
+
required: own.required,
|
|
196
|
+
disabled: own.disabled,
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
const wrapper = computed(() => fieldWrapperBinds(wrapped.value, field.value, state(), attrs))
|
|
200
|
+
/* The attributes for the control itself: all of them, less the class and style the wrapper takes. */
|
|
201
|
+
const controlAttrs = computed(() => {
|
|
202
|
+
if (!wrapped.value) return attrs
|
|
203
|
+
const { class: _class, style: _style, ...rest } = attrs
|
|
204
|
+
return rest
|
|
205
|
+
})
|
|
206
|
+
return { wrapped, field, wrapper, controlAttrs, ownAttrs: () => !wrapped.value }
|
|
207
|
+
}
|
|
208
|
+
|
|
157
209
|
/** The state of a control that takes its id from the app and draws no wrapper of its own. */
|
|
158
210
|
export function useAttrsField(state: () => { invalid?: boolean; disabled?: boolean }) {
|
|
159
211
|
const attrs = useAttrs()
|
|
@@ -62,11 +62,13 @@ function merge(...classes: (string | undefined | null | false)[]): string {
|
|
|
62
62
|
*/
|
|
63
63
|
export function useMergedClass(
|
|
64
64
|
own: MaybeRefOrGetter<string | undefined>,
|
|
65
|
+
/** False while something else takes the consumer's class (a field wrapper). */
|
|
66
|
+
takesAttrs: () => boolean = () => true,
|
|
65
67
|
): ComputedRef<string> {
|
|
66
68
|
const attrs = useAttrs()
|
|
67
69
|
// `cn` returns undefined when everything it was given was empty; a class
|
|
68
70
|
// binding is happier with "" than with undefined.
|
|
69
|
-
return computed(() => merge(toValue(own), attrs.class as string | undefined))
|
|
71
|
+
return computed(() => merge(toValue(own), takesAttrs() ? (attrs.class as string | undefined) : undefined))
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
/**
|
|
@@ -85,16 +87,20 @@ export function useMergedClass(
|
|
|
85
87
|
* Vue appends that second `class` to the merged one, and stylesheet order —
|
|
86
88
|
* not the caller — decides which wins.
|
|
87
89
|
*/
|
|
88
|
-
export function useMergedAttrs(
|
|
90
|
+
export function useMergedAttrs(
|
|
91
|
+
own: MaybeRefOrGetter<string | undefined>,
|
|
92
|
+
/** False while a field wrapper takes the consumer's class and style (useOwnField's `ownAttrs`). */
|
|
93
|
+
takesAttrs: () => boolean = () => true,
|
|
94
|
+
): {
|
|
89
95
|
class: ComputedRef<string>
|
|
90
96
|
attrs: ComputedRef<Record<string, unknown>>
|
|
91
97
|
} {
|
|
92
98
|
const attrs = useAttrs()
|
|
93
99
|
return {
|
|
94
|
-
class: useMergedClass(own),
|
|
100
|
+
class: useMergedClass(own, takesAttrs),
|
|
95
101
|
attrs: computed(() => {
|
|
96
|
-
const { class: _omitted, ...rest } = attrs
|
|
97
|
-
return rest
|
|
102
|
+
const { class: _omitted, style, ...rest } = attrs
|
|
103
|
+
return takesAttrs() ? { ...rest, ...(style === undefined ? {} : { style }) } : rest
|
|
98
104
|
}),
|
|
99
105
|
}
|
|
100
106
|
}
|
|
@@ -1,393 +0,0 @@
|
|
|
1
|
-
import { defineComponent, defineAsyncComponent, computed, watch, ref, onMounted, openBlock, createElementBlock, mergeProps, unref, createVNode, withCtx, createElementVNode, createBlock, createCommentVNode, createTextVNode, toDisplayString, renderSlot, normalizeClass } from "vue";
|
|
2
|
-
import { useAsyncTask } from "../composables/useAsyncTask.js";
|
|
3
|
-
import { OSM_TILES, createNominatimProvider, emptyAddress, formatAddress } from "../components/address.js";
|
|
4
|
-
import { flagEmoji, loadCountries } from "../components/countries.js";
|
|
5
|
-
import { _ as _sfc_main$2 } from "./LpAutocomplete-DkY6CSyD.js";
|
|
6
|
-
import { _ as _sfc_main$3 } from "./LpButton-DSHioIfD.js";
|
|
7
|
-
import { _ as _sfc_main$1 } from "./LpFormField-BKYn9dyP.js";
|
|
8
|
-
import { _ as _sfc_main$4 } from "./LpIcon-C6JpQ5td.js";
|
|
9
|
-
import { _ as _sfc_main$5 } from "./LpInput-D0O6dpuA.js";
|
|
10
|
-
import { _ as _sfc_main$7 } from "./LpModal-DDvrSAnf.js";
|
|
11
|
-
import { _ as _sfc_main$6 } from "./LpSelect-Y-iuSymW.js";
|
|
12
|
-
import { useFieldControl, mergeDescribedBy } from "../composables/useField.js";
|
|
13
|
-
import { useLabels } from "../composables/useLpConfig.js";
|
|
14
|
-
import { useMergedAttrs } from "../composables/useMergedClass.js";
|
|
15
|
-
const _hoisted_1 = ["aria-labelledby", "aria-invalid", "aria-describedby"];
|
|
16
|
-
const _hoisted_2 = { class: "flex items-stretch gap-2" };
|
|
17
|
-
const _hoisted_3 = { class: "grid gap-3 sm:grid-cols-[minmax(0,1fr)_10rem]" };
|
|
18
|
-
const _hoisted_4 = {
|
|
19
|
-
key: 1,
|
|
20
|
-
class: "flex items-center gap-2"
|
|
21
|
-
};
|
|
22
|
-
const _hoisted_5 = {
|
|
23
|
-
key: 0,
|
|
24
|
-
class: "text-xs text-muted"
|
|
25
|
-
};
|
|
26
|
-
const __default__ = { inheritAttrs: false };
|
|
27
|
-
const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
28
|
-
...__default__,
|
|
29
|
-
__name: "LpAddressInput",
|
|
30
|
-
props: {
|
|
31
|
-
modelValue: {},
|
|
32
|
-
provider: {},
|
|
33
|
-
tiles: { default: () => OSM_TILES },
|
|
34
|
-
only: {},
|
|
35
|
-
secondLine: { type: Boolean },
|
|
36
|
-
minChars: { default: 3 },
|
|
37
|
-
debounce: { default: 350 },
|
|
38
|
-
language: {},
|
|
39
|
-
disabled: { type: Boolean },
|
|
40
|
-
invalid: { type: Boolean },
|
|
41
|
-
size: { default: "md" },
|
|
42
|
-
hideMap: { type: Boolean },
|
|
43
|
-
labels: {}
|
|
44
|
-
},
|
|
45
|
-
emits: ["update:modelValue", "pick"],
|
|
46
|
-
setup(__props, { emit: __emit }) {
|
|
47
|
-
const LpMapPicker = defineAsyncComponent(() => import("../components/LpMapPicker.vue.js"));
|
|
48
|
-
const props = __props;
|
|
49
|
-
const emit = __emit;
|
|
50
|
-
const DEFAULT_LABELS = {
|
|
51
|
-
line: "Street and number",
|
|
52
|
-
linePlaceholder: "Baker Street 221B",
|
|
53
|
-
secondLine: "Apartment, suite (optional)",
|
|
54
|
-
secondLinePlaceholder: "Flat 4",
|
|
55
|
-
city: "City",
|
|
56
|
-
cityPlaceholder: "London",
|
|
57
|
-
postalCode: "Postcode",
|
|
58
|
-
postalCodePlaceholder: "NW1 6XE",
|
|
59
|
-
country: "Country",
|
|
60
|
-
countryPlaceholder: "Select country…",
|
|
61
|
-
countrySearch: "Search country…",
|
|
62
|
-
pickOnMap: "Pick on map",
|
|
63
|
-
mapTitle: "Pick a location",
|
|
64
|
-
mapHint: "Click the map to drop a pin, drag to move around.",
|
|
65
|
-
use: "Use this address",
|
|
66
|
-
cancel: "Cancel",
|
|
67
|
-
searching: "Searching…",
|
|
68
|
-
noSuggestions: "No suggestions",
|
|
69
|
-
nothingHere: "No address found here"
|
|
70
|
-
};
|
|
71
|
-
const text = useLabels("addressInput", DEFAULT_LABELS, () => props.labels);
|
|
72
|
-
const fallbackProvider = createNominatimProvider();
|
|
73
|
-
const geocoder = computed(() => props.provider ?? fallbackProvider);
|
|
74
|
-
const value = computed(() => props.modelValue ?? emptyAddress());
|
|
75
|
-
let latest;
|
|
76
|
-
watch(
|
|
77
|
-
() => props.modelValue,
|
|
78
|
-
() => latest = void 0
|
|
79
|
-
);
|
|
80
|
-
function patch(changes) {
|
|
81
|
-
latest = { ...latest ?? value.value, ...changes };
|
|
82
|
-
emit("update:modelValue", latest);
|
|
83
|
-
}
|
|
84
|
-
function edit(changes) {
|
|
85
|
-
patch({ ...changes, lat: void 0, lon: void 0 });
|
|
86
|
-
}
|
|
87
|
-
const allCountries = ref([]);
|
|
88
|
-
const countries = computed(() => {
|
|
89
|
-
const only = props.only?.map((code) => code.toUpperCase());
|
|
90
|
-
return only?.length ? allCountries.value.filter((c) => only.includes(c.iso2)) : allCountries.value;
|
|
91
|
-
});
|
|
92
|
-
const countryOptions = computed(
|
|
93
|
-
() => countries.value.map((country) => ({
|
|
94
|
-
value: country.iso2,
|
|
95
|
-
label: `${flagEmoji(country.iso2)} ${country.name}`
|
|
96
|
-
}))
|
|
97
|
-
);
|
|
98
|
-
onMounted(async () => {
|
|
99
|
-
allCountries.value = await loadCountries();
|
|
100
|
-
});
|
|
101
|
-
function useSuggestions(field2) {
|
|
102
|
-
const options = ref([]);
|
|
103
|
-
const task = useAsyncTask();
|
|
104
|
-
function query(term) {
|
|
105
|
-
if (term.trim().length < props.minChars) {
|
|
106
|
-
task.cancel();
|
|
107
|
-
options.value = [];
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
task.run(
|
|
111
|
-
(signal) => geocoder.value.search(term.trim(), field2, {
|
|
112
|
-
country: value.value.country || void 0,
|
|
113
|
-
language: props.language,
|
|
114
|
-
signal
|
|
115
|
-
}),
|
|
116
|
-
{
|
|
117
|
-
delay: props.debounce,
|
|
118
|
-
apply: (found) => options.value = found,
|
|
119
|
-
// Network failure: leave the field usable as free text.
|
|
120
|
-
fail: () => options.value = []
|
|
121
|
-
}
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
return { options, loading: task.pending, query };
|
|
125
|
-
}
|
|
126
|
-
const lineSuggestions = useSuggestions("line");
|
|
127
|
-
const citySuggestions = useSuggestions("city");
|
|
128
|
-
function applySuggestion(suggestion, keep) {
|
|
129
|
-
const parsed = suggestion.address;
|
|
130
|
-
const changes = { lat: suggestion.lat, lon: suggestion.lon };
|
|
131
|
-
if (parsed.line && keep !== "line") changes.line = parsed.line;
|
|
132
|
-
if (parsed.city) changes.city = parsed.city;
|
|
133
|
-
if (parsed.postalCode) changes.postalCode = parsed.postalCode;
|
|
134
|
-
if (parsed.country) changes.country = parsed.country;
|
|
135
|
-
patch(changes);
|
|
136
|
-
}
|
|
137
|
-
function onLineSelect(chosen) {
|
|
138
|
-
const suggestion = lineSuggestions.options.value.find((option) => option.value === chosen);
|
|
139
|
-
if (suggestion) applySuggestion(suggestion, "line");
|
|
140
|
-
}
|
|
141
|
-
function onCitySelect(chosen) {
|
|
142
|
-
const suggestion = citySuggestions.options.value.find((option) => option.value === chosen);
|
|
143
|
-
if (suggestion) applySuggestion(suggestion, "city");
|
|
144
|
-
}
|
|
145
|
-
const mapOpen = ref(false);
|
|
146
|
-
const point = ref();
|
|
147
|
-
const resolved = ref();
|
|
148
|
-
const resolving = ref(false);
|
|
149
|
-
computed(
|
|
150
|
-
() => countries.value.find((c) => c.iso2 === value.value.country)?.name
|
|
151
|
-
);
|
|
152
|
-
const preview = computed(() => {
|
|
153
|
-
if (resolving.value) return text.value.searching;
|
|
154
|
-
if (!point.value) return text.value.mapHint;
|
|
155
|
-
if (!resolved.value) return text.value.nothingHere;
|
|
156
|
-
const parsed = resolved.value.address;
|
|
157
|
-
return formatAddress(
|
|
158
|
-
{
|
|
159
|
-
line: parsed.line ?? "",
|
|
160
|
-
city: parsed.city ?? "",
|
|
161
|
-
postalCode: parsed.postalCode ?? "",
|
|
162
|
-
country: parsed.country ?? ""
|
|
163
|
-
},
|
|
164
|
-
countries.value.find((c) => c.iso2 === parsed.country)?.name
|
|
165
|
-
);
|
|
166
|
-
});
|
|
167
|
-
watch(mapOpen, (open) => {
|
|
168
|
-
if (!open) return;
|
|
169
|
-
point.value = value.value.lat != null && value.value.lon != null ? { lat: value.value.lat, lon: value.value.lon } : void 0;
|
|
170
|
-
resolved.value = void 0;
|
|
171
|
-
});
|
|
172
|
-
const reverse = useAsyncTask();
|
|
173
|
-
watch(reverse.pending, (busy) => resolving.value = busy);
|
|
174
|
-
function onPick(next) {
|
|
175
|
-
point.value = next;
|
|
176
|
-
resolved.value = void 0;
|
|
177
|
-
reverse.run(
|
|
178
|
-
() => geocoder.value.reverse(next.lat, next.lon, { language: props.language }),
|
|
179
|
-
{
|
|
180
|
-
apply: (found) => resolved.value = found,
|
|
181
|
-
fail: () => resolved.value = void 0
|
|
182
|
-
}
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
function confirmPoint() {
|
|
186
|
-
if (!point.value) return;
|
|
187
|
-
const parsed = resolved.value?.address ?? {};
|
|
188
|
-
patch({
|
|
189
|
-
line: parsed.line ?? value.value.line,
|
|
190
|
-
city: parsed.city ?? value.value.city,
|
|
191
|
-
postalCode: parsed.postalCode ?? value.value.postalCode,
|
|
192
|
-
country: parsed.country ?? value.value.country,
|
|
193
|
-
lat: point.value.lat,
|
|
194
|
-
lon: point.value.lon
|
|
195
|
-
});
|
|
196
|
-
emit("pick", point.value);
|
|
197
|
-
mapOpen.value = false;
|
|
198
|
-
}
|
|
199
|
-
const field = useFieldControl(() => ({ wrapped: false, invalid: props.invalid, disabled: props.disabled }));
|
|
200
|
-
const { class: rootClass, attrs: rest } = useMergedAttrs(
|
|
201
|
-
"flex flex-col gap-3"
|
|
202
|
-
);
|
|
203
|
-
return (_ctx, _cache) => {
|
|
204
|
-
return openBlock(), createElementBlock("div", mergeProps({
|
|
205
|
-
role: "group",
|
|
206
|
-
"aria-labelledby": unref(field).labelId,
|
|
207
|
-
"aria-invalid": unref(field).invalid || void 0,
|
|
208
|
-
class: unref(rootClass)
|
|
209
|
-
}, unref(rest), {
|
|
210
|
-
"aria-describedby": unref(mergeDescribedBy)(unref(rest)["aria-describedby"], unref(field).describedBy)
|
|
211
|
-
}), [
|
|
212
|
-
createVNode(_sfc_main$1, {
|
|
213
|
-
label: unref(text).line
|
|
214
|
-
}, {
|
|
215
|
-
default: withCtx(() => [
|
|
216
|
-
createElementVNode("div", _hoisted_2, [
|
|
217
|
-
createVNode(_sfc_main$2, {
|
|
218
|
-
class: "min-w-0 flex-1",
|
|
219
|
-
"model-value": value.value.line,
|
|
220
|
-
options: unref(lineSuggestions).options.value,
|
|
221
|
-
loading: unref(lineSuggestions).loading.value,
|
|
222
|
-
filter: false,
|
|
223
|
-
"min-chars": __props.minChars,
|
|
224
|
-
disabled: unref(field).disabled,
|
|
225
|
-
invalid: unref(field).invalid,
|
|
226
|
-
size: __props.size,
|
|
227
|
-
placeholder: unref(text).linePlaceholder,
|
|
228
|
-
"empty-text": unref(text).noSuggestions,
|
|
229
|
-
"loading-text": unref(text).searching,
|
|
230
|
-
icon: "lucide:map-pin",
|
|
231
|
-
"onUpdate:modelValue": _cache[0] || (_cache[0] = (next) => {
|
|
232
|
-
edit({ line: next });
|
|
233
|
-
unref(lineSuggestions).query(next);
|
|
234
|
-
}),
|
|
235
|
-
onSelect: onLineSelect
|
|
236
|
-
}, null, 8, ["model-value", "options", "loading", "min-chars", "disabled", "invalid", "size", "placeholder", "empty-text", "loading-text"]),
|
|
237
|
-
!__props.hideMap ? (openBlock(), createBlock(_sfc_main$3, {
|
|
238
|
-
key: 0,
|
|
239
|
-
variant: "soft",
|
|
240
|
-
size: __props.size,
|
|
241
|
-
square: "",
|
|
242
|
-
disabled: unref(field).disabled,
|
|
243
|
-
title: unref(text).pickOnMap,
|
|
244
|
-
"aria-label": unref(text).pickOnMap,
|
|
245
|
-
onClick: _cache[1] || (_cache[1] = ($event) => mapOpen.value = true)
|
|
246
|
-
}, {
|
|
247
|
-
default: withCtx(() => [
|
|
248
|
-
createVNode(_sfc_main$4, {
|
|
249
|
-
name: "lucide:map",
|
|
250
|
-
size: 16
|
|
251
|
-
})
|
|
252
|
-
]),
|
|
253
|
-
_: 1
|
|
254
|
-
}, 8, ["size", "disabled", "title", "aria-label"])) : createCommentVNode("", true)
|
|
255
|
-
])
|
|
256
|
-
]),
|
|
257
|
-
_: 1
|
|
258
|
-
}, 8, ["label"]),
|
|
259
|
-
__props.secondLine ? (openBlock(), createBlock(_sfc_main$5, {
|
|
260
|
-
key: 0,
|
|
261
|
-
"model-value": value.value.line2 ?? "",
|
|
262
|
-
label: unref(text).secondLine,
|
|
263
|
-
placeholder: unref(text).secondLinePlaceholder,
|
|
264
|
-
disabled: unref(field).disabled,
|
|
265
|
-
invalid: unref(field).invalid,
|
|
266
|
-
size: __props.size,
|
|
267
|
-
autocomplete: "address-line2",
|
|
268
|
-
"onUpdate:modelValue": _cache[2] || (_cache[2] = (next) => patch({ line2: next }))
|
|
269
|
-
}, null, 8, ["model-value", "label", "placeholder", "disabled", "invalid", "size"])) : createCommentVNode("", true),
|
|
270
|
-
createElementVNode("div", _hoisted_3, [
|
|
271
|
-
createVNode(_sfc_main$1, {
|
|
272
|
-
label: unref(text).city
|
|
273
|
-
}, {
|
|
274
|
-
default: withCtx(() => [
|
|
275
|
-
createVNode(_sfc_main$2, {
|
|
276
|
-
"model-value": value.value.city,
|
|
277
|
-
options: unref(citySuggestions).options.value,
|
|
278
|
-
loading: unref(citySuggestions).loading.value,
|
|
279
|
-
filter: false,
|
|
280
|
-
"min-chars": __props.minChars,
|
|
281
|
-
disabled: unref(field).disabled,
|
|
282
|
-
invalid: unref(field).invalid,
|
|
283
|
-
size: __props.size,
|
|
284
|
-
placeholder: unref(text).cityPlaceholder,
|
|
285
|
-
"empty-text": unref(text).noSuggestions,
|
|
286
|
-
"loading-text": unref(text).searching,
|
|
287
|
-
"onUpdate:modelValue": _cache[3] || (_cache[3] = (next) => {
|
|
288
|
-
edit({ city: next });
|
|
289
|
-
unref(citySuggestions).query(next);
|
|
290
|
-
}),
|
|
291
|
-
onSelect: onCitySelect
|
|
292
|
-
}, null, 8, ["model-value", "options", "loading", "min-chars", "disabled", "invalid", "size", "placeholder", "empty-text", "loading-text"])
|
|
293
|
-
]),
|
|
294
|
-
_: 1
|
|
295
|
-
}, 8, ["label"]),
|
|
296
|
-
createVNode(_sfc_main$5, {
|
|
297
|
-
"model-value": value.value.postalCode,
|
|
298
|
-
label: unref(text).postalCode,
|
|
299
|
-
placeholder: unref(text).postalCodePlaceholder,
|
|
300
|
-
disabled: unref(field).disabled,
|
|
301
|
-
invalid: unref(field).invalid,
|
|
302
|
-
size: __props.size,
|
|
303
|
-
autocomplete: "postal-code",
|
|
304
|
-
"onUpdate:modelValue": _cache[4] || (_cache[4] = (next) => edit({ postalCode: next }))
|
|
305
|
-
}, null, 8, ["model-value", "label", "placeholder", "disabled", "invalid", "size"])
|
|
306
|
-
]),
|
|
307
|
-
createVNode(_sfc_main$1, {
|
|
308
|
-
label: unref(text).country
|
|
309
|
-
}, {
|
|
310
|
-
default: withCtx(() => [
|
|
311
|
-
createVNode(_sfc_main$6, {
|
|
312
|
-
"model-value": value.value.country,
|
|
313
|
-
options: countryOptions.value,
|
|
314
|
-
placeholder: unref(text).countryPlaceholder,
|
|
315
|
-
"search-placeholder": unref(text).countrySearch,
|
|
316
|
-
disabled: unref(field).disabled,
|
|
317
|
-
invalid: unref(field).invalid,
|
|
318
|
-
size: __props.size,
|
|
319
|
-
searchable: "",
|
|
320
|
-
"onUpdate:modelValue": _cache[5] || (_cache[5] = (next) => edit({ country: String(next ?? "") }))
|
|
321
|
-
}, null, 8, ["model-value", "options", "placeholder", "search-placeholder", "disabled", "invalid", "size"])
|
|
322
|
-
]),
|
|
323
|
-
_: 1
|
|
324
|
-
}, 8, ["label"]),
|
|
325
|
-
!__props.hideMap ? (openBlock(), createElementBlock("div", _hoisted_4, [
|
|
326
|
-
createVNode(_sfc_main$3, {
|
|
327
|
-
variant: "soft",
|
|
328
|
-
size: __props.size === "lg" ? "md" : "sm",
|
|
329
|
-
disabled: unref(field).disabled,
|
|
330
|
-
onClick: _cache[6] || (_cache[6] = ($event) => mapOpen.value = true)
|
|
331
|
-
}, {
|
|
332
|
-
default: withCtx(() => [
|
|
333
|
-
createVNode(_sfc_main$4, {
|
|
334
|
-
name: "lucide:map",
|
|
335
|
-
size: 15
|
|
336
|
-
}),
|
|
337
|
-
createTextVNode(" " + toDisplayString(unref(text).pickOnMap), 1)
|
|
338
|
-
]),
|
|
339
|
-
_: 1
|
|
340
|
-
}, 8, ["size", "disabled"]),
|
|
341
|
-
value.value.lat != null && value.value.lon != null ? (openBlock(), createElementBlock("span", _hoisted_5, toDisplayString(value.value.lat.toFixed(5)) + ", " + toDisplayString(value.value.lon.toFixed(5)), 1)) : createCommentVNode("", true)
|
|
342
|
-
])) : createCommentVNode("", true),
|
|
343
|
-
createVNode(_sfc_main$7, {
|
|
344
|
-
open: mapOpen.value,
|
|
345
|
-
"onUpdate:open": _cache[8] || (_cache[8] = ($event) => mapOpen.value = $event),
|
|
346
|
-
title: unref(text).mapTitle,
|
|
347
|
-
size: "2xl"
|
|
348
|
-
}, {
|
|
349
|
-
footer: withCtx(() => [
|
|
350
|
-
createVNode(_sfc_main$3, {
|
|
351
|
-
variant: "ghost",
|
|
352
|
-
onClick: _cache[7] || (_cache[7] = ($event) => mapOpen.value = false)
|
|
353
|
-
}, {
|
|
354
|
-
default: withCtx(() => [
|
|
355
|
-
createTextVNode(toDisplayString(unref(text).cancel), 1)
|
|
356
|
-
]),
|
|
357
|
-
_: 1
|
|
358
|
-
}),
|
|
359
|
-
createVNode(_sfc_main$3, {
|
|
360
|
-
disabled: !point.value,
|
|
361
|
-
onClick: confirmPoint
|
|
362
|
-
}, {
|
|
363
|
-
default: withCtx(() => [
|
|
364
|
-
createTextVNode(toDisplayString(unref(text).use), 1)
|
|
365
|
-
]),
|
|
366
|
-
_: 1
|
|
367
|
-
}, 8, ["disabled"])
|
|
368
|
-
]),
|
|
369
|
-
default: withCtx(() => [
|
|
370
|
-
renderSlot(_ctx.$slots, "map", {
|
|
371
|
-
point: point.value,
|
|
372
|
-
setPoint: onPick,
|
|
373
|
-
tiles: __props.tiles
|
|
374
|
-
}, () => [
|
|
375
|
-
createVNode(unref(LpMapPicker), {
|
|
376
|
-
"model-value": point.value,
|
|
377
|
-
tiles: __props.tiles,
|
|
378
|
-
"onUpdate:modelValue": onPick
|
|
379
|
-
}, null, 8, ["model-value", "tiles"])
|
|
380
|
-
]),
|
|
381
|
-
createElementVNode("p", {
|
|
382
|
-
class: normalizeClass(["mt-3 min-h-5 text-sm", resolved.value || resolving.value ? "text-ink" : "text-muted"])
|
|
383
|
-
}, toDisplayString(preview.value), 3)
|
|
384
|
-
]),
|
|
385
|
-
_: 3
|
|
386
|
-
}, 8, ["open", "title"])
|
|
387
|
-
], 16, _hoisted_1);
|
|
388
|
-
};
|
|
389
|
-
}
|
|
390
|
-
});
|
|
391
|
-
export {
|
|
392
|
-
_sfc_main as _
|
|
393
|
-
};
|