@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.
Files changed (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +144 -0
  3. package/components/SwCategoryNavigation.vue +44 -0
  4. package/components/SwCategoryNavigationLink.vue +57 -0
  5. package/components/SwContactForm.vue +392 -0
  6. package/components/SwListingProductPrice.vue +88 -0
  7. package/components/SwMedia3D.vue +34 -0
  8. package/components/SwNewsletterForm.vue +347 -0
  9. package/components/SwPagination.vue +106 -0
  10. package/components/SwProductAddToCart.vue +93 -0
  11. package/components/SwProductCard.vue +285 -0
  12. package/components/SwProductGallery.vue +39 -0
  13. package/components/SwProductListingFilter.vue +42 -0
  14. package/components/SwProductListingFilters.vue +292 -0
  15. package/components/SwProductPrice.vue +99 -0
  16. package/components/SwProductReviews.vue +99 -0
  17. package/components/SwProductUnits.vue +54 -0
  18. package/components/SwSharedPrice.vue +19 -0
  19. package/components/SwSlider.vue +328 -0
  20. package/components/SwVariantConfigurator.vue +116 -0
  21. package/components/listing-filters/SwFilterPrice.vue +160 -0
  22. package/components/listing-filters/SwFilterProperties.vue +123 -0
  23. package/components/listing-filters/SwFilterRating.vue +101 -0
  24. package/components/listing-filters/SwFilterShippingFree.vue +104 -0
  25. package/components/public/cms/CmsGenericBlock.md +27 -0
  26. package/components/public/cms/CmsGenericBlock.vue +63 -0
  27. package/components/public/cms/CmsGenericElement.md +31 -0
  28. package/components/public/cms/CmsGenericElement.vue +38 -0
  29. package/components/public/cms/CmsNoComponent.vue +27 -0
  30. package/components/public/cms/CmsPage.md +36 -0
  31. package/components/public/cms/CmsPage.vue +65 -0
  32. package/components/public/cms/block/CmsBlockCategoryNavigation.vue +16 -0
  33. package/components/public/cms/block/CmsBlockCenterText.vue +26 -0
  34. package/components/public/cms/block/CmsBlockCrossSelling.vue +15 -0
  35. package/components/public/cms/block/CmsBlockCustomForm.vue +17 -0
  36. package/components/public/cms/block/CmsBlockDefault.vue +14 -0
  37. package/components/public/cms/block/CmsBlockForm.vue +17 -0
  38. package/components/public/cms/block/CmsBlockGalleryBuybox.vue +25 -0
  39. package/components/public/cms/block/CmsBlockImage.vue +16 -0
  40. package/components/public/cms/block/CmsBlockImageBubbleRow.vue +32 -0
  41. package/components/public/cms/block/CmsBlockImageCover.vue +17 -0
  42. package/components/public/cms/block/CmsBlockImageFourColumn.vue +29 -0
  43. package/components/public/cms/block/CmsBlockImageGallery.vue +18 -0
  44. package/components/public/cms/block/CmsBlockImageHighlightRow.vue +27 -0
  45. package/components/public/cms/block/CmsBlockImageSimpleGrid.vue +24 -0
  46. package/components/public/cms/block/CmsBlockImageSlider.vue +17 -0
  47. package/components/public/cms/block/CmsBlockImageText.vue +19 -0
  48. package/components/public/cms/block/CmsBlockImageTextBubble.vue +51 -0
  49. package/components/public/cms/block/CmsBlockImageTextCover.vue +25 -0
  50. package/components/public/cms/block/CmsBlockImageTextGallery.vue +85 -0
  51. package/components/public/cms/block/CmsBlockImageTextRow.vue +43 -0
  52. package/components/public/cms/block/CmsBlockImageThreeColumn.vue +21 -0
  53. package/components/public/cms/block/CmsBlockImageThreeCover.vue +27 -0
  54. package/components/public/cms/block/CmsBlockImageTwoColumn.vue +25 -0
  55. package/components/public/cms/block/CmsBlockProductDescriptionReviews.vue +15 -0
  56. package/components/public/cms/block/CmsBlockProductHeading.vue +26 -0
  57. package/components/public/cms/block/CmsBlockProductListing.vue +17 -0
  58. package/components/public/cms/block/CmsBlockProductSlider.vue +16 -0
  59. package/components/public/cms/block/CmsBlockProductThreeColumn.vue +22 -0
  60. package/components/public/cms/block/CmsBlockSidebarFilter.vue +17 -0
  61. package/components/public/cms/block/CmsBlockText.vue +15 -0
  62. package/components/public/cms/block/CmsBlockTextHero.vue +15 -0
  63. package/components/public/cms/block/CmsBlockTextOnImage.vue +20 -0
  64. package/components/public/cms/block/CmsBlockTextTeaser.vue +16 -0
  65. package/components/public/cms/block/CmsBlockTextTeaserSection.vue +21 -0
  66. package/components/public/cms/block/CmsBlockTextThreeColumn.vue +22 -0
  67. package/components/public/cms/block/CmsBlockTextTwoColumn.vue +28 -0
  68. package/components/public/cms/block/CmsBlockVimeoVideo.vue +17 -0
  69. package/components/public/cms/block/CmsBlockYoutubeVideo.vue +17 -0
  70. package/components/public/cms/element/CmsElementBuyBox.md +1 -0
  71. package/components/public/cms/element/CmsElementBuyBox.vue +190 -0
  72. package/components/public/cms/element/CmsElementCategoryNavigation.md +1 -0
  73. package/components/public/cms/element/CmsElementCategoryNavigation.vue +167 -0
  74. package/components/public/cms/element/CmsElementCrossSelling.md +1 -0
  75. package/components/public/cms/element/CmsElementCrossSelling.vue +106 -0
  76. package/components/public/cms/element/CmsElementCustomForm.md +1 -0
  77. package/components/public/cms/element/CmsElementCustomForm.vue +27 -0
  78. package/components/public/cms/element/CmsElementForm.md +1 -0
  79. package/components/public/cms/element/CmsElementForm.vue +27 -0
  80. package/components/public/cms/element/CmsElementImage.md +1 -0
  81. package/components/public/cms/element/CmsElementImage.vue +105 -0
  82. package/components/public/cms/element/CmsElementImageGallery.md +1 -0
  83. package/components/public/cms/element/CmsElementImageGallery.vue +249 -0
  84. package/components/public/cms/element/CmsElementImageGallery3dPlaceholder.vue +53 -0
  85. package/components/public/cms/element/CmsElementImageSlider.md +1 -0
  86. package/components/public/cms/element/CmsElementImageSlider.vue +29 -0
  87. package/components/public/cms/element/CmsElementManufacturerLogo.md +1 -0
  88. package/components/public/cms/element/CmsElementManufacturerLogo.vue +11 -0
  89. package/components/public/cms/element/CmsElementProductBox.md +1 -0
  90. package/components/public/cms/element/CmsElementProductBox.vue +14 -0
  91. package/components/public/cms/element/CmsElementProductDescriptionReviews.md +1 -0
  92. package/components/public/cms/element/CmsElementProductDescriptionReviews.vue +109 -0
  93. package/components/public/cms/element/CmsElementProductListing.md +1 -0
  94. package/components/public/cms/element/CmsElementProductListing.vue +245 -0
  95. package/components/public/cms/element/CmsElementProductName.md +1 -0
  96. package/components/public/cms/element/CmsElementProductName.vue +10 -0
  97. package/components/public/cms/element/CmsElementProductSlider.md +1 -0
  98. package/components/public/cms/element/CmsElementProductSlider.vue +80 -0
  99. package/components/public/cms/element/CmsElementSidebarFilter.md +1 -0
  100. package/components/public/cms/element/CmsElementSidebarFilter.vue +12 -0
  101. package/components/public/cms/element/CmsElementText.md +1 -0
  102. package/components/public/cms/element/CmsElementText.vue +186 -0
  103. package/components/public/cms/element/CmsElementVimeoVideo.md +1 -0
  104. package/components/public/cms/element/CmsElementVimeoVideo.vue +63 -0
  105. package/components/public/cms/element/CmsElementYoutubeVideo.md +1 -0
  106. package/components/public/cms/element/CmsElementYoutubeVideo.vue +43 -0
  107. package/components/public/cms/section/CmsSectionDefault.md +3 -0
  108. package/components/public/cms/section/CmsSectionDefault.vue +21 -0
  109. package/components/public/cms/section/CmsSectionSidebar.md +3 -0
  110. package/components/public/cms/section/CmsSectionSidebar.vue +49 -0
  111. package/components/public/cms/skeleton/ProductCardSkeleton.vue +44 -0
  112. package/dist/index.d.mts +5 -0
  113. package/dist/index.d.ts +5 -0
  114. package/dist/index.mjs +31 -0
  115. package/helpers/clientOnly.ts +11 -0
  116. package/helpers/html-to-vue/ast.ts +72 -0
  117. package/helpers/html-to-vue/getOptionsFromNode.test.ts +129 -0
  118. package/helpers/html-to-vue/getOptionsFromNode.ts +52 -0
  119. package/helpers/html-to-vue/renderToHtml.ts +45 -0
  120. package/helpers/html-to-vue/renderer.ts +56 -0
  121. package/helpers/media/isSpatial.ts +8 -0
  122. package/index.cjs +7 -0
  123. package/nuxt.config.ts +21 -0
  124. 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,10 @@
1
+ <script setup lang="ts">
2
+ import type { CmsElementProductName } from "@shopware/composables";
3
+
4
+ defineProps<{
5
+ content: CmsElementProductName;
6
+ }>();
7
+ </script>
8
+ <template>
9
+ <CmsElementText :content="content as any" />
10
+ </template>
@@ -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,3 @@
1
+ **Renders a generic block type**
2
+
3
+ See the [`<CmsPage/>`](#cmspage) source code to see how it's used
@@ -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>
@@ -0,0 +1,3 @@
1
+ **Renders a generic block type**
2
+
3
+ See the [`<CmsPage/>`](#cmspage) source code to see how it's used