sprintify-ui 0.12.26 → 0.12.28
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/README.md +13 -0
- package/dist/sprintify-ui.es.js +10175 -9935
- package/dist/tailwindcss/table.js +27 -0
- package/dist/types/components/BaseAutocompleteDrawer.vue.d.ts +1 -1
- package/dist/types/components/BaseBelongsTo.vue.d.ts +1 -1
- package/dist/types/components/BaseBelongsToFetch.vue.d.ts +1 -1
- package/dist/types/components/BaseHasMany.vue.d.ts +1 -1
- package/dist/types/composables/placesAutocomplete.d.ts +31 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/svg/BaseGoogleMapsLogo.vue.d.ts +2 -0
- package/package.json +2 -2
- package/src/components/BaseAddressForm.stories.js +25 -0
- package/src/components/BaseAddressForm.vue +244 -51
- package/src/components/BaseCard.stories.js +57 -0
- package/src/composables/placesAutocomplete.ts +194 -0
- package/src/index.ts +8 -1
- package/src/svg/BaseGoogleMapsLogo.vue +63 -0
|
@@ -87,5 +87,32 @@ export default function ({ addVariant, addComponents, e, theme }) {
|
|
|
87
87
|
'border-style': borderStyle,
|
|
88
88
|
'border-color': borderColor,
|
|
89
89
|
},
|
|
90
|
+
// Outer borders removal (ex: table inside a BaseCard)
|
|
91
|
+
...outerBorderRemoval('.tabulation.tabulation-border-t-0', ['top']),
|
|
92
|
+
...outerBorderRemoval('.tabulation.tabulation-border-b-0', ['bottom']),
|
|
93
|
+
...outerBorderRemoval('.tabulation.tabulation-border-l-0', ['left']),
|
|
94
|
+
...outerBorderRemoval('.tabulation.tabulation-border-r-0', ['right']),
|
|
95
|
+
...outerBorderRemoval('.tabulation.tabulation-border-0', ['top', 'bottom', 'left', 'right']),
|
|
90
96
|
});
|
|
91
97
|
}
|
|
98
|
+
|
|
99
|
+
// Rows can be inside thead/tbody/tfoot or directly inside the table
|
|
100
|
+
const outerCells = {
|
|
101
|
+
top: ['> :first-child > tr:first-child > *', '> tr:first-child > *'],
|
|
102
|
+
bottom: ['> :last-child > tr:last-child > *', '> tr:last-child > *'],
|
|
103
|
+
left: ['> * > tr > :first-child', '> tr > :first-child'],
|
|
104
|
+
right: ['> * > tr > :last-child', '> tr > :last-child'],
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
function outerBorderRemoval(table, sides) {
|
|
108
|
+
const components = {};
|
|
109
|
+
|
|
110
|
+
sides.forEach((side) => {
|
|
111
|
+
const selectors = [table, ...outerCells[side].map((cell) => `${table} ${cell}`)];
|
|
112
|
+
components[selectors.join(', ')] = {
|
|
113
|
+
[`border-${side}-width`]: '0',
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return components;
|
|
118
|
+
}
|
|
@@ -111,9 +111,9 @@ declare const __VLS_component: import("vue").DefineComponent<import("vue").Extra
|
|
|
111
111
|
}>, {
|
|
112
112
|
size: "xs" | "sm" | "md" | "lg" | "xl";
|
|
113
113
|
options: NormalizedOption[];
|
|
114
|
-
loading: boolean;
|
|
115
114
|
selected: NormalizedOption | NormalizedOption[] | null | undefined;
|
|
116
115
|
keywords: string;
|
|
116
|
+
loading: boolean;
|
|
117
117
|
loadingBottom: boolean;
|
|
118
118
|
optionColor: (option: RawOption) => string;
|
|
119
119
|
optionIcon: (option: RawOption) => string;
|
|
@@ -250,10 +250,10 @@ declare const __VLS_component: import("vue").DefineComponent<import("vue").Extra
|
|
|
250
250
|
modelValue: string | number | null | undefined;
|
|
251
251
|
hasError: boolean;
|
|
252
252
|
visibleFocus: boolean;
|
|
253
|
-
primaryKey: string;
|
|
254
253
|
optionColor: (option: RawOption) => string;
|
|
255
254
|
optionIcon: (option: RawOption) => string;
|
|
256
255
|
optionSubtitle: (option: RawOption) => string;
|
|
256
|
+
primaryKey: string;
|
|
257
257
|
dropdownShow: "always" | "focus";
|
|
258
258
|
showModelValue: boolean;
|
|
259
259
|
focusOnMount: boolean;
|
|
@@ -82,10 +82,10 @@ declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, {
|
|
|
82
82
|
modelValue: string | number | null;
|
|
83
83
|
hasError: boolean;
|
|
84
84
|
visibleFocus: boolean;
|
|
85
|
-
primaryKey: string;
|
|
86
85
|
optionColor: (option: RawOption) => string;
|
|
87
86
|
optionIcon: (option: RawOption) => string;
|
|
88
87
|
optionSubtitle: (option: RawOption) => string;
|
|
88
|
+
primaryKey: string;
|
|
89
89
|
dropdownShow: "focus" | "always";
|
|
90
90
|
showModelValue: boolean;
|
|
91
91
|
focusOnMount: boolean;
|
|
@@ -164,9 +164,9 @@ declare const __VLS_component: import("vue").DefineComponent<import("vue").Extra
|
|
|
164
164
|
modelValue: string | number | (string | number)[] | null | undefined;
|
|
165
165
|
hasError: boolean;
|
|
166
166
|
max: number;
|
|
167
|
-
primaryKey: string;
|
|
168
167
|
optionColor: (option: RawOption) => string;
|
|
169
168
|
optionIcon: (option: RawOption) => string;
|
|
169
|
+
primaryKey: string;
|
|
170
170
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
171
171
|
declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
|
|
172
172
|
export default _default;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Ref } from 'vue';
|
|
2
|
+
/**
|
|
3
|
+
* Which Google Places API the address autocomplete uses.
|
|
4
|
+
* - `auto`: try Places API (New) first, fall back to the legacy Places API
|
|
5
|
+
* when the API key is not allowed to use the new one.
|
|
6
|
+
* - `new`: always use Places API (New).
|
|
7
|
+
* - `legacy`: always use the legacy Places API (google.maps.places.Autocomplete widget).
|
|
8
|
+
*/
|
|
9
|
+
export type GooglePlacesApi = 'auto' | 'new' | 'legacy';
|
|
10
|
+
export type ResolvedGooglePlacesApi = 'new' | 'legacy' | 'unavailable';
|
|
11
|
+
export interface PlaceSuggestion {
|
|
12
|
+
id: string;
|
|
13
|
+
mainText: string;
|
|
14
|
+
secondaryText: string;
|
|
15
|
+
prediction: google.maps.places.PlacePrediction;
|
|
16
|
+
}
|
|
17
|
+
/** Address component, normalized across the legacy and new Places APIs. */
|
|
18
|
+
export interface PlaceAddressComponent {
|
|
19
|
+
types: string[];
|
|
20
|
+
longText: string;
|
|
21
|
+
shortText: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function usePlacesAutocomplete(regionCode: Ref<string | null>, mode: GooglePlacesApi): {
|
|
24
|
+
api: import("vue").ComputedRef<ResolvedGooglePlacesApi | null>;
|
|
25
|
+
usesNewApi: import("vue").ComputedRef<boolean>;
|
|
26
|
+
usesLegacyApi: import("vue").ComputedRef<boolean>;
|
|
27
|
+
fetchSuggestions: (input: string) => Promise<PlaceSuggestion[] | null>;
|
|
28
|
+
fetchAddressComponents: (suggestion: PlaceSuggestion) => Promise<PlaceAddressComponent[]>;
|
|
29
|
+
cancelPendingSuggestions: () => void;
|
|
30
|
+
};
|
|
31
|
+
export declare function normalizeLegacyAddressComponents(components: google.maps.GeocoderAddressComponent[]): PlaceAddressComponent[];
|
package/dist/types/index.d.ts
CHANGED
|
@@ -230,6 +230,7 @@ import { Country } from './types/Country';
|
|
|
230
230
|
import { Region } from './types/Region';
|
|
231
231
|
import { Size } from './utils/sizes';
|
|
232
232
|
import { SettingsStorage } from './utils/storage';
|
|
233
|
+
import type { GooglePlacesApi } from './composables/placesAutocomplete';
|
|
233
234
|
export interface OptionProps {
|
|
234
235
|
size?: Size;
|
|
235
236
|
}
|
|
@@ -243,6 +244,7 @@ export interface Options {
|
|
|
243
244
|
countries?: Country[];
|
|
244
245
|
regions?: Region[];
|
|
245
246
|
props?: OptionProps;
|
|
247
|
+
googlePlacesApi?: GooglePlacesApi;
|
|
246
248
|
}
|
|
247
249
|
declare const config: {
|
|
248
250
|
http: AxiosInstance;
|
|
@@ -253,6 +255,7 @@ declare const config: {
|
|
|
253
255
|
settingsStorage: SettingsStorage;
|
|
254
256
|
countries: Country[];
|
|
255
257
|
regions: Region[];
|
|
258
|
+
googlePlacesApi: GooglePlacesApi;
|
|
256
259
|
props: OptionProps | undefined;
|
|
257
260
|
};
|
|
258
261
|
declare function install(app: App, options?: Options): void;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
declare const _default: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
export default _default;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sprintify-ui",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.28",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"generate-llm-txt": "node scripts/generate-llm-txt.js",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"@tiptap/starter-kit": "^3.13.0",
|
|
62
62
|
"@tiptap/vue-3": "^3.13.0",
|
|
63
63
|
"@types/croppie": "^2.6.4",
|
|
64
|
-
"@types/google.maps": "^3.
|
|
64
|
+
"@types/google.maps": "^3.58.1",
|
|
65
65
|
"@types/humanize-duration": "^3.27.4",
|
|
66
66
|
"@types/lodash": "^4.17.13",
|
|
67
67
|
"@types/luxon": "^3.4.2",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BaseAddressForm } from "."
|
|
2
|
+
import { config } from "..";
|
|
2
3
|
import ShowValue from "../../.storybook/components/ShowValue.vue";
|
|
3
4
|
import { sizes } from "../../.storybook/utils";
|
|
4
5
|
|
|
@@ -112,3 +113,27 @@ export const RestrictCountry = DefaultCountryAndRegionTemplate.bind({});
|
|
|
112
113
|
RestrictCountry.args = {
|
|
113
114
|
restrictCountry: true,
|
|
114
115
|
};
|
|
116
|
+
|
|
117
|
+
// Force a Google Places API instead of detecting it (`auto`).
|
|
118
|
+
// Requires both Places API (New) and Places API (Legacy) on the Storybook key.
|
|
119
|
+
const ForcedPlacesApiTemplate = (googlePlacesApi) => (args) => ({
|
|
120
|
+
components: { BaseAddressForm, ShowValue },
|
|
121
|
+
setup() {
|
|
122
|
+
config.googlePlacesApi = googlePlacesApi;
|
|
123
|
+
onUnmounted(() => {
|
|
124
|
+
config.googlePlacesApi = "auto";
|
|
125
|
+
});
|
|
126
|
+
const form = ref({ shipping_address: { country: "ca" } });
|
|
127
|
+
return { args, form };
|
|
128
|
+
},
|
|
129
|
+
template: `
|
|
130
|
+
<BaseAddressForm v-model="form.shipping_address" prefix="shipping_address" v-bind="args"></BaseAddressForm>
|
|
131
|
+
<ShowValue :value="form" />
|
|
132
|
+
`,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
export const ForceNew = ForcedPlacesApiTemplate("new").bind({});
|
|
136
|
+
ForceNew.args = {};
|
|
137
|
+
|
|
138
|
+
export const ForceLegacy = ForcedPlacesApiTemplate("legacy").bind({});
|
|
139
|
+
ForceLegacy.args = {};
|
|
@@ -19,10 +19,51 @@
|
|
|
19
19
|
prevent-submit
|
|
20
20
|
name="address_search_field"
|
|
21
21
|
:autocomplete="false"
|
|
22
|
-
@update:model-value="
|
|
22
|
+
@update:model-value="onAddress1Input"
|
|
23
|
+
@keydown="onAddress1Keydown"
|
|
23
24
|
/>
|
|
24
25
|
</BaseField>
|
|
25
26
|
|
|
27
|
+
<Teleport to="body">
|
|
28
|
+
<div
|
|
29
|
+
v-if="suggestionsOpened"
|
|
30
|
+
ref="suggestionsRef"
|
|
31
|
+
:style="{ ...floatingStyles, width: address1InputWidth + 'px' }"
|
|
32
|
+
class="fixed z-menu overflow-hidden border border-slate-300 bg-white shadow-2xl input-rounded"
|
|
33
|
+
>
|
|
34
|
+
<BaseAutocompleteDrawer
|
|
35
|
+
ref="suggestionsDrawerRef"
|
|
36
|
+
:options="suggestionOptions"
|
|
37
|
+
:size="size"
|
|
38
|
+
:keywords="normalizedModelValue.address_1 ?? ''"
|
|
39
|
+
tw-drawer="p-1"
|
|
40
|
+
@select="onSuggestionSelect"
|
|
41
|
+
>
|
|
42
|
+
<template #option="{ option, active }">
|
|
43
|
+
<div
|
|
44
|
+
class="rounded px-[0.75em] py-[0.4em] text-sm"
|
|
45
|
+
:class="active ? 'bg-slate-200' : 'bg-white'"
|
|
46
|
+
>
|
|
47
|
+
<p class="block font-semibold">
|
|
48
|
+
{{ option?.mainText }}
|
|
49
|
+
</p>
|
|
50
|
+
<small class="-mt-[0.075em] block text-xs text-slate-500">
|
|
51
|
+
{{ option?.secondaryText }}
|
|
52
|
+
</small>
|
|
53
|
+
</div>
|
|
54
|
+
</template>
|
|
55
|
+
<template #footer>
|
|
56
|
+
<div class="flex justify-end border-t border-slate-200 px-2.5 pb-1.5 pt-2.5">
|
|
57
|
+
<BaseGoogleMapsLogo
|
|
58
|
+
class="h-4"
|
|
59
|
+
translate="no"
|
|
60
|
+
/>
|
|
61
|
+
</div>
|
|
62
|
+
</template>
|
|
63
|
+
</BaseAutocompleteDrawer>
|
|
64
|
+
</div>
|
|
65
|
+
</Teleport>
|
|
66
|
+
|
|
26
67
|
<BaseField
|
|
27
68
|
:name="`${namePrefix}address_2`"
|
|
28
69
|
class="col-span-2"
|
|
@@ -123,7 +164,7 @@
|
|
|
123
164
|
|
|
124
165
|
<script lang="ts" setup>
|
|
125
166
|
import { ComputedRef } from 'vue';
|
|
126
|
-
import { cloneDeep, isArray } from 'lodash';
|
|
167
|
+
import { cloneDeep, debounce, isArray } from 'lodash';
|
|
127
168
|
import { Country } from '@/types/Country';
|
|
128
169
|
import { Region } from '@/types/Region';
|
|
129
170
|
import BaseField from './BaseField.vue';
|
|
@@ -134,6 +175,18 @@ import { t } from '@/i18n';
|
|
|
134
175
|
import { config } from '..';
|
|
135
176
|
import { Size } from '@/utils/sizes';
|
|
136
177
|
import { useInputSize } from '@/composables/inputSize';
|
|
178
|
+
import { useClickOutside } from '@/composables/clickOutside';
|
|
179
|
+
import {
|
|
180
|
+
normalizeLegacyAddressComponents,
|
|
181
|
+
PlaceAddressComponent,
|
|
182
|
+
PlaceSuggestion,
|
|
183
|
+
usePlacesAutocomplete,
|
|
184
|
+
} from '@/composables/placesAutocomplete';
|
|
185
|
+
import { NormalizedOption } from '@/types';
|
|
186
|
+
import BaseAutocompleteDrawer from './BaseAutocompleteDrawer.vue';
|
|
187
|
+
import BaseGoogleMapsLogo from '../svg/BaseGoogleMapsLogo.vue';
|
|
188
|
+
import { autoUpdate, flip, offset, useFloating } from '@floating-ui/vue';
|
|
189
|
+
import { useElementBounding } from '@vueuse/core';
|
|
137
190
|
|
|
138
191
|
const DEFAULT_COUNTRY = 'ca';
|
|
139
192
|
|
|
@@ -253,105 +306,250 @@ const address1Input = computed(
|
|
|
253
306
|
() => address1Ref.value?.$refs.input ?? null
|
|
254
307
|
) as ComputedRef<HTMLInputElement | null>;
|
|
255
308
|
|
|
309
|
+
const MIN_SEARCH_LENGTH = 3;
|
|
310
|
+
|
|
311
|
+
const placesRegionCode = computed((): string | null => {
|
|
312
|
+
if (props.restrictCountry && normalizedCountry.value) {
|
|
313
|
+
return normalizedCountry.value;
|
|
314
|
+
}
|
|
315
|
+
return null;
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const places = usePlacesAutocomplete(placesRegionCode, config.googlePlacesApi);
|
|
319
|
+
|
|
320
|
+
// Places API (New): suggestions are shown in our own dropdown
|
|
321
|
+
|
|
322
|
+
const suggestions = ref<PlaceSuggestion[]>([]);
|
|
323
|
+
const suggestionsRef = ref<HTMLElement | null>(null);
|
|
324
|
+
const suggestionsDrawerRef = ref<InstanceType<typeof BaseAutocompleteDrawer> | null>(null);
|
|
325
|
+
|
|
326
|
+
const suggestionsOpened = computed(() => suggestions.value.length > 0);
|
|
327
|
+
|
|
328
|
+
const suggestionOptions = computed((): NormalizedOption[] => {
|
|
329
|
+
return suggestions.value.map((suggestion) => ({
|
|
330
|
+
value: suggestion.id,
|
|
331
|
+
label: suggestion.mainText,
|
|
332
|
+
option: suggestion,
|
|
333
|
+
}));
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
function onAddress1Input(value: string) {
|
|
337
|
+
update('address_1', value);
|
|
338
|
+
|
|
339
|
+
if (places.usesNewApi.value) {
|
|
340
|
+
places.cancelPendingSuggestions();
|
|
341
|
+
searchSuggestions(value);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// The Google Maps script may have loaded after the form was mounted
|
|
345
|
+
if (places.usesLegacyApi.value && !legacyAutocomplete) {
|
|
346
|
+
attachLegacyAutocompleteAndReplay();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const searchSuggestions = debounce(async (value: string) => {
|
|
351
|
+
if (value.trim().length < MIN_SEARCH_LENGTH) {
|
|
352
|
+
closeSuggestions();
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const result = await places.fetchSuggestions(value);
|
|
357
|
+
|
|
358
|
+
if (result !== null) {
|
|
359
|
+
suggestions.value = result;
|
|
360
|
+
}
|
|
361
|
+
}, 300);
|
|
362
|
+
|
|
363
|
+
function closeSuggestions() {
|
|
364
|
+
searchSuggestions.cancel();
|
|
365
|
+
places.cancelPendingSuggestions();
|
|
366
|
+
suggestions.value = [];
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function onAddress1Keydown(event: KeyboardEvent) {
|
|
370
|
+
if (!suggestionsOpened.value) {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (event.key === 'Escape') {
|
|
375
|
+
// Only close the suggestions, not a dialog containing the form
|
|
376
|
+
event.preventDefault();
|
|
377
|
+
event.stopPropagation();
|
|
378
|
+
closeSuggestions();
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (event.key === 'Tab') {
|
|
383
|
+
closeSuggestions();
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (['ArrowDown', 'ArrowUp', 'Enter'].includes(event.key)) {
|
|
388
|
+
event.preventDefault();
|
|
389
|
+
suggestionsDrawerRef.value?.onKeydown(event);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async function onSuggestionSelect(option: NormalizedOption) {
|
|
394
|
+
closeSuggestions();
|
|
395
|
+
const components = await places.fetchAddressComponents(option.option as PlaceSuggestion);
|
|
396
|
+
fillAddress(components);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
useClickOutside(suggestionsRef, closeSuggestions, {
|
|
400
|
+
includes: () => [address1Input],
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
const address1InputRect = useElementBounding(address1Input);
|
|
404
|
+
const address1InputWidth = computed(() => address1InputRect.width.value);
|
|
405
|
+
|
|
406
|
+
const { floatingStyles } = useFloating(address1Input, suggestionsRef, {
|
|
407
|
+
placement: 'bottom-start',
|
|
408
|
+
middleware: [offset(4), flip()],
|
|
409
|
+
whileElementsMounted: autoUpdate,
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// Legacy Places API: Google's Autocomplete widget is attached to the input
|
|
413
|
+
|
|
256
414
|
// eslint-disable-next-line no-undef
|
|
257
|
-
let
|
|
415
|
+
let legacyAutocomplete = null as google.maps.places.Autocomplete | null;
|
|
258
416
|
|
|
259
417
|
onMounted(() => {
|
|
260
|
-
if (
|
|
261
|
-
|
|
418
|
+
if (places.usesLegacyApi.value) {
|
|
419
|
+
attachLegacyAutocomplete();
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
// In `auto` mode, the API is known after the first request
|
|
424
|
+
watch(places.api, (api) => {
|
|
425
|
+
if (api === 'legacy') {
|
|
426
|
+
closeSuggestions();
|
|
427
|
+
attachLegacyAutocompleteAndReplay();
|
|
428
|
+
}
|
|
429
|
+
if (api === 'unavailable') {
|
|
430
|
+
closeSuggestions();
|
|
262
431
|
}
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
let legacyReplayTimeout = null as ReturnType<typeof setTimeout> | null;
|
|
263
435
|
|
|
264
|
-
|
|
436
|
+
function attachLegacyAutocompleteAndReplay() {
|
|
437
|
+
if (!attachLegacyAutocomplete()) {
|
|
265
438
|
return;
|
|
266
439
|
}
|
|
267
440
|
|
|
268
|
-
|
|
441
|
+
// The widget only reacts to input typed after it was attached, so replay the
|
|
442
|
+
// current value to show suggestions right away. It finishes its setup after
|
|
443
|
+
// the current task: replaying synchronously leaves its dropdown hidden.
|
|
444
|
+
// Only the field being typed in: other forms on the page are left untouched.
|
|
445
|
+
legacyReplayTimeout = setTimeout(() => {
|
|
446
|
+
const input = address1Input.value;
|
|
447
|
+
if (input && document.activeElement === input) {
|
|
448
|
+
// The widget only shows its dropdown after it saw the field gain focus
|
|
449
|
+
input.dispatchEvent(new FocusEvent('focus'));
|
|
450
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
onBeforeUnmount(() => {
|
|
456
|
+
if (legacyReplayTimeout) {
|
|
457
|
+
clearTimeout(legacyReplayTimeout);
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
function attachLegacyAutocomplete(): boolean {
|
|
462
|
+
if (legacyAutocomplete || !address1Input.value || !window.google?.maps) {
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (!window.google.maps.places) {
|
|
467
|
+
console.warn('[BaseAddressForm] The Google Maps script must be loaded with `libraries=places`. Address autocomplete is disabled.');
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
legacyAutocomplete = new window.google.maps.places.Autocomplete(
|
|
269
472
|
address1Input.value
|
|
270
473
|
);
|
|
271
474
|
|
|
272
|
-
|
|
475
|
+
setLegacyAutocompleteOptions();
|
|
273
476
|
|
|
274
|
-
|
|
275
|
-
|
|
477
|
+
legacyAutocomplete.addListener('place_changed', () => {
|
|
478
|
+
const place = legacyAutocomplete?.getPlace();
|
|
479
|
+
if (!place?.address_components) {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
fillAddress(normalizeLegacyAddressComponents(place.address_components));
|
|
483
|
+
});
|
|
276
484
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
() => {
|
|
280
|
-
setAutocompleteOptions();
|
|
281
|
-
}
|
|
282
|
-
);
|
|
485
|
+
return true;
|
|
486
|
+
}
|
|
283
487
|
|
|
284
|
-
|
|
285
|
-
|
|
488
|
+
watch(placesRegionCode, () => {
|
|
489
|
+
setLegacyAutocompleteOptions();
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
function setLegacyAutocompleteOptions() {
|
|
493
|
+
if (!legacyAutocomplete) {
|
|
286
494
|
return;
|
|
287
495
|
}
|
|
288
496
|
|
|
289
|
-
|
|
497
|
+
// eslint-disable-next-line no-undef
|
|
498
|
+
const options: google.maps.places.AutocompleteOptions = {
|
|
290
499
|
fields: ['address_components'],
|
|
291
500
|
types: ['address'],
|
|
292
|
-
}
|
|
501
|
+
};
|
|
293
502
|
|
|
294
|
-
if (
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
};
|
|
299
|
-
}
|
|
503
|
+
if (placesRegionCode.value) {
|
|
504
|
+
options.componentRestrictions = {
|
|
505
|
+
country: placesRegionCode.value,
|
|
506
|
+
};
|
|
300
507
|
}
|
|
301
508
|
|
|
302
|
-
|
|
509
|
+
legacyAutocomplete.setOptions(options);
|
|
303
510
|
}
|
|
304
511
|
|
|
305
|
-
function fillAddress() {
|
|
306
|
-
if (
|
|
512
|
+
function fillAddress(components: PlaceAddressComponent[]) {
|
|
513
|
+
if (components.length === 0) {
|
|
307
514
|
return;
|
|
308
515
|
}
|
|
309
516
|
|
|
310
|
-
// Get the place details from the autocomplete object.
|
|
311
|
-
const place = autocomplete.getPlace();
|
|
312
517
|
let address1 = '';
|
|
313
518
|
let postcode = '';
|
|
314
519
|
let region = '';
|
|
315
520
|
|
|
316
521
|
const newForm = cloneDeep(props.modelValue ?? {});
|
|
317
522
|
|
|
318
|
-
if (!place.address_components) {
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
523
|
// Get each component of the address from the place details,
|
|
323
524
|
// and then fill-in the corresponding field on the form.
|
|
324
|
-
|
|
325
|
-
// which are documented at http://goo.gle/3l5i5Mr
|
|
326
|
-
// eslint-disable-next-line no-undef
|
|
327
|
-
for (const component of place.address_components as google.maps.GeocoderAddressComponent[]) {
|
|
525
|
+
for (const component of components) {
|
|
328
526
|
const componentType = component.types[0];
|
|
329
527
|
switch (componentType) {
|
|
330
528
|
case 'street_number': {
|
|
331
|
-
address1 = `${component.
|
|
529
|
+
address1 = `${component.longText} ${address1}`;
|
|
332
530
|
break;
|
|
333
531
|
}
|
|
334
532
|
case 'route': {
|
|
335
|
-
address1 += component.
|
|
533
|
+
address1 += component.longText;
|
|
336
534
|
break;
|
|
337
535
|
}
|
|
338
536
|
case 'postal_code': {
|
|
339
|
-
postcode = `${component.
|
|
537
|
+
postcode = `${component.longText}${postcode}`;
|
|
340
538
|
break;
|
|
341
539
|
}
|
|
342
540
|
case 'postal_code_suffix': {
|
|
343
|
-
postcode = `${postcode}-${component.
|
|
541
|
+
postcode = `${postcode}-${component.longText}`;
|
|
344
542
|
break;
|
|
345
543
|
}
|
|
346
544
|
case 'locality':
|
|
347
|
-
newForm.city = component.
|
|
545
|
+
newForm.city = component.longText;
|
|
348
546
|
break;
|
|
349
547
|
case 'administrative_area_level_1': {
|
|
350
|
-
region = component.
|
|
548
|
+
region = component.shortText.toLowerCase();
|
|
351
549
|
break;
|
|
352
550
|
}
|
|
353
551
|
case 'country':
|
|
354
|
-
newForm.country = component.
|
|
552
|
+
newForm.country = component.shortText.toLowerCase();
|
|
355
553
|
break;
|
|
356
554
|
}
|
|
357
555
|
}
|
|
@@ -361,11 +559,6 @@ function fillAddress() {
|
|
|
361
559
|
newForm.postal_code = postcode;
|
|
362
560
|
|
|
363
561
|
nextTick(() => {
|
|
364
|
-
// Force value change
|
|
365
|
-
/* if (address1Input.value) {
|
|
366
|
-
address1Input.value.value = address1;
|
|
367
|
-
} */
|
|
368
|
-
|
|
369
562
|
emit('update:model-value', newForm);
|
|
370
563
|
});
|
|
371
564
|
}
|
|
@@ -61,3 +61,60 @@ const Template = (args) => ({
|
|
|
61
61
|
|
|
62
62
|
export const Demo = Template.bind({});
|
|
63
63
|
Demo.args = {};
|
|
64
|
+
|
|
65
|
+
const tabulationRows = `
|
|
66
|
+
<thead>
|
|
67
|
+
<tr><th class="text-left">Name</th><th class="text-left">Age</th></tr>
|
|
68
|
+
</thead>
|
|
69
|
+
<tbody>
|
|
70
|
+
<tr><td>John Doe</td><td>35</td></tr>
|
|
71
|
+
<tr><td>Jane Doe</td><td>32</td></tr>
|
|
72
|
+
</tbody>
|
|
73
|
+
`;
|
|
74
|
+
|
|
75
|
+
const TabulationTemplate = (args) => ({
|
|
76
|
+
components: { BaseCard, BaseCardRow },
|
|
77
|
+
setup() {
|
|
78
|
+
return { args };
|
|
79
|
+
},
|
|
80
|
+
template: `
|
|
81
|
+
<div class="bg-slate-100 p-5 space-y-5">
|
|
82
|
+
<div>
|
|
83
|
+
<p class="mb-2 text-sm font-medium">Standalone</p>
|
|
84
|
+
<table class="tabulation w-full">${tabulationRows}</table>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<div>
|
|
88
|
+
<p class="mb-2 text-sm font-medium">In a BaseCard (double bottom border)</p>
|
|
89
|
+
<BaseCard>
|
|
90
|
+
<table class="tabulation w-full">${tabulationRows}</table>
|
|
91
|
+
</BaseCard>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<div>
|
|
95
|
+
<p class="mb-2 text-sm font-medium">In a BaseCard with tabulation-border-b-0</p>
|
|
96
|
+
<BaseCard>
|
|
97
|
+
<table class="tabulation tabulation-border-b-0 w-full">${tabulationRows}</table>
|
|
98
|
+
</BaseCard>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<div>
|
|
102
|
+
<p class="mb-2 text-sm font-medium">In a BaseCard with a footer (keep the bottom border)</p>
|
|
103
|
+
<BaseCard>
|
|
104
|
+
<table class="tabulation w-full">${tabulationRows}</table>
|
|
105
|
+
<BaseCardRow size="sm">Footer</BaseCardRow>
|
|
106
|
+
</BaseCard>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<div>
|
|
110
|
+
<p class="mb-2 text-sm font-medium">Grid in a BaseCard with tabulation-border-0</p>
|
|
111
|
+
<BaseCard clipped>
|
|
112
|
+
<table class="tabulation tabulation-grid tabulation-border-0 w-full">${tabulationRows}</table>
|
|
113
|
+
</BaseCard>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
`,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
export const WithTabulation = TabulationTemplate.bind({});
|
|
120
|
+
WithTabulation.args = {};
|