@shopware/cms-base-layer 1.5.0 → 2.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 +328 -13
- package/app/app.config.ts +7 -0
- package/app/assets/icons/check-circle.svg +3 -0
- package/app/assets/icons/checkmark.svg +3 -0
- package/app/assets/icons/chevron.svg +3 -0
- package/app/assets/icons/exclamation-circle.svg +3 -0
- package/app/assets/icons/star-empty.svg +3 -0
- package/app/assets/icons/star-filled.svg +3 -0
- package/app/assets/icons/user.svg +1 -0
- package/app/components/SwCategoryNavigation.vue +76 -0
- package/app/components/SwCategoryNavigationLink.vue +128 -0
- package/{components → app/components}/SwContactForm.vue +27 -27
- package/app/components/SwFilterChips.vue +144 -0
- package/app/components/SwListingProductPrice.vue +89 -0
- package/{components → app/components}/SwNewsletterForm.vue +45 -34
- package/{components → app/components}/SwPagination.vue +3 -5
- package/{components → app/components}/SwProductAddToCart.vue +22 -27
- package/app/components/SwProductCard.vue +170 -0
- package/app/components/SwProductCardDetails.vue +57 -0
- package/app/components/SwProductCardImage.vue +87 -0
- package/app/components/SwProductCardSkeleton.vue +33 -0
- package/app/components/SwProductListingFilter.vue +64 -0
- package/app/components/SwProductListingFilters.vue +308 -0
- package/{components → app/components}/SwProductReviews.vue +28 -13
- package/app/components/SwProductReviewsForm.vue +292 -0
- package/app/components/SwQuantitySelect.vue +106 -0
- package/{components → app/components}/SwSlider.vue +4 -4
- package/app/components/SwSortDropdown.vue +83 -0
- package/app/components/SwStockInfo.vue +44 -0
- package/{components → app/components}/SwVariantConfigurator.vue +1 -1
- package/app/components/listing-filters/SwFilterPrice.vue +214 -0
- package/app/components/listing-filters/SwFilterProperties.vue +113 -0
- package/app/components/listing-filters/SwFilterRating.vue +90 -0
- package/app/components/listing-filters/SwFilterShippingFree.vue +107 -0
- package/{components → app/components}/public/cms/CmsPage.vue +19 -4
- package/{components → app/components}/public/cms/block/CmsBlockGalleryBuybox.vue +5 -5
- package/{components → app/components}/public/cms/block/CmsBlockImageBubbleRow.vue +5 -5
- package/app/components/public/cms/block/CmsBlockImageFourColumn.vue +41 -0
- package/app/components/public/cms/block/CmsBlockImageGalleryBig.vue +42 -0
- package/app/components/public/cms/block/CmsBlockImageHighlightRow.vue +37 -0
- package/{components → app/components}/public/cms/block/CmsBlockImageSimpleGrid.vue +11 -5
- package/{components → app/components}/public/cms/block/CmsBlockImageText.vue +7 -3
- package/{components → app/components}/public/cms/block/CmsBlockImageTextBubble.vue +13 -16
- package/{components → app/components}/public/cms/block/CmsBlockImageTextCover.vue +7 -9
- package/app/components/public/cms/block/CmsBlockImageTextGallery.vue +88 -0
- package/app/components/public/cms/block/CmsBlockImageTextRow.vue +53 -0
- package/{components → app/components}/public/cms/block/CmsBlockImageThreeColumn.vue +10 -4
- package/app/components/public/cms/block/CmsBlockImageThreeCover.vue +37 -0
- package/app/components/public/cms/block/CmsBlockImageTwoColumn.vue +37 -0
- package/{components → app/components}/public/cms/block/CmsBlockProductHeading.vue +1 -1
- package/{components → app/components}/public/cms/block/CmsBlockProductThreeColumn.vue +10 -4
- package/{components → app/components}/public/cms/block/CmsBlockSidebarFilter.vue +3 -1
- package/app/components/public/cms/block/CmsBlockTextOnImage.vue +30 -0
- package/{components → app/components}/public/cms/block/CmsBlockTextTeaserSection.vue +4 -4
- package/{components → app/components}/public/cms/block/CmsBlockTextTwoColumn.vue +3 -5
- package/app/components/public/cms/element/CmsElementBuyBox.vue +145 -0
- package/app/components/public/cms/element/CmsElementCategoryNavigation.vue +53 -0
- package/{components → app/components}/public/cms/element/CmsElementCrossSelling.vue +3 -3
- package/{components → app/components}/public/cms/element/CmsElementImage.vue +52 -13
- package/app/components/public/cms/element/CmsElementImageGallery.vue +158 -0
- package/{components → app/components}/public/cms/element/CmsElementImageSlider.vue +2 -2
- package/{components → app/components}/public/cms/element/CmsElementProductBox.vue +2 -1
- package/app/components/public/cms/element/CmsElementProductDescriptionReviews.vue +217 -0
- package/{components → app/components}/public/cms/element/CmsElementProductListing.vue +23 -94
- package/app/components/public/cms/element/CmsElementProductName.vue +11 -0
- package/{components → app/components}/public/cms/element/CmsElementProductSlider.vue +4 -4
- package/{components → app/components}/public/cms/element/CmsElementText.vue +8 -2
- package/{components → app/components}/public/cms/element/CmsElementYoutubeVideo.vue +8 -2
- package/app/components/public/cms/element/SwProductListingPagination.vue +70 -0
- package/{components → app/components}/public/cms/section/CmsSectionDefault.vue +1 -1
- package/app/components/public/cms/section/CmsSectionSidebar.vue +36 -0
- package/app/components/public/cms/skeleton/ProductCardSkeleton.vue +28 -0
- package/app/components/ui/BaseButton.vue +99 -0
- package/app/components/ui/BaseIcon.vue +15 -0
- package/app/components/ui/Checkbox.vue +49 -0
- package/app/components/ui/CheckmarkIcon.vue +23 -0
- package/app/components/ui/ChevronIcon.vue +37 -0
- package/app/components/ui/ExclamationIcon.vue +11 -0
- package/app/components/ui/IconButton.vue +32 -0
- package/app/components/ui/RadioButton.vue +26 -0
- package/app/components/ui/StarIcon.vue +18 -0
- package/app/components/ui/SwitchButton.vue +100 -0
- package/app/components/ui/UserIcon.vue +11 -0
- package/app/components/ui/WishlistIcon.vue +20 -0
- package/app/composables/useImagePlaceholder.ts +27 -0
- package/{helpers → app/helpers}/clientOnly.ts +5 -0
- package/app/providers/shopware.test.ts +213 -0
- package/app/providers/shopware.ts +107 -0
- package/dist/index.d.mts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.mjs +2 -2
- package/index.d.ts +12 -0
- package/nuxt.config.ts +80 -6
- package/package.json +29 -21
- package/uno.config.ts +83 -0
- package/components/SwCategoryNavigation.vue +0 -44
- package/components/SwCategoryNavigationLink.vue +0 -57
- package/components/SwListingProductPrice.vue +0 -89
- package/components/SwProductCard.vue +0 -286
- package/components/SwProductListingFilter.vue +0 -42
- package/components/SwProductListingFilters.vue +0 -292
- package/components/listing-filters/SwFilterPrice.vue +0 -160
- package/components/listing-filters/SwFilterProperties.vue +0 -123
- package/components/listing-filters/SwFilterRating.vue +0 -101
- package/components/listing-filters/SwFilterShippingFree.vue +0 -104
- package/components/public/cms/block/CmsBlockImageFourColumn.vue +0 -29
- package/components/public/cms/block/CmsBlockImageHighlightRow.vue +0 -27
- package/components/public/cms/block/CmsBlockImageTextGallery.vue +0 -85
- package/components/public/cms/block/CmsBlockImageTextRow.vue +0 -43
- package/components/public/cms/block/CmsBlockImageThreeCover.vue +0 -27
- package/components/public/cms/block/CmsBlockImageTwoColumn.vue +0 -25
- package/components/public/cms/block/CmsBlockTextOnImage.vue +0 -20
- package/components/public/cms/element/CmsBlockHtml.md +0 -1
- package/components/public/cms/element/CmsElementBuyBox.vue +0 -190
- package/components/public/cms/element/CmsElementCategoryNavigation.vue +0 -167
- package/components/public/cms/element/CmsElementImageGallery.vue +0 -249
- package/components/public/cms/element/CmsElementProductDescriptionReviews.vue +0 -123
- package/components/public/cms/element/CmsElementProductName.vue +0 -10
- package/components/public/cms/section/CmsSectionSidebar.vue +0 -49
- package/components/public/cms/skeleton/ProductCardSkeleton.vue +0 -44
- /package/{components → app/components}/SwMedia3D.vue +0 -0
- /package/{components → app/components}/SwProductGallery.vue +0 -0
- /package/{components → app/components}/SwProductPrice.vue +0 -0
- /package/{components → app/components}/SwProductUnits.vue +0 -0
- /package/{components → app/components}/SwSharedPrice.vue +0 -0
- /package/{components → app/components}/public/cms/CmsGenericBlock.md +0 -0
- /package/{components → app/components}/public/cms/CmsGenericBlock.vue +0 -0
- /package/{components → app/components}/public/cms/CmsGenericElement.md +0 -0
- /package/{components → app/components}/public/cms/CmsGenericElement.vue +0 -0
- /package/{components → app/components}/public/cms/CmsNoComponent.vue +0 -0
- /package/{components → app/components}/public/cms/CmsPage.md +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockCategoryNavigation.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockCenterText.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockCrossSelling.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockCustomForm.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockDefault.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockForm.vue +0 -0
- /package/{components/public/cms/element → app/components/public/cms/block}/CmsBlockHtml.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockImage.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockImageCover.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockImageGallery.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockImageSlider.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockProductDescriptionReviews.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockProductListing.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockProductSlider.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockText.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockTextHero.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockTextTeaser.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockTextThreeColumn.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockVimeoVideo.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockYoutubeVideo.vue +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementBuyBox.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementCategoryNavigation.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementCrossSelling.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementCustomForm.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementCustomForm.vue +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementForm.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementForm.vue +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementHtml.vue +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementImage.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementImageGallery.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementImageGallery3dPlaceholder.vue +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementImageSlider.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementManufacturerLogo.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementManufacturerLogo.vue +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementProductBox.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementProductDescriptionReviews.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementProductListing.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementProductName.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementProductSlider.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementSidebarFilter.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementSidebarFilter.vue +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementText.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementVimeoVideo.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementVimeoVideo.vue +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementYoutubeVideo.md +0 -0
- /package/{components → app/components}/public/cms/section/CmsSectionDefault.md +0 -0
- /package/{components → app/components}/public/cms/section/CmsSectionSidebar.md +0 -0
- /package/{helpers → app/helpers}/html-to-vue/ast.ts +0 -0
- /package/{helpers → app/helpers}/html-to-vue/getOptionsFromNode.test.ts +0 -0
- /package/{helpers → app/helpers}/html-to-vue/getOptionsFromNode.ts +0 -0
- /package/{helpers → app/helpers}/html-to-vue/renderToHtml.ts +0 -0
- /package/{helpers → app/helpers}/html-to-vue/renderer.ts +0 -0
- /package/{helpers → app/helpers}/media/isSpatial.ts +0 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { defu } from "defu";
|
|
3
|
+
import { computed, getCurrentInstance } from "vue";
|
|
4
|
+
import { useCmsTranslations } from "#imports";
|
|
5
|
+
|
|
6
|
+
type Translations = {
|
|
7
|
+
form: {
|
|
8
|
+
quantitySelect: {
|
|
9
|
+
label: string;
|
|
10
|
+
increaseButton: string;
|
|
11
|
+
decreaseButton: string;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
let translations = {
|
|
17
|
+
form: {
|
|
18
|
+
quantitySelect: {
|
|
19
|
+
label: "Quantity",
|
|
20
|
+
increaseButton: "Increase quantity",
|
|
21
|
+
decreaseButton: "Decrease quantity",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
translations = defu(useCmsTranslations(), translations) as Translations;
|
|
27
|
+
|
|
28
|
+
const quantity = defineModel<number>({
|
|
29
|
+
required: true,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const {
|
|
33
|
+
size = "large",
|
|
34
|
+
steps,
|
|
35
|
+
max,
|
|
36
|
+
min,
|
|
37
|
+
id: propId,
|
|
38
|
+
} = defineProps<{
|
|
39
|
+
size?: "small" | "large";
|
|
40
|
+
steps?: number;
|
|
41
|
+
min?: number;
|
|
42
|
+
max?: number;
|
|
43
|
+
id?: string;
|
|
44
|
+
}>();
|
|
45
|
+
|
|
46
|
+
// generate an id that prefers a provided prop and otherwise uses the component uid
|
|
47
|
+
const inputId = computed(() => {
|
|
48
|
+
if (propId) return propId;
|
|
49
|
+
const uid = Math.random().toString(36).substr(2, 9);
|
|
50
|
+
return `sw-quantity-${uid}`;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
function increaseQty() {
|
|
54
|
+
quantity.value++;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function decreaseQty() {
|
|
58
|
+
if (quantity.value > 1) {
|
|
59
|
+
quantity.value--;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const sizeClasses = {
|
|
64
|
+
small: "w-8 h-8",
|
|
65
|
+
large: "w-10 h-10",
|
|
66
|
+
};
|
|
67
|
+
</script>
|
|
68
|
+
<template>
|
|
69
|
+
<div class="rounded outline outline-1 outline-offset-[-1px] outline-outline-outline inline-flex">
|
|
70
|
+
<button
|
|
71
|
+
type="button"
|
|
72
|
+
:class="sizeClasses[size]"
|
|
73
|
+
class="bg-surface-surface border-0 border-r-1 cursor-pointer hover:bg-brand-tertiary-hover font-semibold"
|
|
74
|
+
@click="decreaseQty"
|
|
75
|
+
:aria-label="translations.form.quantitySelect.decreaseButton"
|
|
76
|
+
>
|
|
77
|
+
-
|
|
78
|
+
</button>
|
|
79
|
+
<div class="bg-white border-l border-r border-outline-outline inline-flex flex-col justify-center items-center">
|
|
80
|
+
<!-- visually hidden label for screen readers -->
|
|
81
|
+
<label :for="inputId" class="sr-only">{{ translations.form.quantitySelect.label }}</label>
|
|
82
|
+
|
|
83
|
+
<input
|
|
84
|
+
:id="inputId"
|
|
85
|
+
v-model="quantity"
|
|
86
|
+
type="number"
|
|
87
|
+
:min="min"
|
|
88
|
+
:max="max"
|
|
89
|
+
:step="steps"
|
|
90
|
+
data-testid="product-quantity"
|
|
91
|
+
:class="sizeClasses[size]"
|
|
92
|
+
class="self-stretch text-center justify-start text-surface-on-surface text-xs font-bold leading-[18px] appearance-none [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
|
93
|
+
:aria-label="translations.form.quantitySelect.label"
|
|
94
|
+
/>
|
|
95
|
+
</div>
|
|
96
|
+
<button
|
|
97
|
+
type="button"
|
|
98
|
+
:class="sizeClasses[size]"
|
|
99
|
+
class="w-10 bg-surface-surface border-0 border-l-1 cursor-pointer hover:bg-brand-tertiary-hover font-semibold"
|
|
100
|
+
@click="increaseQty"
|
|
101
|
+
:aria-label="translations.form.quantitySelect.increaseButton"
|
|
102
|
+
>
|
|
103
|
+
+
|
|
104
|
+
</button>
|
|
105
|
+
</div>
|
|
106
|
+
</template>
|
|
@@ -34,7 +34,7 @@ const props = withDefaults(
|
|
|
34
34
|
|
|
35
35
|
const { getConfigValue } = useCmsElementConfig({
|
|
36
36
|
config: props.config,
|
|
37
|
-
} as Schemas["CmsSlot"] & {
|
|
37
|
+
} as Omit<Schemas["CmsSlot"], "config"> & {
|
|
38
38
|
config: SliderElementConfig;
|
|
39
39
|
});
|
|
40
40
|
|
|
@@ -63,12 +63,12 @@ const children = computed<string[]>(() => {
|
|
|
63
63
|
] as string[];
|
|
64
64
|
});
|
|
65
65
|
const emit = defineEmits<(e: "changeSlide", index: number) => void>();
|
|
66
|
-
const slider = useTemplateRef("slider");
|
|
67
|
-
const imageSlider = useTemplateRef("imageSlider");
|
|
66
|
+
const slider = useTemplateRef<HTMLDivElement>("slider");
|
|
67
|
+
const imageSlider = useTemplateRef<HTMLDivElement>("imageSlider");
|
|
68
68
|
const imageSliderTrackStyle = ref<CSSProperties>();
|
|
69
69
|
const activeSlideIndex = ref<number>(0);
|
|
70
70
|
const speed = ref<number>(300);
|
|
71
|
-
const imageSliderTrack = useTemplateRef("imageSliderTrack");
|
|
71
|
+
const imageSliderTrack = useTemplateRef<HTMLDivElement>("imageSliderTrack");
|
|
72
72
|
const autoPlayInterval = ref();
|
|
73
73
|
const isReady = ref<boolean>();
|
|
74
74
|
const isSliding = ref<boolean>();
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { onClickOutside } from "@vueuse/core";
|
|
3
|
+
import { ref, useTemplateRef } from "vue";
|
|
4
|
+
|
|
5
|
+
type SortOption = {
|
|
6
|
+
key: string;
|
|
7
|
+
label: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
defineProps<{
|
|
11
|
+
sortOptions: SortOption[];
|
|
12
|
+
currentSort: string;
|
|
13
|
+
label: string;
|
|
14
|
+
}>();
|
|
15
|
+
|
|
16
|
+
const emit = defineEmits<{
|
|
17
|
+
"sort-change": [string];
|
|
18
|
+
}>();
|
|
19
|
+
|
|
20
|
+
const isSortMenuOpen = ref(false);
|
|
21
|
+
const dropdownElement = useTemplateRef<HTMLDivElement>("dropdownElement");
|
|
22
|
+
|
|
23
|
+
onClickOutside(dropdownElement, () => {
|
|
24
|
+
isSortMenuOpen.value = false;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const handleSortingClick = (key: string) => {
|
|
28
|
+
emit("sort-change", key);
|
|
29
|
+
isSortMenuOpen.value = false;
|
|
30
|
+
};
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<template>
|
|
34
|
+
<div ref="dropdownElement" class="flex items-center">
|
|
35
|
+
<div class="relative inline-block text-left">
|
|
36
|
+
<SwBaseButton
|
|
37
|
+
variant="ghost"
|
|
38
|
+
size="medium"
|
|
39
|
+
type="button"
|
|
40
|
+
@click="isSortMenuOpen = !isSortMenuOpen"
|
|
41
|
+
id="menu-button"
|
|
42
|
+
aria-expanded="false"
|
|
43
|
+
aria-haspopup="true"
|
|
44
|
+
class="group pr-0"
|
|
45
|
+
>
|
|
46
|
+
<span class="inline-flex items-center gap-1">
|
|
47
|
+
{{ label }}
|
|
48
|
+
<SwChevronIcon
|
|
49
|
+
:direction="isSortMenuOpen ? 'up' : 'down'"
|
|
50
|
+
:size="24"
|
|
51
|
+
:aria-label="isSortMenuOpen ? 'Close sort menu' : 'Open sort menu'"
|
|
52
|
+
/>
|
|
53
|
+
</span>
|
|
54
|
+
</SwBaseButton>
|
|
55
|
+
<div
|
|
56
|
+
:class="[isSortMenuOpen ? 'absolute' : 'hidden']"
|
|
57
|
+
class="origin-top-right right-0 mt-2 w-40 rounded-md shadow-2xl bg-surface-surface ring-1 ring-opacity-dark-low focus:outline-none z-1000"
|
|
58
|
+
role="menu"
|
|
59
|
+
aria-orientation="vertical"
|
|
60
|
+
aria-labelledby="menu-button"
|
|
61
|
+
tabindex="-1"
|
|
62
|
+
>
|
|
63
|
+
<div class="py-1" role="none">
|
|
64
|
+
<button
|
|
65
|
+
v-for="sorting in sortOptions"
|
|
66
|
+
:key="sorting.key"
|
|
67
|
+
@click="handleSortingClick(sorting.key)"
|
|
68
|
+
:class="[
|
|
69
|
+
sorting.key === currentSort
|
|
70
|
+
? 'font-medium text-surface-on-surface'
|
|
71
|
+
: 'text-surface-on-surface-variant',
|
|
72
|
+
]"
|
|
73
|
+
class="block px-4 py-2 text-sm bg-transparent hover:bg-surface-surface-container"
|
|
74
|
+
role="menuitem"
|
|
75
|
+
tabindex="-1"
|
|
76
|
+
>
|
|
77
|
+
{{ sorting.label }}
|
|
78
|
+
</button>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</template>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { defu } from "defu";
|
|
3
|
+
|
|
4
|
+
import { useCmsTranslations } from "#imports";
|
|
5
|
+
import type { Schemas } from "#shopware";
|
|
6
|
+
|
|
7
|
+
defineProps<{
|
|
8
|
+
availableStock: number;
|
|
9
|
+
minPurchase: number;
|
|
10
|
+
deliveryTime?: Schemas["DeliveryTime"];
|
|
11
|
+
restockTime?: number;
|
|
12
|
+
}>();
|
|
13
|
+
|
|
14
|
+
type Translations = {
|
|
15
|
+
product: {
|
|
16
|
+
deliveryTime: string;
|
|
17
|
+
days: string;
|
|
18
|
+
noAvailable: string;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
let translations: Translations = {
|
|
23
|
+
product: {
|
|
24
|
+
deliveryTime: "Available, delivery time",
|
|
25
|
+
days: "days",
|
|
26
|
+
noAvailable: "No longer available",
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
translations = defu(useCmsTranslations(), translations) as Translations;
|
|
31
|
+
</script>
|
|
32
|
+
<template>
|
|
33
|
+
<div class="inline-flex justify-start items-center gap-2">
|
|
34
|
+
<div class="w-2 h-2 bg-states-success rounded-full" v-if="availableStock > 0"></div>
|
|
35
|
+
<div class="w-2 h-2 bg-states-error rounded-full" v-else></div>
|
|
36
|
+
<span v-if="availableStock >= minPurchase && deliveryTime">{{ translations.product.deliveryTime }} {{
|
|
37
|
+
deliveryTime?.name }}
|
|
38
|
+
</span>
|
|
39
|
+
<span v-else-if="availableStock < minPurchase && deliveryTime && restockTime">
|
|
40
|
+
{{ translations.product.deliveryTime }} {{ restockTime }}
|
|
41
|
+
{{ translations.product.days }} {{ deliveryTime?.name }}</span>
|
|
42
|
+
<span v-else>{{ translations.product.noAvailable }}</span>
|
|
43
|
+
</div>
|
|
44
|
+
</template>
|
|
@@ -102,7 +102,7 @@ const onHandleChange = async () => {
|
|
|
102
102
|
data-testid="product-variant"
|
|
103
103
|
class="group relative border rounded-md py-3 px-4 flex items-center justify-center text-sm font-medium uppercase hover:bg-gray-50 focus:outline-none sm:flex-1 bg-white shadow-sm text-gray-900 cursor-pointer"
|
|
104
104
|
:class="{
|
|
105
|
-
'border-3 border-
|
|
105
|
+
'border-3 border-brand-primary': isOptionSelected(option.id),
|
|
106
106
|
}"
|
|
107
107
|
@click="handleChange(optionGroup.translated.name, option.id, onHandleChange)"
|
|
108
108
|
>
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
<script setup lang="ts" generic="
|
|
2
|
+
ListingFilter extends {
|
|
3
|
+
code: string;
|
|
4
|
+
min?: number;
|
|
5
|
+
max?: number;
|
|
6
|
+
label: string;
|
|
7
|
+
}
|
|
8
|
+
">
|
|
9
|
+
import { useCmsTranslations } from "@shopware/composables";
|
|
10
|
+
import { onClickOutside, useDebounceFn, useEventListener } from "@vueuse/core";
|
|
11
|
+
import { defu } from "defu";
|
|
12
|
+
import { onMounted, reactive, ref, watch } from "vue";
|
|
13
|
+
import type { Schemas } from "#shopware";
|
|
14
|
+
|
|
15
|
+
const emits =
|
|
16
|
+
defineEmits<
|
|
17
|
+
(e: "select-value", value: { code: string; value: unknown }) => void
|
|
18
|
+
>();
|
|
19
|
+
|
|
20
|
+
const props = defineProps<{
|
|
21
|
+
filter: ListingFilter;
|
|
22
|
+
selectedFilters: Schemas["ProductListingResult"]["currentFilters"];
|
|
23
|
+
}>();
|
|
24
|
+
|
|
25
|
+
type Translations = {
|
|
26
|
+
listing: {
|
|
27
|
+
min: string;
|
|
28
|
+
max: string;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
let translations: Translations = {
|
|
32
|
+
listing: {
|
|
33
|
+
min: "Min",
|
|
34
|
+
max: "Max",
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
translations = defu(useCmsTranslations(), translations) as Translations;
|
|
38
|
+
|
|
39
|
+
const prices = reactive<{ min: number; max: number }>({
|
|
40
|
+
min: 0,
|
|
41
|
+
max: 0,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
onMounted(() => {
|
|
45
|
+
prices.min = Math.floor(
|
|
46
|
+
props.selectedFilters.price?.min ?? props.filter.min ?? 0,
|
|
47
|
+
);
|
|
48
|
+
prices.max = Math.floor(
|
|
49
|
+
props.selectedFilters.price?.max ?? props.filter.max ?? 0,
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const isFilterVisible = ref<boolean>(false);
|
|
54
|
+
const toggle = () => {
|
|
55
|
+
isFilterVisible.value = !isFilterVisible.value;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const dropdownElement = ref(null);
|
|
59
|
+
onClickOutside(dropdownElement, () => {
|
|
60
|
+
isFilterVisible.value = false;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
function onMinPriceChange(newPrice: number, oldPrice: number) {
|
|
64
|
+
if (newPrice === oldPrice || oldPrice === 0) return;
|
|
65
|
+
emits("select-value", {
|
|
66
|
+
code: "min-price",
|
|
67
|
+
value: newPrice,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
const debounceMinPriceUpdate = useDebounceFn(onMinPriceChange, 500);
|
|
71
|
+
watch(() => prices.min, debounceMinPriceUpdate);
|
|
72
|
+
|
|
73
|
+
function onMaxPriceChange(newPrice: number, oldPrice: number) {
|
|
74
|
+
if (newPrice === oldPrice || oldPrice === 0) return;
|
|
75
|
+
emits("select-value", {
|
|
76
|
+
code: "max-price",
|
|
77
|
+
value: newPrice,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
const debounceMaxPriceUpdate = useDebounceFn(onMaxPriceChange, 500);
|
|
81
|
+
watch(() => prices.max, debounceMaxPriceUpdate);
|
|
82
|
+
|
|
83
|
+
// Slider drag logic
|
|
84
|
+
type DragType = "min" | "max" | null;
|
|
85
|
+
const dragging = ref<DragType>(null);
|
|
86
|
+
const sliderRect = ref<DOMRect | null>(null);
|
|
87
|
+
|
|
88
|
+
const getClientX = (event: MouseEvent | TouchEvent): number =>
|
|
89
|
+
event instanceof MouseEvent ? event.clientX : event.touches[0]?.clientX || 0;
|
|
90
|
+
|
|
91
|
+
const updateSliderValue = (clientX: number) => {
|
|
92
|
+
if (!dragging.value || !sliderRect.value) return;
|
|
93
|
+
|
|
94
|
+
const min = props.filter.min ?? 0;
|
|
95
|
+
const max = props.filter.max ?? 100;
|
|
96
|
+
const percent = Math.min(
|
|
97
|
+
Math.max((clientX - sliderRect.value.left) / sliderRect.value.width, 0),
|
|
98
|
+
1,
|
|
99
|
+
);
|
|
100
|
+
const value = Math.round(min + percent * (max - min));
|
|
101
|
+
|
|
102
|
+
if (dragging.value === "min") {
|
|
103
|
+
if (value >= min && value <= prices.max) prices.min = value;
|
|
104
|
+
} else {
|
|
105
|
+
if (value <= max && value >= prices.min) prices.max = value;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const onDrag = (event: MouseEvent | TouchEvent) => {
|
|
110
|
+
if (!dragging.value) return;
|
|
111
|
+
event.preventDefault();
|
|
112
|
+
updateSliderValue(getClientX(event));
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const stopDrag = () => {
|
|
116
|
+
dragging.value = null;
|
|
117
|
+
sliderRect.value = null;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
useEventListener(window, "mousemove", onDrag);
|
|
121
|
+
useEventListener(window, "mouseup", stopDrag);
|
|
122
|
+
useEventListener(window, "touchmove", onDrag, { passive: false });
|
|
123
|
+
useEventListener(window, "touchend", stopDrag);
|
|
124
|
+
|
|
125
|
+
const startDrag = (type: "min" | "max", event: MouseEvent | TouchEvent) => {
|
|
126
|
+
event.preventDefault();
|
|
127
|
+
dragging.value = type;
|
|
128
|
+
const slider = (event.target as HTMLElement).closest(".relative.w-64.h-10");
|
|
129
|
+
if (slider) {
|
|
130
|
+
sliderRect.value = slider.getBoundingClientRect();
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
</script>
|
|
134
|
+
|
|
135
|
+
<template>
|
|
136
|
+
<div class="self-stretch flex flex-col justify-start items-start gap-4">
|
|
137
|
+
<div class="self-stretch flex flex-col justify-center items-center">
|
|
138
|
+
<div
|
|
139
|
+
class="self-stretch py-3 border-b border-outline-outline-variant inline-flex justify-between items-center gap-1 cursor-pointer"
|
|
140
|
+
@click="toggle"
|
|
141
|
+
role="button"
|
|
142
|
+
tabindex="0"
|
|
143
|
+
:aria-expanded="isFilterVisible"
|
|
144
|
+
:aria-controls="`filter-${props.filter.code}`"
|
|
145
|
+
@keydown.enter="toggle"
|
|
146
|
+
@keydown.space.prevent="toggle"
|
|
147
|
+
>
|
|
148
|
+
<div class="flex-1 flex items-center gap-2.5">
|
|
149
|
+
<div class="flex-1 text-surface-on-surface text-base font-bold leading-normal text-left">
|
|
150
|
+
{{ props.filter.label }}
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
<SwIconButton
|
|
154
|
+
type="ghost"
|
|
155
|
+
:aria-label="isFilterVisible ? 'Collapse filter' : 'Expand filter'"
|
|
156
|
+
tabindex="-1"
|
|
157
|
+
>
|
|
158
|
+
<SwChevronIcon :direction="isFilterVisible ? 'up' : 'down'" :size="24" />
|
|
159
|
+
</SwIconButton>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
<transition name="filter-collapse">
|
|
163
|
+
<div v-if="isFilterVisible" :id="props.filter.code"
|
|
164
|
+
class="self-stretch flex flex-col justify-start items-start gap-2.5">
|
|
165
|
+
<div class="self-stretch flex flex-col justify-start items-start gap-1">
|
|
166
|
+
<div class="self-stretch inline-flex justify-between items-center gap-2">
|
|
167
|
+
<div
|
|
168
|
+
class="w-16 h-10 px-2 py-1 rounded-lg outline outline-1 outline-offset-[-1px] outline-outline-outline-variant inline-flex flex-col justify-center items-start gap-2.5">
|
|
169
|
+
<input type="number" :placeholder="translations.listing.min" v-model.number="prices.min"
|
|
170
|
+
class="w-full bg-transparent border-none outline-none text-surface-on-surface text-sm font-normal leading-tight"
|
|
171
|
+
@change="emits('select-value', { code: props.filter.code, value: { min: prices.min, max: prices.max } })"
|
|
172
|
+
:min="props.filter.min" :max="prices.max" />
|
|
173
|
+
</div>
|
|
174
|
+
<div
|
|
175
|
+
class="w-16 h-10 px-2 py-1 rounded-lg outline outline-1 outline-offset-[-1px] outline-outline-outline-variant inline-flex flex-col justify-center items-start gap-2.5">
|
|
176
|
+
<input type="number" :placeholder="translations.listing.max" v-model.number="prices.max"
|
|
177
|
+
class="w-full bg-transparent border-none outline-none text-surface-on-surface text-sm font-normal leading-tight"
|
|
178
|
+
@change="emits('select-value', { code: props.filter.code, value: { min: prices.min, max: prices.max } })"
|
|
179
|
+
:min="prices.min" :max="props.filter.max" />
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
<!-- Custom slider UI -->
|
|
183
|
+
<div class="relative w-64 h-10 mt-2 mx-auto flex items-center select-none">
|
|
184
|
+
<!-- Track -->
|
|
185
|
+
<div
|
|
186
|
+
class="absolute left-0 top-1/2 -translate-y-1/2 w-full h-2 bg-surface-surface-container-highest rounded-full">
|
|
187
|
+
</div>
|
|
188
|
+
<!-- Active range -->
|
|
189
|
+
<div class="absolute top-1/2 -translate-y-1/2 h-2 bg-surface-surface-primary rounded-full" :style="{
|
|
190
|
+
left: ((prices.min - (props.filter.min ?? 0)) / ((props.filter.max ?? 100) - (props.filter.min ?? 0))) * 100 + '%',
|
|
191
|
+
width: ((prices.max - prices.min) / ((props.filter.max ?? 100) - (props.filter.min ?? 0))) * 100 + '%',
|
|
192
|
+
}"></div>
|
|
193
|
+
<!-- Min thumb -->
|
|
194
|
+
<div
|
|
195
|
+
class="absolute top-1/2 -translate-y-1/2 w-5 h-5 bg-brand-primary rounded-full shadow-[2px_2px_10px_0px_rgba(0,0,0,0.15)] cursor-pointer touch-none"
|
|
196
|
+
:style="{
|
|
197
|
+
left: `calc(${((prices.min - (props.filter.min ?? 0)) / ((props.filter.max ?? 100) - (props.filter.min ?? 0))) * 100}% - 10px)`
|
|
198
|
+
}"
|
|
199
|
+
@mousedown.prevent="startDrag('min', $event)"
|
|
200
|
+
@touchstart.prevent="startDrag('min', $event)"></div>
|
|
201
|
+
<!-- Max thumb -->
|
|
202
|
+
<div
|
|
203
|
+
class="absolute top-1/2 -translate-y-1/2 w-5 h-5 bg-brand-primary rounded-full shadow-[2px_2px_10px_0px_rgba(0,0,0,0.15)] cursor-pointer touch-none"
|
|
204
|
+
:style="{
|
|
205
|
+
left: `calc(${((prices.max - (props.filter.min ?? 0)) / ((props.filter.max ?? 100) - (props.filter.min ?? 0))) * 100}% - 10px)`
|
|
206
|
+
}"
|
|
207
|
+
@mousedown.prevent="startDrag('max', $event)"
|
|
208
|
+
@touchstart.prevent="startDrag('max', $event)"></div>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
</transition>
|
|
213
|
+
</div>
|
|
214
|
+
</template>
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
<script
|
|
2
|
+
setup
|
|
3
|
+
lang="ts"
|
|
4
|
+
generic="
|
|
5
|
+
ListingFilter extends {
|
|
6
|
+
code: string;
|
|
7
|
+
label: string;
|
|
8
|
+
name: string;
|
|
9
|
+
options: Array<Schemas['PropertyGroupOption']>;
|
|
10
|
+
entities: Array<Schemas['ProductManufacturer']>;
|
|
11
|
+
}
|
|
12
|
+
"
|
|
13
|
+
>
|
|
14
|
+
import { getTranslatedProperty } from "@shopware/helpers";
|
|
15
|
+
import { computed, ref } from "vue";
|
|
16
|
+
import type { Schemas } from "#shopware";
|
|
17
|
+
|
|
18
|
+
const props = defineProps<{
|
|
19
|
+
filter: ListingFilter;
|
|
20
|
+
selectedFilters: {
|
|
21
|
+
manufacturer?: string[];
|
|
22
|
+
properties?: string[];
|
|
23
|
+
[key: string]: unknown;
|
|
24
|
+
};
|
|
25
|
+
}>();
|
|
26
|
+
|
|
27
|
+
const emits =
|
|
28
|
+
defineEmits<
|
|
29
|
+
(e: "select-value", value: { code: string; value: unknown }) => void
|
|
30
|
+
>();
|
|
31
|
+
|
|
32
|
+
const isFilterVisible = ref<boolean>(false);
|
|
33
|
+
const toggle = () => {
|
|
34
|
+
isFilterVisible.value = !isFilterVisible.value;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const selectedIds = computed(() => {
|
|
38
|
+
if (props.filter.code === "manufacturer") {
|
|
39
|
+
return props.selectedFilters?.manufacturer || [];
|
|
40
|
+
}
|
|
41
|
+
return props.selectedFilters?.properties || [];
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const isChecked = (id: string) => selectedIds.value.includes(id);
|
|
45
|
+
|
|
46
|
+
const selectValue = (id: string) => {
|
|
47
|
+
const emitCode =
|
|
48
|
+
props.filter.code === "manufacturer" ? "manufacturer" : "properties";
|
|
49
|
+
emits("select-value", {
|
|
50
|
+
code: emitCode,
|
|
51
|
+
value: id,
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<template>
|
|
57
|
+
<div class="self-stretch flex flex-col justify-start items-start gap-4">
|
|
58
|
+
<div class="self-stretch flex flex-col justify-center items-center">
|
|
59
|
+
<div
|
|
60
|
+
class="self-stretch py-3 border-b border-outline-outline-variant inline-flex justify-between items-center gap-1 cursor-pointer"
|
|
61
|
+
@click="toggle"
|
|
62
|
+
role="button"
|
|
63
|
+
tabindex="0"
|
|
64
|
+
:aria-expanded="isFilterVisible"
|
|
65
|
+
:aria-controls="props.filter.code"
|
|
66
|
+
:aria-label="props.filter.label"
|
|
67
|
+
@keydown.enter="toggle"
|
|
68
|
+
@keydown.space.prevent="toggle"
|
|
69
|
+
>
|
|
70
|
+
<div class="flex-1 flex items-center gap-2.5">
|
|
71
|
+
<div class="flex-1 text-surface-on-surface text-base font-bold leading-normal text-left">
|
|
72
|
+
{{ props.filter.label }}
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
<SwIconButton
|
|
76
|
+
type="ghost"
|
|
77
|
+
:aria-label="isFilterVisible ? 'Collapse filter' : 'Expand filter'"
|
|
78
|
+
tabindex="-1"
|
|
79
|
+
>
|
|
80
|
+
<SwChevronIcon :direction="isFilterVisible ? 'up' : 'down'" :size="24" />
|
|
81
|
+
</SwIconButton>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
<transition name="filter-collapse">
|
|
85
|
+
<div v-if="isFilterVisible" :id="props.filter.code" class="self-stretch flex flex-col justify-start items-start gap-4">
|
|
86
|
+
<fieldset class="self-stretch flex flex-col justify-start items-start gap-4">
|
|
87
|
+
<legend class="sr-only">{{ props.filter.name }}</legend>
|
|
88
|
+
<label
|
|
89
|
+
v-for="option in props.filter.options || props.filter.entities"
|
|
90
|
+
:key="`${option.id}-${isChecked(option.id)}`"
|
|
91
|
+
class="self-stretch inline-flex justify-start items-start gap-2 cursor-pointer"
|
|
92
|
+
@click="selectValue(option.id)"
|
|
93
|
+
>
|
|
94
|
+
<div class="w-4 self-stretch pt-[3px] flex justify-start items-start gap-2.5">
|
|
95
|
+
<SwCheckbox
|
|
96
|
+
:model-value="isChecked(option.id)"
|
|
97
|
+
@update:model-value="() => selectValue(option.id)"
|
|
98
|
+
@click.stop
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
<div class="flex-1 inline-flex flex-col justify-start items-start gap-0.5">
|
|
102
|
+
<div class="inline-flex justify-start items-center gap-1">
|
|
103
|
+
<div class="flex-1 text-surface-on-surface text-base font-normal leading-normal">
|
|
104
|
+
{{ getTranslatedProperty(option, 'name') }}
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</label>
|
|
109
|
+
</fieldset>
|
|
110
|
+
</div>
|
|
111
|
+
</transition>
|
|
112
|
+
</div>
|
|
113
|
+
</template>
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<script
|
|
2
|
+
setup
|
|
3
|
+
lang="ts"
|
|
4
|
+
generic="
|
|
5
|
+
ListingFilter extends {
|
|
6
|
+
code: string;
|
|
7
|
+
label: string;
|
|
8
|
+
}
|
|
9
|
+
"
|
|
10
|
+
>
|
|
11
|
+
import { computed, ref } from "vue";
|
|
12
|
+
import type { Schemas } from "#shopware";
|
|
13
|
+
|
|
14
|
+
const emits =
|
|
15
|
+
defineEmits<
|
|
16
|
+
(e: "select-value", value: { code: string; value: unknown }) => void
|
|
17
|
+
>();
|
|
18
|
+
|
|
19
|
+
const props = defineProps<{
|
|
20
|
+
filter: ListingFilter;
|
|
21
|
+
selectedFilters: Schemas["ProductListingResult"]["currentFilters"];
|
|
22
|
+
}>();
|
|
23
|
+
const isHoverActive = ref(false);
|
|
24
|
+
const hoveredIndex = ref(0);
|
|
25
|
+
const displayedScore = computed(() =>
|
|
26
|
+
isHoverActive.value ? hoveredIndex.value : props.selectedFilters?.rating || 0,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const hoverRating = (key: number) => {
|
|
30
|
+
hoveredIndex.value = key;
|
|
31
|
+
isHoverActive.value = true;
|
|
32
|
+
};
|
|
33
|
+
const onChangeRating = () => {
|
|
34
|
+
const newValue =
|
|
35
|
+
props.selectedFilters?.rating !== hoveredIndex.value
|
|
36
|
+
? hoveredIndex.value
|
|
37
|
+
: undefined;
|
|
38
|
+
emits("select-value", { code: props.filter?.code, value: newValue });
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const isFilterVisible = ref<boolean>(false);
|
|
42
|
+
const toggle = () => {
|
|
43
|
+
isFilterVisible.value = !isFilterVisible.value;
|
|
44
|
+
};
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<template>
|
|
48
|
+
<div class="self-stretch flex flex-col justify-start items-start gap-4">
|
|
49
|
+
<div class="self-stretch flex flex-col justify-center items-center">
|
|
50
|
+
<div
|
|
51
|
+
class="self-stretch py-3 border-b border-outline-outline-variant inline-flex justify-between items-center gap-1 cursor-pointer"
|
|
52
|
+
@click="toggle"
|
|
53
|
+
role="button"
|
|
54
|
+
tabindex="0"
|
|
55
|
+
:aria-expanded="isFilterVisible"
|
|
56
|
+
:aria-controls="`filter-rating`"
|
|
57
|
+
@keydown.enter="toggle"
|
|
58
|
+
@keydown.space.prevent="toggle"
|
|
59
|
+
>
|
|
60
|
+
<div class="flex-1 flex items-center gap-2.5">
|
|
61
|
+
<div class="flex-1 text-surface-on-surface text-base font-bold leading-normal text-left">
|
|
62
|
+
{{ props.filter.label }}
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
<SwIconButton
|
|
66
|
+
type="ghost"
|
|
67
|
+
:aria-label="isFilterVisible ? 'Collapse filter' : 'Expand filter'"
|
|
68
|
+
tabindex="-1"
|
|
69
|
+
>
|
|
70
|
+
<SwChevronIcon :direction="isFilterVisible ? 'up' : 'down'" :size="24" />
|
|
71
|
+
</SwIconButton>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
<transition name="filter-collapse">
|
|
75
|
+
<div v-if="isFilterVisible" class="self-stretch flex flex-col justify-start items-start gap-4">
|
|
76
|
+
<div class="flex flex-row items-center gap-2 mt-2">
|
|
77
|
+
<div
|
|
78
|
+
v-for="i in 5"
|
|
79
|
+
:key="i"
|
|
80
|
+
:class="['h-6 w-6 cursor-pointer', displayedScore >= i ? 'i-carbon-star-filled' : 'i-carbon-star']"
|
|
81
|
+
@mouseleave="isHoverActive = false"
|
|
82
|
+
@click="hoverRating(i); onChangeRating()"
|
|
83
|
+
@mouseover="hoverRating(i)"
|
|
84
|
+
:aria-label="`${i} star${i !== 1 ? 's' : ''}`"
|
|
85
|
+
/>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</transition>
|
|
89
|
+
</div>
|
|
90
|
+
</template>
|