@iqual/nuxt-icms 1.6.1 → 1.6.2-rc.2
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 +86 -0
- package/__tests__/anchors.spec.ts +43 -0
- package/__tests__/contract/blokkliComponents.spec.ts +59 -0
- package/__tests__/contract/componentFragments.spec.ts +24 -0
- package/__tests__/contract/languages.spec.ts +63 -0
- package/__tests__/contract/project.ts +144 -0
- package/__tests__/hreflang.spec.ts +39 -0
- package/{tests → __tests__}/pages.spec.ts +14 -5
- package/app.config.ts +15 -0
- package/app.vue +3 -21
- package/assets/css/backend.css +56 -0
- package/assets/css/base/base.css +2 -2
- package/assets/css/base/typography.css +2 -2
- package/assets/css/ckeditor.css +3 -4
- package/assets/css/components/decorations.css +1 -1
- package/assets/css/components/editor.css +1 -1
- package/assets/css/components/grid-container.css +19 -19
- package/assets/css/fe-fixes.css +6 -12
- package/assets/css/theme/colors/color-primary.css +33 -11
- package/assets/css/theme/colors/color-secondary.css +33 -11
- package/assets/css/utilities/utilities.css +1 -1
- package/assets/symbols/anchor.svg +3 -0
- package/blokkli.editAdapter.ts +56 -0
- package/bundles.ts +6 -6
- package/cache/__tests__/pageCache.spec.ts +540 -0
- package/cache/cacheTagRegistry.ts +124 -0
- package/cache/enabledForRequest.ts +21 -0
- package/cache/multiCache.serverOptions.ts +58 -0
- package/cache/pageCache.ts +360 -0
- package/components/Blokkli/Anchor/icon.svg +3 -0
- package/components/Blokkli/Anchor/index.vue +13 -0
- package/components/Blokkli/AnchorNavigation/icon.svg +8 -0
- package/components/Blokkli/AnchorNavigation/index.vue +9 -0
- package/components/Blokkli/Card/Element/index.vue +0 -1
- package/components/Blokkli/Collapsible/Element/index.vue +5 -0
- package/components/Blokkli/FAQ/Element/index.vue +0 -1
- package/components/Blokkli/Fragment/Separator/icon.svg +4 -0
- package/components/Blokkli/Fragment/Separator/index.vue +87 -0
- package/components/Blokkli/Gallery/index.vue +1 -3
- package/components/Blokkli/IconText/Element/index.vue +0 -1
- package/components/Blokkli/Introduction/index.vue +0 -1
- package/components/Blokkli/Listing/icon.svg +9 -0
- package/components/Blokkli/Listing/index.vue +24 -0
- package/components/Blokkli/Logo/Element/index.vue +0 -1
- package/components/Blokkli/Table/index.vue +0 -1
- package/components/Blokkli/Team/Element/index.vue +0 -1
- package/components/Blokkli/Team/Layout/index.vue +0 -1
- package/components/Blokkli/Testimonial/Element/index.vue +0 -1
- package/components/Blokkli/Text/index.vue +0 -1
- package/components/DesignSystem/Atoms/Button.vue +3 -0
- package/components/DesignSystem/Atoms/GridRow.vue +3 -1
- package/components/DesignSystem/Molecules/ButtonGroup.vue +5 -4
- package/components/DesignSystem/Molecules/IconText.vue +6 -6
- package/components/DesignSystem/Molecules/MediaLightbox.vue +8 -8
- package/components/DesignSystem/Molecules/MediaSlider.vue +9 -11
- package/components/DesignSystem/Molecules/Partnership.vue +7 -4
- package/components/DesignSystem/Molecules/Table.vue +35 -19
- package/components/DesignSystem/Molecules/TitleComposition.vue +46 -15
- package/components/DesignSystem/Molecules/iFrame.vue +2 -2
- package/components/DesignSystem/Organisms/CallToAction.vue +8 -7
- package/components/DesignSystem/Organisms/CallToActionFeature.vue +8 -7
- package/components/DesignSystem/Organisms/CallToActionFullwidth.vue +9 -8
- package/components/DesignSystem/Organisms/ContentBlock.vue +26 -16
- package/components/DesignSystem/Organisms/FacetedListing.vue +38 -17
- package/components/DesignSystem/Organisms/FactCard.vue +6 -7
- package/components/DesignSystem/Organisms/FeatureCard.vue +14 -14
- package/components/DesignSystem/Organisms/FlipTeaser.vue +19 -19
- package/components/DesignSystem/Organisms/HeaderHero.vue +11 -11
- package/components/DesignSystem/Organisms/HeaderTextImage.vue +10 -10
- package/components/DesignSystem/Organisms/LinkList.vue +20 -12
- package/components/DesignSystem/Organisms/LogoElement.vue +7 -4
- package/components/DesignSystem/Organisms/Map.vue +1 -1
- package/components/DesignSystem/Organisms/SearchResults.vue +15 -6
- package/components/DesignSystem/Organisms/SpotlightCard.vue +18 -18
- package/components/DesignSystem/Organisms/TeamElement.vue +19 -18
- package/components/DesignSystem/Organisms/TeaserCard.vue +16 -17
- package/components/DesignSystem/Organisms/TeaserSimple.vue +4 -31
- package/components/DesignSystem/Organisms/Testimonial.vue +13 -13
- package/components/DesignSystem/Organisms/TextCard.vue +10 -10
- package/components/DesignSystem/Organisms/TimelineElement.vue +20 -19
- package/components/DesignSystem/Organisms/TwoColumnTextElement.vue +5 -5
- package/components/DesignSystem/README.md +59 -40
- package/components/Drupal/Breadcrumb/index.vue +25 -12
- package/components/Drupal/Messages/index.vue +9 -4
- package/components/Drupal/Webform/index.vue +271 -51
- package/components/Drupal/Webform/mutation.graphql +17 -1
- package/components/Form/HiddenInput.vue +1 -4
- package/components/Form/SelectInput.vue +1 -1
- package/components/Form/SubmitButton.vue +2 -2
- package/components/Form/TextAreaInput.vue +3 -3
- package/components/Form/TextEmailInput.vue +1 -1
- package/components/Form/TextInput.vue +1 -1
- package/components/Form/TextPhoneInput.vue +1 -1
- package/components/Form/WebformMarkup.vue +1 -1
- package/components/Form/composables/useFormDefaultValue.ts +9 -13
- package/components/Form/composables/useFormElement.ts +5 -3
- package/components/Form/utils/types.ts +3 -3
- package/components/Media/Image/fragment.graphql +4 -0
- package/components/Media/Image/index.vue +27 -17
- package/components/Media/RemoteVideo/index.vue +3 -3
- package/components/Media/Video/index.vue +1 -1
- package/components/Node/Icms/Page/SearchResult/fragment.graphql +1 -1
- package/components/Node/Icms/Page/SearchResult/index.vue +2 -2
- package/components/Node/Icms/Page/Teaser/index.vue +1 -1
- package/components/Node/Icms/Page/index.vue +8 -2
- package/components/Paragraph/Anchor/fragment.graphql +4 -0
- package/components/Paragraph/Anchor/index.vue +48 -0
- package/components/Paragraph/AnchorNavigation/fragment.graphql +3 -0
- package/components/Paragraph/AnchorNavigation/index.vue +154 -0
- package/components/Paragraph/BlokkliFragment/fragment.graphql +5 -0
- package/components/Paragraph/Button/Element/fragment.graphql +1 -1
- package/components/Paragraph/Button/Element/index.vue +13 -8
- package/components/Paragraph/Card/Element/index.vue +1 -1
- package/components/Paragraph/Card/Layout/index.vue +9 -9
- package/components/Paragraph/Collapsible/Element/index.vue +2 -2
- package/components/Paragraph/Collapsible/Layout/index.vue +19 -18
- package/components/Paragraph/Contact/index.vue +17 -17
- package/components/Paragraph/Downloads/fragment.graphql +1 -1
- package/components/Paragraph/Downloads/index.vue +13 -13
- package/components/Paragraph/FAQ/Element/index.vue +4 -3
- package/components/Paragraph/FAQ/Layout/index.vue +42 -21
- package/components/Paragraph/FromLibrary/fragment.graphql +17 -0
- package/components/Paragraph/Gallery/index.vue +14 -14
- package/components/Paragraph/HeroImage/index.vue +26 -19
- package/components/Paragraph/IconText/Element/index.vue +4 -4
- package/components/Paragraph/IconText/Layout/index.vue +12 -12
- package/components/Paragraph/Introduction/index.vue +17 -17
- package/components/Paragraph/LinkLists/Element/fragment.graphql +1 -1
- package/components/Paragraph/LinkLists/Element/index.vue +5 -5
- package/components/Paragraph/LinkLists/Layout/index.vue +9 -9
- package/components/Paragraph/Listing/fragment.graphql +14 -0
- package/components/Paragraph/Listing/index.vue +91 -0
- package/components/Paragraph/Logo/Element/fragment.graphql +1 -1
- package/components/Paragraph/Logo/Element/index.vue +7 -4
- package/components/Paragraph/Media/index.vue +23 -21
- package/components/Paragraph/Partnerships/Element/fragment.graphql +1 -1
- package/components/Paragraph/Partnerships/Element/index.vue +10 -7
- package/components/Paragraph/Partnerships/Layout/index.vue +13 -13
- package/components/Paragraph/SingleTestimonial/index.vue +13 -13
- package/components/Paragraph/Table/index.vue +12 -12
- package/components/Paragraph/Team/Element/index.vue +16 -14
- package/components/Paragraph/Team/Layout/index.vue +9 -9
- package/components/Paragraph/Teasers/index.vue +14 -17
- package/components/Paragraph/Testimonial/Element/index.vue +19 -18
- package/components/Paragraph/Testimonial/Layout/index.vue +24 -18
- package/components/Paragraph/Text/fragment.graphql +1 -1
- package/components/Paragraph/Text/index.vue +8 -8
- package/components/Paragraph/TextMedia/index.vue +22 -22
- package/components/Paragraph/Timeline/Element/index.vue +19 -19
- package/components/Paragraph/Timeline/Layout/index.vue +51 -20
- package/components/Paragraph/TwoColumnText/Element/index.vue +13 -13
- package/components/Paragraph/TwoColumnText/Layout/index.vue +1 -1
- package/components/Paragraph/iFrame/index.vue +8 -8
- package/components/Search/Facets/FacetDropdownUI.vue +25 -31
- package/components/Search/Facets/FacetedSearchUI.vue +72 -48
- package/components/Search/Facets/fragment.graphql +20 -15
- package/components/Section/Footer/index.vue +55 -32
- package/components/Section/Header/Buttons.vue +4 -5
- package/components/Section/Header/DesktopMainMenu.vue +5 -4
- package/components/Section/Header/DesktopMainMenuDropdown.vue +5 -5
- package/components/Section/Header/LanguageDropdown.vue +2 -3
- package/components/Section/Header/Logo.vue +3 -3
- package/components/Section/Header/MainButtons.vue +4 -5
- package/components/Section/Header/MetaButtons.vue +5 -6
- package/components/Section/Header/MetaMenu.vue +1 -1
- package/components/Section/Header/MobileMainMenu.vue +1 -1
- package/components/Section/Header/MobileMainMenuItem.vue +2 -2
- package/components/Section/Header/index.vue +6 -13
- package/composables/__tests__/useImageSources.spec.ts +91 -0
- package/composables/breadcrumb.ts +12 -7
- package/composables/useAccordionScroll.ts +12 -4
- package/composables/useAllowedFragments.ts +14 -0
- package/composables/useAnchors.ts +48 -0
- package/composables/useChildTitleWeight.ts +1 -1
- package/composables/useCurrentLangcodeEnum.ts +1 -1
- package/composables/useCurrentNodeId.ts +8 -0
- package/composables/useDrupalUrl.ts +15 -0
- package/composables/useEnrichFacetLabels.ts +8 -6
- package/composables/useEntityRegistry.ts +3 -3
- package/composables/useGlobalConfig.ts +1 -2
- package/composables/useGraphqlCacheability.ts +12 -1
- package/composables/useHreflangLinks.ts +54 -0
- package/composables/useImageSources.ts +70 -28
- package/composables/useInitData.ts +1 -2
- package/composables/useIsHomepage.ts +1 -1
- package/composables/useLinkUrl.ts +30 -0
- package/composables/useMediaLightbox.ts +62 -50
- package/composables/useMenu.ts +1 -3
- package/composables/useMessageBus.ts +14 -7
- package/composables/useMobileMenuOverlayBus.ts +1 -1
- package/composables/useNavigationOffset.ts +49 -6
- package/composables/usePageCache.ts +33 -0
- package/composables/useSiteShell.ts +11 -0
- package/composables/useTheme.ts +17 -13
- package/config/__tests__/stubs/nuxt-multi-cache-config.ts +2 -0
- package/config/blokkli.ts +7 -13
- package/config/easyTexts.ts +1 -1
- package/config/graphqlMiddleware.ts +11 -4
- package/config/index.ts +5 -2
- package/config/languageNegotiation.ts +1 -1
- package/config/multiCache.ts +6 -1
- package/config/postcss-drupal/postcss.config.cjs +2 -2
- package/config/runtimeConfig.ts +7 -2
- package/config/svgIconSprite.ts +1 -2
- package/config/vuepal.ts +16 -18
- package/error.vue +1 -22
- package/layouts/default.vue +2 -6
- package/layouts/error.vue +7 -15
- package/modules/blokkliFragments.ts +76 -0
- package/modules/dynamicEntityComponentResolver.ts +47 -47
- package/modules/dynamicParagraphResolver.ts +43 -8
- package/modules/pageCache.ts +106 -0
- package/nuxt.config.ts +62 -33
- package/package.json +23 -22
- package/pages/[...slug]/index.vue +25 -4
- package/pages/index.vue +3 -3
- package/pages/node/[nid]/index.vue +2 -3
- package/patches/@blokkli+editor+2.0.0-alpha.13.patch +15 -1
- package/plugins/currentNode.ts +17 -0
- package/plugins/graphqlMiddleware.ts +10 -2
- package/plugins/initData.ts +15 -7
- package/plugins/registerNodeIcmsPageComponents.ts +6 -2
- package/plugins/siteShell.client.ts +94 -0
- package/queries/facetTermLabels.graphql +1 -5
- package/queries/fragment.linkUrl.graphql +12 -0
- package/queries/query.contentSearch.graphql +17 -2
- package/queries/query.initData.graphql +1 -1
- package/scripts/create-shade.ts +13 -4
- package/scripts/update-icms.ts +3 -1
- package/server/__tests__/graphqlMiddleware.serverOptions.spec.ts +2 -2
- package/server/__tests__/turnstile.spec.ts +119 -0
- package/server/api/icms/blokkli/last-changed.get.ts +24 -0
- package/server/api/icon/[...params].ts +3 -9
- package/server/graphqlMiddleware.serverOptions.ts +9 -8
- package/server/helpers/index.ts +0 -1
- package/server/middleware/turnstile.ts +51 -0
- package/server/plugins/frameAncestors.ts +20 -0
- package/server/plugins/pageCache.ts +59 -0
- package/server/utils/__tests__/accessDeniedHandover.spec.ts +49 -0
- package/server/utils/__tests__/backendRequestHeaders.spec.ts +115 -0
- package/server/utils/accessDeniedHandover.ts +48 -0
- package/server/utils/backendRequestHeaders.ts +51 -0
- package/server/utils/cacheability.ts +4 -4
- package/shared/__tests__/blokkliState.spec.ts +54 -0
- package/shared/__tests__/csp.spec.ts +53 -0
- package/shared/__tests__/drupalUrl.spec.ts +50 -0
- package/shared/blokkliState.ts +40 -0
- package/shared/csp.ts +47 -0
- package/shared/drupalUrl.ts +30 -0
- package/tailwind.config.ts +0 -5
- package/types/breadcrumb.ts +22 -0
- package/types/global.ts +15 -14
- package/types/langcode.ts +16 -0
- package/types/layerConfig.ts +84 -0
- package/utils/pageCache.ts +95 -0
- package/utils/turnstile.ts +35 -0
- package/composables/useWebformEnrichment.ts +0 -68
- package/server/__tests__/graphqlMiddleware.ts +0 -41
- package/server/multiCache.serverOptions.ts +0 -32
- package/vitest.config.mts +0 -28
|
@@ -5,6 +5,12 @@ interface ImageSource {
|
|
|
5
5
|
sizes?: string
|
|
6
6
|
srcset: string
|
|
7
7
|
media?: string
|
|
8
|
+
/**
|
|
9
|
+
* Intrinsic size of the largest candidate, rendered as the width/height
|
|
10
|
+
* attributes so the browser reserves the box before the image has loaded.
|
|
11
|
+
*/
|
|
12
|
+
width?: number
|
|
13
|
+
height?: number
|
|
8
14
|
}
|
|
9
15
|
|
|
10
16
|
interface UseImageSourcesOptions {
|
|
@@ -14,15 +20,18 @@ interface UseImageSourcesOptions {
|
|
|
14
20
|
}
|
|
15
21
|
|
|
16
22
|
// Local implementation of parseSizes from @nuxt/image
|
|
17
|
-
function parseSizes(
|
|
23
|
+
function parseSizes(
|
|
24
|
+
input: Record<string, string | number> | string,
|
|
25
|
+
): Record<string, string> {
|
|
18
26
|
const sizes: Record<string, string> = {}
|
|
19
|
-
if (typeof input ===
|
|
27
|
+
if (typeof input === 'string') {
|
|
20
28
|
for (const entry of input.split(/[\s,]+/).filter((e) => e)) {
|
|
21
|
-
const s = entry.split(
|
|
29
|
+
const s = entry.split(':')
|
|
30
|
+
const [key = '', value = ''] = s
|
|
22
31
|
if (s.length !== 2) {
|
|
23
|
-
sizes[
|
|
32
|
+
sizes['1px'] = key.trim()
|
|
24
33
|
} else {
|
|
25
|
-
sizes[
|
|
34
|
+
sizes[key.trim()] = value.trim()
|
|
26
35
|
}
|
|
27
36
|
}
|
|
28
37
|
} else {
|
|
@@ -32,8 +41,14 @@ function parseSizes(input: Record<string, string | number> | string): Record<str
|
|
|
32
41
|
}
|
|
33
42
|
|
|
34
43
|
// Helper function, copied from NuxtImage.
|
|
35
|
-
function getSizesVariant(
|
|
36
|
-
|
|
44
|
+
function getSizesVariant(
|
|
45
|
+
key: string,
|
|
46
|
+
size: string,
|
|
47
|
+
hwRatio: number,
|
|
48
|
+
img: any,
|
|
49
|
+
): ImageSizesVariant | undefined {
|
|
50
|
+
const screenMaxWidth =
|
|
51
|
+
(img.options.screens && img.options.screens[key]) || Number.parseInt(key)
|
|
37
52
|
const isFluid = size.endsWith('vw')
|
|
38
53
|
if (!isFluid && /^\d+$/.test(size)) {
|
|
39
54
|
size = size + 'px'
|
|
@@ -112,44 +127,56 @@ export function useImageSources(options: UseImageSourcesOptions) {
|
|
|
112
127
|
if (!sourceImage || sourceImage === baseUrl) return []
|
|
113
128
|
|
|
114
129
|
const sources: ImageSource[] = []
|
|
115
|
-
const imgSizes =
|
|
130
|
+
const imgSizes =
|
|
131
|
+
imageSizesRef.value ??
|
|
132
|
+
Object.keys(img.options.screens)
|
|
133
|
+
.map((key) => `${key}:100vw`)
|
|
134
|
+
.join(' ')
|
|
116
135
|
|
|
117
136
|
// No aspect ratios provided.
|
|
118
137
|
if (!imageAspectRatiosRef.value) {
|
|
119
138
|
const imageSize = img.getSizes(sourceImage, {
|
|
120
139
|
sizes: imgSizes,
|
|
121
140
|
modifiers: {
|
|
122
|
-
format: 'webp'
|
|
123
|
-
}
|
|
141
|
+
format: 'webp',
|
|
142
|
+
},
|
|
143
|
+
})
|
|
144
|
+
// Cast the ImageSizes result to our ImageSource interface; the natural
|
|
145
|
+
// aspect ratio comes from the original file.
|
|
146
|
+
sources.push({
|
|
147
|
+
...(imageSize as ImageSource),
|
|
148
|
+
width: mediaRef.value?.dimensions?.width ?? undefined,
|
|
149
|
+
height: mediaRef.value?.dimensions?.height ?? undefined,
|
|
124
150
|
})
|
|
125
|
-
// Cast the ImageSizes result to our ImageSource interface
|
|
126
|
-
sources.push(imageSize as ImageSource)
|
|
127
151
|
return sources
|
|
128
152
|
}
|
|
129
153
|
|
|
130
154
|
// Aspect ratios provided.
|
|
131
155
|
const parsedAspectRatios = parseSizes(imageAspectRatiosRef.value)
|
|
132
156
|
const parsedImageSizes = parseSizes(imgSizes)
|
|
133
|
-
const densities = img.options.densities
|
|
134
|
-
const keys = Object.keys(parsedImageSizes)
|
|
135
|
-
const lastKey = keys[keys.length - 1]
|
|
136
157
|
|
|
137
|
-
for (const keyAspectRatios
|
|
138
|
-
|
|
139
|
-
|
|
158
|
+
for (const [keyAspectRatios, aspectRatio] of Object.entries(
|
|
159
|
+
parsedAspectRatios,
|
|
160
|
+
)) {
|
|
161
|
+
const [l, h] = aspectRatio.split('/').map(Number)
|
|
162
|
+
const hwRatio = l && h ? h / l : 0
|
|
140
163
|
const sizeVariants = []
|
|
141
164
|
const srcsetVariants = []
|
|
142
165
|
|
|
143
166
|
let screenSizes = Object.keys(parsedImageSizes)
|
|
144
167
|
if (Object.keys(parsedImageSizes).includes(keyAspectRatios)) {
|
|
145
|
-
const minSizeIndex =
|
|
168
|
+
const minSizeIndex =
|
|
169
|
+
Object.keys(parsedImageSizes).indexOf(keyAspectRatios)
|
|
146
170
|
screenSizes = Object.keys(parsedImageSizes).slice(minSizeIndex)
|
|
147
171
|
}
|
|
148
172
|
|
|
149
|
-
for (const
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
173
|
+
for (const screenSize of screenSizes) {
|
|
174
|
+
const variant = getSizesVariant(
|
|
175
|
+
screenSize,
|
|
176
|
+
String(parsedImageSizes[screenSize]),
|
|
177
|
+
hwRatio,
|
|
178
|
+
img,
|
|
179
|
+
)
|
|
153
180
|
if (variant === undefined) {
|
|
154
181
|
continue
|
|
155
182
|
}
|
|
@@ -164,17 +191,23 @@ export function useImageSources(options: UseImageSourcesOptions) {
|
|
|
164
191
|
// Add srcset variants and ignore densities (for current 'size' processed).
|
|
165
192
|
srcsetVariants.push({
|
|
166
193
|
width: variant._cWidth,
|
|
194
|
+
height: variant._cHeight,
|
|
167
195
|
src: img(sourceImage, {
|
|
168
196
|
width: variant._cWidth ? variant._cWidth : undefined,
|
|
169
197
|
height: variant._cHeight ? variant._cHeight : undefined,
|
|
170
198
|
fit: 'cover',
|
|
171
199
|
format: 'webp',
|
|
172
|
-
gravity: mediaRef.value?.focalPoint?.focalPoint
|
|
200
|
+
gravity: mediaRef.value?.focalPoint?.focalPoint
|
|
201
|
+
? `${mediaRef.value?.focalPoint?.focalPoint?.x}x${mediaRef.value?.focalPoint?.focalPoint?.y}`
|
|
202
|
+
: 'auto',
|
|
173
203
|
}),
|
|
174
204
|
})
|
|
175
205
|
|
|
176
206
|
// Make sure to not create duplicate size variants for the same screen size.
|
|
177
|
-
if (
|
|
207
|
+
if (
|
|
208
|
+
keyAspectRatios != screenSize &&
|
|
209
|
+
Object.keys(parsedAspectRatios).includes(screenSize)
|
|
210
|
+
) {
|
|
178
211
|
break
|
|
179
212
|
}
|
|
180
213
|
}
|
|
@@ -183,13 +216,22 @@ export function useImageSources(options: UseImageSourcesOptions) {
|
|
|
183
216
|
finaliseSizeVariants(sizeVariants)
|
|
184
217
|
finaliseSrcsetVariants(srcsetVariants)
|
|
185
218
|
|
|
186
|
-
const sizesVal = sizeVariants.length
|
|
219
|
+
const sizesVal = sizeVariants.length
|
|
220
|
+
? sizeVariants
|
|
221
|
+
.map((v) => `${v.media ? v.media + ' ' : ''}${v.size}`)
|
|
222
|
+
.join(', ')
|
|
223
|
+
: undefined
|
|
187
224
|
const suffix = sizesVal ? 'w' : 'x'
|
|
188
|
-
const srcsetVal = srcsetVariants
|
|
225
|
+
const srcsetVal = srcsetVariants
|
|
226
|
+
.map((v) => `${v.src} ${v.width}${suffix}`)
|
|
227
|
+
.join(', ')
|
|
189
228
|
|
|
229
|
+
const largest = srcsetVariants[srcsetVariants.length - 1]
|
|
190
230
|
const source: ImageSource = {
|
|
191
231
|
sizes: sizesVal,
|
|
192
232
|
srcset: srcsetVal,
|
|
233
|
+
width: largest?.height ? largest.width : undefined,
|
|
234
|
+
height: largest?.height ? largest.height : undefined,
|
|
193
235
|
}
|
|
194
236
|
|
|
195
237
|
if (keyAspectRatios != Object.keys(parsedAspectRatios).slice(-1)[0]) {
|
|
@@ -202,7 +244,7 @@ export function useImageSources(options: UseImageSourcesOptions) {
|
|
|
202
244
|
|
|
203
245
|
return {
|
|
204
246
|
placeHolderImage,
|
|
205
|
-
imageSources
|
|
247
|
+
imageSources,
|
|
206
248
|
}
|
|
207
249
|
}
|
|
208
250
|
|
|
@@ -20,7 +20,6 @@ export default function (): UseInitData {
|
|
|
20
20
|
mainMenu: computed(() => data.value?.mainMenuLinks || []),
|
|
21
21
|
footerMenu: computed(() => data.value?.footerMenuLinks || []),
|
|
22
22
|
footerMetaMenu: computed(() => data.value?.footerMetaMenuLinks || []),
|
|
23
|
-
}
|
|
23
|
+
},
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
-
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface LinkFieldUri {
|
|
2
|
+
path?: string | null
|
|
3
|
+
entity?: {
|
|
4
|
+
/** Only selected for media entities by the `linkUrl` fragment. */
|
|
5
|
+
id?: string | null
|
|
6
|
+
} | null
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ResolvedLinkUrl {
|
|
10
|
+
/** The URL to render. */
|
|
11
|
+
url?: string
|
|
12
|
+
/** True when the link points to a media file (e.g. a document picked via linkit). */
|
|
13
|
+
isFile: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolves the target URL of a link field queried with the `linkUrl` fragment.
|
|
18
|
+
*
|
|
19
|
+
* Links referencing a media entity (ICMS-595) resolve to its canonical
|
|
20
|
+
* /media/{id} path, which the backend serves as the file currently attached
|
|
21
|
+
* to that media. Unlike the file URL, it stays valid when the file is
|
|
22
|
+
* replaced. Such links must bypass the Vue router (NuxtLink `external`) since
|
|
23
|
+
* they are served by the backend.
|
|
24
|
+
*/
|
|
25
|
+
export function resolveLinkUrl(uri?: LinkFieldUri | null): ResolvedLinkUrl {
|
|
26
|
+
return {
|
|
27
|
+
url: uri?.path || undefined,
|
|
28
|
+
isFile: !!uri?.entity?.id,
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -22,7 +22,7 @@ export function useMediaLightbox(media: (MediaFragment | null)[] | undefined) {
|
|
|
22
22
|
get: () => isOpen.value,
|
|
23
23
|
set: (value: boolean) => {
|
|
24
24
|
isOpen.value = value
|
|
25
|
-
}
|
|
25
|
+
},
|
|
26
26
|
})
|
|
27
27
|
|
|
28
28
|
// For backward compatibility - fsLightbox used 1-based indexing
|
|
@@ -30,65 +30,77 @@ export function useMediaLightbox(media: (MediaFragment | null)[] | undefined) {
|
|
|
30
30
|
get: () => currentIndex.value + 1,
|
|
31
31
|
set: (value: number) => {
|
|
32
32
|
currentIndex.value = value - 1
|
|
33
|
-
}
|
|
33
|
+
},
|
|
34
34
|
})
|
|
35
35
|
|
|
36
36
|
// Copy exact logic from the working composable
|
|
37
|
-
const computedMedia = computed(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
37
|
+
const computedMedia = computed(
|
|
38
|
+
() =>
|
|
39
|
+
media
|
|
40
|
+
?.map((item) => {
|
|
41
|
+
if (!item) return ''
|
|
42
|
+
switch (item.type) {
|
|
43
|
+
case 'MediaImage':
|
|
44
|
+
return (item as any).sourceUrl?.path || ''
|
|
45
|
+
case 'MediaVideo':
|
|
46
|
+
return (item as any).url?.path || ''
|
|
47
|
+
case 'MediaRemoteVideo':
|
|
48
|
+
return (item as any).link || ''
|
|
49
|
+
default:
|
|
50
|
+
return ''
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
.filter(Boolean) || [],
|
|
51
54
|
)
|
|
52
55
|
|
|
53
56
|
// Expose filtered media objects for Media components
|
|
54
57
|
const mediaObjects = computed(() => media?.filter(Boolean) || [])
|
|
55
58
|
|
|
56
59
|
// Copy exact logic from the working composable
|
|
57
|
-
const computedCustomAttributes = computed(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
switch (item.type) {
|
|
62
|
-
case 'MediaImage':
|
|
63
|
-
return {
|
|
64
|
-
alt: (item as any).alt?.alt || '',
|
|
65
|
-
title: item.title || ''
|
|
66
|
-
}
|
|
60
|
+
const computedCustomAttributes = computed(
|
|
61
|
+
() =>
|
|
62
|
+
media?.map((item) => {
|
|
63
|
+
if (!item) return {}
|
|
67
64
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
65
|
+
switch (item.type) {
|
|
66
|
+
case 'MediaImage':
|
|
67
|
+
return {
|
|
68
|
+
alt: (item as any).alt?.alt || '',
|
|
69
|
+
title: item.title || '',
|
|
70
|
+
}
|
|
75
71
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
72
|
+
case 'MediaVideo':
|
|
73
|
+
return {
|
|
74
|
+
alt:
|
|
75
|
+
((item as any).thumbnailOverride || (item as any).thumbnail)
|
|
76
|
+
?.alt || '',
|
|
77
|
+
title: item.title || '',
|
|
78
|
+
poster:
|
|
79
|
+
((item as any).thumbnailOverride || (item as any).thumbnail)
|
|
80
|
+
?.entity?.uri?.first?.url || '',
|
|
81
|
+
controls: true,
|
|
82
|
+
}
|
|
82
83
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
case 'MediaRemoteVideo':
|
|
85
|
+
return {
|
|
86
|
+
alt:
|
|
87
|
+
((item as any).thumbnailOverride || (item as any).thumbnail)
|
|
88
|
+
?.alt || '',
|
|
89
|
+
title: item.title || '',
|
|
90
|
+
poster:
|
|
91
|
+
((item as any).thumbnailOverride || (item as any).thumbnail)
|
|
92
|
+
?.entity?.uri?.first?.url || '',
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
default:
|
|
96
|
+
return {}
|
|
97
|
+
}
|
|
98
|
+
}) || [],
|
|
87
99
|
)
|
|
88
100
|
|
|
89
101
|
// Copy exact logic from the working composable
|
|
90
|
-
const computedCaptions = computed(
|
|
91
|
-
media?.map(item => (item as any)?.caption || '') || []
|
|
102
|
+
const computedCaptions = computed(
|
|
103
|
+
() => media?.map((item) => (item as any)?.caption || '') || [],
|
|
92
104
|
)
|
|
93
105
|
|
|
94
106
|
return {
|
|
@@ -98,17 +110,17 @@ export function useMediaLightbox(media: (MediaFragment | null)[] | undefined) {
|
|
|
98
110
|
openLightbox,
|
|
99
111
|
closeLightbox,
|
|
100
112
|
setCurrentIndex,
|
|
101
|
-
|
|
113
|
+
|
|
102
114
|
// Backward compatibility with fsLightbox API
|
|
103
115
|
toggleLightbox,
|
|
104
116
|
currentSlide,
|
|
105
|
-
|
|
117
|
+
|
|
106
118
|
// Data for lightbox component
|
|
107
119
|
sources: computedMedia,
|
|
108
120
|
customAttributes: computedCustomAttributes,
|
|
109
121
|
captions: computedCaptions,
|
|
110
|
-
|
|
122
|
+
|
|
111
123
|
// Media objects for Media components
|
|
112
|
-
mediaObjects
|
|
124
|
+
mediaObjects,
|
|
113
125
|
}
|
|
114
|
-
}
|
|
126
|
+
}
|
package/composables/useMenu.ts
CHANGED
|
@@ -29,9 +29,7 @@ export async function useMenu(menuName: MenuName) {
|
|
|
29
29
|
})
|
|
30
30
|
|
|
31
31
|
const menuItemActive = computed(() => {
|
|
32
|
-
return menuLinks.value.find((l) =>
|
|
33
|
-
isLinkTreeActive(l, currentRoute.path),
|
|
34
|
-
)
|
|
32
|
+
return menuLinks.value.find((l) => isLinkTreeActive(l, currentRoute.path))
|
|
35
33
|
})
|
|
36
34
|
|
|
37
35
|
const menuItemActiveLevel1 = computed(() => {
|
|
@@ -33,7 +33,7 @@ export type BusMessage = {
|
|
|
33
33
|
message?: any
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
const messageBus: Ref<Array<BusMessage>> = ref([])
|
|
36
|
+
const messageBus: Ref<Array<BusMessage>> = ref([])
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
39
|
* Sends a message to the message bus.
|
|
@@ -43,7 +43,7 @@ const messageBus: Ref<Array<BusMessage>> = ref([]);
|
|
|
43
43
|
* @param type - Optional message type identifier.
|
|
44
44
|
*/
|
|
45
45
|
function send(message: any, receiver?: string, type?: string) {
|
|
46
|
-
messageBus.value.push({ receiver, type, message })
|
|
46
|
+
messageBus.value.push({ receiver, type, message })
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/**
|
|
@@ -55,9 +55,16 @@ function send(message: any, receiver?: string, type?: string) {
|
|
|
55
55
|
* @returns Array of matching BusMessage objects (deep cloned).
|
|
56
56
|
*/
|
|
57
57
|
function receive(receiver?: string, type?: string): BusMessage[] {
|
|
58
|
-
return messageBus.value
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
return messageBus.value
|
|
59
|
+
.filter(
|
|
60
|
+
(msg) =>
|
|
61
|
+
(receiver === undefined || msg.receiver === receiver) &&
|
|
62
|
+
(type === undefined || msg.type === type),
|
|
63
|
+
)
|
|
64
|
+
.map((msg) => ({
|
|
65
|
+
...msg,
|
|
66
|
+
message: JSON.parse(JSON.stringify(msg.message)),
|
|
67
|
+
}))
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
/**
|
|
@@ -70,5 +77,5 @@ export function useMessageBus() {
|
|
|
70
77
|
bus: messageBus,
|
|
71
78
|
send,
|
|
72
79
|
receive,
|
|
73
|
-
}
|
|
74
|
-
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -1,14 +1,57 @@
|
|
|
1
1
|
import { useIsMobile } from '@iqual/nuxt-icms/composables/useIsMobile'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
const
|
|
3
|
+
// Used during SSR and until the header has been measured.
|
|
4
|
+
const fallbackMainHeightMobile = 57
|
|
5
|
+
const fallbackMainHeightDesktop = 81
|
|
6
|
+
const fallbackMetaHeight = 66
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
// Helper function for retrieving the element's height
|
|
9
|
+
const measure = (selector: string) =>
|
|
10
|
+
document.querySelector<HTMLElement>(selector)?.offsetHeight ?? 0
|
|
11
|
+
|
|
12
|
+
export function useNavigationOffset(): {
|
|
13
|
+
metaHeight: ComputedRef<number>
|
|
14
|
+
headerHeight: ComputedRef<number>
|
|
15
|
+
navigationOffset: ComputedRef<number>
|
|
16
|
+
} {
|
|
7
17
|
const { isMobile } = useIsMobile()
|
|
8
18
|
|
|
9
|
-
|
|
10
|
-
|
|
19
|
+
// null until measured, so a non-existent element stays 0
|
|
20
|
+
const mainHeight = ref<number | null>(null)
|
|
21
|
+
const metaHeight = ref<number | null>(null)
|
|
22
|
+
|
|
23
|
+
const update = () => {
|
|
24
|
+
mainHeight.value = measure('[data-navigation-main]')
|
|
25
|
+
metaHeight.value = measure('[data-navigation-meta]')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
onMounted(() => {
|
|
29
|
+
requestAnimationFrame(update)
|
|
30
|
+
window.addEventListener('resize', update, { passive: true })
|
|
11
31
|
})
|
|
12
32
|
|
|
13
|
-
|
|
33
|
+
onBeforeUnmount(() => window.removeEventListener('resize', update))
|
|
34
|
+
|
|
35
|
+
// The part of the header that stays on screen. The meta row is pulled out of
|
|
36
|
+
// view by the header's negative top offset, so it does not count.
|
|
37
|
+
const headerHeight = computed(
|
|
38
|
+
() =>
|
|
39
|
+
mainHeight.value ??
|
|
40
|
+
(isMobile.value ? fallbackMainHeightMobile : fallbackMainHeightDesktop),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
// The whole header, for scrolling something to just below it.
|
|
44
|
+
const navigationOffset = computed(
|
|
45
|
+
() =>
|
|
46
|
+
headerHeight.value +
|
|
47
|
+
(isMobile.value ? 0 : (metaHeight.value ?? fallbackMetaHeight)) +
|
|
48
|
+
4,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
// Only what was measured, so the header does not shift on the first frame.
|
|
53
|
+
metaHeight: computed(() => metaHeight.value ?? 0),
|
|
54
|
+
headerHeight,
|
|
55
|
+
navigationOffset,
|
|
56
|
+
}
|
|
14
57
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opt the current page into the in-memory route (full page) cache.
|
|
3
|
+
*
|
|
4
|
+
* The page is only stored if every GraphQL response that went into it was
|
|
5
|
+
* cacheable (see useGraphqlCacheability) and it rendered with status 200.
|
|
6
|
+
* Requests with a Drupal session and blökkli requests never reach the route
|
|
7
|
+
* cache. In `varnish` mode the route cache stays off: Varnish caches the
|
|
8
|
+
* page instead.
|
|
9
|
+
*/
|
|
10
|
+
export function usePageCache() {
|
|
11
|
+
if (!import.meta.server) {
|
|
12
|
+
return
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const config = resolvePageCacheConfig(useRuntimeConfig().pageCache)
|
|
16
|
+
if (config.mode === 'varnish' || !config.routeMaxSize) {
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
useRouteCache((helper) => {
|
|
21
|
+
helper.setCacheable().setMaxAge(config.routeMaxAge)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
// The cached page is served for every URL with the same cache key, which
|
|
25
|
+
// ignores tracking parameters (utm_*, gclid, …). Nuxt embeds the rendered
|
|
26
|
+
// URL in the payload, and on hydration the client router prefers it over
|
|
27
|
+
// the browser URL whenever they differ — it would hand the first visitor's
|
|
28
|
+
// click IDs to everyone. Without a query the client router takes the query
|
|
29
|
+
// from the browser instead. The server render itself still sees the full
|
|
30
|
+
// URL, so redirects keep the parameters.
|
|
31
|
+
const nuxtApp = useNuxtApp()
|
|
32
|
+
nuxtApp.payload.path = nuxtApp.payload.path?.split('?')[0]
|
|
33
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the frontend is shown inside the Drupal backend's site shell.
|
|
3
|
+
*
|
|
4
|
+
* Editors browse the frontend in an iframe of the backend (Drupal keeps its
|
|
5
|
+
* navigation and top bar), so the frontend hides its own admin UI then.
|
|
6
|
+
*/
|
|
7
|
+
export default function useSiteShell() {
|
|
8
|
+
return useState<{ framed: boolean }>('icmsSiteShell', () => ({
|
|
9
|
+
framed: false,
|
|
10
|
+
}))
|
|
11
|
+
}
|
package/composables/useTheme.ts
CHANGED
|
@@ -4,11 +4,11 @@ import cn from '@iqual/nuxt-icms/composables/useCn'
|
|
|
4
4
|
/**
|
|
5
5
|
* A composable that provides theme-aware class generation for Vue components.
|
|
6
6
|
* Automatically detects the calling component and merges theme classes with component props and default classes.
|
|
7
|
-
*
|
|
7
|
+
*
|
|
8
8
|
* @param componentName - Component name to manually override auto-detection
|
|
9
9
|
* @param themeName - The name of the theme to use from app config (defaults to 'default')
|
|
10
10
|
* @returns A function that generates themed classes for component elements
|
|
11
|
-
*
|
|
11
|
+
*
|
|
12
12
|
* @example
|
|
13
13
|
* ```vue
|
|
14
14
|
* <template>
|
|
@@ -16,21 +16,21 @@ import cn from '@iqual/nuxt-icms/composables/useCn'
|
|
|
16
16
|
* <span :class="theme('label')">Click me</span>
|
|
17
17
|
* </button>
|
|
18
18
|
* </template>
|
|
19
|
-
*
|
|
19
|
+
*
|
|
20
20
|
* <script setup>
|
|
21
21
|
* // Uses default theme
|
|
22
22
|
* const customTheme = useTheme('CustomComponent')
|
|
23
|
-
*
|
|
23
|
+
*
|
|
24
24
|
* // Use a specific theme
|
|
25
25
|
* const darkTheme = useTheme('CustomComponent', 'dark')
|
|
26
|
-
*
|
|
26
|
+
*
|
|
27
27
|
* // The returned function merges:
|
|
28
28
|
* // 1. Default classes passed as second parameter
|
|
29
29
|
* // 2. Theme classes from app.config.ts
|
|
30
30
|
* // 3. Component prop classes (e.g., buttonClass, labelClass props)
|
|
31
31
|
* </script>
|
|
32
32
|
* ```
|
|
33
|
-
*
|
|
33
|
+
*
|
|
34
34
|
* @remarks
|
|
35
35
|
* The composable expects theme configuration in app.config.ts structured as:
|
|
36
36
|
* ```typescript
|
|
@@ -50,26 +50,30 @@ export function useTheme(componentName: string, themeName: string = 'default') {
|
|
|
50
50
|
const { props } = instance || {}
|
|
51
51
|
const appConfig = useAppConfig()
|
|
52
52
|
|
|
53
|
-
const componentTheme =
|
|
53
|
+
const componentTheme =
|
|
54
|
+
(appConfig?.theme as any)?.[themeName]?.[componentName] ?? {}
|
|
54
55
|
|
|
55
56
|
/**
|
|
56
57
|
* Generates themed classes by merging default classes, theme classes, and component prop classes.
|
|
57
|
-
*
|
|
58
|
+
*
|
|
58
59
|
* @param elementName - The name of the element to style (should match theme config keys and props)
|
|
59
60
|
* @param defaultClass - Optional default CSS classes to include
|
|
60
61
|
* @returns Merged CSS class string using the cn() utility
|
|
61
|
-
*
|
|
62
|
+
*
|
|
62
63
|
* @example
|
|
63
64
|
* ```typescript
|
|
64
65
|
* // Returns merged classes from theme config, props, and defaults
|
|
65
66
|
* const buttonClasses = theme('button', 'rounded-lg')
|
|
66
|
-
*
|
|
67
|
-
* // Just use theme and prop classes without defaults
|
|
67
|
+
*
|
|
68
|
+
* // Just use theme and prop classes without defaults
|
|
68
69
|
* const labelClasses = theme('label')
|
|
69
70
|
* ```
|
|
70
71
|
*/
|
|
71
72
|
return function (elementName: string, defaultClass?: string) {
|
|
72
|
-
return cn(
|
|
73
|
+
return cn(
|
|
74
|
+
defaultClass,
|
|
75
|
+
componentTheme[elementName] as string,
|
|
76
|
+
props?.[elementName] as string,
|
|
77
|
+
)
|
|
73
78
|
}
|
|
74
79
|
}
|
|
75
|
-
|