@ethlete/contentful 4.0.0-next.12 → 4.0.0-next.14
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/CHANGELOG.md +29 -0
- package/fesm2022/ethlete-contentful.mjs +327 -128
- package/fesm2022/ethlete-contentful.mjs.map +1 -1
- package/generators/generators.json +6 -1
- package/generators/migrate-contentful-asset-classes/migration.js +163 -0
- package/generators/migrate-contentful-asset-classes/schema.json +26 -0
- package/generators/migrate-contentful-default-components/contentful-default-components.js +15 -1
- package/generators/migrate-contentful-default-components/migration.js +22 -5
- package/generators/migrate-contentful-default-components/schema.json +1 -1
- package/generators/migrate-to-contentful-v5/migration.js +3 -3
- package/migrations.json +12 -1
- package/package.json +6 -6
- package/types/ethlete-contentful.d.ts +88 -26
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { NgClass, DOCUMENT } from '@angular/common';
|
|
2
1
|
import * as i0 from '@angular/core';
|
|
3
|
-
import { input, computed, ViewEncapsulation, Component, InjectionToken, inject, numberAttribute, booleanAttribute, isDevMode, ViewContainerRef, ElementRef, effect, untracked, signal, reflectComponentType, inputBinding } from '@angular/core';
|
|
2
|
+
import { input, computed, ViewEncapsulation, Component, InjectionToken, inject, numberAttribute, booleanAttribute, isDevMode, ViewContainerRef, ElementRef, Injector, effect, untracked, signal, reflectComponentType, inputBinding } from '@angular/core';
|
|
4
3
|
import { formatFileSize, normalizePictureSizes, PictureComponent } from '@ethlete/components';
|
|
5
|
-
import { defineLabels, toProvideFn, toInjectFn, toToken, injectUrl, RuntimeError, isObject,
|
|
4
|
+
import { defineLabels, toProvideFn, toInjectFn, toToken, injectUrl, RuntimeError, isObject, getObjectProperty, injectRenderer } from '@ethlete/core';
|
|
5
|
+
import { DOCUMENT, Location } from '@angular/common';
|
|
6
6
|
import { Router, RouterLink } from '@angular/router';
|
|
7
7
|
|
|
8
8
|
const GQL_FRAGMENT_CONTENTFUL_ASSET = `
|
|
@@ -29,12 +29,6 @@ class ContentfulAudioComponent {
|
|
|
29
29
|
constructor() {
|
|
30
30
|
this.asset = input.required(/* @ts-ignore */
|
|
31
31
|
...(ngDevMode ? [{ debugName: "asset" }] : /* istanbul ignore next */ []));
|
|
32
|
-
this.audioClass = input(null, /* @ts-ignore */
|
|
33
|
-
...(ngDevMode ? [{ debugName: "audioClass" }] : /* istanbul ignore next */ []));
|
|
34
|
-
this.figureClass = input(null, /* @ts-ignore */
|
|
35
|
-
...(ngDevMode ? [{ debugName: "figureClass" }] : /* istanbul ignore next */ []));
|
|
36
|
-
this.figcaptionClass = input(null, /* @ts-ignore */
|
|
37
|
-
...(ngDevMode ? [{ debugName: "figcaptionClass" }] : /* istanbul ignore next */ []));
|
|
38
32
|
this.data = computed(() => {
|
|
39
33
|
const asset = this.asset();
|
|
40
34
|
if (!asset) {
|
|
@@ -57,14 +51,16 @@ class ContentfulAudioComponent {
|
|
|
57
51
|
...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
|
|
58
52
|
}
|
|
59
53
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulAudioComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
60
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: ContentfulAudioComponent, isStandalone: true, selector: "et-contentful-audio", inputs: { asset: { classPropertyName: "asset", publicName: "asset", isSignal: true, isRequired: true, transformFunction: null }
|
|
54
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: ContentfulAudioComponent, isStandalone: true, selector: "et-contentful-audio", inputs: { asset: { classPropertyName: "asset", publicName: "asset", isSignal: true, isRequired: true, transformFunction: null } }, host: { classAttribute: "et-contentful-audio" }, ngImport: i0, template: `
|
|
61
55
|
@if (data(); as data) {
|
|
62
|
-
<figure
|
|
63
|
-
|
|
64
|
-
|
|
56
|
+
<figure class="et-contentful-audio-figure">
|
|
57
|
+
@if (data.title) {
|
|
58
|
+
<figcaption class="et-contentful-audio-figcaption">{{ data.title }}</figcaption>
|
|
59
|
+
}
|
|
60
|
+
<audio [src]="data.url" class="et-contentful-audio-audio" controls></audio>
|
|
65
61
|
</figure>
|
|
66
62
|
}
|
|
67
|
-
`, isInline: true,
|
|
63
|
+
`, isInline: true, encapsulation: i0.ViewEncapsulation.None }); }
|
|
68
64
|
}
|
|
69
65
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulAudioComponent, decorators: [{
|
|
70
66
|
type: Component,
|
|
@@ -72,19 +68,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
72
68
|
selector: 'et-contentful-audio',
|
|
73
69
|
template: `
|
|
74
70
|
@if (data(); as data) {
|
|
75
|
-
<figure
|
|
76
|
-
|
|
77
|
-
|
|
71
|
+
<figure class="et-contentful-audio-figure">
|
|
72
|
+
@if (data.title) {
|
|
73
|
+
<figcaption class="et-contentful-audio-figcaption">{{ data.title }}</figcaption>
|
|
74
|
+
}
|
|
75
|
+
<audio [src]="data.url" class="et-contentful-audio-audio" controls></audio>
|
|
78
76
|
</figure>
|
|
79
77
|
}
|
|
80
78
|
`,
|
|
81
79
|
encapsulation: ViewEncapsulation.None,
|
|
82
|
-
imports: [NgClass],
|
|
83
80
|
host: {
|
|
84
81
|
class: 'et-contentful-audio',
|
|
85
82
|
},
|
|
86
83
|
}]
|
|
87
|
-
}], propDecorators: { asset: [{ type: i0.Input, args: [{ isSignal: true, alias: "asset", required: true }] }]
|
|
84
|
+
}], propDecorators: { asset: [{ type: i0.Input, args: [{ isSignal: true, alias: "asset", required: true }] }] } });
|
|
88
85
|
|
|
89
86
|
/** The built-in English labels. */
|
|
90
87
|
const DEFAULT_CONTENTFUL_FILE_LABELS = {
|
|
@@ -107,8 +104,6 @@ class ContentfulFileComponent {
|
|
|
107
104
|
this.labels = injectContentfulFileLabels();
|
|
108
105
|
this.asset = input.required(/* @ts-ignore */
|
|
109
106
|
...(ngDevMode ? [{ debugName: "asset" }] : /* istanbul ignore next */ []));
|
|
110
|
-
this.fileClass = input(null, /* @ts-ignore */
|
|
111
|
-
...(ngDevMode ? [{ debugName: "fileClass" }] : /* istanbul ignore next */ []));
|
|
112
107
|
this.data = computed(() => {
|
|
113
108
|
const asset = this.asset();
|
|
114
109
|
if (!asset) {
|
|
@@ -133,16 +128,16 @@ class ContentfulFileComponent {
|
|
|
133
128
|
...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
|
|
134
129
|
}
|
|
135
130
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulFileComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
136
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: ContentfulFileComponent, isStandalone: true, selector: "et-contentful-file", inputs: { asset: { classPropertyName: "asset", publicName: "asset", isSignal: true, isRequired: true, transformFunction: null }
|
|
131
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: ContentfulFileComponent, isStandalone: true, selector: "et-contentful-file", inputs: { asset: { classPropertyName: "asset", publicName: "asset", isSignal: true, isRequired: true, transformFunction: null } }, host: { classAttribute: "et-contentful-file" }, ngImport: i0, template: `
|
|
137
132
|
@if (data(); as data) {
|
|
138
|
-
<a [href]="data.url"
|
|
133
|
+
<a [href]="data.url" class="et-contentful-file-anchor" target="_blank" rel="noopener noreferrer">
|
|
139
134
|
{{ data.title }}
|
|
140
135
|
@if (data.size !== null) {
|
|
141
136
|
{{ labels().fileSize(data.size) }}
|
|
142
137
|
}
|
|
143
138
|
</a>
|
|
144
139
|
}
|
|
145
|
-
`, isInline: true,
|
|
140
|
+
`, isInline: true, encapsulation: i0.ViewEncapsulation.None }); }
|
|
146
141
|
}
|
|
147
142
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulFileComponent, decorators: [{
|
|
148
143
|
type: Component,
|
|
@@ -150,7 +145,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
150
145
|
selector: 'et-contentful-file',
|
|
151
146
|
template: `
|
|
152
147
|
@if (data(); as data) {
|
|
153
|
-
<a [href]="data.url"
|
|
148
|
+
<a [href]="data.url" class="et-contentful-file-anchor" target="_blank" rel="noopener noreferrer">
|
|
154
149
|
{{ data.title }}
|
|
155
150
|
@if (data.size !== null) {
|
|
156
151
|
{{ labels().fileSize(data.size) }}
|
|
@@ -159,12 +154,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
159
154
|
}
|
|
160
155
|
`,
|
|
161
156
|
encapsulation: ViewEncapsulation.None,
|
|
162
|
-
imports: [NgClass],
|
|
163
157
|
host: {
|
|
164
158
|
class: 'et-contentful-file',
|
|
165
159
|
},
|
|
166
160
|
}]
|
|
167
|
-
}], propDecorators: { asset: [{ type: i0.Input, args: [{ isSignal: true, alias: "asset", required: true }] }]
|
|
161
|
+
}], propDecorators: { asset: [{ type: i0.Input, args: [{ isSignal: true, alias: "asset", required: true }] }] } });
|
|
168
162
|
|
|
169
163
|
const CONTENTFUL_CONFIG = new InjectionToken('ContentfulConfig');
|
|
170
164
|
|
|
@@ -175,6 +169,7 @@ const CONTENTFUL_CONFIG = new InjectionToken('ContentfulConfig');
|
|
|
175
169
|
*/
|
|
176
170
|
const CONTENTFUL_FALLBACK_CONFIG = {
|
|
177
171
|
internalHosts: [],
|
|
172
|
+
entryHref: null,
|
|
178
173
|
components: {},
|
|
179
174
|
customComponents: {},
|
|
180
175
|
imageOptions: {
|
|
@@ -183,13 +178,16 @@ const CONTENTFUL_FALLBACK_CONFIG = {
|
|
|
183
178
|
backgroundColor: null,
|
|
184
179
|
},
|
|
185
180
|
};
|
|
181
|
+
const withoutUndefined = (value) => Object.fromEntries(Object.entries(value ?? {}).filter(([, entry]) => entry !== undefined));
|
|
186
182
|
const createContentfulConfig = (options) => {
|
|
187
183
|
const config = options ?? {};
|
|
188
184
|
return {
|
|
189
185
|
...CONTENTFUL_FALLBACK_CONFIG,
|
|
190
186
|
...config,
|
|
191
187
|
components: config.components ?? CONTENTFUL_FALLBACK_CONFIG.components,
|
|
192
|
-
|
|
188
|
+
customComponents: config.customComponents ?? CONTENTFUL_FALLBACK_CONFIG.customComponents,
|
|
189
|
+
internalHosts: config.internalHosts ?? CONTENTFUL_FALLBACK_CONFIG.internalHosts,
|
|
190
|
+
imageOptions: { ...CONTENTFUL_FALLBACK_CONFIG.imageOptions, ...withoutUndefined(config.imageOptions) },
|
|
193
191
|
};
|
|
194
192
|
};
|
|
195
193
|
/**
|
|
@@ -199,29 +197,8 @@ const createContentfulConfig = (options) => {
|
|
|
199
197
|
const injectContentfulConfig = () => inject(CONTENTFUL_CONFIG, { optional: true }) ?? CONTENTFUL_FALLBACK_CONFIG;
|
|
200
198
|
|
|
201
199
|
const generateDefaultContentfulImageSource = (data) => {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
return {
|
|
205
|
-
type: '',
|
|
206
|
-
srcset: '',
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
return {
|
|
210
|
-
type: data.contentType,
|
|
211
|
-
srcset: data.url,
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
const file = data.fields.file;
|
|
215
|
-
if (!file?.contentType || !file.url) {
|
|
216
|
-
return {
|
|
217
|
-
type: '',
|
|
218
|
-
srcset: '',
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
return {
|
|
222
|
-
type: file.contentType,
|
|
223
|
-
srcset: file.url,
|
|
224
|
-
};
|
|
200
|
+
const { url, contentType } = isContentfulGqlAsset(data) ? data : (data.fields.file ?? {});
|
|
201
|
+
return url ? { type: contentType ?? '', srcset: url } : { type: '', srcset: '' };
|
|
225
202
|
};
|
|
226
203
|
/**
|
|
227
204
|
* Parses source set sizes into an object containing with and height. Eg.
|
|
@@ -267,7 +244,7 @@ const generateContentfulImageSources = (data, options = {}) => {
|
|
|
267
244
|
const queryParams = [];
|
|
268
245
|
queryParams.push(`fm=${type.split('/')[1]}`);
|
|
269
246
|
if (backgroundColor) {
|
|
270
|
-
queryParams.push(`bg=rgb:${backgroundColor}`);
|
|
247
|
+
queryParams.push(`bg=rgb:${backgroundColor.replace(/^#/, '')}`);
|
|
271
248
|
}
|
|
272
249
|
if (quality !== null && Number.isFinite(quality)) {
|
|
273
250
|
queryParams.push(`q=${quality}`);
|
|
@@ -292,6 +269,11 @@ const generateContentfulImageSources = (data, options = {}) => {
|
|
|
292
269
|
const derivedWidth = Math.round((height * imageDimensions.width) / imageDimensions.height);
|
|
293
270
|
sourceSets.push(`${urlWithParams}&w=${derivedWidth}&h=${height} ${derivedWidth}w`);
|
|
294
271
|
}
|
|
272
|
+
else if (ngDevMode && type === SOURCE_TYPES[0]) {
|
|
273
|
+
console.warn(height
|
|
274
|
+
? `Contentful image: srcsetSizes entry "${size}" is dropped. A height-only size needs the asset's width and height to derive a width descriptor.`
|
|
275
|
+
: `Contentful image: srcsetSizes entry "${size}" is not a valid size and is dropped. Use "400", "400w", "400h" or "400x300"; viewport sizes like "50vw" belong in sizes.`);
|
|
276
|
+
}
|
|
295
277
|
}
|
|
296
278
|
}
|
|
297
279
|
sources.push({
|
|
@@ -377,7 +359,7 @@ class ContentfulImageComponent {
|
|
|
377
359
|
[sizes]="sizes()"
|
|
378
360
|
[sources]="sourcesValue()"
|
|
379
361
|
/>
|
|
380
|
-
`, isInline: true, dependencies: [{ kind: "component", type: PictureComponent, selector: "et-picture", inputs: ["sources", "defaultSrc", "alt", "figcaption", "priority", "width", "height", "aspectRatio", "sizes", "fit"], outputs: ["
|
|
362
|
+
`, isInline: true, dependencies: [{ kind: "component", type: PictureComponent, selector: "et-picture", inputs: ["sources", "defaultSrc", "alt", "figcaption", "priority", "width", "height", "aspectRatio", "sizes", "fit"], outputs: ["imageLoad", "imageError"] }], encapsulation: i0.ViewEncapsulation.None }); }
|
|
381
363
|
}
|
|
382
364
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulImageComponent, decorators: [{
|
|
383
365
|
type: Component,
|
|
@@ -414,29 +396,46 @@ const parseWebUrl = (href) => {
|
|
|
414
396
|
return null;
|
|
415
397
|
}
|
|
416
398
|
};
|
|
417
|
-
const
|
|
399
|
+
const parseConfiguredHost = (host) => {
|
|
418
400
|
const value = host.trim().toLowerCase();
|
|
401
|
+
const wildcard = value.startsWith('*.');
|
|
402
|
+
const bare = wildcard ? value.slice(2) : value;
|
|
419
403
|
try {
|
|
420
|
-
|
|
404
|
+
const url = new URL(bare.includes('://') ? bare : 'https://' + bare);
|
|
405
|
+
return { hostname: url.hostname, port: url.port, wildcard };
|
|
421
406
|
}
|
|
422
407
|
catch {
|
|
423
|
-
return
|
|
408
|
+
return { hostname: bare.split(':')[0] ?? bare, port: '', wildcard };
|
|
424
409
|
}
|
|
425
410
|
};
|
|
426
|
-
const
|
|
427
|
-
const
|
|
428
|
-
|
|
411
|
+
const matchesConfiguredHost = (url, configuredHost) => {
|
|
412
|
+
const { hostname, port, wildcard } = parseConfiguredHost(configuredHost);
|
|
413
|
+
if (!hostname || (port && url.port !== port))
|
|
414
|
+
return false;
|
|
415
|
+
return wildcard ? url.hostname.endsWith('.' + hostname) : url.hostname === hostname;
|
|
429
416
|
};
|
|
430
417
|
const isInternalWebUrl = (url, { location, internalHosts }) => (url.hostname === location.hostname && url.port === location.port) ||
|
|
431
|
-
internalHosts.some((host) =>
|
|
418
|
+
internalHosts.some((host) => matchesConfiguredHost(url, host));
|
|
432
419
|
const isExternalWebHref = (href, context) => {
|
|
433
420
|
const url = parseWebUrl(href);
|
|
434
421
|
return url !== null && !isInternalWebUrl(url, context);
|
|
435
422
|
};
|
|
423
|
+
const internalWebHrefPath = (href, context) => {
|
|
424
|
+
const url = parseWebUrl(href);
|
|
425
|
+
return url && isInternalWebUrl(url, context) ? url.pathname + url.search + url.hash : null;
|
|
426
|
+
};
|
|
427
|
+
const hasUrlScheme = (href) => /^[a-z][a-z\d+.-]*:/i.test(href);
|
|
428
|
+
const isRouteRelativeHref = (href) => !hasUrlScheme(href) && !href.startsWith('/');
|
|
429
|
+
const resolveHrefAgainstRoute = (href, routerUrl) => {
|
|
430
|
+
const resolved = new URL(href, 'https://contentful.invalid' + routerUrl);
|
|
431
|
+
return resolved.pathname + resolved.search + resolved.hash;
|
|
432
|
+
};
|
|
436
433
|
|
|
434
|
+
const RICH_TEXT_ANCHOR_CLASSES = ['et-contentful-rich-text-default-element', 'et-contentful-rich-text-default-a'];
|
|
437
435
|
class ContentfulLinkComponent {
|
|
438
436
|
constructor() {
|
|
439
437
|
this.document = inject(DOCUMENT);
|
|
438
|
+
this.location = inject(Location);
|
|
440
439
|
this.router = inject(Router);
|
|
441
440
|
this.config = injectContentfulConfig();
|
|
442
441
|
this.url = injectUrl();
|
|
@@ -444,10 +443,10 @@ class ContentfulLinkComponent {
|
|
|
444
443
|
...(ngDevMode ? [{ debugName: "href" }] : /* istanbul ignore next */ []));
|
|
445
444
|
this.text = input.required(/* @ts-ignore */
|
|
446
445
|
...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
|
|
447
|
-
this.
|
|
448
|
-
...(ngDevMode ? [{ debugName: "
|
|
449
|
-
this.
|
|
450
|
-
...(ngDevMode ? [{ debugName: "
|
|
446
|
+
this.marks = input([], /* @ts-ignore */
|
|
447
|
+
...(ngDevMode ? [{ debugName: "marks" }] : /* istanbul ignore next */ []));
|
|
448
|
+
this.richText = input(false, /* @ts-ignore */
|
|
449
|
+
...(ngDevMode ? [{ debugName: "richText" }] : /* istanbul ignore next */ []));
|
|
451
450
|
this.usesRouterLink = computed(() => {
|
|
452
451
|
const href = this.href();
|
|
453
452
|
const absoluteUrl = parseWebUrl(href);
|
|
@@ -457,9 +456,14 @@ class ContentfulLinkComponent {
|
|
|
457
456
|
internalHosts: this.config.internalHosts,
|
|
458
457
|
});
|
|
459
458
|
}
|
|
460
|
-
return !href.startsWith('#') &&
|
|
459
|
+
return !href.startsWith('#') && !hasUrlScheme(href);
|
|
461
460
|
}, /* @ts-ignore */
|
|
462
461
|
...(ngDevMode ? [{ debugName: "usesRouterLink" }] : /* istanbul ignore next */ []));
|
|
462
|
+
this.anchorHref = computed(() => {
|
|
463
|
+
const href = this.href();
|
|
464
|
+
return href.startsWith('#') ? this.location.prepareExternalUrl(resolveHrefAgainstRoute(href, this.url())) : href;
|
|
465
|
+
}, /* @ts-ignore */
|
|
466
|
+
...(ngDevMode ? [{ debugName: "anchorHref" }] : /* istanbul ignore next */ []));
|
|
463
467
|
this.openInNewTab = computed(() => Boolean(parseWebUrl(this.href())) && !this.usesRouterLink(), /* @ts-ignore */
|
|
464
468
|
...(ngDevMode ? [{ debugName: "openInNewTab" }] : /* istanbul ignore next */ []));
|
|
465
469
|
this.internalPath = computed(() => {
|
|
@@ -471,25 +475,28 @@ class ContentfulLinkComponent {
|
|
|
471
475
|
if (href.startsWith('/')) {
|
|
472
476
|
return href;
|
|
473
477
|
}
|
|
474
|
-
|
|
475
|
-
return resolved.pathname + resolved.search + resolved.hash;
|
|
478
|
+
return resolveHrefAgainstRoute(href, this.url());
|
|
476
479
|
}, /* @ts-ignore */
|
|
477
480
|
...(ngDevMode ? [{ debugName: "internalPath" }] : /* istanbul ignore next */ []));
|
|
478
481
|
this.internalUrlTree = computed(() => this.router.parseUrl(this.internalPath()), /* @ts-ignore */
|
|
479
482
|
...(ngDevMode ? [{ debugName: "internalUrlTree" }] : /* istanbul ignore next */ []));
|
|
480
|
-
this.
|
|
481
|
-
|
|
483
|
+
this.modifierClasses = computed(() => [
|
|
484
|
+
...(this.richText() ? RICH_TEXT_ANCHOR_CLASSES : []),
|
|
485
|
+
...this.marks().map((mark) => `et-contentful-rich-text-mark-${mark}`),
|
|
486
|
+
], /* @ts-ignore */
|
|
487
|
+
...(ngDevMode ? [{ debugName: "modifierClasses" }] : /* istanbul ignore next */ []));
|
|
482
488
|
}
|
|
483
489
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulLinkComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
484
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: ContentfulLinkComponent, isStandalone: true, selector: "et-contentful-link", inputs: { href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: true, transformFunction: null }, text: { classPropertyName: "text", publicName: "text", isSignal: true, isRequired: true, transformFunction: null },
|
|
490
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: ContentfulLinkComponent, isStandalone: true, selector: "et-contentful-link", inputs: { href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: true, transformFunction: null }, text: { classPropertyName: "text", publicName: "text", isSignal: true, isRequired: true, transformFunction: null }, marks: { classPropertyName: "marks", publicName: "marks", isSignal: true, isRequired: false, transformFunction: null }, richText: { classPropertyName: "richText", publicName: "richText", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "et-contentful-link" }, ngImport: i0, template: `
|
|
485
491
|
@if (usesRouterLink()) {
|
|
486
|
-
<a [class]="
|
|
492
|
+
<a [class]="modifierClasses()" [routerLink]="internalUrlTree()" class="et-contentful-link-anchor">{{ text() }}</a>
|
|
487
493
|
} @else {
|
|
488
494
|
<a
|
|
489
|
-
[class]="
|
|
490
|
-
[href]="
|
|
491
|
-
[target]="openInNewTab() ? '_blank' : null"
|
|
492
|
-
[rel]="openInNewTab() ? 'noopener noreferrer' : null"
|
|
495
|
+
[class]="modifierClasses()"
|
|
496
|
+
[href]="anchorHref()"
|
|
497
|
+
[attr.target]="openInNewTab() ? '_blank' : null"
|
|
498
|
+
[attr.rel]="openInNewTab() ? 'noopener noreferrer' : null"
|
|
499
|
+
class="et-contentful-link-anchor"
|
|
493
500
|
>{{ text() }}</a
|
|
494
501
|
>
|
|
495
502
|
}
|
|
@@ -499,20 +506,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
499
506
|
type: Component,
|
|
500
507
|
args: [{ selector: 'et-contentful-link', template: `
|
|
501
508
|
@if (usesRouterLink()) {
|
|
502
|
-
<a [class]="
|
|
509
|
+
<a [class]="modifierClasses()" [routerLink]="internalUrlTree()" class="et-contentful-link-anchor">{{ text() }}</a>
|
|
503
510
|
} @else {
|
|
504
511
|
<a
|
|
505
|
-
[class]="
|
|
506
|
-
[href]="
|
|
507
|
-
[target]="openInNewTab() ? '_blank' : null"
|
|
508
|
-
[rel]="openInNewTab() ? 'noopener noreferrer' : null"
|
|
512
|
+
[class]="modifierClasses()"
|
|
513
|
+
[href]="anchorHref()"
|
|
514
|
+
[attr.target]="openInNewTab() ? '_blank' : null"
|
|
515
|
+
[attr.rel]="openInNewTab() ? 'noopener noreferrer' : null"
|
|
516
|
+
class="et-contentful-link-anchor"
|
|
509
517
|
>{{ text() }}</a
|
|
510
518
|
>
|
|
511
519
|
}
|
|
512
520
|
`, encapsulation: ViewEncapsulation.None, imports: [RouterLink], host: {
|
|
513
521
|
class: 'et-contentful-link',
|
|
514
522
|
}, styles: ["@layer components{.et-contentful-link{display:contents}}\n"] }]
|
|
515
|
-
}], propDecorators: { href: [{ type: i0.Input, args: [{ isSignal: true, alias: "href", required: true }] }], text: [{ type: i0.Input, args: [{ isSignal: true, alias: "text", required: true }] }],
|
|
523
|
+
}], propDecorators: { href: [{ type: i0.Input, args: [{ isSignal: true, alias: "href", required: true }] }], text: [{ type: i0.Input, args: [{ isSignal: true, alias: "text", required: true }] }], marks: [{ type: i0.Input, args: [{ isSignal: true, alias: "marks", required: false }] }], richText: [{ type: i0.Input, args: [{ isSignal: true, alias: "richText", required: false }] }] } });
|
|
516
524
|
|
|
517
525
|
/**
|
|
518
526
|
* The `BLOCKS` node type strings of `@contentful/rich-text-types`, as a plain literal map.
|
|
@@ -558,8 +566,9 @@ const CF_INLINES = {
|
|
|
558
566
|
};
|
|
559
567
|
|
|
560
568
|
const RICH_TEXT_RENDERER_ERRORS = {
|
|
561
|
-
|
|
569
|
+
rich_text_not_object: (data) => `The value at richTextPath is a ${data.type}, not a rich-text document object. Point richTextPath at the rich-text field itself (e.g. items[0].fields.body).`,
|
|
562
570
|
rich_text_wrong_type: 'The rich text object does not satisfy the RichTextResponse interface. It should contain a property named "nodeType" with the value "document".',
|
|
571
|
+
rich_text_conflicting_inputs: 'Set exactly one rich-text source: a GraphQL rich-text field through gqlRichText, a REST response through content and richTextPath, or a bare document through richText (with includes).',
|
|
563
572
|
asset_id_not_found: 'The asset ID was not found. This node is not supported.',
|
|
564
573
|
entry_id_not_found: 'The entry ID was not found. This node is not supported.',
|
|
565
574
|
text_parent_not_found: 'The parent node is not found. This structure is not supported.',
|
|
@@ -567,8 +576,9 @@ const RICH_TEXT_RENDERER_ERRORS = {
|
|
|
567
576
|
cached_command_not_component: 'The rendered node for a component command is not a component. The renderer state is out of sync.',
|
|
568
577
|
};
|
|
569
578
|
const RICH_TEXT_RENDERER_ERROR_CODES = {
|
|
570
|
-
|
|
579
|
+
rich_text_not_object: 0,
|
|
571
580
|
rich_text_wrong_type: 1,
|
|
581
|
+
rich_text_conflicting_inputs: 11,
|
|
572
582
|
asset_id_not_found: 2,
|
|
573
583
|
entry_id_not_found: 3,
|
|
574
584
|
text_parent_not_found: 7,
|
|
@@ -576,10 +586,80 @@ const RICH_TEXT_RENDERER_ERROR_CODES = {
|
|
|
576
586
|
cached_command_not_component: 10,
|
|
577
587
|
};
|
|
578
588
|
const richTextRendererError = (code, data) => {
|
|
579
|
-
const
|
|
589
|
+
const entry = RICH_TEXT_RENDERER_ERRORS[code];
|
|
590
|
+
const message = `<et-contentful-rich-text-renderer>: ${typeof entry === 'function' ? entry(data) : entry}`;
|
|
580
591
|
throw new RuntimeError(RICH_TEXT_RENDERER_ERROR_CODES[code], message, data);
|
|
581
592
|
};
|
|
582
593
|
|
|
594
|
+
const lowerCaseFirst = (value) => value.charAt(0).toLowerCase() + value.slice(1);
|
|
595
|
+
const toGqlTypeName = (contentTypeId) => {
|
|
596
|
+
const pascal = contentTypeId.replace(/[^a-z0-9]+(.)?/gi, (_, next) => next?.toUpperCase() ?? '');
|
|
597
|
+
return pascal.charAt(0).toUpperCase() + pascal.slice(1);
|
|
598
|
+
};
|
|
599
|
+
const toContentTypeId = (typename, contentTypeIds) => contentTypeIds.find((id) => toGqlTypeName(id) === typename) ?? lowerCaseFirst(typename);
|
|
600
|
+
const toRestAsset = (asset) => ({
|
|
601
|
+
sys: { type: 'Asset', id: asset.sys.id, createdAt: '', updatedAt: '', locale: '' },
|
|
602
|
+
fields: {
|
|
603
|
+
title: asset.title ?? '',
|
|
604
|
+
description: asset.description ?? '',
|
|
605
|
+
file: {
|
|
606
|
+
url: asset.url,
|
|
607
|
+
details: {
|
|
608
|
+
size: asset.size,
|
|
609
|
+
...(asset.width !== null && asset.height !== null
|
|
610
|
+
? { image: { width: asset.width, height: asset.height } }
|
|
611
|
+
: {}),
|
|
612
|
+
},
|
|
613
|
+
fileName: asset.fileName ?? null,
|
|
614
|
+
contentType: asset.contentType,
|
|
615
|
+
},
|
|
616
|
+
},
|
|
617
|
+
metadata: { tags: [] },
|
|
618
|
+
});
|
|
619
|
+
const toRestEntry = (entry, contentTypeIds) => {
|
|
620
|
+
const { __typename, sys, ...fields } = entry;
|
|
621
|
+
if (!__typename) {
|
|
622
|
+
return null;
|
|
623
|
+
}
|
|
624
|
+
return {
|
|
625
|
+
sys: {
|
|
626
|
+
type: 'Entry',
|
|
627
|
+
id: sys.id,
|
|
628
|
+
createdAt: '',
|
|
629
|
+
updatedAt: '',
|
|
630
|
+
locale: '',
|
|
631
|
+
contentType: { sys: { type: 'Link', linkType: 'ContentType', id: toContentTypeId(__typename, contentTypeIds) } },
|
|
632
|
+
},
|
|
633
|
+
fields,
|
|
634
|
+
metadata: { tags: [] },
|
|
635
|
+
};
|
|
636
|
+
};
|
|
637
|
+
const present = (items) => (items ?? []).filter((item) => item !== null && item !== undefined);
|
|
638
|
+
/**
|
|
639
|
+
* Maps the `links` of a GraphQL rich-text field to the entries and assets the renderer resolves
|
|
640
|
+
* embeds against. An entry's content type id is the id in `contentTypeIds` whose GraphQL type name
|
|
641
|
+
* (PascalCase, separators dropped: `product-teaser` → `ProductTeaser`) equals its `__typename`, else the
|
|
642
|
+
* `__typename` with the first letter lower-cased. Entries without a `__typename` are dropped.
|
|
643
|
+
*/
|
|
644
|
+
const mapContentfulGqlLinks = (links, contentTypeIds = []) => {
|
|
645
|
+
const assets = new Map();
|
|
646
|
+
const entries = new Map();
|
|
647
|
+
for (const asset of [...present(links?.assets?.block), ...present(links?.assets?.hyperlink)]) {
|
|
648
|
+
assets.set(asset.sys.id, toRestAsset(asset));
|
|
649
|
+
}
|
|
650
|
+
for (const gqlEntry of [
|
|
651
|
+
...present(links?.entries?.block),
|
|
652
|
+
...present(links?.entries?.inline),
|
|
653
|
+
...present(links?.entries?.hyperlink),
|
|
654
|
+
]) {
|
|
655
|
+
const entry = toRestEntry(gqlEntry, contentTypeIds);
|
|
656
|
+
if (entry) {
|
|
657
|
+
entries.set(entry.sys.id, entry);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
return { assets: [...assets.values()], entries: [...entries.values()] };
|
|
661
|
+
};
|
|
662
|
+
|
|
583
663
|
const isRichTextRootNode = (node) => {
|
|
584
664
|
return isObject(node) && 'nodeType' in node && node['nodeType'] === 'document';
|
|
585
665
|
};
|
|
@@ -669,6 +749,12 @@ const isExecutedComponentCommandCacheItem = (cache) => {
|
|
|
669
749
|
return cache.command.kind === 'component';
|
|
670
750
|
};
|
|
671
751
|
const ET_CONTENTFUL_ANY_ENTRY_CONTENT_TYPE_SYS_ID = '$$$_et-contentful-any-entry-content-type-sys-id';
|
|
752
|
+
const includeMapLookups = /* @__PURE__ */ new WeakMap();
|
|
753
|
+
const lookupOf = (includes) => includeMapLookups.get(includes) ?? {
|
|
754
|
+
entry: (id) => includes.getEntry(id, ET_CONTENTFUL_ANY_ENTRY_CONTENT_TYPE_SYS_ID),
|
|
755
|
+
asset: (id) => includes.getAsset(id),
|
|
756
|
+
};
|
|
757
|
+
const ANCESTOR_ENTRY_IDS = /* @__PURE__ */ new InjectionToken('ContentfulRichTextAncestorEntryIds');
|
|
672
758
|
/**
|
|
673
759
|
* Create a contentful include map using the provided entries and assets.
|
|
674
760
|
*/
|
|
@@ -681,7 +767,7 @@ const createContentfulIncludeMap = (config) => {
|
|
|
681
767
|
const entry = entryMap.get(id);
|
|
682
768
|
if (!entry) {
|
|
683
769
|
if (isDevMode()) {
|
|
684
|
-
console.warn('Entry not found! Will return null. Is the include query param too low?', { id
|
|
770
|
+
console.warn('Entry not found! Will return null. Is it unpublished or deleted, or is the include query param too low?', { id });
|
|
685
771
|
}
|
|
686
772
|
return null;
|
|
687
773
|
}
|
|
@@ -713,12 +799,29 @@ const createContentfulIncludeMap = (config) => {
|
|
|
713
799
|
const getAssets = (ids) => {
|
|
714
800
|
return ids.map((id) => getAsset(id)).filter((asset) => asset !== null);
|
|
715
801
|
};
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
802
|
+
const includeMap = { getEntry, getEntries, getAsset, getAssets };
|
|
803
|
+
includeMapLookups.set(includeMap, {
|
|
804
|
+
entry: (id) => entryMap.get(id) ?? null,
|
|
805
|
+
asset: (id) => assetMap.get(id) ?? null,
|
|
806
|
+
});
|
|
807
|
+
return includeMap;
|
|
808
|
+
};
|
|
809
|
+
const warnedRichTextPaths = /* @__PURE__ */ new Set();
|
|
810
|
+
const warnOnUnresolvedParentPath = (content, richTextPath) => {
|
|
811
|
+
const segments = richTextPath.split('.');
|
|
812
|
+
const root = content;
|
|
813
|
+
if (segments.length < 2 || isObject(getObjectProperty(root, segments.slice(0, -1).join('.'))))
|
|
814
|
+
return;
|
|
815
|
+
if (warnedRichTextPaths.has(richTextPath))
|
|
816
|
+
return;
|
|
817
|
+
warnedRichTextPaths.add(richTextPath);
|
|
818
|
+
let resolvedSegments = 0;
|
|
819
|
+
while (resolvedSegments < segments.length - 1 &&
|
|
820
|
+
isObject(getObjectProperty(root, segments.slice(0, resolvedSegments + 1).join('.')))) {
|
|
821
|
+
resolvedSegments++;
|
|
822
|
+
}
|
|
823
|
+
const resolvedPath = segments.slice(0, resolvedSegments).join('.') || '(the content itself)';
|
|
824
|
+
console.warn(`<et-contentful-rich-text-renderer>: richTextPath "${richTextPath}" does not resolve, so nothing is rendered. The deepest part that resolves is ${resolvedPath}; check the next segment for a typo or a missing item.`, { content, richTextPath });
|
|
722
825
|
};
|
|
723
826
|
class ContentfulRichTextRendererComponent {
|
|
724
827
|
constructor() {
|
|
@@ -727,43 +830,102 @@ class ContentfulRichTextRendererComponent {
|
|
|
727
830
|
this.elementRef = inject(ElementRef);
|
|
728
831
|
this.config = injectContentfulConfig();
|
|
729
832
|
this.document = inject(DOCUMENT);
|
|
833
|
+
this.location = inject(Location);
|
|
834
|
+
this.url = injectUrl();
|
|
835
|
+
this.injector = inject(Injector);
|
|
730
836
|
/**
|
|
731
837
|
* The contentful response gotten via their REST api.
|
|
732
838
|
* @see https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/entries/entries-collection
|
|
733
839
|
*/
|
|
734
|
-
this.content = input
|
|
735
|
-
...(ngDevMode ? [{ debugName: "content" }] : /* istanbul ignore next */ []));
|
|
840
|
+
this.content = input(/* @ts-ignore */
|
|
841
|
+
...(ngDevMode ? [undefined, { debugName: "content" }] : /* istanbul ignore next */ []));
|
|
736
842
|
/**
|
|
737
843
|
* The path to where the rich text field is inside the contentful response. Dot and array notation is allowed.
|
|
738
844
|
* @example "items[0].fields.html"
|
|
739
845
|
*/
|
|
740
|
-
this.richTextPath = input
|
|
741
|
-
...(ngDevMode ? [{ debugName: "richTextPath" }] : /* istanbul ignore next */ []));
|
|
846
|
+
this.richTextPath = input(/* @ts-ignore */
|
|
847
|
+
...(ngDevMode ? [undefined, { debugName: "richTextPath" }] : /* istanbul ignore next */ []));
|
|
848
|
+
/**
|
|
849
|
+
* A rich-text field from the Contentful GraphQL API, passed as-is. An alternative to `content` and
|
|
850
|
+
* `richTextPath`; setting both is an error in dev mode.
|
|
851
|
+
* @example { json, links: { assets: { block, hyperlink }, entries: { block, inline, hyperlink } } }
|
|
852
|
+
*/
|
|
853
|
+
this.gqlRichText = input(/* @ts-ignore */
|
|
854
|
+
...(ngDevMode ? [undefined, { debugName: "gqlRichText" }] : /* istanbul ignore next */ []));
|
|
855
|
+
/**
|
|
856
|
+
* A bare rich-text document, e.g. a rich-text field of an embedded entry. Pair it with `includes` so its
|
|
857
|
+
* embeds resolve. An alternative to `content` / `richTextPath` and `gqlRichText`.
|
|
858
|
+
* @example <et-contentful-rich-text-renderer [richText]="fields().body" [includes]="includes()" />
|
|
859
|
+
*/
|
|
860
|
+
this.richText = input(/* @ts-ignore */
|
|
861
|
+
...(ngDevMode ? [undefined, { debugName: "richText" }] : /* istanbul ignore next */ []));
|
|
862
|
+
/** The linked entries and assets `richText` resolves its embeds against, e.g. a custom component's `includes`. */
|
|
863
|
+
this.includes = input(/* @ts-ignore */
|
|
864
|
+
...(ngDevMode ? [undefined, { debugName: "includes" }] : /* istanbul ignore next */ []));
|
|
865
|
+
this.ancestorEntryIds = inject(ANCESTOR_ENTRY_IDS, { optional: true }) ?? new Set();
|
|
742
866
|
this.executedCommandsCache = new Map();
|
|
743
867
|
this.contentIncludesMap = computed(() => {
|
|
868
|
+
const includes = this.includes();
|
|
869
|
+
if (includes) {
|
|
870
|
+
return includes;
|
|
871
|
+
}
|
|
872
|
+
const gqlRichText = this.gqlRichText();
|
|
873
|
+
if (gqlRichText) {
|
|
874
|
+
return createContentfulIncludeMap(mapContentfulGqlLinks(gqlRichText.links, Object.keys(this.config.customComponents)));
|
|
875
|
+
}
|
|
744
876
|
const content = this.content();
|
|
745
|
-
const
|
|
746
|
-
const
|
|
747
|
-
|
|
877
|
+
const entries = new Map();
|
|
878
|
+
for (const entry of [...(content?.items ?? []), ...(content?.includes?.Entry ?? [])]) {
|
|
879
|
+
entries.set(entry.sys.id, entry);
|
|
880
|
+
}
|
|
881
|
+
return createContentfulIncludeMap({ assets: content?.includes?.Asset ?? [], entries: [...entries.values()] });
|
|
748
882
|
}, /* @ts-ignore */
|
|
749
883
|
...(ngDevMode ? [{ debugName: "contentIncludesMap" }] : /* istanbul ignore next */ []));
|
|
884
|
+
this.linkLookup = computed(() => lookupOf(this.contentIncludesMap()), /* @ts-ignore */
|
|
885
|
+
...(ngDevMode ? [{ debugName: "linkLookup" }] : /* istanbul ignore next */ []));
|
|
750
886
|
this.richTextData = computed(() => {
|
|
887
|
+
const gqlRichText = this.gqlRichText();
|
|
751
888
|
const content = this.content();
|
|
752
889
|
const richTextPath = this.richTextPath();
|
|
890
|
+
const richText = this.richText();
|
|
891
|
+
if (ngDevMode && [gqlRichText, content || richTextPath, richText].filter(Boolean).length > 1) {
|
|
892
|
+
throw richTextRendererError('rich_text_conflicting_inputs', { gqlRichText, content, richTextPath, richText });
|
|
893
|
+
}
|
|
894
|
+
if (richText) {
|
|
895
|
+
if (!isRichTextRootNode(richText)) {
|
|
896
|
+
throw richTextRendererError('rich_text_wrong_type', { richText });
|
|
897
|
+
}
|
|
898
|
+
return richText;
|
|
899
|
+
}
|
|
900
|
+
if (gqlRichText) {
|
|
901
|
+
if (!isRichTextRootNode(gqlRichText.json)) {
|
|
902
|
+
throw richTextRendererError('rich_text_wrong_type', { gqlRichText });
|
|
903
|
+
}
|
|
904
|
+
return gqlRichText.json;
|
|
905
|
+
}
|
|
753
906
|
if (!content) {
|
|
754
907
|
return null;
|
|
755
908
|
}
|
|
756
|
-
|
|
757
|
-
|
|
909
|
+
if (!richTextPath) {
|
|
910
|
+
if (ngDevMode) {
|
|
911
|
+
console.warn('<et-contentful-rich-text-renderer>: content is set without richTextPath, so nothing is rendered. Set richTextPath, or pass a GraphQL field through gqlRichText.');
|
|
912
|
+
}
|
|
913
|
+
return null;
|
|
914
|
+
}
|
|
915
|
+
const pathValue = getObjectProperty(content, richTextPath);
|
|
916
|
+
if (pathValue === null || pathValue === undefined) {
|
|
917
|
+
if (ngDevMode && pathValue === undefined) {
|
|
918
|
+
warnOnUnresolvedParentPath(content, richTextPath);
|
|
919
|
+
}
|
|
758
920
|
return null;
|
|
759
921
|
}
|
|
760
|
-
if (!isObject(
|
|
761
|
-
throw richTextRendererError('
|
|
922
|
+
if (!isObject(pathValue)) {
|
|
923
|
+
throw richTextRendererError('rich_text_not_object', { content, richTextPath, type: typeof pathValue });
|
|
762
924
|
}
|
|
763
|
-
if (!isRichTextRootNode(
|
|
925
|
+
if (!isRichTextRootNode(pathValue)) {
|
|
764
926
|
throw richTextRendererError('rich_text_wrong_type', { content, richTextPath });
|
|
765
927
|
}
|
|
766
|
-
return
|
|
928
|
+
return pathValue;
|
|
767
929
|
}, /* @ts-ignore */
|
|
768
930
|
...(ngDevMode ? [{ debugName: "richTextData" }] : /* istanbul ignore next */ []));
|
|
769
931
|
this.renderCommands = computed(() => {
|
|
@@ -883,7 +1045,7 @@ class ContentfulRichTextRendererComponent {
|
|
|
883
1045
|
if (!assetId) {
|
|
884
1046
|
throw richTextRendererError('asset_id_not_found', { node });
|
|
885
1047
|
}
|
|
886
|
-
const asset = this.
|
|
1048
|
+
const asset = this.linkLookup().asset(assetId);
|
|
887
1049
|
if (!asset) {
|
|
888
1050
|
if (isDevMode()) {
|
|
889
1051
|
console.warn('Embedded asset is missing from the includes! The asset will be skipped. Is it unpublished or deleted?', { assetId, node });
|
|
@@ -892,8 +1054,7 @@ class ContentfulRichTextRendererComponent {
|
|
|
892
1054
|
}
|
|
893
1055
|
const contentType = asset.fields.file?.contentType;
|
|
894
1056
|
const assetComponents = this.config.components;
|
|
895
|
-
|
|
896
|
-
if (isMissing) {
|
|
1057
|
+
if (!asset.fields.file?.url) {
|
|
897
1058
|
if (isDevMode()) {
|
|
898
1059
|
console.warn('Asset is missing file data! Asset will be skipped. Did you forget to upload a file for the current translation in Contentful?', { asset });
|
|
899
1060
|
}
|
|
@@ -940,16 +1101,32 @@ class ContentfulRichTextRendererComponent {
|
|
|
940
1101
|
}
|
|
941
1102
|
else if (node.nodeType === CF_INLINES.ASSET_HYPERLINK) {
|
|
942
1103
|
const assetId = node.data['target']?.sys?.id;
|
|
943
|
-
const asset = assetId ? this.
|
|
1104
|
+
const asset = assetId ? this.linkLookup().asset(assetId) : null;
|
|
944
1105
|
const url = asset?.fields.file?.url;
|
|
945
1106
|
href = url ? normalizeHref(url) : null;
|
|
946
1107
|
}
|
|
1108
|
+
else if (this.config.entryHref) {
|
|
1109
|
+
const entryId = node.data['target']?.sys?.id;
|
|
1110
|
+
const entry = entryId ? this.linkLookup().entry(entryId) : null;
|
|
1111
|
+
const entryHref = entry ? this.config.entryHref(entry) : null;
|
|
1112
|
+
href = entryHref ? normalizeHref(entryHref) : null;
|
|
1113
|
+
}
|
|
947
1114
|
const linkTexts = node.content.filter((child) => child.nodeType === 'text' && Boolean(child.value));
|
|
1115
|
+
if (!linkTexts.length) {
|
|
1116
|
+
break;
|
|
1117
|
+
}
|
|
948
1118
|
const linkText = linkTexts.map((child) => child.value).join('');
|
|
949
1119
|
const linkComponent = href ? this.config.components.link : null;
|
|
950
1120
|
if (href && !linkComponent) {
|
|
951
|
-
const
|
|
952
|
-
|
|
1121
|
+
const webUrlContext = { location: this.document.location, internalHosts: this.config.internalHosts };
|
|
1122
|
+
const internalPath = internalWebHrefPath(href, webUrlContext);
|
|
1123
|
+
const anchorHref = internalPath !== null
|
|
1124
|
+
? this.location.prepareExternalUrl(internalPath)
|
|
1125
|
+
: isRouteRelativeHref(href)
|
|
1126
|
+
? this.location.prepareExternalUrl(resolveHrefAgainstRoute(href, this.url()))
|
|
1127
|
+
: href;
|
|
1128
|
+
const attributes = { class: DEFAULT_ANCHOR_CLASS, href: anchorHref };
|
|
1129
|
+
if (isExternalWebHref(href, webUrlContext)) {
|
|
953
1130
|
attributes['target'] = '_blank';
|
|
954
1131
|
attributes['rel'] = 'noopener noreferrer';
|
|
955
1132
|
}
|
|
@@ -993,7 +1170,6 @@ class ContentfulRichTextRendererComponent {
|
|
|
993
1170
|
break;
|
|
994
1171
|
}
|
|
995
1172
|
const sharedMarks = linkTexts[0]?.marks.filter((mark) => linkTexts.every((child) => child.marks.some((other) => other.type === mark.type))) ?? [];
|
|
996
|
-
const textClass = marksToClass(sharedMarks);
|
|
997
1173
|
let linkComponentId = componentIdMap.get(LINK_COMPONENT_TYPE) ?? -1;
|
|
998
1174
|
const linkId = LINK_COMPONENT_TYPE + ++linkComponentId;
|
|
999
1175
|
componentIdMap.set(LINK_COMPONENT_TYPE, linkComponentId);
|
|
@@ -1003,7 +1179,7 @@ class ContentfulRichTextRendererComponent {
|
|
|
1003
1179
|
domPosition,
|
|
1004
1180
|
parentId: openElementIds.at(-1) ?? null,
|
|
1005
1181
|
component: linkComponent,
|
|
1006
|
-
inputs: { href, text: linkText,
|
|
1182
|
+
inputs: { href, text: linkText, marks: sharedMarks.map((mark) => mark.type), richText: true },
|
|
1007
1183
|
id: linkId,
|
|
1008
1184
|
});
|
|
1009
1185
|
domPosition++;
|
|
@@ -1015,10 +1191,16 @@ class ContentfulRichTextRendererComponent {
|
|
|
1015
1191
|
if (!entryId) {
|
|
1016
1192
|
throw richTextRendererError('entry_id_not_found', { node });
|
|
1017
1193
|
}
|
|
1018
|
-
|
|
1194
|
+
if (this.ancestorEntryIds.has(entryId)) {
|
|
1195
|
+
if (isDevMode()) {
|
|
1196
|
+
console.warn('Embedded entry embeds itself through its own rich text! The nested occurrence is skipped to stop the cycle.', { entryId, node });
|
|
1197
|
+
}
|
|
1198
|
+
break;
|
|
1199
|
+
}
|
|
1200
|
+
const entry = this.linkLookup().entry(entryId);
|
|
1019
1201
|
if (!entry) {
|
|
1020
1202
|
if (isDevMode()) {
|
|
1021
|
-
console.warn('Embedded entry is missing from the includes! The entry will be skipped. Is it unpublished or deleted?', { entryId, node });
|
|
1203
|
+
console.warn('Embedded entry is missing from the includes! The entry will be skipped. Is it unpublished or deleted, or is the include query param too low?', { entryId, node });
|
|
1022
1204
|
}
|
|
1023
1205
|
break;
|
|
1024
1206
|
}
|
|
@@ -1046,11 +1228,22 @@ class ContentfulRichTextRendererComponent {
|
|
|
1046
1228
|
sys: entry.sys,
|
|
1047
1229
|
includes: this.contentIncludesMap(),
|
|
1048
1230
|
},
|
|
1231
|
+
entryId,
|
|
1049
1232
|
id,
|
|
1050
1233
|
});
|
|
1051
1234
|
domPosition++;
|
|
1052
1235
|
break;
|
|
1053
1236
|
}
|
|
1237
|
+
case CF_BLOCKS.EMBEDDED_RESOURCE:
|
|
1238
|
+
case CF_INLINES.EMBEDDED_RESOURCE: {
|
|
1239
|
+
if (isDevMode()) {
|
|
1240
|
+
console.warn(`Rich text node type "${node.nodeType}" is not supported and is skipped.`, {
|
|
1241
|
+
urn: node.data['target']?.sys?.urn,
|
|
1242
|
+
node,
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
break;
|
|
1246
|
+
}
|
|
1054
1247
|
default: {
|
|
1055
1248
|
const tag = translateContentfulNodeTypeToHtmlTag(node.nodeType);
|
|
1056
1249
|
const attributes = {
|
|
@@ -1137,7 +1330,15 @@ class ContentfulRichTextRendererComponent {
|
|
|
1137
1330
|
const bindings = declaredInputs
|
|
1138
1331
|
.filter((declared) => declared.propName in command.inputs)
|
|
1139
1332
|
.map((declared) => inputBinding(declared.templateName, () => inputs()[declared.propName]));
|
|
1140
|
-
const
|
|
1333
|
+
const injector = command.entryId
|
|
1334
|
+
? Injector.create({
|
|
1335
|
+
providers: [
|
|
1336
|
+
{ provide: ANCESTOR_ENTRY_IDS, useValue: new Set([...this.ancestorEntryIds, command.entryId]) },
|
|
1337
|
+
],
|
|
1338
|
+
parent: this.injector,
|
|
1339
|
+
})
|
|
1340
|
+
: undefined;
|
|
1341
|
+
const componentRef = this.viewContainerRef.createComponent(command.component, { bindings, injector });
|
|
1141
1342
|
const rootNode = this.getComponentRootNode(componentRef);
|
|
1142
1343
|
this.renderInsertAfter(rootNode, { parentElement, previousSibling });
|
|
1143
1344
|
this.executedCommandsCache.set(command.id, {
|
|
@@ -1263,7 +1464,7 @@ class ContentfulRichTextRendererComponent {
|
|
|
1263
1464
|
this.renderer.insertBefore(parentElement, node, previousSibling ? previousSibling.nextSibling : parentElement.firstChild);
|
|
1264
1465
|
}
|
|
1265
1466
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulRichTextRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1266
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.6", type: ContentfulRichTextRendererComponent, isStandalone: true, selector: "et-contentful-rich-text-renderer", inputs: { content: { classPropertyName: "content", publicName: "content", isSignal: true, isRequired:
|
|
1467
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.6", type: ContentfulRichTextRendererComponent, isStandalone: true, selector: "et-contentful-rich-text-renderer", inputs: { content: { classPropertyName: "content", publicName: "content", isSignal: true, isRequired: false, transformFunction: null }, richTextPath: { classPropertyName: "richTextPath", publicName: "richTextPath", isSignal: true, isRequired: false, transformFunction: null }, gqlRichText: { classPropertyName: "gqlRichText", publicName: "gqlRichText", isSignal: true, isRequired: false, transformFunction: null }, richText: { classPropertyName: "richText", publicName: "richText", isSignal: true, isRequired: false, transformFunction: null }, includes: { classPropertyName: "includes", publicName: "includes", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "ngSkipHydration": "true" }, classAttribute: "et-contentful-rich-text-renderer" }, ngImport: i0, template: ``, isInline: true, encapsulation: i0.ViewEncapsulation.None }); }
|
|
1267
1468
|
}
|
|
1268
1469
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulRichTextRendererComponent, decorators: [{
|
|
1269
1470
|
type: Component,
|
|
@@ -1273,16 +1474,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
1273
1474
|
encapsulation: ViewEncapsulation.None,
|
|
1274
1475
|
host: {
|
|
1275
1476
|
class: 'et-contentful-rich-text-renderer',
|
|
1477
|
+
ngSkipHydration: 'true',
|
|
1276
1478
|
},
|
|
1277
1479
|
}]
|
|
1278
|
-
}], ctorParameters: () => [], propDecorators: { content: [{ type: i0.Input, args: [{ isSignal: true, alias: "content", required:
|
|
1480
|
+
}], ctorParameters: () => [], propDecorators: { content: [{ type: i0.Input, args: [{ isSignal: true, alias: "content", required: false }] }], richTextPath: [{ type: i0.Input, args: [{ isSignal: true, alias: "richTextPath", required: false }] }], gqlRichText: [{ type: i0.Input, args: [{ isSignal: true, alias: "gqlRichText", required: false }] }], richText: [{ type: i0.Input, args: [{ isSignal: true, alias: "richText", required: false }] }], includes: [{ type: i0.Input, args: [{ isSignal: true, alias: "includes", required: false }] }] } });
|
|
1279
1481
|
|
|
1280
1482
|
class ContentfulVideoComponent {
|
|
1281
1483
|
constructor() {
|
|
1282
1484
|
this.asset = input.required(/* @ts-ignore */
|
|
1283
1485
|
...(ngDevMode ? [{ debugName: "asset" }] : /* istanbul ignore next */ []));
|
|
1284
|
-
this.videoClass = input(null, /* @ts-ignore */
|
|
1285
|
-
...(ngDevMode ? [{ debugName: "videoClass" }] : /* istanbul ignore next */ []));
|
|
1286
1486
|
this.data = computed(() => {
|
|
1287
1487
|
const asset = this.asset();
|
|
1288
1488
|
if (!asset) {
|
|
@@ -1305,13 +1505,13 @@ class ContentfulVideoComponent {
|
|
|
1305
1505
|
...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
|
|
1306
1506
|
}
|
|
1307
1507
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulVideoComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1308
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: ContentfulVideoComponent, isStandalone: true, selector: "et-contentful-video", inputs: { asset: { classPropertyName: "asset", publicName: "asset", isSignal: true, isRequired: true, transformFunction: null }
|
|
1508
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: ContentfulVideoComponent, isStandalone: true, selector: "et-contentful-video", inputs: { asset: { classPropertyName: "asset", publicName: "asset", isSignal: true, isRequired: true, transformFunction: null } }, host: { classAttribute: "et-contentful-video" }, ngImport: i0, template: `
|
|
1309
1509
|
@if (data(); as data) {
|
|
1310
|
-
<video
|
|
1311
|
-
<source [src]="data.url" [type]="data.contentType" />
|
|
1510
|
+
<video class="et-contentful-video-video" controls>
|
|
1511
|
+
<source [src]="data.url" [attr.type]="data.contentType || null" class="et-contentful-video-source" />
|
|
1312
1512
|
</video>
|
|
1313
1513
|
}
|
|
1314
|
-
`, isInline: true,
|
|
1514
|
+
`, isInline: true, encapsulation: i0.ViewEncapsulation.None }); }
|
|
1315
1515
|
}
|
|
1316
1516
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulVideoComponent, decorators: [{
|
|
1317
1517
|
type: Component,
|
|
@@ -1319,18 +1519,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
1319
1519
|
selector: 'et-contentful-video',
|
|
1320
1520
|
template: `
|
|
1321
1521
|
@if (data(); as data) {
|
|
1322
|
-
<video
|
|
1323
|
-
<source [src]="data.url" [type]="data.contentType" />
|
|
1522
|
+
<video class="et-contentful-video-video" controls>
|
|
1523
|
+
<source [src]="data.url" [attr.type]="data.contentType || null" class="et-contentful-video-source" />
|
|
1324
1524
|
</video>
|
|
1325
1525
|
}
|
|
1326
1526
|
`,
|
|
1327
1527
|
encapsulation: ViewEncapsulation.None,
|
|
1328
|
-
imports: [NgClass],
|
|
1329
1528
|
host: {
|
|
1330
1529
|
class: 'et-contentful-video',
|
|
1331
1530
|
},
|
|
1332
1531
|
}]
|
|
1333
|
-
}], propDecorators: { asset: [{ type: i0.Input, args: [{ isSignal: true, alias: "asset", required: true }] }]
|
|
1532
|
+
}], propDecorators: { asset: [{ type: i0.Input, args: [{ isSignal: true, alias: "asset", required: true }] }] } });
|
|
1334
1533
|
|
|
1335
1534
|
const ContentfulImports = [
|
|
1336
1535
|
ContentfulAudioComponent,
|
|
@@ -1367,5 +1566,5 @@ const isContentfulEntryType = (entry, type) => entry.sys.contentType.sys.id ===
|
|
|
1367
1566
|
* Generated bundle index. Do not edit.
|
|
1368
1567
|
*/
|
|
1369
1568
|
|
|
1370
|
-
export { CONTENTFUL_CONFIG, CONTENTFUL_DEFAULT_COMPONENTS, CONTENTFUL_FILE_LABELS, ContentfulAudioComponent, ContentfulFileComponent, ContentfulImageComponent, ContentfulImports, ContentfulLinkComponent, ContentfulRichTextRendererComponent, ContentfulVideoComponent, DEFAULT_CONTENTFUL_FILE_LABELS, ET_CONTENTFUL_ANY_ENTRY_CONTENT_TYPE_SYS_ID, GQL_FRAGMENT_CONTENTFUL_ASSET, createContentfulConfig, createContentfulIncludeMap, generateContentfulImageSources, generateDefaultContentfulImageSource, injectContentfulConfig, injectContentfulFileLabels, isContentfulEntryType, isContentfulGqlAsset, isRichTextRootNode, marksToClass, marksToTags, parseContentfulImageSize, provideContentfulConfig, provideContentfulFileLabels, translateContentfulNodeTypeToHtmlTag };
|
|
1569
|
+
export { CONTENTFUL_CONFIG, CONTENTFUL_DEFAULT_COMPONENTS, CONTENTFUL_FILE_LABELS, ContentfulAudioComponent, ContentfulFileComponent, ContentfulImageComponent, ContentfulImports, ContentfulLinkComponent, ContentfulRichTextRendererComponent, ContentfulVideoComponent, DEFAULT_CONTENTFUL_FILE_LABELS, ET_CONTENTFUL_ANY_ENTRY_CONTENT_TYPE_SYS_ID, GQL_FRAGMENT_CONTENTFUL_ASSET, createContentfulConfig, createContentfulIncludeMap, generateContentfulImageSources, generateDefaultContentfulImageSource, injectContentfulConfig, injectContentfulFileLabels, isContentfulEntryType, isContentfulGqlAsset, isRichTextRootNode, mapContentfulGqlLinks, marksToClass, marksToTags, parseContentfulImageSize, provideContentfulConfig, provideContentfulFileLabels, translateContentfulNodeTypeToHtmlTag };
|
|
1371
1570
|
//# sourceMappingURL=ethlete-contentful.mjs.map
|