@shopware/cms-base-layer 0.0.0-canary-20250116171244
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/LICENSE +21 -0
- package/README.md +144 -0
- package/components/SwCategoryNavigation.vue +44 -0
- package/components/SwCategoryNavigationLink.vue +57 -0
- package/components/SwContactForm.vue +392 -0
- package/components/SwListingProductPrice.vue +88 -0
- package/components/SwMedia3D.vue +34 -0
- package/components/SwNewsletterForm.vue +347 -0
- package/components/SwPagination.vue +106 -0
- package/components/SwProductAddToCart.vue +93 -0
- package/components/SwProductCard.vue +285 -0
- package/components/SwProductGallery.vue +39 -0
- package/components/SwProductListingFilter.vue +42 -0
- package/components/SwProductListingFilters.vue +292 -0
- package/components/SwProductPrice.vue +99 -0
- package/components/SwProductReviews.vue +99 -0
- package/components/SwProductUnits.vue +54 -0
- package/components/SwSharedPrice.vue +19 -0
- package/components/SwSlider.vue +328 -0
- package/components/SwVariantConfigurator.vue +116 -0
- package/components/listing-filters/SwFilterPrice.vue +160 -0
- package/components/listing-filters/SwFilterProperties.vue +123 -0
- package/components/listing-filters/SwFilterRating.vue +101 -0
- package/components/listing-filters/SwFilterShippingFree.vue +104 -0
- package/components/public/cms/CmsGenericBlock.md +27 -0
- package/components/public/cms/CmsGenericBlock.vue +63 -0
- package/components/public/cms/CmsGenericElement.md +31 -0
- package/components/public/cms/CmsGenericElement.vue +38 -0
- package/components/public/cms/CmsNoComponent.vue +27 -0
- package/components/public/cms/CmsPage.md +36 -0
- package/components/public/cms/CmsPage.vue +65 -0
- package/components/public/cms/block/CmsBlockCategoryNavigation.vue +16 -0
- package/components/public/cms/block/CmsBlockCenterText.vue +26 -0
- package/components/public/cms/block/CmsBlockCrossSelling.vue +15 -0
- package/components/public/cms/block/CmsBlockCustomForm.vue +17 -0
- package/components/public/cms/block/CmsBlockDefault.vue +14 -0
- package/components/public/cms/block/CmsBlockForm.vue +17 -0
- package/components/public/cms/block/CmsBlockGalleryBuybox.vue +25 -0
- package/components/public/cms/block/CmsBlockImage.vue +16 -0
- package/components/public/cms/block/CmsBlockImageBubbleRow.vue +32 -0
- package/components/public/cms/block/CmsBlockImageCover.vue +17 -0
- package/components/public/cms/block/CmsBlockImageFourColumn.vue +29 -0
- package/components/public/cms/block/CmsBlockImageGallery.vue +18 -0
- package/components/public/cms/block/CmsBlockImageHighlightRow.vue +27 -0
- package/components/public/cms/block/CmsBlockImageSimpleGrid.vue +24 -0
- package/components/public/cms/block/CmsBlockImageSlider.vue +17 -0
- package/components/public/cms/block/CmsBlockImageText.vue +19 -0
- package/components/public/cms/block/CmsBlockImageTextBubble.vue +51 -0
- package/components/public/cms/block/CmsBlockImageTextCover.vue +25 -0
- package/components/public/cms/block/CmsBlockImageTextGallery.vue +85 -0
- package/components/public/cms/block/CmsBlockImageTextRow.vue +43 -0
- package/components/public/cms/block/CmsBlockImageThreeColumn.vue +21 -0
- package/components/public/cms/block/CmsBlockImageThreeCover.vue +27 -0
- package/components/public/cms/block/CmsBlockImageTwoColumn.vue +25 -0
- package/components/public/cms/block/CmsBlockProductDescriptionReviews.vue +15 -0
- package/components/public/cms/block/CmsBlockProductHeading.vue +26 -0
- package/components/public/cms/block/CmsBlockProductListing.vue +17 -0
- package/components/public/cms/block/CmsBlockProductSlider.vue +16 -0
- package/components/public/cms/block/CmsBlockProductThreeColumn.vue +22 -0
- package/components/public/cms/block/CmsBlockSidebarFilter.vue +17 -0
- package/components/public/cms/block/CmsBlockText.vue +15 -0
- package/components/public/cms/block/CmsBlockTextHero.vue +15 -0
- package/components/public/cms/block/CmsBlockTextOnImage.vue +20 -0
- package/components/public/cms/block/CmsBlockTextTeaser.vue +16 -0
- package/components/public/cms/block/CmsBlockTextTeaserSection.vue +21 -0
- package/components/public/cms/block/CmsBlockTextThreeColumn.vue +22 -0
- package/components/public/cms/block/CmsBlockTextTwoColumn.vue +28 -0
- package/components/public/cms/block/CmsBlockVimeoVideo.vue +17 -0
- package/components/public/cms/block/CmsBlockYoutubeVideo.vue +17 -0
- package/components/public/cms/element/CmsElementBuyBox.md +1 -0
- package/components/public/cms/element/CmsElementBuyBox.vue +190 -0
- package/components/public/cms/element/CmsElementCategoryNavigation.md +1 -0
- package/components/public/cms/element/CmsElementCategoryNavigation.vue +167 -0
- package/components/public/cms/element/CmsElementCrossSelling.md +1 -0
- package/components/public/cms/element/CmsElementCrossSelling.vue +106 -0
- package/components/public/cms/element/CmsElementCustomForm.md +1 -0
- package/components/public/cms/element/CmsElementCustomForm.vue +27 -0
- package/components/public/cms/element/CmsElementForm.md +1 -0
- package/components/public/cms/element/CmsElementForm.vue +27 -0
- package/components/public/cms/element/CmsElementImage.md +1 -0
- package/components/public/cms/element/CmsElementImage.vue +105 -0
- package/components/public/cms/element/CmsElementImageGallery.md +1 -0
- package/components/public/cms/element/CmsElementImageGallery.vue +249 -0
- package/components/public/cms/element/CmsElementImageGallery3dPlaceholder.vue +53 -0
- package/components/public/cms/element/CmsElementImageSlider.md +1 -0
- package/components/public/cms/element/CmsElementImageSlider.vue +29 -0
- package/components/public/cms/element/CmsElementManufacturerLogo.md +1 -0
- package/components/public/cms/element/CmsElementManufacturerLogo.vue +11 -0
- package/components/public/cms/element/CmsElementProductBox.md +1 -0
- package/components/public/cms/element/CmsElementProductBox.vue +14 -0
- package/components/public/cms/element/CmsElementProductDescriptionReviews.md +1 -0
- package/components/public/cms/element/CmsElementProductDescriptionReviews.vue +109 -0
- package/components/public/cms/element/CmsElementProductListing.md +1 -0
- package/components/public/cms/element/CmsElementProductListing.vue +245 -0
- package/components/public/cms/element/CmsElementProductName.md +1 -0
- package/components/public/cms/element/CmsElementProductName.vue +10 -0
- package/components/public/cms/element/CmsElementProductSlider.md +1 -0
- package/components/public/cms/element/CmsElementProductSlider.vue +80 -0
- package/components/public/cms/element/CmsElementSidebarFilter.md +1 -0
- package/components/public/cms/element/CmsElementSidebarFilter.vue +12 -0
- package/components/public/cms/element/CmsElementText.md +1 -0
- package/components/public/cms/element/CmsElementText.vue +186 -0
- package/components/public/cms/element/CmsElementVimeoVideo.md +1 -0
- package/components/public/cms/element/CmsElementVimeoVideo.vue +63 -0
- package/components/public/cms/element/CmsElementYoutubeVideo.md +1 -0
- package/components/public/cms/element/CmsElementYoutubeVideo.vue +43 -0
- package/components/public/cms/section/CmsSectionDefault.md +3 -0
- package/components/public/cms/section/CmsSectionDefault.vue +21 -0
- package/components/public/cms/section/CmsSectionSidebar.md +3 -0
- package/components/public/cms/section/CmsSectionSidebar.vue +49 -0
- package/components/public/cms/skeleton/ProductCardSkeleton.vue +44 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.mjs +31 -0
- package/helpers/clientOnly.ts +11 -0
- package/helpers/html-to-vue/ast.ts +72 -0
- package/helpers/html-to-vue/getOptionsFromNode.test.ts +129 -0
- package/helpers/html-to-vue/getOptionsFromNode.ts +52 -0
- package/helpers/html-to-vue/renderToHtml.ts +45 -0
- package/helpers/html-to-vue/renderer.ts +56 -0
- package/helpers/media/isSpatial.ts +8 -0
- package/index.cjs +7 -0
- package/nuxt.config.ts +21 -0
- package/package.json +69 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { CmsElementProductListing } from "@shopware/composables";
|
|
3
|
+
import { useCmsTranslations } from "@shopware/composables";
|
|
4
|
+
import { defu } from "defu";
|
|
5
|
+
import { computed, ref, useTemplateRef, watch } from "vue";
|
|
6
|
+
import { useRoute, useRouter } from "vue-router";
|
|
7
|
+
import { useCategoryListing } from "#imports";
|
|
8
|
+
import type { Schemas, operations } from "#shopware";
|
|
9
|
+
|
|
10
|
+
const props = defineProps<{
|
|
11
|
+
content: CmsElementProductListing;
|
|
12
|
+
}>();
|
|
13
|
+
|
|
14
|
+
const defaultLimit = 15;
|
|
15
|
+
const defaultPage = 1;
|
|
16
|
+
const defaultOrder = "name-asc";
|
|
17
|
+
const productListElement = useTemplateRef("productListElement");
|
|
18
|
+
|
|
19
|
+
type Translations = {
|
|
20
|
+
listing: {
|
|
21
|
+
noProducts: string;
|
|
22
|
+
perPage: string;
|
|
23
|
+
product: string;
|
|
24
|
+
products: string;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
let translations: Translations = {
|
|
28
|
+
listing: {
|
|
29
|
+
noProducts: "No products found 😔",
|
|
30
|
+
perPage: "Per Page:",
|
|
31
|
+
product: "Product",
|
|
32
|
+
products: "Products",
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
translations = defu(useCmsTranslations(), translations) as Translations;
|
|
36
|
+
|
|
37
|
+
const {
|
|
38
|
+
changeCurrentPage,
|
|
39
|
+
getCurrentPage,
|
|
40
|
+
getElements,
|
|
41
|
+
getTotalPagesCount,
|
|
42
|
+
loading,
|
|
43
|
+
setInitialListing,
|
|
44
|
+
} = useCategoryListing();
|
|
45
|
+
const route = useRoute();
|
|
46
|
+
const router = useRouter();
|
|
47
|
+
const limit = ref(
|
|
48
|
+
route.query.limit
|
|
49
|
+
? Number(route.query.limit)
|
|
50
|
+
: props.content?.data?.listing?.limit
|
|
51
|
+
? Number(props.content?.data?.listing?.limit)
|
|
52
|
+
: defaultLimit,
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const initalRoute = defu(route);
|
|
56
|
+
watch(
|
|
57
|
+
() => route,
|
|
58
|
+
(newRoute) => {
|
|
59
|
+
if (initalRoute.path !== newRoute.path) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (Object.keys(newRoute.query).length > 0) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
// this fires to reset the page when query are removed/empty on client side navigation for the same page (without hard reload)
|
|
66
|
+
changeCurrentPage(defaultPage, {
|
|
67
|
+
limit: defaultLimit,
|
|
68
|
+
p: defaultPage,
|
|
69
|
+
order: defaultOrder,
|
|
70
|
+
} as unknown as operations["searchPage post /search"]["body"]);
|
|
71
|
+
},
|
|
72
|
+
{ deep: true },
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const changePage = async (page: number) => {
|
|
76
|
+
await router.push({
|
|
77
|
+
query: {
|
|
78
|
+
...route.query,
|
|
79
|
+
p: page,
|
|
80
|
+
limit: limit.value,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
await changeCurrentPage(
|
|
84
|
+
page,
|
|
85
|
+
route.query as unknown as operations["searchPage post /search"]["body"],
|
|
86
|
+
);
|
|
87
|
+
productListElement.value?.scrollIntoView({ behavior: "smooth" });
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const changeLimit = async (limit: Event) => {
|
|
91
|
+
const select = limit.target as HTMLSelectElement;
|
|
92
|
+
|
|
93
|
+
await router.push({
|
|
94
|
+
query: {
|
|
95
|
+
...route.query,
|
|
96
|
+
limit: select.value,
|
|
97
|
+
p: defaultPage,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
await changeCurrentPage(
|
|
101
|
+
defaultPage,
|
|
102
|
+
route.query as unknown as operations["searchPage post /search"]["body"],
|
|
103
|
+
);
|
|
104
|
+
productListElement.value?.scrollIntoView({ behavior: "smooth" });
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const isProductListing = computed(
|
|
108
|
+
() => props.content?.type === "product-listing",
|
|
109
|
+
);
|
|
110
|
+
// This is a workaround because vercel caching with the nuxt preset does not support query params at the moment
|
|
111
|
+
// @see https://github.com/shopware/frontends/issues/687#issuecomment-1988392091
|
|
112
|
+
const compareRouteQueryWithInitialListing = async () => {
|
|
113
|
+
const limitListing = props?.content?.data?.listing.limit ?? defaultLimit;
|
|
114
|
+
const pageListing = props?.content?.data?.listing.page ?? defaultPage;
|
|
115
|
+
const orderListing = props?.content?.data?.listing.sorting ?? defaultOrder;
|
|
116
|
+
|
|
117
|
+
const isChangePageNeeded =
|
|
118
|
+
(route.query.limit && limit.value !== limitListing) ||
|
|
119
|
+
(route.query.p && Number(route.query.p) !== pageListing) ||
|
|
120
|
+
(route.query.order && route.query.order !== orderListing);
|
|
121
|
+
|
|
122
|
+
if (isChangePageNeeded) {
|
|
123
|
+
const limitQuery = route.query.limit
|
|
124
|
+
? Number(route.query.limit)
|
|
125
|
+
: defaultLimit;
|
|
126
|
+
const pageQuery = route.query.p ? Number(route.query.p) : defaultPage;
|
|
127
|
+
const orderQuery = route.query.order
|
|
128
|
+
? (route.query.order as string)
|
|
129
|
+
: defaultOrder;
|
|
130
|
+
const newQuery = {
|
|
131
|
+
limit: limitQuery,
|
|
132
|
+
p: pageQuery,
|
|
133
|
+
order: orderQuery,
|
|
134
|
+
};
|
|
135
|
+
console.warn(
|
|
136
|
+
"The current route does not match the initial listing. Changing the route to match the initial listing.",
|
|
137
|
+
);
|
|
138
|
+
limit.value = limitQuery;
|
|
139
|
+
await changeCurrentPage(
|
|
140
|
+
pageQuery,
|
|
141
|
+
newQuery as unknown as operations["searchPage post /search"]["body"],
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
setInitialListing(
|
|
147
|
+
props?.content?.data?.listing as Schemas["ProductListingResult"],
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
compareRouteQueryWithInitialListing();
|
|
151
|
+
</script>
|
|
152
|
+
|
|
153
|
+
<template>
|
|
154
|
+
<div class="bg-white">
|
|
155
|
+
<div class="max-w-2xl mx-auto lg:max-w-full">
|
|
156
|
+
<div class="mt-6">
|
|
157
|
+
<div
|
|
158
|
+
v-if="!loading"
|
|
159
|
+
ref="productListElement"
|
|
160
|
+
class="flex justify-center flex-wrap p-4 md:p-6 lg:p-8 productListElement"
|
|
161
|
+
>
|
|
162
|
+
<SwProductCard
|
|
163
|
+
v-for="product in getElements"
|
|
164
|
+
:key="product.id"
|
|
165
|
+
:product="product"
|
|
166
|
+
:is-product-listing="isProductListing"
|
|
167
|
+
class="p-4 border rounded-lg shadow-md hover:shadow-lg transition-shadow duration-200 ease-in-out w-full lg:w-3/7 2xl:w-7/24 mr-0 sm:mr-8 mb-8"
|
|
168
|
+
/>
|
|
169
|
+
</div>
|
|
170
|
+
<div
|
|
171
|
+
v-if="loading"
|
|
172
|
+
data-testid="loading"
|
|
173
|
+
class="flex justify-center flex-wrap p-4 md:p-6 lg:p-8"
|
|
174
|
+
>
|
|
175
|
+
<ProductCardSkeleton
|
|
176
|
+
v-for="index in limit"
|
|
177
|
+
:key="index"
|
|
178
|
+
class="w-full mb-8 sm:w-3/7 lg:w-2/7 2xl:w-7/24 mr-0 sm:mr-8 mb-8"
|
|
179
|
+
/>
|
|
180
|
+
</div>
|
|
181
|
+
<div
|
|
182
|
+
class="grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-6 lg:gap-8 p-4 md:p-6 lg:p-8"
|
|
183
|
+
>
|
|
184
|
+
<div class="text-center place-self-center">
|
|
185
|
+
<SwPagination
|
|
186
|
+
:total="getTotalPagesCount"
|
|
187
|
+
:current="Number(getCurrentPage)"
|
|
188
|
+
@change-page="changePage"
|
|
189
|
+
/>
|
|
190
|
+
</div>
|
|
191
|
+
<div class="text-center place-self-center mt-2 lg:mt-0">
|
|
192
|
+
<div
|
|
193
|
+
class="inline-block align-top text-center md:text-left"
|
|
194
|
+
data-testid="listing-pagination-limit-box"
|
|
195
|
+
>
|
|
196
|
+
<label
|
|
197
|
+
for="limit"
|
|
198
|
+
class="inline mr-4"
|
|
199
|
+
data-testid="listing-pagination-limit-label"
|
|
200
|
+
>{{ translations.listing.perPage }}</label
|
|
201
|
+
>
|
|
202
|
+
<select
|
|
203
|
+
id="limit"
|
|
204
|
+
v-model="limit"
|
|
205
|
+
name="limitchoices"
|
|
206
|
+
class="inline appearance-none bg-white border border-gray-400 hover:border-gray-500 px-4 py-2 pr-8 rounded shadow leading-tight focus:outline-none focus:shadow-outline"
|
|
207
|
+
data-testid="listing-pagination-limit-select"
|
|
208
|
+
@change="changeLimit"
|
|
209
|
+
>
|
|
210
|
+
<option :value="1">1 {{ translations.listing.product }}</option>
|
|
211
|
+
<option :value="15">
|
|
212
|
+
15 {{ translations.listing.products }}
|
|
213
|
+
</option>
|
|
214
|
+
<option :value="30">
|
|
215
|
+
30 {{ translations.listing.products }}
|
|
216
|
+
</option>
|
|
217
|
+
<option :value="45">
|
|
218
|
+
45 {{ translations.listing.products }}
|
|
219
|
+
</option>
|
|
220
|
+
</select>
|
|
221
|
+
<div
|
|
222
|
+
class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700"
|
|
223
|
+
>
|
|
224
|
+
<svg
|
|
225
|
+
class="fill-current h-4 w-4"
|
|
226
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
227
|
+
viewBox="0 0 20 20"
|
|
228
|
+
>
|
|
229
|
+
<path
|
|
230
|
+
d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"
|
|
231
|
+
/>
|
|
232
|
+
</svg>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
<!-- <div v-else>
|
|
239
|
+
<h2 class="mx-auto text-center">
|
|
240
|
+
{{ translations.listing.noProducts }}
|
|
241
|
+
</h2>
|
|
242
|
+
</div> -->
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
</template>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Display a name for a product
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Display a slider of provided products
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type {
|
|
3
|
+
CmsElementProductSlider,
|
|
4
|
+
SliderElementConfig,
|
|
5
|
+
} from "@shopware/composables";
|
|
6
|
+
import { computed, onMounted, ref, useTemplateRef } from "vue";
|
|
7
|
+
import type { ComputedRef } from "vue";
|
|
8
|
+
import { useCmsElementConfig } from "#imports";
|
|
9
|
+
|
|
10
|
+
const props = defineProps<{
|
|
11
|
+
content: CmsElementProductSlider;
|
|
12
|
+
}>();
|
|
13
|
+
const { getConfigValue } = useCmsElementConfig(props.content);
|
|
14
|
+
|
|
15
|
+
const productSlider = useTemplateRef("productSlider");
|
|
16
|
+
const slidesToShow = ref<number>();
|
|
17
|
+
const products = computed(() => props.content?.data?.products ?? []);
|
|
18
|
+
const config: ComputedRef<SliderElementConfig> = computed(() => ({
|
|
19
|
+
minHeight: {
|
|
20
|
+
value: "300px",
|
|
21
|
+
source: "static",
|
|
22
|
+
},
|
|
23
|
+
verticalAlign: {
|
|
24
|
+
source: "static",
|
|
25
|
+
value: getConfigValue("verticalAlign") || "",
|
|
26
|
+
},
|
|
27
|
+
displayMode: {
|
|
28
|
+
value: "contain",
|
|
29
|
+
source: "static",
|
|
30
|
+
},
|
|
31
|
+
navigationDots: {
|
|
32
|
+
value: "",
|
|
33
|
+
source: "static",
|
|
34
|
+
},
|
|
35
|
+
navigationArrows: {
|
|
36
|
+
value: getConfigValue("navigation") ? "outside" : "",
|
|
37
|
+
source: "static",
|
|
38
|
+
},
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
onMounted(() => {
|
|
42
|
+
setTimeout(() => {
|
|
43
|
+
let temp = 1;
|
|
44
|
+
const minWidth = +getConfigValue("elMinWidth").replace(/\D+/g, "");
|
|
45
|
+
if (productSlider.value?.clientWidth) {
|
|
46
|
+
temp = Math.ceil(productSlider.value?.clientWidth / (minWidth * 1.2));
|
|
47
|
+
}
|
|
48
|
+
slidesToShow.value = temp;
|
|
49
|
+
}, 100);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const autoplay = computed(() => getConfigValue("rotate"));
|
|
53
|
+
const title = computed(() => getConfigValue("title"));
|
|
54
|
+
const border = computed(() => getConfigValue("border"));
|
|
55
|
+
</script>
|
|
56
|
+
<template>
|
|
57
|
+
<div ref="productSlider" class="cms-element-product-slider">
|
|
58
|
+
<h3 v-if="title" class="mb-5 text-lg font-bold text-secondary-700">
|
|
59
|
+
{{ title }}
|
|
60
|
+
</h3>
|
|
61
|
+
<div :class="{ 'py-5 border border-secondary-300': border }">
|
|
62
|
+
<SwSlider
|
|
63
|
+
:config="config"
|
|
64
|
+
gap="1.25rem"
|
|
65
|
+
:slides-to-show="slidesToShow"
|
|
66
|
+
:slides-to-scroll="1"
|
|
67
|
+
:autoplay="autoplay"
|
|
68
|
+
>
|
|
69
|
+
<SwProductCard
|
|
70
|
+
v-for="product of products"
|
|
71
|
+
:key="product.id"
|
|
72
|
+
class="h-full"
|
|
73
|
+
:product="product"
|
|
74
|
+
:layout-type="getConfigValue('boxLayout')"
|
|
75
|
+
:display-mode="getConfigValue('displayMode')"
|
|
76
|
+
/>
|
|
77
|
+
</SwSlider>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</template>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Display a sidebar containing filters for an active product listing
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { CmsElementSidebarFilter } from "@shopware/composables";
|
|
3
|
+
|
|
4
|
+
defineProps<{
|
|
5
|
+
content: CmsElementSidebarFilter;
|
|
6
|
+
}>();
|
|
7
|
+
</script>
|
|
8
|
+
<template>
|
|
9
|
+
<div class="max-w-screen-xl mx-auto">
|
|
10
|
+
<SwProductListingFilters :content="content" />
|
|
11
|
+
</div>
|
|
12
|
+
</template>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Display a text. Html to Vue mechanism is used to render buttons, links, images accordingly as Vue elements
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { CmsElementText } from "@shopware/composables";
|
|
3
|
+
import { decodeHTML } from "entities";
|
|
4
|
+
import { computed, defineComponent, getCurrentInstance, h } from "vue";
|
|
5
|
+
import type { CSSProperties, VNode, VNodeArrayChildren } from "vue";
|
|
6
|
+
import { useCmsElementConfig, useUrlResolver } from "#imports";
|
|
7
|
+
import { getOptionsFromNode } from "../../../../helpers/html-to-vue/getOptionsFromNode";
|
|
8
|
+
import type { NodeObject } from "../../../../helpers/html-to-vue/getOptionsFromNode";
|
|
9
|
+
import { renderHtml } from "../../../../helpers/html-to-vue/renderToHtml";
|
|
10
|
+
type RawChildren = string | number | boolean | VNode | VNodeArrayChildren;
|
|
11
|
+
|
|
12
|
+
const props = defineProps<{
|
|
13
|
+
content: CmsElementText;
|
|
14
|
+
}>();
|
|
15
|
+
const context = getCurrentInstance();
|
|
16
|
+
const { getConfigValue } = useCmsElementConfig(props.content);
|
|
17
|
+
|
|
18
|
+
const mappedContent = computed<string>(() => {
|
|
19
|
+
return props.content?.data?.content || getConfigValue("content");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const style = computed<CSSProperties>(() => ({
|
|
23
|
+
alignItems: getConfigValue("verticalAlign"),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
const hasVerticalAlignment = computed(() => !!style.value.alignItems);
|
|
27
|
+
|
|
28
|
+
const CmsTextRender = defineComponent({
|
|
29
|
+
setup() {
|
|
30
|
+
const { resolveUrl } = useUrlResolver();
|
|
31
|
+
|
|
32
|
+
const config = {
|
|
33
|
+
textTransformer: (text: string) => decodeHTML(text),
|
|
34
|
+
extraComponentsMap: {
|
|
35
|
+
link: {
|
|
36
|
+
conditions(node: NodeObject) {
|
|
37
|
+
return (
|
|
38
|
+
node.type === "tag" &&
|
|
39
|
+
node.name === "a" &&
|
|
40
|
+
!node.attrs?.class?.match(/btn\s?/)
|
|
41
|
+
);
|
|
42
|
+
},
|
|
43
|
+
renderer(
|
|
44
|
+
node: NodeObject,
|
|
45
|
+
children: RawChildren[],
|
|
46
|
+
createElement: typeof h,
|
|
47
|
+
) {
|
|
48
|
+
return createElement(
|
|
49
|
+
"a",
|
|
50
|
+
{
|
|
51
|
+
class:
|
|
52
|
+
"underline text-base font-normal text-primary hover:text-secondary-900",
|
|
53
|
+
...getOptionsFromNode(node, resolveUrl).attrs,
|
|
54
|
+
},
|
|
55
|
+
[...children],
|
|
56
|
+
);
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
button: {
|
|
60
|
+
conditions(node: NodeObject) {
|
|
61
|
+
return (
|
|
62
|
+
node.type === "tag" &&
|
|
63
|
+
node.name === "a" &&
|
|
64
|
+
node.attrs?.class?.match(/btn\s?/)
|
|
65
|
+
);
|
|
66
|
+
},
|
|
67
|
+
renderer(
|
|
68
|
+
node: NodeObject,
|
|
69
|
+
children: RawChildren[],
|
|
70
|
+
createElement: typeof h,
|
|
71
|
+
) {
|
|
72
|
+
let _class = "";
|
|
73
|
+
if (node?.attrs?.class) {
|
|
74
|
+
const btnClass =
|
|
75
|
+
"rounded-md inline-block my-2 py-2 px-4 border border-transparent text-sm font-medium focus:outline-none disabled:opacity-75";
|
|
76
|
+
|
|
77
|
+
_class = node.attrs.class
|
|
78
|
+
.replace("btn-secondary", `${btnClass} bg-dark text-white`)
|
|
79
|
+
.replace("btn-primary", `${btnClass} bg-primary text-white`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return createElement(
|
|
83
|
+
"a",
|
|
84
|
+
{
|
|
85
|
+
class: _class,
|
|
86
|
+
...getOptionsFromNode(node, resolveUrl).attrs,
|
|
87
|
+
},
|
|
88
|
+
[...children],
|
|
89
|
+
);
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
font: {
|
|
93
|
+
conditions(node: NodeObject) {
|
|
94
|
+
return node.type === "tag" && node.name === "font";
|
|
95
|
+
},
|
|
96
|
+
renderer(
|
|
97
|
+
node: NodeObject,
|
|
98
|
+
children: RawChildren[],
|
|
99
|
+
createElement: typeof h,
|
|
100
|
+
) {
|
|
101
|
+
// convert from <font color="#ce0000">Headline 1</font> to <span style="color:#ce0000">Headline 1</span>
|
|
102
|
+
let newStyle = null;
|
|
103
|
+
const styleColor = node?.attrs?.color;
|
|
104
|
+
if (styleColor && node.attrs) {
|
|
105
|
+
const currentStyle = node.attrs?.style ?? "";
|
|
106
|
+
newStyle = `color:${styleColor};${currentStyle}`;
|
|
107
|
+
const { color: _, ...attrsWithoutColor } = node.attrs;
|
|
108
|
+
node.attrs = attrsWithoutColor;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return createElement(
|
|
112
|
+
"span",
|
|
113
|
+
{
|
|
114
|
+
style: newStyle,
|
|
115
|
+
...getOptionsFromNode(node, resolveUrl).attrs,
|
|
116
|
+
},
|
|
117
|
+
[...children],
|
|
118
|
+
);
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
img: {
|
|
122
|
+
conditions(node: NodeObject) {
|
|
123
|
+
return node.type === "tag" && node.name === "img";
|
|
124
|
+
},
|
|
125
|
+
renderer(
|
|
126
|
+
node: NodeObject,
|
|
127
|
+
children: RawChildren[],
|
|
128
|
+
createElement: typeof h,
|
|
129
|
+
) {
|
|
130
|
+
return createElement(
|
|
131
|
+
"img",
|
|
132
|
+
getOptionsFromNode(node, resolveUrl)?.attrs,
|
|
133
|
+
);
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
const rawHtml =
|
|
139
|
+
mappedContent.value?.length > 0
|
|
140
|
+
? mappedContent.value
|
|
141
|
+
: "<div class='cms-element-text missing-content-element'></div>";
|
|
142
|
+
|
|
143
|
+
return () =>
|
|
144
|
+
h("div", {}, renderHtml(rawHtml, config, h, context, resolveUrl));
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
</script>
|
|
148
|
+
<template>
|
|
149
|
+
<div
|
|
150
|
+
:class="{ flex: hasVerticalAlignment, 'flex-row': hasVerticalAlignment }"
|
|
151
|
+
:style="style"
|
|
152
|
+
>
|
|
153
|
+
<CmsTextRender />
|
|
154
|
+
</div>
|
|
155
|
+
</template>
|
|
156
|
+
<style scoped>
|
|
157
|
+
/** Global CSS styles for text elements */
|
|
158
|
+
h1,
|
|
159
|
+
h2,
|
|
160
|
+
h3,
|
|
161
|
+
h4,
|
|
162
|
+
h5 {
|
|
163
|
+
margin-bottom: 10px;
|
|
164
|
+
font-weight: 600;
|
|
165
|
+
}
|
|
166
|
+
h1 {
|
|
167
|
+
line-height: 2.5rem;
|
|
168
|
+
font-size: 2.25rem;
|
|
169
|
+
}
|
|
170
|
+
h2 {
|
|
171
|
+
line-height: 2rem;
|
|
172
|
+
font-size: 1.75rem;
|
|
173
|
+
}
|
|
174
|
+
h3 {
|
|
175
|
+
line-height: 1.5rem;
|
|
176
|
+
font-size: 1.25rem;
|
|
177
|
+
}
|
|
178
|
+
ol,
|
|
179
|
+
ul,
|
|
180
|
+
dl {
|
|
181
|
+
list-style-type: disc;
|
|
182
|
+
padding-left: 40px;
|
|
183
|
+
margin-top: 0;
|
|
184
|
+
margin-bottom: 1rem;
|
|
185
|
+
}
|
|
186
|
+
</style>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Display a player for Vimeo media
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { CmsElementVimeoVideo } from "@shopware/composables";
|
|
3
|
+
import { ref } from "vue";
|
|
4
|
+
import type { Ref } from "vue";
|
|
5
|
+
import { useCmsElementConfig } from "#imports";
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{
|
|
8
|
+
content: CmsElementVimeoVideo;
|
|
9
|
+
}>();
|
|
10
|
+
const { getConfigValue } = useCmsElementConfig(props.content);
|
|
11
|
+
// TODO CMS add proper mapping or config type. This Component needs rework.
|
|
12
|
+
type CmsElementVimeoVideoConfigKey = keyof CmsElementVimeoVideo["config"];
|
|
13
|
+
|
|
14
|
+
const vimeoConfigMapping = {
|
|
15
|
+
byLine: "byline",
|
|
16
|
+
color: "color",
|
|
17
|
+
doNotTrack: "dnt",
|
|
18
|
+
loop: "loop",
|
|
19
|
+
mute: "mute",
|
|
20
|
+
title: "title",
|
|
21
|
+
portrait: "portrait",
|
|
22
|
+
controls: "controls",
|
|
23
|
+
videoID: "videoID",
|
|
24
|
+
autoplay: "autoplay",
|
|
25
|
+
previewMedia: "previewMedia",
|
|
26
|
+
needsConfirmation: "needsConfirmation",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const videoUrl: Ref = ref(
|
|
30
|
+
`https://player.vimeo.com/video/${getConfigValue("videoID")}?`,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const convertAttr = (
|
|
34
|
+
value: string,
|
|
35
|
+
configKey: CmsElementVimeoVideoConfigKey,
|
|
36
|
+
) => {
|
|
37
|
+
if (configKey === "color")
|
|
38
|
+
return value
|
|
39
|
+
? `${vimeoConfigMapping[configKey]}=${value}&`.replace("#", "")
|
|
40
|
+
: "";
|
|
41
|
+
|
|
42
|
+
return value ? `${vimeoConfigMapping[configKey]}=${value}&` : "";
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
for (const key in props.content.config) {
|
|
46
|
+
if (Object.prototype.hasOwnProperty.call(vimeoConfigMapping, key)) {
|
|
47
|
+
videoUrl.value += convertAttr(
|
|
48
|
+
props.content.config[key as CmsElementVimeoVideoConfigKey]
|
|
49
|
+
.value as string,
|
|
50
|
+
key as CmsElementVimeoVideoConfigKey,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
</script>
|
|
55
|
+
<template>
|
|
56
|
+
<div class="cms-element-vimeo-video">
|
|
57
|
+
<iframe
|
|
58
|
+
class="w-full inset-0 aspect-video"
|
|
59
|
+
:src="videoUrl.replace(/ /g, '')"
|
|
60
|
+
>
|
|
61
|
+
</iframe>
|
|
62
|
+
</div>
|
|
63
|
+
</template>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Display a player for YouTube video
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { CmsElementYoutubeVideo } from "@shopware/composables";
|
|
3
|
+
import { computed } from "vue";
|
|
4
|
+
import { useCmsElementConfig } from "#imports";
|
|
5
|
+
|
|
6
|
+
const props = defineProps<{
|
|
7
|
+
content: CmsElementYoutubeVideo;
|
|
8
|
+
}>();
|
|
9
|
+
|
|
10
|
+
const { getConfigValue } = useCmsElementConfig(props.content);
|
|
11
|
+
|
|
12
|
+
const config = computed(() => ({
|
|
13
|
+
videoID: getConfigValue("videoID"),
|
|
14
|
+
relatedVideos: "rel=0&",
|
|
15
|
+
loop: getConfigValue("loop")
|
|
16
|
+
? `loop=1&playlist=${getConfigValue("videoID")}&`
|
|
17
|
+
: "",
|
|
18
|
+
showControls: getConfigValue("showControls") ? "controls=0&" : "",
|
|
19
|
+
start:
|
|
20
|
+
Number.parseInt(getConfigValue("start")) !== 0
|
|
21
|
+
? `start=${getConfigValue("start")}&`
|
|
22
|
+
: "",
|
|
23
|
+
end:
|
|
24
|
+
Number.parseInt(getConfigValue("end")) !== 0
|
|
25
|
+
? `end=${getConfigValue("end")}&`
|
|
26
|
+
: "",
|
|
27
|
+
disableKeyboard: "disablekb=1",
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
const videoUrl = `https://www.youtube-nocookie.com/embed/\
|
|
31
|
+
${config.value.videoID}?\
|
|
32
|
+
${config.value.relatedVideos}\
|
|
33
|
+
${config.value.loop}\
|
|
34
|
+
${config.value.showControls}\
|
|
35
|
+
${config.value.start}\
|
|
36
|
+
${config.value.end}\
|
|
37
|
+
${config.value.disableKeyboard}`.replace(/ /g, "");
|
|
38
|
+
</script>
|
|
39
|
+
<template>
|
|
40
|
+
<div class="cms-element-youtube-video">
|
|
41
|
+
<iframe class="w-full inset-0 aspect-video" :src="videoUrl"> </iframe>
|
|
42
|
+
</div>
|
|
43
|
+
</template>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { CmsSectionDefault } from "@shopware/composables";
|
|
3
|
+
import { getCmsLayoutConfiguration } from "@shopware/helpers";
|
|
4
|
+
|
|
5
|
+
const props = defineProps<{
|
|
6
|
+
content: CmsSectionDefault;
|
|
7
|
+
}>();
|
|
8
|
+
|
|
9
|
+
const { cssClasses, layoutStyles } = getCmsLayoutConfiguration(props.content);
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<template>
|
|
13
|
+
<div class="cms-section-default" :class="cssClasses" :styles="layoutStyles">
|
|
14
|
+
<CmsGenericBlock
|
|
15
|
+
v-for="cmsBlock in content.blocks"
|
|
16
|
+
:key="cmsBlock.id"
|
|
17
|
+
class="overflow-auto"
|
|
18
|
+
:content="cmsBlock"
|
|
19
|
+
/>
|
|
20
|
+
</div>
|
|
21
|
+
</template>
|