@osimart/create-storefront 0.0.0-stage → 1.0.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/README.md +80 -2
- package/THEME_DEV_API.md +80 -0
- package/app/app.vue +154 -0
- package/app/assets/css/osi-controls.css +824 -0
- package/app/components/SectionRenderer.vue +46 -0
- package/app/components/builder/BuilderOverlay.vue +582 -0
- package/app/components/checkout/ExtraFees.vue +329 -0
- package/app/components/checkout/PaymentHandoff.vue +388 -0
- package/app/components/common/Banner.vue +35 -0
- package/app/components/common/ConfirmDeleteModal.vue +117 -0
- package/app/components/common/DropdownSelect.vue +432 -0
- package/app/components/common/Loader.vue +32 -0
- package/app/components/common/OsiField.vue +218 -0
- package/app/components/common/PhoneNumInput.vue +434 -0
- package/app/composables/UseBodyScrollLock.js +122 -0
- package/app/composables/UseThemeGroup.js +575 -0
- package/app/composables/useAddToCart.js +87 -0
- package/app/composables/useAnalytics.js +15 -0
- package/app/composables/useAnnouncements.js +29 -0
- package/app/composables/useApiErrorToast.js +104 -0
- package/app/composables/useAuth.js +99 -0
- package/app/composables/useBanners.js +25 -0
- package/app/composables/useBuilderPreview.js +320 -0
- package/app/composables/useButtonStyle.js +107 -0
- package/app/composables/useCarousel.js +160 -0
- package/app/composables/useCart.js +109 -0
- package/app/composables/useCheckout.js +76 -0
- package/app/composables/useClassificationItems.js +269 -0
- package/app/composables/useClassificationPage.js +545 -0
- package/app/composables/useClassificationUrl.js +63 -0
- package/app/composables/useColorScheme.js +44 -0
- package/app/composables/useContactForm.js +53 -0
- package/app/composables/useContentTree.js +770 -0
- package/app/composables/useDragScroll.js +73 -0
- package/app/composables/useFooterSocials.js +107 -0
- package/app/composables/useInputStyle.js +45 -0
- package/app/composables/useLayoutShell.js +114 -0
- package/app/composables/useLazyVideo.js +36 -0
- package/app/composables/useMediaTypes.js +66 -0
- package/app/composables/useMediaUrl.js +71 -0
- package/app/composables/useMenu.js +100 -0
- package/app/composables/useNavStyle.js +219 -0
- package/app/composables/usePlaceholders.js +294 -0
- package/app/composables/usePolicies.js +26 -0
- package/app/composables/useProductCardMedia.js +135 -0
- package/app/composables/useProductCardStyle.js +132 -0
- package/app/composables/useProductDetail.js +982 -0
- package/app/composables/useProductGallery.js +166 -0
- package/app/composables/useProductItems.js +270 -0
- package/app/composables/useProductUrl.js +60 -0
- package/app/composables/useProductVariants.js +210 -0
- package/app/composables/useQuickShop.js +115 -0
- package/app/composables/useRelatedProducts.js +186 -0
- package/app/composables/useRenderContext.js +45 -0
- package/app/composables/useScopedCss.js +106 -0
- package/app/composables/useSearch.js +35 -0
- package/app/composables/useSectionData.js +462 -0
- package/app/composables/useSectionShell.js +113 -0
- package/app/composables/useSidebar.js +69 -0
- package/app/composables/useStoreData.js +57 -0
- package/app/composables/useStoreInit.js +52 -0
- package/app/composables/useStorefront.js +239 -0
- package/app/composables/useT.js +21 -0
- package/app/composables/useTextStyles.js +84 -0
- package/app/composables/useThemeControls.js +34 -0
- package/app/composables/useThemeSeo.js +91 -0
- package/app/composables/useThemeTokens.js +171 -0
- package/app/composables/useWishlist.js +66 -0
- package/app/logic/components/AnnouncementBar.js +118 -0
- package/app/logic/components/CardGrid.js +125 -0
- package/app/logic/components/CardList.js +98 -0
- package/app/logic/components/CartDrawer.js +273 -0
- package/app/logic/components/CartItem.js +31 -0
- package/app/logic/components/ClassificationBento.js +6 -0
- package/app/logic/components/ClassificationCard.js +6 -0
- package/app/logic/components/ClassificationCarousel.js +115 -0
- package/app/logic/components/ClassificationEditorial.js +41 -0
- package/app/logic/components/ClassificationFeatured.js +57 -0
- package/app/logic/components/ClassificationGrid.js +6 -0
- package/app/logic/components/ClassificationNav.js +61 -0
- package/app/logic/components/ClassificationPart.js +72 -0
- package/app/logic/components/FooterBlock.js +91 -0
- package/app/logic/components/FooterSocials.js +60 -0
- package/app/logic/components/HeaderDropdownList.js +29 -0
- package/app/logic/components/HeaderIcon.js +68 -0
- package/app/logic/components/HeaderLogo.js +39 -0
- package/app/logic/components/HeaderNav.js +268 -0
- package/app/logic/components/MobileMenu.js +280 -0
- package/app/logic/components/ProductBuyButtons.js +66 -0
- package/app/logic/components/ProductCard.js +160 -0
- package/app/logic/components/ProductFilters.js +37 -0
- package/app/logic/components/ProductGallery.js +19 -0
- package/app/logic/components/ProductInfoBlock.js +411 -0
- package/app/logic/components/ProductLightbox.js +12 -0
- package/app/logic/components/ProductQuantity.js +31 -0
- package/app/logic/components/ProductVariantPicker.js +125 -0
- package/app/logic/components/QuickBuy.js +194 -0
- package/app/logic/components/SearchOverlay.js +41 -0
- package/app/logic/components/SidebarMenuItem.js +39 -0
- package/app/logic/components/SiteFooter.js +476 -0
- package/app/logic/components/SiteHeader.js +266 -0
- package/app/logic/components/WhatsAppButton.js +75 -0
- package/app/logic/layouts/default.js +66 -0
- package/app/logic/sections/AnnouncementBarSection.js +6 -0
- package/app/logic/sections/BlockCarousel.js +103 -0
- package/app/logic/sections/ClassificationListSection.js +40 -0
- package/app/logic/sections/ContactFormSection.js +294 -0
- package/app/logic/sections/ContactInfoSection.js +419 -0
- package/app/logic/sections/ContentNode.js +189 -0
- package/app/logic/sections/CountdownSection.js +254 -0
- package/app/logic/sections/CustomHtmlSection.js +51 -0
- package/app/logic/sections/DividerSection.js +31 -0
- package/app/logic/sections/FaqSection.js +53 -0
- package/app/logic/sections/FeatureListSection.js +6 -0
- package/app/logic/sections/FeaturedProductSection.js +40 -0
- package/app/logic/sections/FooterSection.js +16 -0
- package/app/logic/sections/FooterUtilitiesSection.js +24 -0
- package/app/logic/sections/GallerySection.js +232 -0
- package/app/logic/sections/HeaderSection.js +24 -0
- package/app/logic/sections/HeroSection.js +24 -0
- package/app/logic/sections/LogoListSection.js +6 -0
- package/app/logic/sections/MainClassificationSection.js +473 -0
- package/app/logic/sections/MainProductSection.js +211 -0
- package/app/logic/sections/MainSearchSection.js +6 -0
- package/app/logic/sections/MapSection.js +92 -0
- package/app/logic/sections/MediaContentSection.js +38 -0
- package/app/logic/sections/MediaWithTextSection.js +238 -0
- package/app/logic/sections/NewsletterSection.js +126 -0
- package/app/logic/sections/ProductHighlightSection.js +201 -0
- package/app/logic/sections/ProductListSection.js +45 -0
- package/app/logic/sections/ProductRecommendationsSection.js +113 -0
- package/app/logic/sections/RichTextSection.js +6 -0
- package/app/logic/sections/ScrollingTextSection.js +69 -0
- package/app/logic/sections/SearchHeaderSection.js +22 -0
- package/app/logic/sections/SlideshowSection.js +118 -0
- package/app/logic/sections/SpacerSection.js +37 -0
- package/app/logic/sections/SplitShowcaseSection.js +6 -0
- package/app/logic/sections/TestimonialListSection.js +6 -0
- package/app/logic/sections/VideoSection.js +235 -0
- package/app/pages/[category]/[product].vue +196 -0
- package/app/pages/__builder-preview.vue +38 -0
- package/app/pages/categories/[slug].vue +208 -0
- package/app/pages/checkout.vue +1972 -0
- package/app/pages/forgot-password.vue +115 -0
- package/app/pages/index.vue +64 -0
- package/app/pages/login.vue +585 -0
- package/app/pages/order-detail.vue +442 -0
- package/app/pages/pages/[handle].vue +58 -0
- package/app/pages/privacy-policy.vue +46 -0
- package/app/pages/resetpassword.vue +138 -0
- package/app/pages/resetverificationcode.vue +163 -0
- package/app/pages/result.vue +203 -0
- package/app/pages/return-policy.vue +50 -0
- package/app/pages/search.vue +100 -0
- package/app/pages/shipping-policy.vue +46 -0
- package/app/pages/user-profile/components/AddAddressModal.vue +452 -0
- package/app/pages/user-profile/components/Whishlist.vue +137 -0
- package/app/pages/user-profile/components/left-bar.vue +233 -0
- package/app/pages/user-profile/components/orders.vue +162 -0
- package/app/pages/user-profile/components/profile-details.vue +566 -0
- package/app/pages/user-profile/components/settings.vue +194 -0
- package/app/pages/user-profile/index.vue +252 -0
- package/app/pages/verificationcode.vue +188 -0
- package/app/plugins/analytics.client.js +99 -0
- package/app/plugins/geo.ts +24 -0
- package/app/plugins/reveal.client.js +53 -0
- package/app/plugins/toast.js +10 -0
- package/app/plugins/vue-query.ts +5 -0
- package/app/services/ApiServices.js +74 -0
- package/app/services/AuthBus.js +162 -0
- package/app/services/Authentication/AuthenticationService.js +133 -0
- package/app/services/Authentication/TokenService.js +62 -0
- package/app/services/CartServices.js +91 -0
- package/app/services/CartToken.js +26 -0
- package/app/services/CategoryServices.js +54 -0
- package/app/services/CheckoutServices.js +113 -0
- package/app/services/HomeServices.js +133 -0
- package/app/services/NewsletterServices.js +29 -0
- package/app/services/PaymentServices.js +71 -0
- package/app/services/PoliciesServices.js +16 -0
- package/app/services/ProductServices.js +79 -0
- package/app/services/SearchServices.js +18 -0
- package/app/services/UserServices.js +105 -0
- package/app/services/UtilsServices.js +46 -0
- package/app/services/WishlistServices.js +51 -0
- package/app/services/storefront.service.js +57 -0
- package/app/stores/userStore.js +13 -0
- package/app/theme/categoryTree.js +68 -0
- package/app/theme/controls.js +88 -0
- package/app/theme/registry.js +56 -0
- package/app/utils/analytics.js +864 -0
- package/app/utils/device.js +50 -0
- package/app/utils/eventBus.js +24 -0
- package/app/utils/icon-presets.js +24 -0
- package/app/utils/productOffers.js +118 -0
- package/app/utils/requestCache.js +36 -0
- package/app/utils/scrollToError.js +59 -0
- package/app/utils/toPlainError.js +30 -0
- package/bin/osimart-theme.mjs +259 -0
- package/nuxt/osi-sections.ts +56 -0
- package/nuxt.config.ts +60 -0
- package/package.json +43 -3
- package/public/css/input.css +29 -0
- package/public/css/output.css +2716 -0
- package/public/favicon-32x32.png +0 -0
- package/public/robots.txt +2 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="relative flex justify-center" ref="rootRef"
|
|
3
|
+
:class="fullWidth ? 'w-full' : 'w-fit'"
|
|
4
|
+
:data-field="field"
|
|
5
|
+
>
|
|
6
|
+
<!-- Trigger -->
|
|
7
|
+
<slot name="trigger" :toggle="toggleDropdown" :disabled="disabled" :open="open">
|
|
8
|
+
<button
|
|
9
|
+
type="button"
|
|
10
|
+
ref="triggerBtnRef"
|
|
11
|
+
class="border border-gray-300 rounded-md min-w-[170px] shadow-sm text-left focus:outline-none focus-visible:outline-none [-webkit-tap-highlight-color:transparent] flex items-center justify-between text-input"
|
|
12
|
+
:class="[
|
|
13
|
+
popupDropdown ? 'p-3' : 'px-4 py-1',
|
|
14
|
+
disabled ? 'bg-gray-100 cursor-not-allowed' : '',
|
|
15
|
+
error ? 'border-red-500' : 'border-gray-300',
|
|
16
|
+
autoWidth ? 'w-fit max-w-full' : 'w-full',
|
|
17
|
+
notRequired ? 'text-gray-400 border !border-gray-200' : ''
|
|
18
|
+
]"
|
|
19
|
+
:disabled="disabled"
|
|
20
|
+
:aria-disabled="disabled ? 'true' : 'false'"
|
|
21
|
+
@click="!disabled && (open = !open)"
|
|
22
|
+
>
|
|
23
|
+
<div class="flex items-center min-h-[24px] text-sm" :class="autoWidth ? 'flex-nowrap whitespace-nowrap text-fifthosiblue gap-1' : 'flex-wrap gap-1'">
|
|
24
|
+
<template v-if="disabled && disabledMessage">
|
|
25
|
+
<span class="text-fifthosiblue">{{ disabledMessage }}</span>
|
|
26
|
+
</template>
|
|
27
|
+
<template v-else-if="!multi && !selectedSingleLabel">
|
|
28
|
+
<span class='text-fifthosiblue'>{{ placeholder }}</span>
|
|
29
|
+
</template>
|
|
30
|
+
<template v-else-if="multi && (!modelValue || modelValue.length === 0)">
|
|
31
|
+
<span class="text-fifthosiblue">{{ placeholder }}</span>
|
|
32
|
+
</template>
|
|
33
|
+
|
|
34
|
+
<!-- Multi selected tags -->
|
|
35
|
+
<template v-if="multi">
|
|
36
|
+
<span
|
|
37
|
+
v-for="opt in selectedTags"
|
|
38
|
+
:key="opt.value"
|
|
39
|
+
class="bg-osiblue text-white rounded px-2 py-0.5 flex items-center gap-1 text-xs"
|
|
40
|
+
>
|
|
41
|
+
<img
|
|
42
|
+
v-if="opt.flag || opt.image"
|
|
43
|
+
:src="opt.flag || opt.image"
|
|
44
|
+
alt=""
|
|
45
|
+
class="w-5 h-4 object-cover shrink-0 rounded-sm"
|
|
46
|
+
/>
|
|
47
|
+
<div class="flex flex-col">
|
|
48
|
+
<span>{{ opt.label }}</span>
|
|
49
|
+
<button
|
|
50
|
+
v-if="showPlacement"
|
|
51
|
+
type="button"
|
|
52
|
+
@click.stop="$emit('placementClick', opt.value)"
|
|
53
|
+
class="text-[10px] text-blue-200 dark:text-blue-500 hover:text-white underline text-left"
|
|
54
|
+
>
|
|
55
|
+
Position: {{ placements[opt.value] || 1 }} (click to set)
|
|
56
|
+
</button>
|
|
57
|
+
</div>
|
|
58
|
+
<button type="button" @click.stop="removeOption(opt.value)" class="ml-1">
|
|
59
|
+
×
|
|
60
|
+
</button>
|
|
61
|
+
</span>
|
|
62
|
+
</template>
|
|
63
|
+
|
|
64
|
+
<!-- Single selection -->
|
|
65
|
+
<template v-else-if="selectedSingleLabel">
|
|
66
|
+
<img
|
|
67
|
+
v-if="selectedSingleOption?.flag || selectedSingleOption?.image"
|
|
68
|
+
:src="selectedSingleOption?.flag || selectedSingleOption?.image"
|
|
69
|
+
alt=""
|
|
70
|
+
class="w-5 h-4 object-cover shrink-0 rounded-sm mr-1"
|
|
71
|
+
/>
|
|
72
|
+
<span class="whitespace-nowrap">{{ selectedSingleLabel }}</span>
|
|
73
|
+
</template>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<svg
|
|
77
|
+
class="w-4 h-4 shrink-0"
|
|
78
|
+
:class="[autoWidth ? 'ml-2 text-fifthosiblue':'text-osiblue']"
|
|
79
|
+
viewBox="0 0 24 24"
|
|
80
|
+
fill="none"
|
|
81
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
82
|
+
>
|
|
83
|
+
<path
|
|
84
|
+
d="M5.70711 16.1359C5.31659 16.5264 5.31659 17.1596 5.70711 17.5501L10.5993 22.4375C11.3805 23.2179 12.6463 23.2176 13.4271 22.4369L18.3174 17.5465C18.708 17.156 18.708 16.5228 18.3174 16.1323C17.9269 15.7418 17.2937 15.7418 16.9032 16.1323L12.7176 20.3179C12.3271 20.7085 11.6939 20.7085 11.3034 20.3179L7.12132 16.1359C6.7308 15.7454 6.09763 15.7454 5.70711 16.1359Z"
|
|
85
|
+
fill="currentColor"
|
|
86
|
+
></path>
|
|
87
|
+
<path
|
|
88
|
+
d="M18.3174 7.88675C18.708 7.49623 18.708 6.86307 18.3174 6.47254L13.4252 1.58509C12.644 0.804698 11.3783 0.805008 10.5975 1.58579L5.70711 6.47615C5.31658 6.86667 5.31658 7.49984 5.70711 7.89036C6.09763 8.28089 6.7308 8.28089 7.12132 7.89036L11.307 3.70472C11.6975 3.31419 12.3307 3.31419 12.7212 3.70472L16.9032 7.88675C17.2937 8.27728 17.9269 8.27728 18.3174 7.88675Z"
|
|
89
|
+
fill="currentColor"
|
|
90
|
+
></path>
|
|
91
|
+
</svg>
|
|
92
|
+
</button>
|
|
93
|
+
</slot>
|
|
94
|
+
|
|
95
|
+
<!-- Dropdown -->
|
|
96
|
+
<Teleport defer to="body">
|
|
97
|
+
<div
|
|
98
|
+
v-if="open && !disabled"
|
|
99
|
+
ref="menuRef"
|
|
100
|
+
:style="[floatingStyles, menuWidthStyle, { zIndex: dropdownZIndex }]"
|
|
101
|
+
class="absolute font-poppins bg-white border border-gray-300 rounded-md shadow-lg max-h-60 overflow-auto dropdown-select-popup"
|
|
102
|
+
:class="dropdownWidth ? '' : 'w-max max-w-xs'"
|
|
103
|
+
@scroll.passive="onScroll"
|
|
104
|
+
>
|
|
105
|
+
<!-- Search -->
|
|
106
|
+
<div v-if="searchable" class="p-2">
|
|
107
|
+
<input
|
|
108
|
+
v-model="searchQuery"
|
|
109
|
+
class="bg-input w-full border px-2 py-1 rounded text-sm focus:outline-none"
|
|
110
|
+
placeholder="Search..."
|
|
111
|
+
/>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<ul v-if="filteredOptions.length > 0">
|
|
115
|
+
<li
|
|
116
|
+
v-for="opt in filteredOptions"
|
|
117
|
+
:key="opt.value"
|
|
118
|
+
:style="{ color: opt.color }"
|
|
119
|
+
class="px-4 py-2 flex justify-between items-center hover:bg-gray-100 cursor-pointer"
|
|
120
|
+
@click="toggleOption(opt.value)"
|
|
121
|
+
>
|
|
122
|
+
<div class="flex items-center gap-2 min-w-0">
|
|
123
|
+
<img
|
|
124
|
+
v-if="opt.image || opt.flag"
|
|
125
|
+
:src="opt.image || opt.flag"
|
|
126
|
+
alt=""
|
|
127
|
+
class="w-5 h-4 object-cover shrink-0 rounded-sm"
|
|
128
|
+
/>
|
|
129
|
+
<span class="truncate block" :class="smallText ? 'text-[10px]' : 'text-sm'">{{ opt.label }}</span>
|
|
130
|
+
</div>
|
|
131
|
+
<svg
|
|
132
|
+
v-if="multi ? (modelValue || []).includes(opt.value) : modelValue === opt.value"
|
|
133
|
+
class="w-5 h-5 text-osiblue shrink-0"
|
|
134
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
135
|
+
fill="none"
|
|
136
|
+
viewBox="0 0 24 24"
|
|
137
|
+
stroke="currentColor"
|
|
138
|
+
>
|
|
139
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
|
140
|
+
</svg>
|
|
141
|
+
</li>
|
|
142
|
+
</ul>
|
|
143
|
+
|
|
144
|
+
<div v-else-if="!loading" class="px-4 py-2 text-fifthosiblue text-center text-sm">
|
|
145
|
+
No results
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<div v-if="loading" class="flex justify-center py-2">
|
|
149
|
+
<Loader size="20px" />
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
</Teleport>
|
|
153
|
+
</div>
|
|
154
|
+
</template>
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
<script setup>
|
|
158
|
+
import { ref, watch, computed, onMounted, onBeforeUnmount, onUnmounted,inject } from "vue";
|
|
159
|
+
import { useFloating, offset, flip, shift, autoUpdate } from "@floating-ui/vue";
|
|
160
|
+
import Loader from "./Loader.vue";
|
|
161
|
+
import { eventBus } from "../../utils/eventBus";
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
const props = defineProps({
|
|
165
|
+
modelValue: { type: [Array, String, Number, null], default: null },
|
|
166
|
+
options: { type: Array, default: () => [] },
|
|
167
|
+
multi: { type: Boolean, default: false },
|
|
168
|
+
searchable: { type: Boolean, default: false },
|
|
169
|
+
placeholder: { type: String, default: "Select..." },
|
|
170
|
+
loading: { type: Boolean, default: false },
|
|
171
|
+
hasMore: { type: Boolean, default: false },
|
|
172
|
+
searchDebounce: { type: Number, default: 400 },
|
|
173
|
+
allowDeselect: { type: Boolean, default: false },
|
|
174
|
+
disabled: { type: Boolean, default: false },
|
|
175
|
+
disabledMessage: { type: String, default: "" },
|
|
176
|
+
popupDropdown: { type: Boolean, default: false },
|
|
177
|
+
showPlacement: { type: Boolean, default: false },
|
|
178
|
+
placements: { type: Object, default: () => ({}) },
|
|
179
|
+
error: { type: Boolean, default: false },
|
|
180
|
+
field: { type: String, default: null },
|
|
181
|
+
smallText: { type: Boolean, default: false },
|
|
182
|
+
fullWidth: { type: Boolean, default: true },
|
|
183
|
+
type: { type: String, default: null },
|
|
184
|
+
localSearch: { type: Boolean, default: false },
|
|
185
|
+
// When true, indicates this field is optional. The trigger's text and
|
|
186
|
+
// icon are visually de-emphasized (dimmed) while no value is selected,
|
|
187
|
+
// instead of looking like a normal "active/required" input.
|
|
188
|
+
notRequired: { type: Boolean, default: false },
|
|
189
|
+
// When true, the trigger button shrinks to fit its content (the selected
|
|
190
|
+
// option / placeholder) instead of stretching to fullWidth, while the
|
|
191
|
+
// dropdown menu keeps its own independent (wider) sizing. Animates smoothly.
|
|
192
|
+
autoWidth: { type: Boolean, default: false },
|
|
193
|
+
// Explicit width for the dropdown menu (any valid CSS width, e.g. '320px',
|
|
194
|
+
// '24rem', '100%'). Overrides the default w-max/max-w-xs/minWidth-match behavior.
|
|
195
|
+
dropdownWidth: { type: String, default: null },
|
|
196
|
+
});
|
|
197
|
+
const parentModalZIndex = inject("modalZIndex", null);
|
|
198
|
+
const dropdownZIndex = computed(() =>
|
|
199
|
+
parentModalZIndex ? parentModalZIndex + 1 : 50
|
|
200
|
+
);
|
|
201
|
+
const emit = defineEmits(["update:modelValue", "loadMore", "search", "placementClick", "open", "close"]);
|
|
202
|
+
|
|
203
|
+
const open = ref(false);
|
|
204
|
+
function toggleDropdown() {
|
|
205
|
+
if (!props.disabled) open.value = !open.value;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const searchQuery = ref("");
|
|
209
|
+
const menuRef = ref(null);
|
|
210
|
+
const rootRef = ref(null);
|
|
211
|
+
const triggerBtnRef = ref(null);
|
|
212
|
+
const triggerWidth = ref(0);
|
|
213
|
+
|
|
214
|
+
const menuWidthStyle = computed(() => {
|
|
215
|
+
if (props.dropdownWidth) return { width: props.dropdownWidth };
|
|
216
|
+
if (!props.autoWidth) return { minWidth: triggerWidth.value + 'px' };
|
|
217
|
+
return {};
|
|
218
|
+
});
|
|
219
|
+
let searchTimeoutId = null;
|
|
220
|
+
|
|
221
|
+
const { floatingStyles } = useFloating(rootRef, menuRef, {
|
|
222
|
+
placement: 'bottom-start',
|
|
223
|
+
middleware: [offset(4), flip(), shift({ padding: 8 })],
|
|
224
|
+
whileElementsMounted: autoUpdate,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Cache labels for any option value we encounter so selected tags persist
|
|
228
|
+
// even when the current options list (e.g., search results) doesn't include them.
|
|
229
|
+
const labelCache = ref(new Map());
|
|
230
|
+
|
|
231
|
+
function getOptionsArray() {
|
|
232
|
+
return Array.isArray(props.options) ? props.options : [];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function syncLabelCacheFromOptions() {
|
|
236
|
+
const opts = getOptionsArray();
|
|
237
|
+
for (const o of opts) {
|
|
238
|
+
if (o && o.value !== undefined && o.label !== undefined) {
|
|
239
|
+
labelCache.value.set(o.value, o.label);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
watch(
|
|
245
|
+
() => props.options,
|
|
246
|
+
() => {
|
|
247
|
+
syncLabelCacheFromOptions();
|
|
248
|
+
},
|
|
249
|
+
{ immediate: true }
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
watch(
|
|
253
|
+
() => props.modelValue,
|
|
254
|
+
(val) => {
|
|
255
|
+
if (props.multi) {
|
|
256
|
+
const arr = Array.isArray(val) ? val : [];
|
|
257
|
+
for (const v of arr) {
|
|
258
|
+
if (!labelCache.value.has(v)) {
|
|
259
|
+
const found = getOptionsArray().find((o) => o.value === v);
|
|
260
|
+
labelCache.value.set(v, found ? found.label : String(v));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
} else if (val !== null && val !== undefined) {
|
|
264
|
+
if (!labelCache.value.has(val)) {
|
|
265
|
+
const found = getOptionsArray().find((o) => o.value === val);
|
|
266
|
+
labelCache.value.set(val, found ? found.label : String(val));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
{ immediate: true }
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
// For single selection: get the full option object to access flag/image
|
|
274
|
+
const selectedSingleOption = computed(() => {
|
|
275
|
+
if (props.multi || props.modelValue === null || props.modelValue === undefined) return null;
|
|
276
|
+
return getOptionsArray().find((o) => o.value === props.modelValue) ?? null;
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
const selectedTags = computed(() => {
|
|
280
|
+
if (!props.multi) return [];
|
|
281
|
+
const values = Array.isArray(props.modelValue) ? props.modelValue : [];
|
|
282
|
+
return values.map((v) => {
|
|
283
|
+
const opt = getOptionsArray().find((o) => o.value === v);
|
|
284
|
+
return {
|
|
285
|
+
value: v,
|
|
286
|
+
label: labelCache.value.get(v) || String(v),
|
|
287
|
+
flag: opt?.flag || opt?.image || null,
|
|
288
|
+
};
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const selectedSingleLabel = computed(() => {
|
|
293
|
+
if (props.multi) return "";
|
|
294
|
+
if (props.modelValue === null || props.modelValue === undefined) return "";
|
|
295
|
+
return labelCache.value.get(props.modelValue) || String(props.modelValue);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// Whether the field currently has a meaningful selection (used by notRequired styling)
|
|
299
|
+
const hasValue = computed(() => {
|
|
300
|
+
if (props.multi) {
|
|
301
|
+
return Array.isArray(props.modelValue) && props.modelValue.length > 0;
|
|
302
|
+
}
|
|
303
|
+
return props.modelValue !== null && props.modelValue !== undefined && props.modelValue !== "";
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
const filteredOptions = computed(() => {
|
|
307
|
+
const opts = getOptionsArray();
|
|
308
|
+
if (!props.localSearch || !searchQuery.value || !searchQuery.value.trim()) {
|
|
309
|
+
return opts;
|
|
310
|
+
}
|
|
311
|
+
const q = searchQuery.value.trim().toLowerCase();
|
|
312
|
+
return opts.filter(opt => {
|
|
313
|
+
const label = (opt.label || '').toString().toLowerCase();
|
|
314
|
+
const value = (opt.value || '').toString().toLowerCase();
|
|
315
|
+
return label.includes(q) || value.includes(q);
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
function toggleOption(value) {
|
|
320
|
+
const found = getOptionsArray().find((o) => o.value === value);
|
|
321
|
+
if (found) {
|
|
322
|
+
labelCache.value.set(value, found.label);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (props.multi) {
|
|
326
|
+
const newVal = [...(props.modelValue || [])];
|
|
327
|
+
const idx = newVal.indexOf(value);
|
|
328
|
+
if (idx > -1) newVal.splice(idx, 1);
|
|
329
|
+
else newVal.push(value);
|
|
330
|
+
emit("update:modelValue", newVal);
|
|
331
|
+
} else {
|
|
332
|
+
if (props.allowDeselect && props.modelValue === value) {
|
|
333
|
+
emit("update:modelValue", null);
|
|
334
|
+
} else {
|
|
335
|
+
emit("update:modelValue", value);
|
|
336
|
+
open.value = false;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function removeOption(value) {
|
|
342
|
+
if (!props.multi) return;
|
|
343
|
+
const newVal = [...(props.modelValue || [])].filter((v) => v !== value);
|
|
344
|
+
emit("update:modelValue", newVal);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const isFetchingMore = ref(false);
|
|
348
|
+
|
|
349
|
+
function onScroll(e) {
|
|
350
|
+
const el = e.target;
|
|
351
|
+
if (
|
|
352
|
+
el.scrollTop + el.clientHeight >= el.scrollHeight - 5 &&
|
|
353
|
+
props.hasMore &&
|
|
354
|
+
!props.loading &&
|
|
355
|
+
!isFetchingMore.value
|
|
356
|
+
) {
|
|
357
|
+
isFetchingMore.value = true;
|
|
358
|
+
emit("loadMore");
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
watch(() => props.loading, (val) => {
|
|
363
|
+
if (!val) {
|
|
364
|
+
isFetchingMore.value = false;
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
watch(searchQuery, (q) => {
|
|
369
|
+
if (!props.searchable) return;
|
|
370
|
+
if (searchTimeoutId) clearTimeout(searchTimeoutId);
|
|
371
|
+
searchTimeoutId = setTimeout(() => {
|
|
372
|
+
emit("search", q);
|
|
373
|
+
}, props.searchDebounce);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
onUnmounted(() => {
|
|
377
|
+
if (searchTimeoutId) clearTimeout(searchTimeoutId);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// Close dropdown if it becomes disabled while open
|
|
381
|
+
watch(
|
|
382
|
+
() => props.disabled,
|
|
383
|
+
(isDisabled) => {
|
|
384
|
+
if (isDisabled) open.value = false;
|
|
385
|
+
}
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
// --- Global coordination & outside-click closing ---
|
|
389
|
+
const instanceId = Symbol("dropdown-instance");
|
|
390
|
+
|
|
391
|
+
function onGlobalDropdownOpened(id) {
|
|
392
|
+
if (id !== instanceId) {
|
|
393
|
+
open.value = false;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function handleDocumentClick(e) {
|
|
398
|
+
const root = rootRef.value;
|
|
399
|
+
const menu = menuRef.value;
|
|
400
|
+
if (
|
|
401
|
+
(root && root.contains(e.target)) ||
|
|
402
|
+
(menu && menu.contains(e.target))
|
|
403
|
+
) {
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
open.value = false;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
watch(open, (isOpen) => {
|
|
410
|
+
if (isOpen) {
|
|
411
|
+
eventBus.emit("dropdown:opened", instanceId);
|
|
412
|
+
triggerWidth.value = rootRef.value ? rootRef.value.offsetWidth : 0;
|
|
413
|
+
document.addEventListener("mousedown", handleDocumentClick, true);
|
|
414
|
+
emit("open");
|
|
415
|
+
} else {
|
|
416
|
+
document.removeEventListener("mousedown", handleDocumentClick, true);
|
|
417
|
+
emit("close");
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
onMounted(() => {
|
|
422
|
+
eventBus.on("dropdown:opened", onGlobalDropdownOpened);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
onBeforeUnmount(() => {
|
|
426
|
+
document.removeEventListener("mousedown", handleDocumentClick, true);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
onUnmounted(() => {
|
|
430
|
+
eventBus.off("dropdown:opened", onGlobalDropdownOpened);
|
|
431
|
+
});
|
|
432
|
+
</script>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex items-center justify-center">
|
|
3
|
+
<div
|
|
4
|
+
class="spinner border-solid border-osiblue border-t-white dark:border-white dark:border-t-osiblue rounded-full animate-spin"
|
|
5
|
+
:style="{
|
|
6
|
+
'--spinner-size': size,
|
|
7
|
+
'--spinner-border': border
|
|
8
|
+
}"
|
|
9
|
+
></div>
|
|
10
|
+
</div>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<script setup>
|
|
14
|
+
defineProps({
|
|
15
|
+
size: {
|
|
16
|
+
type: String,
|
|
17
|
+
default: "2rem",
|
|
18
|
+
},
|
|
19
|
+
border: {
|
|
20
|
+
type: String,
|
|
21
|
+
default: "4px",
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<style scoped>
|
|
27
|
+
.spinner {
|
|
28
|
+
width: var(--spinner-size);
|
|
29
|
+
height: var(--spinner-size);
|
|
30
|
+
border-width: var(--spinner-border);
|
|
31
|
+
}
|
|
32
|
+
</style>
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="osi-field"
|
|
4
|
+
:class="[
|
|
5
|
+
`osi-field--label-${resolvedLabelStyle.toLowerCase()}`,
|
|
6
|
+
`osi-field--input-${resolvedInputStyle.toLowerCase()}`,
|
|
7
|
+
{
|
|
8
|
+
'osi-field--error': !!error,
|
|
9
|
+
'osi-field--floated': floated || alwaysFloats,
|
|
10
|
+
'osi-field--area': as === 'textarea',
|
|
11
|
+
'osi-field--suffix': !!$slots.suffix,
|
|
12
|
+
'osi-field--disabled': disabled,
|
|
13
|
+
},
|
|
14
|
+
attrClass,
|
|
15
|
+
]"
|
|
16
|
+
:style="[fieldVars, attrStyle]"
|
|
17
|
+
:data-field="field || undefined"
|
|
18
|
+
>
|
|
19
|
+
<div class="osi-field-control">
|
|
20
|
+
<!-- Custom control (dropdown trigger, phone input…). It receives the
|
|
21
|
+
classes and id it should use so it lines up with every other field. -->
|
|
22
|
+
<slot
|
|
23
|
+
name="control"
|
|
24
|
+
:id="controlId"
|
|
25
|
+
:input-class="inputClass"
|
|
26
|
+
:placeholder-text="placeholderText"
|
|
27
|
+
:invalid="!!error"
|
|
28
|
+
>
|
|
29
|
+
<textarea
|
|
30
|
+
v-if="as === 'textarea'"
|
|
31
|
+
:id="controlId"
|
|
32
|
+
:class="inputClass"
|
|
33
|
+
:rows="rows"
|
|
34
|
+
:value="modelValue ?? ''"
|
|
35
|
+
:placeholder="resolvedPlaceholder"
|
|
36
|
+
:disabled="disabled"
|
|
37
|
+
:required="required"
|
|
38
|
+
:aria-invalid="!!error"
|
|
39
|
+
:aria-describedby="error ? errorId : undefined"
|
|
40
|
+
v-bind="controlAttrs"
|
|
41
|
+
@input="onInput"
|
|
42
|
+
/>
|
|
43
|
+
|
|
44
|
+
<select
|
|
45
|
+
v-else-if="as === 'select'"
|
|
46
|
+
:id="controlId"
|
|
47
|
+
:class="inputClass"
|
|
48
|
+
:value="modelValue ?? ''"
|
|
49
|
+
:disabled="disabled"
|
|
50
|
+
:required="required"
|
|
51
|
+
:aria-invalid="!!error"
|
|
52
|
+
:aria-describedby="error ? errorId : undefined"
|
|
53
|
+
v-bind="controlAttrs"
|
|
54
|
+
@change="onInput"
|
|
55
|
+
>
|
|
56
|
+
<option value="">{{ placeholderText || placeholder || "" }}</option>
|
|
57
|
+
<option
|
|
58
|
+
v-for="option in normalizedOptions"
|
|
59
|
+
:key="option.value"
|
|
60
|
+
:value="option.value"
|
|
61
|
+
>
|
|
62
|
+
{{ option.label }}
|
|
63
|
+
</option>
|
|
64
|
+
</select>
|
|
65
|
+
|
|
66
|
+
<input
|
|
67
|
+
v-else
|
|
68
|
+
:id="controlId"
|
|
69
|
+
:class="inputClass"
|
|
70
|
+
:type="type"
|
|
71
|
+
:value="modelValue ?? ''"
|
|
72
|
+
:placeholder="resolvedPlaceholder"
|
|
73
|
+
:disabled="disabled"
|
|
74
|
+
:required="required"
|
|
75
|
+
:aria-invalid="!!error"
|
|
76
|
+
:aria-describedby="error ? errorId : undefined"
|
|
77
|
+
v-bind="controlAttrs"
|
|
78
|
+
@input="onInput"
|
|
79
|
+
/>
|
|
80
|
+
</slot>
|
|
81
|
+
|
|
82
|
+
<label v-if="label" class="osi-field-label" :for="controlId">
|
|
83
|
+
{{ label }}<span
|
|
84
|
+
v-if="required && showRequiredMark"
|
|
85
|
+
class="osi-field-required"
|
|
86
|
+
aria-hidden="true"
|
|
87
|
+
>*</span>
|
|
88
|
+
</label>
|
|
89
|
+
|
|
90
|
+
<span v-if="$slots.suffix" class="osi-field-suffix">
|
|
91
|
+
<slot name="suffix" />
|
|
92
|
+
</span>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<p v-if="error && typeof error === 'string'" :id="errorId" class="osi-field-error">
|
|
96
|
+
{{ error }}
|
|
97
|
+
</p>
|
|
98
|
+
<p v-else-if="$slots.hint" class="osi-field-hint"><slot name="hint" /></p>
|
|
99
|
+
</div>
|
|
100
|
+
</template>
|
|
101
|
+
|
|
102
|
+
<script setup>
|
|
103
|
+
import { INPUT_STYLES, LABEL_STYLES, pick } from "#osi/theme/controls";
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* A labelled form field that follows the theme's Inputs group.
|
|
107
|
+
*
|
|
108
|
+
* Same behaviour as CONTACT_FORM's fields — FLOATING / ABOVE / PLACEHOLDER
|
|
109
|
+
* labels over OUTLINE / FILLED / UNDERLINE boxes — but reading the theme
|
|
110
|
+
* instead of one section's settings, so login, register, checkout, the
|
|
111
|
+
* account pages and the address modal all match whatever the merchant set.
|
|
112
|
+
*
|
|
113
|
+
* Individual overrides:
|
|
114
|
+
* label-style / input-style win over the theme's choice for this field
|
|
115
|
+
* :vars="inputVars(s)" --in-* overrides (useInputStyle), e.g. from a
|
|
116
|
+
* section's own INPUT_* settings
|
|
117
|
+
*
|
|
118
|
+
* Everything that isn't a prop (autocomplete, inputmode, maxlength, @blur,
|
|
119
|
+
* @keydown…) is forwarded to the input, except class/style which go on the
|
|
120
|
+
* wrapper.
|
|
121
|
+
*
|
|
122
|
+
* <OsiField v-model="email" label="Email" type="email" :error="errors.email" />
|
|
123
|
+
*
|
|
124
|
+
* <OsiField label="Country" :floated="!!country">
|
|
125
|
+
* <template #control="{ id, inputClass }">
|
|
126
|
+
* <button :id="id" :class="[inputClass, 'osi-select-trigger']">…</button>
|
|
127
|
+
* </template>
|
|
128
|
+
* </OsiField>
|
|
129
|
+
*/
|
|
130
|
+
defineOptions({ inheritAttrs: false });
|
|
131
|
+
|
|
132
|
+
const props = defineProps({
|
|
133
|
+
modelValue: { type: [String, Number], default: "" },
|
|
134
|
+
label: { type: String, default: "" },
|
|
135
|
+
type: { type: String, default: "text" },
|
|
136
|
+
as: { type: String, default: "input" }, // input | textarea | select
|
|
137
|
+
options: { type: Array, default: () => [] },
|
|
138
|
+
id: { type: String, default: "" },
|
|
139
|
+
placeholder: { type: String, default: "" },
|
|
140
|
+
// String first: with Boolean first, Vue casts "" to true, and every field
|
|
141
|
+
// whose error was an empty string rendered red.
|
|
142
|
+
error: { type: [String, Boolean], default: false },
|
|
143
|
+
required: { type: Boolean, default: false },
|
|
144
|
+
disabled: { type: Boolean, default: false },
|
|
145
|
+
rows: { type: [Number, String], default: 4 },
|
|
146
|
+
/** Force the floated state — for custom controls that have a value. */
|
|
147
|
+
floated: { type: Boolean, default: false },
|
|
148
|
+
/** data-field, for scrollToFirstError(). */
|
|
149
|
+
field: { type: String, default: "" },
|
|
150
|
+
labelStyle: { type: String, default: "" },
|
|
151
|
+
inputStyle: { type: String, default: "" },
|
|
152
|
+
vars: { type: Object, default: () => ({}) },
|
|
153
|
+
inputClassExtra: { type: [String, Array, Object], default: "" },
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const emit = defineEmits(["update:modelValue"]);
|
|
157
|
+
|
|
158
|
+
const attrs = useAttrs();
|
|
159
|
+
const { controls } = useThemeControls();
|
|
160
|
+
|
|
161
|
+
const uid = useId();
|
|
162
|
+
const controlId = computed(() => props.id || `osi-field-${uid}`);
|
|
163
|
+
const errorId = computed(() => `${controlId.value}-error`);
|
|
164
|
+
|
|
165
|
+
const attrClass = computed(() => attrs.class);
|
|
166
|
+
const attrStyle = computed(() => attrs.style);
|
|
167
|
+
const controlAttrs = computed(() => {
|
|
168
|
+
const { class: _c, style: _s, ...rest } = attrs;
|
|
169
|
+
return rest;
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const resolvedLabelStyle = computed(
|
|
173
|
+
() => pick(props.labelStyle, LABEL_STYLES) ?? controls.value.labelStyle
|
|
174
|
+
);
|
|
175
|
+
const resolvedInputStyle = computed(
|
|
176
|
+
() => pick(props.inputStyle, INPUT_STYLES) ?? controls.value.inputStyle
|
|
177
|
+
);
|
|
178
|
+
const showRequiredMark = computed(() => controls.value.showRequiredMark);
|
|
179
|
+
|
|
180
|
+
const inputClass = computed(() => [
|
|
181
|
+
"osi-input",
|
|
182
|
+
`osi-input--${resolvedInputStyle.value.toLowerCase()}`,
|
|
183
|
+
props.inputClassExtra,
|
|
184
|
+
]);
|
|
185
|
+
|
|
186
|
+
/** Selects, dates and custom controls never report :placeholder-shown. */
|
|
187
|
+
const alwaysFloats = computed(
|
|
188
|
+
() => props.as === "select" || ["date", "time", "datetime-local", "month"].includes(props.type)
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* FLOATING needs :placeholder-shown to know the field is empty, so the
|
|
193
|
+
* placeholder must be a single space there. PLACEHOLDER mode shows the label
|
|
194
|
+
* as the placeholder, since the label itself is visually hidden.
|
|
195
|
+
*/
|
|
196
|
+
const placeholderText = computed(() =>
|
|
197
|
+
resolvedLabelStyle.value === "PLACEHOLDER" ? props.placeholder || props.label : ""
|
|
198
|
+
);
|
|
199
|
+
const resolvedPlaceholder = computed(() => {
|
|
200
|
+
if (resolvedLabelStyle.value === "FLOATING") return " ";
|
|
201
|
+
if (resolvedLabelStyle.value === "PLACEHOLDER") return placeholderText.value;
|
|
202
|
+
return props.placeholder;
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const fieldVars = computed(() => props.vars ?? {});
|
|
206
|
+
|
|
207
|
+
const normalizedOptions = computed(() =>
|
|
208
|
+
(props.options ?? []).map((option) =>
|
|
209
|
+
option && typeof option === "object"
|
|
210
|
+
? { value: option.value, label: option.label ?? option.value }
|
|
211
|
+
: { value: option, label: option }
|
|
212
|
+
)
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
function onInput(event) {
|
|
216
|
+
emit("update:modelValue", event.target.value);
|
|
217
|
+
}
|
|
218
|
+
</script>
|