@ethlete/contentful 4.0.0-next.11 → 4.0.0-next.13
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/README.md +6 -1
- package/fesm2022/ethlete-contentful.mjs +382 -262
- package/fesm2022/ethlete-contentful.mjs.map +1 -1
- package/generators/generators.json +10 -0
- 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 +73 -0
- package/generators/migrate-contentful-default-components/migration.js +74 -0
- package/generators/migrate-contentful-default-components/schema.json +26 -0
- package/generators/migrate-to-contentful-v5/migration.js +3 -3
- package/migrations.json +19 -0
- package/package.json +6 -6
- package/types/ethlete-contentful.d.ts +90 -24
|
@@ -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,
|
|
2
|
+
import { input, computed, ViewEncapsulation, Component, InjectionToken, inject, numberAttribute, booleanAttribute, isDevMode, ViewContainerRef, ElementRef, 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,178 +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 }] }]
|
|
168
|
-
|
|
169
|
-
const parseWebUrl = (href) => {
|
|
170
|
-
if (!/^(?:https?:)?\/\//i.test(href)) {
|
|
171
|
-
return null;
|
|
172
|
-
}
|
|
173
|
-
try {
|
|
174
|
-
return new URL(href.startsWith('//') ? 'https:' + href : href);
|
|
175
|
-
}
|
|
176
|
-
catch {
|
|
177
|
-
return null;
|
|
178
|
-
}
|
|
179
|
-
};
|
|
180
|
-
const normalizeHostname = (host) => {
|
|
181
|
-
const value = host.trim().toLowerCase();
|
|
182
|
-
try {
|
|
183
|
-
return new URL(value.includes('://') ? value : 'https://' + value).hostname;
|
|
184
|
-
}
|
|
185
|
-
catch {
|
|
186
|
-
return value.split(':')[0] ?? value;
|
|
187
|
-
}
|
|
188
|
-
};
|
|
189
|
-
const matchesHostname = (hostname, configuredHost) => {
|
|
190
|
-
const normalizedHost = normalizeHostname(configuredHost);
|
|
191
|
-
return hostname === normalizedHost || hostname.endsWith('.' + normalizedHost);
|
|
192
|
-
};
|
|
193
|
-
const isInternalWebUrl = (url, { location, internalHosts }) => (url.hostname === location.hostname && url.port === location.port) ||
|
|
194
|
-
internalHosts.some((host) => matchesHostname(url.hostname, host));
|
|
195
|
-
const isExternalWebHref = (href, context) => {
|
|
196
|
-
const url = parseWebUrl(href);
|
|
197
|
-
return url !== null && !isInternalWebUrl(url, context);
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
class ContentfulLinkComponent {
|
|
201
|
-
constructor() {
|
|
202
|
-
this.document = inject(DOCUMENT);
|
|
203
|
-
this.router = inject(Router);
|
|
204
|
-
this.config = injectContentfulConfig();
|
|
205
|
-
this.url = injectUrl();
|
|
206
|
-
this.href = input.required(/* @ts-ignore */
|
|
207
|
-
...(ngDevMode ? [{ debugName: "href" }] : /* istanbul ignore next */ []));
|
|
208
|
-
this.text = input.required(/* @ts-ignore */
|
|
209
|
-
...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
|
|
210
|
-
this.textClass = input('', /* @ts-ignore */
|
|
211
|
-
...(ngDevMode ? [{ debugName: "textClass" }] : /* istanbul ignore next */ []));
|
|
212
|
-
this.anchorClass = input('', /* @ts-ignore */
|
|
213
|
-
...(ngDevMode ? [{ debugName: "anchorClass" }] : /* istanbul ignore next */ []));
|
|
214
|
-
this.usesRouterLink = computed(() => {
|
|
215
|
-
const href = this.href();
|
|
216
|
-
const absoluteUrl = parseWebUrl(href);
|
|
217
|
-
if (absoluteUrl) {
|
|
218
|
-
return isInternalWebUrl(absoluteUrl, {
|
|
219
|
-
location: this.document.location,
|
|
220
|
-
internalHosts: this.config.internalHosts,
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
return !href.startsWith('#') && !/^[a-z][a-z\d+.-]*:/i.test(href);
|
|
224
|
-
}, /* @ts-ignore */
|
|
225
|
-
...(ngDevMode ? [{ debugName: "usesRouterLink" }] : /* istanbul ignore next */ []));
|
|
226
|
-
this.openInNewTab = computed(() => Boolean(parseWebUrl(this.href())) && !this.usesRouterLink(), /* @ts-ignore */
|
|
227
|
-
...(ngDevMode ? [{ debugName: "openInNewTab" }] : /* istanbul ignore next */ []));
|
|
228
|
-
this.internalPath = computed(() => {
|
|
229
|
-
const href = this.href();
|
|
230
|
-
const url = parseWebUrl(href);
|
|
231
|
-
if (url) {
|
|
232
|
-
return url.pathname + url.search + url.hash;
|
|
233
|
-
}
|
|
234
|
-
if (href.startsWith('/')) {
|
|
235
|
-
return href;
|
|
236
|
-
}
|
|
237
|
-
const resolved = new URL(href, 'https://contentful.invalid' + this.url());
|
|
238
|
-
return resolved.pathname + resolved.search + resolved.hash;
|
|
239
|
-
}, /* @ts-ignore */
|
|
240
|
-
...(ngDevMode ? [{ debugName: "internalPath" }] : /* istanbul ignore next */ []));
|
|
241
|
-
this.internalUrlTree = computed(() => this.router.parseUrl(this.internalPath()), /* @ts-ignore */
|
|
242
|
-
...(ngDevMode ? [{ debugName: "internalUrlTree" }] : /* istanbul ignore next */ []));
|
|
243
|
-
this.linkClass = computed(() => [this.anchorClass(), this.textClass()].filter(Boolean).join(' ') || null, /* @ts-ignore */
|
|
244
|
-
...(ngDevMode ? [{ debugName: "linkClass" }] : /* istanbul ignore next */ []));
|
|
245
|
-
}
|
|
246
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulLinkComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
247
|
-
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 }, textClass: { classPropertyName: "textClass", publicName: "textClass", isSignal: true, isRequired: false, transformFunction: null }, anchorClass: { classPropertyName: "anchorClass", publicName: "anchorClass", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "et-contentful-link" }, ngImport: i0, template: `
|
|
248
|
-
@if (usesRouterLink()) {
|
|
249
|
-
<a [class]="linkClass()" [routerLink]="internalUrlTree()">{{ text() }}</a>
|
|
250
|
-
} @else {
|
|
251
|
-
<a
|
|
252
|
-
[class]="linkClass()"
|
|
253
|
-
[href]="href()"
|
|
254
|
-
[target]="openInNewTab() ? '_blank' : null"
|
|
255
|
-
[rel]="openInNewTab() ? 'noopener noreferrer' : null"
|
|
256
|
-
>{{ text() }}</a
|
|
257
|
-
>
|
|
258
|
-
}
|
|
259
|
-
`, isInline: true, styles: ["@layer components{.et-contentful-link{display:contents}}\n"], dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }], encapsulation: i0.ViewEncapsulation.None }); }
|
|
260
|
-
}
|
|
261
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulLinkComponent, decorators: [{
|
|
262
|
-
type: Component,
|
|
263
|
-
args: [{ selector: 'et-contentful-link', template: `
|
|
264
|
-
@if (usesRouterLink()) {
|
|
265
|
-
<a [class]="linkClass()" [routerLink]="internalUrlTree()">{{ text() }}</a>
|
|
266
|
-
} @else {
|
|
267
|
-
<a
|
|
268
|
-
[class]="linkClass()"
|
|
269
|
-
[href]="href()"
|
|
270
|
-
[target]="openInNewTab() ? '_blank' : null"
|
|
271
|
-
[rel]="openInNewTab() ? 'noopener noreferrer' : null"
|
|
272
|
-
>{{ text() }}</a
|
|
273
|
-
>
|
|
274
|
-
}
|
|
275
|
-
`, encapsulation: ViewEncapsulation.None, imports: [RouterLink], host: {
|
|
276
|
-
class: 'et-contentful-link',
|
|
277
|
-
}, styles: ["@layer components{.et-contentful-link{display:contents}}\n"] }]
|
|
278
|
-
}], propDecorators: { href: [{ type: i0.Input, args: [{ isSignal: true, alias: "href", required: true }] }], text: [{ type: i0.Input, args: [{ isSignal: true, alias: "text", required: true }] }], textClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "textClass", required: false }] }], anchorClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "anchorClass", required: false }] }] } });
|
|
279
|
-
|
|
280
|
-
class ContentfulVideoComponent {
|
|
281
|
-
constructor() {
|
|
282
|
-
this.asset = input.required(/* @ts-ignore */
|
|
283
|
-
...(ngDevMode ? [{ debugName: "asset" }] : /* istanbul ignore next */ []));
|
|
284
|
-
this.videoClass = input(null, /* @ts-ignore */
|
|
285
|
-
...(ngDevMode ? [{ debugName: "videoClass" }] : /* istanbul ignore next */ []));
|
|
286
|
-
this.data = computed(() => {
|
|
287
|
-
const asset = this.asset();
|
|
288
|
-
if (!asset) {
|
|
289
|
-
return null;
|
|
290
|
-
}
|
|
291
|
-
if (isContentfulGqlAsset(asset) && asset.url) {
|
|
292
|
-
return {
|
|
293
|
-
url: asset.url,
|
|
294
|
-
contentType: asset.contentType,
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
if (!isContentfulGqlAsset(asset) && asset.fields.file?.url) {
|
|
298
|
-
return {
|
|
299
|
-
url: asset.fields.file.url,
|
|
300
|
-
contentType: asset.fields.file.contentType,
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
return null;
|
|
304
|
-
}, /* @ts-ignore */
|
|
305
|
-
...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
|
|
306
|
-
}
|
|
307
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulVideoComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
308
|
-
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 }, videoClass: { classPropertyName: "videoClass", publicName: "videoClass", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "et-contentful-video" }, ngImport: i0, template: `
|
|
309
|
-
@if (data(); as data) {
|
|
310
|
-
<video [ngClass]="videoClass()" controls>
|
|
311
|
-
<source [src]="data.url" [type]="data.contentType" />
|
|
312
|
-
</video>
|
|
313
|
-
}
|
|
314
|
-
`, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], encapsulation: i0.ViewEncapsulation.None }); }
|
|
315
|
-
}
|
|
316
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulVideoComponent, decorators: [{
|
|
317
|
-
type: Component,
|
|
318
|
-
args: [{
|
|
319
|
-
selector: 'et-contentful-video',
|
|
320
|
-
template: `
|
|
321
|
-
@if (data(); as data) {
|
|
322
|
-
<video [ngClass]="videoClass()" controls>
|
|
323
|
-
<source [src]="data.url" [type]="data.contentType" />
|
|
324
|
-
</video>
|
|
325
|
-
}
|
|
326
|
-
`,
|
|
327
|
-
encapsulation: ViewEncapsulation.None,
|
|
328
|
-
imports: [NgClass],
|
|
329
|
-
host: {
|
|
330
|
-
class: 'et-contentful-video',
|
|
331
|
-
},
|
|
332
|
-
}]
|
|
333
|
-
}], propDecorators: { asset: [{ type: i0.Input, args: [{ isSignal: true, alias: "asset", required: true }] }], videoClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "videoClass", required: false }] }] } });
|
|
161
|
+
}], propDecorators: { asset: [{ type: i0.Input, args: [{ isSignal: true, alias: "asset", required: true }] }] } });
|
|
334
162
|
|
|
335
163
|
const CONTENTFUL_CONFIG = new InjectionToken('ContentfulConfig');
|
|
336
164
|
|
|
@@ -349,28 +177,16 @@ const CONTENTFUL_FALLBACK_CONFIG = {
|
|
|
349
177
|
backgroundColor: null,
|
|
350
178
|
},
|
|
351
179
|
};
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
components: {
|
|
356
|
-
audio: ContentfulAudioComponent,
|
|
357
|
-
file: ContentfulFileComponent,
|
|
358
|
-
image: ContentfulImageComponent,
|
|
359
|
-
video: ContentfulVideoComponent,
|
|
360
|
-
link: ContentfulLinkComponent,
|
|
361
|
-
},
|
|
362
|
-
customComponents: {},
|
|
363
|
-
imageOptions: {
|
|
364
|
-
srcsetSizes: ['375w', '1280w', '1920w', '2560w'],
|
|
365
|
-
sizes: ['100vw'],
|
|
366
|
-
backgroundColor: null,
|
|
367
|
-
},
|
|
368
|
-
};
|
|
180
|
+
const withoutUndefined = (value) => Object.fromEntries(Object.entries(value ?? {}).filter(([, entry]) => entry !== undefined));
|
|
181
|
+
const createContentfulConfig = (options) => {
|
|
182
|
+
const config = options ?? {};
|
|
369
183
|
return {
|
|
370
|
-
...
|
|
184
|
+
...CONTENTFUL_FALLBACK_CONFIG,
|
|
371
185
|
...config,
|
|
372
|
-
components:
|
|
373
|
-
|
|
186
|
+
components: config.components ?? CONTENTFUL_FALLBACK_CONFIG.components,
|
|
187
|
+
customComponents: config.customComponents ?? CONTENTFUL_FALLBACK_CONFIG.customComponents,
|
|
188
|
+
internalHosts: config.internalHosts ?? CONTENTFUL_FALLBACK_CONFIG.internalHosts,
|
|
189
|
+
imageOptions: { ...CONTENTFUL_FALLBACK_CONFIG.imageOptions, ...withoutUndefined(config.imageOptions) },
|
|
374
190
|
};
|
|
375
191
|
};
|
|
376
192
|
/**
|
|
@@ -380,29 +196,8 @@ const createContentfulConfig = (config) => {
|
|
|
380
196
|
const injectContentfulConfig = () => inject(CONTENTFUL_CONFIG, { optional: true }) ?? CONTENTFUL_FALLBACK_CONFIG;
|
|
381
197
|
|
|
382
198
|
const generateDefaultContentfulImageSource = (data) => {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
return {
|
|
386
|
-
type: '',
|
|
387
|
-
srcset: '',
|
|
388
|
-
};
|
|
389
|
-
}
|
|
390
|
-
return {
|
|
391
|
-
type: data.contentType,
|
|
392
|
-
srcset: data.url,
|
|
393
|
-
};
|
|
394
|
-
}
|
|
395
|
-
const file = data.fields.file;
|
|
396
|
-
if (!file?.contentType || !file.url) {
|
|
397
|
-
return {
|
|
398
|
-
type: '',
|
|
399
|
-
srcset: '',
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
return {
|
|
403
|
-
type: file.contentType,
|
|
404
|
-
srcset: file.url,
|
|
405
|
-
};
|
|
199
|
+
const { url, contentType } = isContentfulGqlAsset(data) ? data : (data.fields.file ?? {});
|
|
200
|
+
return url ? { type: contentType ?? '', srcset: url } : { type: '', srcset: '' };
|
|
406
201
|
};
|
|
407
202
|
/**
|
|
408
203
|
* Parses source set sizes into an object containing with and height. Eg.
|
|
@@ -448,7 +243,7 @@ const generateContentfulImageSources = (data, options = {}) => {
|
|
|
448
243
|
const queryParams = [];
|
|
449
244
|
queryParams.push(`fm=${type.split('/')[1]}`);
|
|
450
245
|
if (backgroundColor) {
|
|
451
|
-
queryParams.push(`bg=rgb:${backgroundColor}`);
|
|
246
|
+
queryParams.push(`bg=rgb:${backgroundColor.replace(/^#/, '')}`);
|
|
452
247
|
}
|
|
453
248
|
if (quality !== null && Number.isFinite(quality)) {
|
|
454
249
|
queryParams.push(`q=${quality}`);
|
|
@@ -473,6 +268,11 @@ const generateContentfulImageSources = (data, options = {}) => {
|
|
|
473
268
|
const derivedWidth = Math.round((height * imageDimensions.width) / imageDimensions.height);
|
|
474
269
|
sourceSets.push(`${urlWithParams}&w=${derivedWidth}&h=${height} ${derivedWidth}w`);
|
|
475
270
|
}
|
|
271
|
+
else if (ngDevMode && type === SOURCE_TYPES[0]) {
|
|
272
|
+
console.warn(height
|
|
273
|
+
? `Contentful image: srcsetSizes entry "${size}" is dropped. A height-only size needs the asset's width and height to derive a width descriptor.`
|
|
274
|
+
: `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.`);
|
|
275
|
+
}
|
|
476
276
|
}
|
|
477
277
|
}
|
|
478
278
|
sources.push({
|
|
@@ -558,7 +358,7 @@ class ContentfulImageComponent {
|
|
|
558
358
|
[sizes]="sizes()"
|
|
559
359
|
[sources]="sourcesValue()"
|
|
560
360
|
/>
|
|
561
|
-
`, isInline: true, dependencies: [{ kind: "component", type: PictureComponent, selector: "et-picture", inputs: ["sources", "defaultSrc", "alt", "figcaption", "priority", "width", "height", "aspectRatio", "sizes", "fit"], outputs: ["
|
|
361
|
+
`, 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 }); }
|
|
562
362
|
}
|
|
563
363
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulImageComponent, decorators: [{
|
|
564
364
|
type: Component,
|
|
@@ -584,6 +384,134 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
584
384
|
}]
|
|
585
385
|
}], propDecorators: { asset: [{ type: i0.Input, args: [{ isSignal: true, alias: "asset", required: true }] }], backgroundColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "backgroundColor", required: false }] }], srcsetSizes: [{ type: i0.Input, args: [{ isSignal: true, alias: "srcsetSizes", required: false }] }], quality: [{ type: i0.Input, args: [{ isSignal: true, alias: "quality", required: false }] }], focusArea: [{ type: i0.Input, args: [{ isSignal: true, alias: "focusArea", required: false }] }], resizeBehavior: [{ type: i0.Input, args: [{ isSignal: true, alias: "resizeBehavior", required: false }] }], priority: [{ type: i0.Input, args: [{ isSignal: true, alias: "priority", required: false }] }], sizes: [{ type: i0.Input, args: [{ isSignal: true, alias: "sizes", required: false }] }] } });
|
|
586
386
|
|
|
387
|
+
const parseWebUrl = (href) => {
|
|
388
|
+
if (!/^(?:https?:)?\/\//i.test(href)) {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
try {
|
|
392
|
+
return new URL(href.startsWith('//') ? 'https:' + href : href);
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
const normalizeHostname = (host) => {
|
|
399
|
+
const value = host.trim().toLowerCase();
|
|
400
|
+
try {
|
|
401
|
+
return new URL(value.includes('://') ? value : 'https://' + value).hostname;
|
|
402
|
+
}
|
|
403
|
+
catch {
|
|
404
|
+
return value.split(':')[0] ?? value;
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
const matchesHostname = (hostname, configuredHost) => {
|
|
408
|
+
const normalizedHost = normalizeHostname(configuredHost);
|
|
409
|
+
return hostname === normalizedHost || hostname.endsWith('.' + normalizedHost);
|
|
410
|
+
};
|
|
411
|
+
const isInternalWebUrl = (url, { location, internalHosts }) => (url.hostname === location.hostname && url.port === location.port) ||
|
|
412
|
+
internalHosts.some((host) => matchesHostname(url.hostname, host));
|
|
413
|
+
const isExternalWebHref = (href, context) => {
|
|
414
|
+
const url = parseWebUrl(href);
|
|
415
|
+
return url !== null && !isInternalWebUrl(url, context);
|
|
416
|
+
};
|
|
417
|
+
const hasUrlScheme = (href) => /^[a-z][a-z\d+.-]*:/i.test(href);
|
|
418
|
+
const isRouteRelativeHref = (href) => !hasUrlScheme(href) && !href.startsWith('/');
|
|
419
|
+
const resolveHrefAgainstRoute = (href, routerUrl) => {
|
|
420
|
+
const resolved = new URL(href, 'https://contentful.invalid' + routerUrl);
|
|
421
|
+
return resolved.pathname + resolved.search + resolved.hash;
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
const RICH_TEXT_ANCHOR_CLASSES = ['et-contentful-rich-text-default-element', 'et-contentful-rich-text-default-a'];
|
|
425
|
+
class ContentfulLinkComponent {
|
|
426
|
+
constructor() {
|
|
427
|
+
this.document = inject(DOCUMENT);
|
|
428
|
+
this.location = inject(Location);
|
|
429
|
+
this.router = inject(Router);
|
|
430
|
+
this.config = injectContentfulConfig();
|
|
431
|
+
this.url = injectUrl();
|
|
432
|
+
this.href = input.required(/* @ts-ignore */
|
|
433
|
+
...(ngDevMode ? [{ debugName: "href" }] : /* istanbul ignore next */ []));
|
|
434
|
+
this.text = input.required(/* @ts-ignore */
|
|
435
|
+
...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
|
|
436
|
+
this.marks = input([], /* @ts-ignore */
|
|
437
|
+
...(ngDevMode ? [{ debugName: "marks" }] : /* istanbul ignore next */ []));
|
|
438
|
+
this.richText = input(false, /* @ts-ignore */
|
|
439
|
+
...(ngDevMode ? [{ debugName: "richText" }] : /* istanbul ignore next */ []));
|
|
440
|
+
this.usesRouterLink = computed(() => {
|
|
441
|
+
const href = this.href();
|
|
442
|
+
const absoluteUrl = parseWebUrl(href);
|
|
443
|
+
if (absoluteUrl) {
|
|
444
|
+
return isInternalWebUrl(absoluteUrl, {
|
|
445
|
+
location: this.document.location,
|
|
446
|
+
internalHosts: this.config.internalHosts,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
return !href.startsWith('#') && !hasUrlScheme(href);
|
|
450
|
+
}, /* @ts-ignore */
|
|
451
|
+
...(ngDevMode ? [{ debugName: "usesRouterLink" }] : /* istanbul ignore next */ []));
|
|
452
|
+
this.anchorHref = computed(() => {
|
|
453
|
+
const href = this.href();
|
|
454
|
+
return href.startsWith('#') ? this.location.prepareExternalUrl(resolveHrefAgainstRoute(href, this.url())) : href;
|
|
455
|
+
}, /* @ts-ignore */
|
|
456
|
+
...(ngDevMode ? [{ debugName: "anchorHref" }] : /* istanbul ignore next */ []));
|
|
457
|
+
this.openInNewTab = computed(() => Boolean(parseWebUrl(this.href())) && !this.usesRouterLink(), /* @ts-ignore */
|
|
458
|
+
...(ngDevMode ? [{ debugName: "openInNewTab" }] : /* istanbul ignore next */ []));
|
|
459
|
+
this.internalPath = computed(() => {
|
|
460
|
+
const href = this.href();
|
|
461
|
+
const url = parseWebUrl(href);
|
|
462
|
+
if (url) {
|
|
463
|
+
return url.pathname + url.search + url.hash;
|
|
464
|
+
}
|
|
465
|
+
if (href.startsWith('/')) {
|
|
466
|
+
return href;
|
|
467
|
+
}
|
|
468
|
+
return resolveHrefAgainstRoute(href, this.url());
|
|
469
|
+
}, /* @ts-ignore */
|
|
470
|
+
...(ngDevMode ? [{ debugName: "internalPath" }] : /* istanbul ignore next */ []));
|
|
471
|
+
this.internalUrlTree = computed(() => this.router.parseUrl(this.internalPath()), /* @ts-ignore */
|
|
472
|
+
...(ngDevMode ? [{ debugName: "internalUrlTree" }] : /* istanbul ignore next */ []));
|
|
473
|
+
this.modifierClasses = computed(() => [
|
|
474
|
+
...(this.richText() ? RICH_TEXT_ANCHOR_CLASSES : []),
|
|
475
|
+
...this.marks().map((mark) => `et-contentful-rich-text-mark-${mark}`),
|
|
476
|
+
], /* @ts-ignore */
|
|
477
|
+
...(ngDevMode ? [{ debugName: "modifierClasses" }] : /* istanbul ignore next */ []));
|
|
478
|
+
}
|
|
479
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulLinkComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
480
|
+
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: `
|
|
481
|
+
@if (usesRouterLink()) {
|
|
482
|
+
<a [class]="modifierClasses()" [routerLink]="internalUrlTree()" class="et-contentful-link-anchor">{{ text() }}</a>
|
|
483
|
+
} @else {
|
|
484
|
+
<a
|
|
485
|
+
[class]="modifierClasses()"
|
|
486
|
+
[href]="anchorHref()"
|
|
487
|
+
[attr.target]="openInNewTab() ? '_blank' : null"
|
|
488
|
+
[attr.rel]="openInNewTab() ? 'noopener noreferrer' : null"
|
|
489
|
+
class="et-contentful-link-anchor"
|
|
490
|
+
>{{ text() }}</a
|
|
491
|
+
>
|
|
492
|
+
}
|
|
493
|
+
`, isInline: true, styles: ["@layer components{.et-contentful-link{display:contents}}\n"], dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }], encapsulation: i0.ViewEncapsulation.None }); }
|
|
494
|
+
}
|
|
495
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulLinkComponent, decorators: [{
|
|
496
|
+
type: Component,
|
|
497
|
+
args: [{ selector: 'et-contentful-link', template: `
|
|
498
|
+
@if (usesRouterLink()) {
|
|
499
|
+
<a [class]="modifierClasses()" [routerLink]="internalUrlTree()" class="et-contentful-link-anchor">{{ text() }}</a>
|
|
500
|
+
} @else {
|
|
501
|
+
<a
|
|
502
|
+
[class]="modifierClasses()"
|
|
503
|
+
[href]="anchorHref()"
|
|
504
|
+
[attr.target]="openInNewTab() ? '_blank' : null"
|
|
505
|
+
[attr.rel]="openInNewTab() ? 'noopener noreferrer' : null"
|
|
506
|
+
class="et-contentful-link-anchor"
|
|
507
|
+
>{{ text() }}</a
|
|
508
|
+
>
|
|
509
|
+
}
|
|
510
|
+
`, encapsulation: ViewEncapsulation.None, imports: [RouterLink], host: {
|
|
511
|
+
class: 'et-contentful-link',
|
|
512
|
+
}, styles: ["@layer components{.et-contentful-link{display:contents}}\n"] }]
|
|
513
|
+
}], 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 }] }] } });
|
|
514
|
+
|
|
587
515
|
/**
|
|
588
516
|
* The `BLOCKS` node type strings of `@contentful/rich-text-types`, as a plain literal map.
|
|
589
517
|
* Keep the values in sync with the enum - they are part of the Contentful Delivery API.
|
|
@@ -628,8 +556,9 @@ const CF_INLINES = {
|
|
|
628
556
|
};
|
|
629
557
|
|
|
630
558
|
const RICH_TEXT_RENDERER_ERRORS = {
|
|
631
|
-
|
|
559
|
+
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).`,
|
|
632
560
|
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".',
|
|
561
|
+
rich_text_conflicting_inputs: 'gqlRichText cannot be combined with content or richTextPath. Pass either a GraphQL rich-text field through gqlRichText, or a REST response through content and richTextPath.',
|
|
633
562
|
asset_id_not_found: 'The asset ID was not found. This node is not supported.',
|
|
634
563
|
entry_id_not_found: 'The entry ID was not found. This node is not supported.',
|
|
635
564
|
text_parent_not_found: 'The parent node is not found. This structure is not supported.',
|
|
@@ -637,8 +566,9 @@ const RICH_TEXT_RENDERER_ERRORS = {
|
|
|
637
566
|
cached_command_not_component: 'The rendered node for a component command is not a component. The renderer state is out of sync.',
|
|
638
567
|
};
|
|
639
568
|
const RICH_TEXT_RENDERER_ERROR_CODES = {
|
|
640
|
-
|
|
569
|
+
rich_text_not_object: 0,
|
|
641
570
|
rich_text_wrong_type: 1,
|
|
571
|
+
rich_text_conflicting_inputs: 11,
|
|
642
572
|
asset_id_not_found: 2,
|
|
643
573
|
entry_id_not_found: 3,
|
|
644
574
|
text_parent_not_found: 7,
|
|
@@ -646,10 +576,75 @@ const RICH_TEXT_RENDERER_ERROR_CODES = {
|
|
|
646
576
|
cached_command_not_component: 10,
|
|
647
577
|
};
|
|
648
578
|
const richTextRendererError = (code, data) => {
|
|
649
|
-
const
|
|
579
|
+
const entry = RICH_TEXT_RENDERER_ERRORS[code];
|
|
580
|
+
const message = `<et-contentful-rich-text-renderer>: ${typeof entry === 'function' ? entry(data) : entry}`;
|
|
650
581
|
throw new RuntimeError(RICH_TEXT_RENDERER_ERROR_CODES[code], message, data);
|
|
651
582
|
};
|
|
652
583
|
|
|
584
|
+
const lowerCaseFirst = (value) => value.charAt(0).toLowerCase() + value.slice(1);
|
|
585
|
+
const toRestAsset = (asset) => ({
|
|
586
|
+
sys: { type: 'Asset', id: asset.sys.id, createdAt: '', updatedAt: '', locale: '' },
|
|
587
|
+
fields: {
|
|
588
|
+
title: asset.title ?? '',
|
|
589
|
+
description: asset.description ?? '',
|
|
590
|
+
file: {
|
|
591
|
+
url: asset.url,
|
|
592
|
+
details: {
|
|
593
|
+
size: asset.size,
|
|
594
|
+
...(asset.width !== null && asset.height !== null
|
|
595
|
+
? { image: { width: asset.width, height: asset.height } }
|
|
596
|
+
: {}),
|
|
597
|
+
},
|
|
598
|
+
fileName: asset.fileName ?? null,
|
|
599
|
+
contentType: asset.contentType,
|
|
600
|
+
},
|
|
601
|
+
},
|
|
602
|
+
metadata: { tags: [] },
|
|
603
|
+
});
|
|
604
|
+
const toRestEntry = (entry) => {
|
|
605
|
+
const { __typename, sys, ...fields } = entry;
|
|
606
|
+
if (!__typename) {
|
|
607
|
+
return null;
|
|
608
|
+
}
|
|
609
|
+
return {
|
|
610
|
+
sys: {
|
|
611
|
+
type: 'Entry',
|
|
612
|
+
id: sys.id,
|
|
613
|
+
createdAt: '',
|
|
614
|
+
updatedAt: '',
|
|
615
|
+
locale: '',
|
|
616
|
+
contentType: { sys: { type: 'Link', linkType: 'ContentType', id: lowerCaseFirst(__typename) } },
|
|
617
|
+
},
|
|
618
|
+
fields,
|
|
619
|
+
metadata: { tags: [] },
|
|
620
|
+
};
|
|
621
|
+
};
|
|
622
|
+
const present = (items) => (items ?? []).filter((item) => item !== null && item !== undefined);
|
|
623
|
+
/**
|
|
624
|
+
* Maps the `links` of a GraphQL rich-text field to the entries and assets the renderer resolves
|
|
625
|
+
* embeds against. An entry's content type id is its `__typename` with the first letter lower-cased,
|
|
626
|
+
* which is how Contentful derives a GraphQL type name from a content type id. Entries without a
|
|
627
|
+
* `__typename` are dropped.
|
|
628
|
+
*/
|
|
629
|
+
const mapContentfulGqlLinks = (links) => {
|
|
630
|
+
const assets = new Map();
|
|
631
|
+
const entries = new Map();
|
|
632
|
+
for (const asset of [...present(links?.assets?.block), ...present(links?.assets?.hyperlink)]) {
|
|
633
|
+
assets.set(asset.sys.id, toRestAsset(asset));
|
|
634
|
+
}
|
|
635
|
+
for (const gqlEntry of [
|
|
636
|
+
...present(links?.entries?.block),
|
|
637
|
+
...present(links?.entries?.inline),
|
|
638
|
+
...present(links?.entries?.hyperlink),
|
|
639
|
+
]) {
|
|
640
|
+
const entry = toRestEntry(gqlEntry);
|
|
641
|
+
if (entry) {
|
|
642
|
+
entries.set(entry.sys.id, entry);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return { assets: [...assets.values()], entries: [...entries.values()] };
|
|
646
|
+
};
|
|
647
|
+
|
|
653
648
|
const isRichTextRootNode = (node) => {
|
|
654
649
|
return isObject(node) && 'nodeType' in node && node['nodeType'] === 'document';
|
|
655
650
|
};
|
|
@@ -751,7 +746,7 @@ const createContentfulIncludeMap = (config) => {
|
|
|
751
746
|
const entry = entryMap.get(id);
|
|
752
747
|
if (!entry) {
|
|
753
748
|
if (isDevMode()) {
|
|
754
|
-
console.warn('Entry not found! Will return null. Is the include query param too low?', { id
|
|
749
|
+
console.warn('Entry not found! Will return null. Is it unpublished or deleted, or is the include query param too low?', { id });
|
|
755
750
|
}
|
|
756
751
|
return null;
|
|
757
752
|
}
|
|
@@ -790,6 +785,23 @@ const createContentfulIncludeMap = (config) => {
|
|
|
790
785
|
getAssets,
|
|
791
786
|
};
|
|
792
787
|
};
|
|
788
|
+
const warnedRichTextPaths = /* @__PURE__ */ new Set();
|
|
789
|
+
const warnOnUnresolvedParentPath = (content, richTextPath) => {
|
|
790
|
+
const segments = richTextPath.split('.');
|
|
791
|
+
const root = content;
|
|
792
|
+
if (segments.length < 2 || isObject(getObjectProperty(root, segments.slice(0, -1).join('.'))))
|
|
793
|
+
return;
|
|
794
|
+
if (warnedRichTextPaths.has(richTextPath))
|
|
795
|
+
return;
|
|
796
|
+
warnedRichTextPaths.add(richTextPath);
|
|
797
|
+
let resolvedSegments = 0;
|
|
798
|
+
while (resolvedSegments < segments.length - 1 &&
|
|
799
|
+
isObject(getObjectProperty(root, segments.slice(0, resolvedSegments + 1).join('.')))) {
|
|
800
|
+
resolvedSegments++;
|
|
801
|
+
}
|
|
802
|
+
const resolvedPath = segments.slice(0, resolvedSegments).join('.') || '(the content itself)';
|
|
803
|
+
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 });
|
|
804
|
+
};
|
|
793
805
|
class ContentfulRichTextRendererComponent {
|
|
794
806
|
constructor() {
|
|
795
807
|
this.viewContainerRef = inject(ViewContainerRef);
|
|
@@ -797,38 +809,72 @@ class ContentfulRichTextRendererComponent {
|
|
|
797
809
|
this.elementRef = inject(ElementRef);
|
|
798
810
|
this.config = injectContentfulConfig();
|
|
799
811
|
this.document = inject(DOCUMENT);
|
|
812
|
+
this.location = inject(Location);
|
|
813
|
+
this.url = injectUrl();
|
|
800
814
|
/**
|
|
801
815
|
* The contentful response gotten via their REST api.
|
|
802
816
|
* @see https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/entries/entries-collection
|
|
803
817
|
*/
|
|
804
|
-
this.content = input
|
|
805
|
-
...(ngDevMode ? [{ debugName: "content" }] : /* istanbul ignore next */ []));
|
|
818
|
+
this.content = input(/* @ts-ignore */
|
|
819
|
+
...(ngDevMode ? [undefined, { debugName: "content" }] : /* istanbul ignore next */ []));
|
|
806
820
|
/**
|
|
807
821
|
* The path to where the rich text field is inside the contentful response. Dot and array notation is allowed.
|
|
808
822
|
* @example "items[0].fields.html"
|
|
809
823
|
*/
|
|
810
|
-
this.richTextPath = input
|
|
811
|
-
...(ngDevMode ? [{ debugName: "richTextPath" }] : /* istanbul ignore next */ []));
|
|
824
|
+
this.richTextPath = input(/* @ts-ignore */
|
|
825
|
+
...(ngDevMode ? [undefined, { debugName: "richTextPath" }] : /* istanbul ignore next */ []));
|
|
826
|
+
/**
|
|
827
|
+
* A rich-text field from the Contentful GraphQL API, passed as-is. An alternative to `content` and
|
|
828
|
+
* `richTextPath`; setting both is an error in dev mode.
|
|
829
|
+
* @example { json, links: { assets: { block, hyperlink }, entries: { block, inline, hyperlink } } }
|
|
830
|
+
*/
|
|
831
|
+
this.gqlRichText = input(/* @ts-ignore */
|
|
832
|
+
...(ngDevMode ? [undefined, { debugName: "gqlRichText" }] : /* istanbul ignore next */ []));
|
|
812
833
|
this.executedCommandsCache = new Map();
|
|
813
|
-
this.
|
|
814
|
-
const
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
834
|
+
this.includes = computed(() => {
|
|
835
|
+
const gqlRichText = this.gqlRichText();
|
|
836
|
+
if (gqlRichText) {
|
|
837
|
+
return mapContentfulGqlLinks(gqlRichText.links);
|
|
838
|
+
}
|
|
839
|
+
const includes = this.content()?.includes;
|
|
840
|
+
return { assets: includes?.Asset ?? [], entries: includes?.Entry ?? [] };
|
|
818
841
|
}, /* @ts-ignore */
|
|
842
|
+
...(ngDevMode ? [{ debugName: "includes" }] : /* istanbul ignore next */ []));
|
|
843
|
+
this.contentIncludesMap = computed(() => createContentfulIncludeMap(this.includes()), /* @ts-ignore */
|
|
819
844
|
...(ngDevMode ? [{ debugName: "contentIncludesMap" }] : /* istanbul ignore next */ []));
|
|
845
|
+
this.includedEntries = computed(() => new Map(this.includes().entries.map((entry) => [entry.sys.id, entry])), /* @ts-ignore */
|
|
846
|
+
...(ngDevMode ? [{ debugName: "includedEntries" }] : /* istanbul ignore next */ []));
|
|
820
847
|
this.richTextData = computed(() => {
|
|
848
|
+
const gqlRichText = this.gqlRichText();
|
|
821
849
|
const content = this.content();
|
|
822
850
|
const richTextPath = this.richTextPath();
|
|
851
|
+
if (ngDevMode && gqlRichText && (content || richTextPath)) {
|
|
852
|
+
throw richTextRendererError('rich_text_conflicting_inputs', { gqlRichText, content, richTextPath });
|
|
853
|
+
}
|
|
854
|
+
if (gqlRichText) {
|
|
855
|
+
if (!isRichTextRootNode(gqlRichText.json)) {
|
|
856
|
+
throw richTextRendererError('rich_text_wrong_type', { gqlRichText });
|
|
857
|
+
}
|
|
858
|
+
return gqlRichText.json;
|
|
859
|
+
}
|
|
823
860
|
if (!content) {
|
|
824
861
|
return null;
|
|
825
862
|
}
|
|
863
|
+
if (!richTextPath) {
|
|
864
|
+
if (ngDevMode) {
|
|
865
|
+
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.');
|
|
866
|
+
}
|
|
867
|
+
return null;
|
|
868
|
+
}
|
|
826
869
|
const richText = getObjectProperty(content, richTextPath);
|
|
827
870
|
if (richText === null || richText === undefined) {
|
|
871
|
+
if (ngDevMode && richText === undefined) {
|
|
872
|
+
warnOnUnresolvedParentPath(content, richTextPath);
|
|
873
|
+
}
|
|
828
874
|
return null;
|
|
829
875
|
}
|
|
830
876
|
if (!isObject(richText)) {
|
|
831
|
-
throw richTextRendererError('
|
|
877
|
+
throw richTextRendererError('rich_text_not_object', { content, richTextPath, type: typeof richText });
|
|
832
878
|
}
|
|
833
879
|
if (!isRichTextRootNode(richText)) {
|
|
834
880
|
throw richTextRendererError('rich_text_wrong_type', { content, richTextPath });
|
|
@@ -962,8 +1008,7 @@ class ContentfulRichTextRendererComponent {
|
|
|
962
1008
|
}
|
|
963
1009
|
const contentType = asset.fields.file?.contentType;
|
|
964
1010
|
const assetComponents = this.config.components;
|
|
965
|
-
|
|
966
|
-
if (isMissing) {
|
|
1011
|
+
if (!asset.fields.file?.url) {
|
|
967
1012
|
if (isDevMode()) {
|
|
968
1013
|
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 });
|
|
969
1014
|
}
|
|
@@ -1015,10 +1060,16 @@ class ContentfulRichTextRendererComponent {
|
|
|
1015
1060
|
href = url ? normalizeHref(url) : null;
|
|
1016
1061
|
}
|
|
1017
1062
|
const linkTexts = node.content.filter((child) => child.nodeType === 'text' && Boolean(child.value));
|
|
1063
|
+
if (!linkTexts.length) {
|
|
1064
|
+
break;
|
|
1065
|
+
}
|
|
1018
1066
|
const linkText = linkTexts.map((child) => child.value).join('');
|
|
1019
1067
|
const linkComponent = href ? this.config.components.link : null;
|
|
1020
1068
|
if (href && !linkComponent) {
|
|
1021
|
-
const
|
|
1069
|
+
const anchorHref = isRouteRelativeHref(href)
|
|
1070
|
+
? this.location.prepareExternalUrl(resolveHrefAgainstRoute(href, this.url()))
|
|
1071
|
+
: href;
|
|
1072
|
+
const attributes = { class: DEFAULT_ANCHOR_CLASS, href: anchorHref };
|
|
1022
1073
|
if (isExternalWebHref(href, { location: this.document.location, internalHosts: this.config.internalHosts })) {
|
|
1023
1074
|
attributes['target'] = '_blank';
|
|
1024
1075
|
attributes['rel'] = 'noopener noreferrer';
|
|
@@ -1063,7 +1114,6 @@ class ContentfulRichTextRendererComponent {
|
|
|
1063
1114
|
break;
|
|
1064
1115
|
}
|
|
1065
1116
|
const sharedMarks = linkTexts[0]?.marks.filter((mark) => linkTexts.every((child) => child.marks.some((other) => other.type === mark.type))) ?? [];
|
|
1066
|
-
const textClass = marksToClass(sharedMarks);
|
|
1067
1117
|
let linkComponentId = componentIdMap.get(LINK_COMPONENT_TYPE) ?? -1;
|
|
1068
1118
|
const linkId = LINK_COMPONENT_TYPE + ++linkComponentId;
|
|
1069
1119
|
componentIdMap.set(LINK_COMPONENT_TYPE, linkComponentId);
|
|
@@ -1073,7 +1123,7 @@ class ContentfulRichTextRendererComponent {
|
|
|
1073
1123
|
domPosition,
|
|
1074
1124
|
parentId: openElementIds.at(-1) ?? null,
|
|
1075
1125
|
component: linkComponent,
|
|
1076
|
-
inputs: { href, text: linkText,
|
|
1126
|
+
inputs: { href, text: linkText, marks: sharedMarks.map((mark) => mark.type), richText: true },
|
|
1077
1127
|
id: linkId,
|
|
1078
1128
|
});
|
|
1079
1129
|
domPosition++;
|
|
@@ -1085,10 +1135,10 @@ class ContentfulRichTextRendererComponent {
|
|
|
1085
1135
|
if (!entryId) {
|
|
1086
1136
|
throw richTextRendererError('entry_id_not_found', { node });
|
|
1087
1137
|
}
|
|
1088
|
-
const entry = this.
|
|
1138
|
+
const entry = this.includedEntries().get(entryId);
|
|
1089
1139
|
if (!entry) {
|
|
1090
1140
|
if (isDevMode()) {
|
|
1091
|
-
console.warn('Embedded entry is missing from the includes! The entry will be skipped. Is it unpublished or deleted?', { entryId, node });
|
|
1141
|
+
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 });
|
|
1092
1142
|
}
|
|
1093
1143
|
break;
|
|
1094
1144
|
}
|
|
@@ -1333,7 +1383,7 @@ class ContentfulRichTextRendererComponent {
|
|
|
1333
1383
|
this.renderer.insertBefore(parentElement, node, previousSibling ? previousSibling.nextSibling : parentElement.firstChild);
|
|
1334
1384
|
}
|
|
1335
1385
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulRichTextRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1336
|
-
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:
|
|
1386
|
+
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 } }, host: { classAttribute: "et-contentful-rich-text-renderer" }, ngImport: i0, template: ``, isInline: true, encapsulation: i0.ViewEncapsulation.None }); }
|
|
1337
1387
|
}
|
|
1338
1388
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulRichTextRendererComponent, decorators: [{
|
|
1339
1389
|
type: Component,
|
|
@@ -1345,7 +1395,59 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
1345
1395
|
class: 'et-contentful-rich-text-renderer',
|
|
1346
1396
|
},
|
|
1347
1397
|
}]
|
|
1348
|
-
}], ctorParameters: () => [], propDecorators: { content: [{ type: i0.Input, args: [{ isSignal: true, alias: "content", required:
|
|
1398
|
+
}], 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 }] }] } });
|
|
1399
|
+
|
|
1400
|
+
class ContentfulVideoComponent {
|
|
1401
|
+
constructor() {
|
|
1402
|
+
this.asset = input.required(/* @ts-ignore */
|
|
1403
|
+
...(ngDevMode ? [{ debugName: "asset" }] : /* istanbul ignore next */ []));
|
|
1404
|
+
this.data = computed(() => {
|
|
1405
|
+
const asset = this.asset();
|
|
1406
|
+
if (!asset) {
|
|
1407
|
+
return null;
|
|
1408
|
+
}
|
|
1409
|
+
if (isContentfulGqlAsset(asset) && asset.url) {
|
|
1410
|
+
return {
|
|
1411
|
+
url: asset.url,
|
|
1412
|
+
contentType: asset.contentType,
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
if (!isContentfulGqlAsset(asset) && asset.fields.file?.url) {
|
|
1416
|
+
return {
|
|
1417
|
+
url: asset.fields.file.url,
|
|
1418
|
+
contentType: asset.fields.file.contentType,
|
|
1419
|
+
};
|
|
1420
|
+
}
|
|
1421
|
+
return null;
|
|
1422
|
+
}, /* @ts-ignore */
|
|
1423
|
+
...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
|
|
1424
|
+
}
|
|
1425
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulVideoComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1426
|
+
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: `
|
|
1427
|
+
@if (data(); as data) {
|
|
1428
|
+
<video class="et-contentful-video-video" controls>
|
|
1429
|
+
<source [src]="data.url" [attr.type]="data.contentType || null" class="et-contentful-video-source" />
|
|
1430
|
+
</video>
|
|
1431
|
+
}
|
|
1432
|
+
`, isInline: true, encapsulation: i0.ViewEncapsulation.None }); }
|
|
1433
|
+
}
|
|
1434
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulVideoComponent, decorators: [{
|
|
1435
|
+
type: Component,
|
|
1436
|
+
args: [{
|
|
1437
|
+
selector: 'et-contentful-video',
|
|
1438
|
+
template: `
|
|
1439
|
+
@if (data(); as data) {
|
|
1440
|
+
<video class="et-contentful-video-video" controls>
|
|
1441
|
+
<source [src]="data.url" [attr.type]="data.contentType || null" class="et-contentful-video-source" />
|
|
1442
|
+
</video>
|
|
1443
|
+
}
|
|
1444
|
+
`,
|
|
1445
|
+
encapsulation: ViewEncapsulation.None,
|
|
1446
|
+
host: {
|
|
1447
|
+
class: 'et-contentful-video',
|
|
1448
|
+
},
|
|
1449
|
+
}]
|
|
1450
|
+
}], propDecorators: { asset: [{ type: i0.Input, args: [{ isSignal: true, alias: "asset", required: true }] }] } });
|
|
1349
1451
|
|
|
1350
1452
|
const ContentfulImports = [
|
|
1351
1453
|
ContentfulAudioComponent,
|
|
@@ -1355,6 +1457,24 @@ const ContentfulImports = [
|
|
|
1355
1457
|
ContentfulRichTextRendererComponent,
|
|
1356
1458
|
];
|
|
1357
1459
|
|
|
1460
|
+
/**
|
|
1461
|
+
* The shipped audio, file, image, video and link components, to spread into `provideContentfulConfig`.
|
|
1462
|
+
* An app that brings its own components, or renders text only, leaves this out and bundles none of them.
|
|
1463
|
+
* To replace one, spread the map instead: `components: { ...CONTENTFUL_DEFAULT_COMPONENTS.components, image: MyImage }`.
|
|
1464
|
+
*
|
|
1465
|
+
* @example
|
|
1466
|
+
* provideContentfulConfig({ ...CONTENTFUL_DEFAULT_COMPONENTS });
|
|
1467
|
+
*/
|
|
1468
|
+
const CONTENTFUL_DEFAULT_COMPONENTS = {
|
|
1469
|
+
components: {
|
|
1470
|
+
audio: ContentfulAudioComponent,
|
|
1471
|
+
file: ContentfulFileComponent,
|
|
1472
|
+
image: ContentfulImageComponent,
|
|
1473
|
+
video: ContentfulVideoComponent,
|
|
1474
|
+
link: ContentfulLinkComponent,
|
|
1475
|
+
},
|
|
1476
|
+
};
|
|
1477
|
+
|
|
1358
1478
|
const provideContentfulConfig = (contentfulConfig) => {
|
|
1359
1479
|
return { provide: CONTENTFUL_CONFIG, useValue: createContentfulConfig(contentfulConfig) };
|
|
1360
1480
|
};
|
|
@@ -1364,5 +1484,5 @@ const isContentfulEntryType = (entry, type) => entry.sys.contentType.sys.id ===
|
|
|
1364
1484
|
* Generated bundle index. Do not edit.
|
|
1365
1485
|
*/
|
|
1366
1486
|
|
|
1367
|
-
export { CONTENTFUL_CONFIG, 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 };
|
|
1487
|
+
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 };
|
|
1368
1488
|
//# sourceMappingURL=ethlete-contentful.mjs.map
|