@ethlete/contentful 4.0.0-next.7 → 4.0.0-next.8
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 +10 -0
- package/fesm2022/ethlete-contentful.mjs +342 -327
- package/fesm2022/ethlete-contentful.mjs.map +1 -1
- package/generators/migrate-to-contentful-v5/migration-scope.js +12 -6
- package/generators/migrate-to-contentful-v5/migration.js +96 -32
- package/package.json +2 -10
- package/types/ethlete-contentful.d.ts +28 -80
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { NgClass, DOCUMENT } from '@angular/common';
|
|
2
2
|
import * as i0 from '@angular/core';
|
|
3
|
-
import { input, computed, ViewEncapsulation, Component,
|
|
3
|
+
import { input, computed, ViewEncapsulation, Component, inject, InjectionToken, numberAttribute, booleanAttribute, isDevMode, ViewContainerRef, ElementRef, effect, untracked, signal, reflectComponentType, inputBinding } from '@angular/core';
|
|
4
4
|
import { normalizePictureSizes, PictureComponent } from '@ethlete/components';
|
|
5
|
-
import { RouterLink } from '@angular/router';
|
|
5
|
+
import { Router, RouterLink } from '@angular/router';
|
|
6
6
|
import { RuntimeError, isObject, injectRenderer, getObjectProperty } from '@ethlete/core';
|
|
7
7
|
|
|
8
8
|
const GQL_FRAGMENT_CONTENTFUL_ASSET = `
|
|
9
|
-
fragment
|
|
9
|
+
fragment ContentfulAssetData on Asset {
|
|
10
10
|
sys {
|
|
11
11
|
id
|
|
12
12
|
}
|
|
@@ -21,7 +21,7 @@ const GQL_FRAGMENT_CONTENTFUL_ASSET = `
|
|
|
21
21
|
`;
|
|
22
22
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
23
23
|
const isContentfulGqlAsset = (asset) => {
|
|
24
|
-
return asset
|
|
24
|
+
return Boolean(asset && typeof asset === 'object' && asset.sys?.id && !('fields' in asset));
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
class ContentfulAudioComponent {
|
|
@@ -39,25 +39,28 @@ class ContentfulAudioComponent {
|
|
|
39
39
|
if (!asset) {
|
|
40
40
|
return null;
|
|
41
41
|
}
|
|
42
|
-
if (isContentfulGqlAsset(asset)) {
|
|
42
|
+
if (isContentfulGqlAsset(asset) && asset.url) {
|
|
43
43
|
return {
|
|
44
44
|
url: asset.url,
|
|
45
45
|
title: asset.title,
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
if (!isContentfulGqlAsset(asset) && asset.fields.file.url) {
|
|
49
|
+
return {
|
|
50
|
+
url: asset.fields.file.url,
|
|
51
|
+
title: asset.fields.title,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
52
55
|
}, /* @ts-ignore */
|
|
53
56
|
...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
|
|
54
57
|
}
|
|
55
58
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ContentfulAudioComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
56
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: ContentfulAudioComponent, isStandalone: true, selector: "et-contentful-audio", inputs: { asset: { classPropertyName: "asset", publicName: "asset", isSignal: true, isRequired: true, transformFunction: null }, audioClass: { classPropertyName: "audioClass", publicName: "audioClass", isSignal: true, isRequired: false, transformFunction: null }, figureClass: { classPropertyName: "figureClass", publicName: "figureClass", isSignal: true, isRequired: false, transformFunction: null }, figcaptionClass: { classPropertyName: "figcaptionClass", publicName: "figcaptionClass", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
|
|
59
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: ContentfulAudioComponent, isStandalone: true, selector: "et-contentful-audio", inputs: { asset: { classPropertyName: "asset", publicName: "asset", isSignal: true, isRequired: true, transformFunction: null }, audioClass: { classPropertyName: "audioClass", publicName: "audioClass", isSignal: true, isRequired: false, transformFunction: null }, figureClass: { classPropertyName: "figureClass", publicName: "figureClass", isSignal: true, isRequired: false, transformFunction: null }, figcaptionClass: { classPropertyName: "figcaptionClass", publicName: "figcaptionClass", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "et-contentful-audio" }, ngImport: i0, template: `
|
|
57
60
|
@if (data(); as data) {
|
|
58
61
|
<figure [ngClass]="figureClass()">
|
|
59
62
|
<figcaption [ngClass]="figcaptionClass()">{{ data.title }}</figcaption>
|
|
60
|
-
<audio [ngClass]="audioClass()"
|
|
63
|
+
<audio [ngClass]="audioClass()" [src]="data.url" controls></audio>
|
|
61
64
|
</figure>
|
|
62
65
|
}
|
|
63
66
|
`, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], encapsulation: i0.ViewEncapsulation.None }); }
|
|
@@ -70,12 +73,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
|
|
|
70
73
|
@if (data(); as data) {
|
|
71
74
|
<figure [ngClass]="figureClass()">
|
|
72
75
|
<figcaption [ngClass]="figcaptionClass()">{{ data.title }}</figcaption>
|
|
73
|
-
<audio [ngClass]="audioClass()"
|
|
76
|
+
<audio [ngClass]="audioClass()" [src]="data.url" controls></audio>
|
|
74
77
|
</figure>
|
|
75
78
|
}
|
|
76
79
|
`,
|
|
77
80
|
encapsulation: ViewEncapsulation.None,
|
|
78
81
|
imports: [NgClass],
|
|
82
|
+
host: {
|
|
83
|
+
class: 'et-contentful-audio',
|
|
84
|
+
},
|
|
79
85
|
}]
|
|
80
86
|
}], propDecorators: { asset: [{ type: i0.Input, args: [{ isSignal: true, alias: "asset", required: true }] }], audioClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "audioClass", required: false }] }], figureClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "figureClass", required: false }] }], figcaptionClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "figcaptionClass", required: false }] }] } });
|
|
81
87
|
|
|
@@ -90,28 +96,32 @@ class ContentfulFileComponent {
|
|
|
90
96
|
if (!asset) {
|
|
91
97
|
return null;
|
|
92
98
|
}
|
|
93
|
-
if (isContentfulGqlAsset(asset)) {
|
|
99
|
+
if (isContentfulGqlAsset(asset) && asset.url) {
|
|
94
100
|
return {
|
|
95
101
|
url: asset.url,
|
|
96
|
-
contentType: asset.contentType,
|
|
97
102
|
size: asset.size,
|
|
98
103
|
title: asset.title,
|
|
99
104
|
};
|
|
100
105
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
106
|
+
if (!isContentfulGqlAsset(asset) && asset.fields.file.url) {
|
|
107
|
+
return {
|
|
108
|
+
url: asset.fields.file.url,
|
|
109
|
+
size: asset.fields.file.details.size,
|
|
110
|
+
title: asset.fields.title,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
107
114
|
}, /* @ts-ignore */
|
|
108
115
|
...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
|
|
109
116
|
}
|
|
110
117
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ContentfulFileComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
111
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: ContentfulFileComponent, isStandalone: true, selector: "et-contentful-file", inputs: { asset: { classPropertyName: "asset", publicName: "asset", isSignal: true, isRequired: true, transformFunction: null }, fileClass: { classPropertyName: "fileClass", publicName: "fileClass", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
|
|
118
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: ContentfulFileComponent, isStandalone: true, selector: "et-contentful-file", inputs: { asset: { classPropertyName: "asset", publicName: "asset", isSignal: true, isRequired: true, transformFunction: null }, fileClass: { classPropertyName: "fileClass", publicName: "fileClass", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "et-contentful-file" }, ngImport: i0, template: `
|
|
112
119
|
@if (data(); as data) {
|
|
113
|
-
<a [href]="data.url" [ngClass]="fileClass()"
|
|
114
|
-
{{ data.title }}
|
|
120
|
+
<a [href]="data.url" [ngClass]="fileClass()" target="_blank" rel="noopener noreferrer">
|
|
121
|
+
{{ data.title }}
|
|
122
|
+
@if (data.size !== null) {
|
|
123
|
+
({{ data.size }} Bytes)
|
|
124
|
+
}
|
|
115
125
|
</a>
|
|
116
126
|
}
|
|
117
127
|
`, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], encapsulation: i0.ViewEncapsulation.None }); }
|
|
@@ -122,18 +132,218 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
|
|
|
122
132
|
selector: 'et-contentful-file',
|
|
123
133
|
template: `
|
|
124
134
|
@if (data(); as data) {
|
|
125
|
-
<a [href]="data.url" [ngClass]="fileClass()"
|
|
126
|
-
{{ data.title }}
|
|
135
|
+
<a [href]="data.url" [ngClass]="fileClass()" target="_blank" rel="noopener noreferrer">
|
|
136
|
+
{{ data.title }}
|
|
137
|
+
@if (data.size !== null) {
|
|
138
|
+
({{ data.size }} Bytes)
|
|
139
|
+
}
|
|
127
140
|
</a>
|
|
128
141
|
}
|
|
129
142
|
`,
|
|
130
143
|
encapsulation: ViewEncapsulation.None,
|
|
131
144
|
imports: [NgClass],
|
|
145
|
+
host: {
|
|
146
|
+
class: 'et-contentful-file',
|
|
147
|
+
},
|
|
132
148
|
}]
|
|
133
149
|
}], propDecorators: { asset: [{ type: i0.Input, args: [{ isSignal: true, alias: "asset", required: true }] }], fileClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "fileClass", required: false }] }] } });
|
|
134
150
|
|
|
151
|
+
const parseWebUrl = (href) => {
|
|
152
|
+
if (!/^(?:https?:)?\/\//i.test(href)) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
return new URL(href.startsWith('//') ? 'https:' + href : href);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
const normalizeHostname = (host) => {
|
|
163
|
+
const value = host.trim().toLowerCase();
|
|
164
|
+
try {
|
|
165
|
+
return new URL(value.includes('://') ? value : 'https://' + value).hostname;
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return value.split(':')[0] ?? value;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
const matchesHostname = (hostname, configuredHost) => {
|
|
172
|
+
const normalizedHost = normalizeHostname(configuredHost);
|
|
173
|
+
return hostname === normalizedHost || hostname.endsWith('.' + normalizedHost);
|
|
174
|
+
};
|
|
175
|
+
class ContentfulLinkComponent {
|
|
176
|
+
constructor() {
|
|
177
|
+
this.document = inject(DOCUMENT);
|
|
178
|
+
this.router = inject(Router);
|
|
179
|
+
this.config = injectContentfulConfig();
|
|
180
|
+
this.href = input.required(/* @ts-ignore */
|
|
181
|
+
...(ngDevMode ? [{ debugName: "href" }] : /* istanbul ignore next */ []));
|
|
182
|
+
this.text = input.required(/* @ts-ignore */
|
|
183
|
+
...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
|
|
184
|
+
this.textClass = input('', /* @ts-ignore */
|
|
185
|
+
...(ngDevMode ? [{ debugName: "textClass" }] : /* istanbul ignore next */ []));
|
|
186
|
+
this.usesRouterLink = computed(() => {
|
|
187
|
+
const href = this.href();
|
|
188
|
+
const absoluteUrl = parseWebUrl(href);
|
|
189
|
+
if (absoluteUrl) {
|
|
190
|
+
const internalHosts = [this.document.location.hostname, ...this.config.internalHosts];
|
|
191
|
+
return internalHosts.some((host) => matchesHostname(absoluteUrl.hostname, host));
|
|
192
|
+
}
|
|
193
|
+
return !href.startsWith('#') && !/^[a-z][a-z\d+.-]*:/i.test(href);
|
|
194
|
+
}, /* @ts-ignore */
|
|
195
|
+
...(ngDevMode ? [{ debugName: "usesRouterLink" }] : /* istanbul ignore next */ []));
|
|
196
|
+
this.openInNewTab = computed(() => Boolean(parseWebUrl(this.href())) && !this.usesRouterLink(), /* @ts-ignore */
|
|
197
|
+
...(ngDevMode ? [{ debugName: "openInNewTab" }] : /* istanbul ignore next */ []));
|
|
198
|
+
this.internalPath = computed(() => {
|
|
199
|
+
const href = this.href();
|
|
200
|
+
const url = parseWebUrl(href);
|
|
201
|
+
if (!url) {
|
|
202
|
+
return href;
|
|
203
|
+
}
|
|
204
|
+
return url.pathname + url.search + url.hash;
|
|
205
|
+
}, /* @ts-ignore */
|
|
206
|
+
...(ngDevMode ? [{ debugName: "internalPath" }] : /* istanbul ignore next */ []));
|
|
207
|
+
this.internalUrlTree = computed(() => this.router.parseUrl(this.internalPath()), /* @ts-ignore */
|
|
208
|
+
...(ngDevMode ? [{ debugName: "internalUrlTree" }] : /* istanbul ignore next */ []));
|
|
209
|
+
this.linkClass = computed(() => {
|
|
210
|
+
const base = 'et-contentful-rich-text-default-element et-contentful-rich-text-default-a';
|
|
211
|
+
const extra = this.textClass();
|
|
212
|
+
return extra ? `${base} ${extra}` : base;
|
|
213
|
+
}, /* @ts-ignore */
|
|
214
|
+
...(ngDevMode ? [{ debugName: "linkClass" }] : /* istanbul ignore next */ []));
|
|
215
|
+
}
|
|
216
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ContentfulLinkComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
217
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", 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 } }, host: { classAttribute: "et-contentful-link" }, ngImport: i0, template: `
|
|
218
|
+
@if (usesRouterLink()) {
|
|
219
|
+
<a [class]="linkClass()" [routerLink]="internalUrlTree()">{{ text() }}</a>
|
|
220
|
+
} @else {
|
|
221
|
+
<a
|
|
222
|
+
[class]="linkClass()"
|
|
223
|
+
[href]="href()"
|
|
224
|
+
[target]="openInNewTab() ? '_blank' : null"
|
|
225
|
+
[rel]="openInNewTab() ? 'noopener noreferrer' : null"
|
|
226
|
+
>{{ text() }}</a
|
|
227
|
+
>
|
|
228
|
+
}
|
|
229
|
+
`, 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 }); }
|
|
230
|
+
}
|
|
231
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ContentfulLinkComponent, decorators: [{
|
|
232
|
+
type: Component,
|
|
233
|
+
args: [{ selector: 'et-contentful-link', template: `
|
|
234
|
+
@if (usesRouterLink()) {
|
|
235
|
+
<a [class]="linkClass()" [routerLink]="internalUrlTree()">{{ text() }}</a>
|
|
236
|
+
} @else {
|
|
237
|
+
<a
|
|
238
|
+
[class]="linkClass()"
|
|
239
|
+
[href]="href()"
|
|
240
|
+
[target]="openInNewTab() ? '_blank' : null"
|
|
241
|
+
[rel]="openInNewTab() ? 'noopener noreferrer' : null"
|
|
242
|
+
>{{ text() }}</a
|
|
243
|
+
>
|
|
244
|
+
}
|
|
245
|
+
`, encapsulation: ViewEncapsulation.None, imports: [RouterLink], host: {
|
|
246
|
+
class: 'et-contentful-link',
|
|
247
|
+
}, styles: ["@layer components{.et-contentful-link{display:contents}}\n"] }]
|
|
248
|
+
}], 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 }] }] } });
|
|
249
|
+
|
|
250
|
+
class ContentfulVideoComponent {
|
|
251
|
+
constructor() {
|
|
252
|
+
this.asset = input.required(/* @ts-ignore */
|
|
253
|
+
...(ngDevMode ? [{ debugName: "asset" }] : /* istanbul ignore next */ []));
|
|
254
|
+
this.videoClass = input(null, /* @ts-ignore */
|
|
255
|
+
...(ngDevMode ? [{ debugName: "videoClass" }] : /* istanbul ignore next */ []));
|
|
256
|
+
this.data = computed(() => {
|
|
257
|
+
const asset = this.asset();
|
|
258
|
+
if (!asset) {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
if (isContentfulGqlAsset(asset) && asset.url) {
|
|
262
|
+
return {
|
|
263
|
+
url: asset.url,
|
|
264
|
+
contentType: asset.contentType,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
if (!isContentfulGqlAsset(asset) && asset.fields.file.url) {
|
|
268
|
+
return {
|
|
269
|
+
url: asset.fields.file.url,
|
|
270
|
+
contentType: asset.fields.file.contentType,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
return null;
|
|
274
|
+
}, /* @ts-ignore */
|
|
275
|
+
...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
|
|
276
|
+
}
|
|
277
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ContentfulVideoComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
278
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", 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: `
|
|
279
|
+
@if (data(); as data) {
|
|
280
|
+
<video [ngClass]="videoClass()" controls>
|
|
281
|
+
<source [src]="data.url" [type]="data.contentType" />
|
|
282
|
+
</video>
|
|
283
|
+
}
|
|
284
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], encapsulation: i0.ViewEncapsulation.None }); }
|
|
285
|
+
}
|
|
286
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ContentfulVideoComponent, decorators: [{
|
|
287
|
+
type: Component,
|
|
288
|
+
args: [{
|
|
289
|
+
selector: 'et-contentful-video',
|
|
290
|
+
template: `
|
|
291
|
+
@if (data(); as data) {
|
|
292
|
+
<video [ngClass]="videoClass()" controls>
|
|
293
|
+
<source [src]="data.url" [type]="data.contentType" />
|
|
294
|
+
</video>
|
|
295
|
+
}
|
|
296
|
+
`,
|
|
297
|
+
encapsulation: ViewEncapsulation.None,
|
|
298
|
+
imports: [NgClass],
|
|
299
|
+
host: {
|
|
300
|
+
class: 'et-contentful-video',
|
|
301
|
+
},
|
|
302
|
+
}]
|
|
303
|
+
}], propDecorators: { asset: [{ type: i0.Input, args: [{ isSignal: true, alias: "asset", required: true }] }], videoClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "videoClass", required: false }] }] } });
|
|
304
|
+
|
|
135
305
|
const CONTENTFUL_CONFIG = new InjectionToken('ContentfulConfig');
|
|
136
306
|
|
|
307
|
+
/**
|
|
308
|
+
* The config used when no `provideContentfulConfig()` is in scope. It declares no
|
|
309
|
+
* `components`, so embedded assets are skipped and hyperlinks render as plain anchors
|
|
310
|
+
* until a config is provided.
|
|
311
|
+
*/
|
|
312
|
+
const CONTENTFUL_FALLBACK_CONFIG = {
|
|
313
|
+
internalHosts: [],
|
|
314
|
+
components: {},
|
|
315
|
+
customComponents: {},
|
|
316
|
+
imageOptions: {
|
|
317
|
+
srcsetSizes: ['375w', '1280w', '1920w', '2560w'],
|
|
318
|
+
sizes: ['100vw'],
|
|
319
|
+
backgroundColor: null,
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
const createContentfulConfig = (config) => ({
|
|
323
|
+
...{
|
|
324
|
+
internalHosts: [],
|
|
325
|
+
components: {
|
|
326
|
+
audio: ContentfulAudioComponent,
|
|
327
|
+
file: ContentfulFileComponent,
|
|
328
|
+
image: ContentfulImageComponent,
|
|
329
|
+
video: ContentfulVideoComponent,
|
|
330
|
+
link: ContentfulLinkComponent,
|
|
331
|
+
},
|
|
332
|
+
customComponents: {},
|
|
333
|
+
imageOptions: {
|
|
334
|
+
srcsetSizes: ['375w', '1280w', '1920w', '2560w'],
|
|
335
|
+
sizes: ['100vw'],
|
|
336
|
+
backgroundColor: null,
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
...(config ?? {}),
|
|
340
|
+
});
|
|
341
|
+
/**
|
|
342
|
+
* Reads the contentful config from the current injector. Falls back to a config without
|
|
343
|
+
* any `components` when none was provided via `provideContentfulConfig()`.
|
|
344
|
+
*/
|
|
345
|
+
const injectContentfulConfig = () => inject(CONTENTFUL_CONFIG, { optional: true }) ?? CONTENTFUL_FALLBACK_CONFIG;
|
|
346
|
+
|
|
137
347
|
const generateDefaultContentfulImageSource = (data) => {
|
|
138
348
|
if (isContentfulGqlAsset(data)) {
|
|
139
349
|
if (!data.contentType || !data.url) {
|
|
@@ -167,41 +377,37 @@ const generateDefaultContentfulImageSource = (data) => {
|
|
|
167
377
|
* - `"400wx300h"` - 400px width and 300px height
|
|
168
378
|
**/
|
|
169
379
|
const parseContentfulImageSize = (size) => {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
else if (size.includes('h')) {
|
|
178
|
-
height = size;
|
|
380
|
+
const widthAndHeight = /^(\d+)w?x(\d+)h?$/.exec(size);
|
|
381
|
+
if (widthAndHeight) {
|
|
382
|
+
return {
|
|
383
|
+
width: Number(widthAndHeight[1]),
|
|
384
|
+
height: Number(widthAndHeight[2]),
|
|
385
|
+
};
|
|
179
386
|
}
|
|
180
|
-
|
|
181
|
-
|
|
387
|
+
const height = /^(\d+)h$/.exec(size);
|
|
388
|
+
if (height) {
|
|
389
|
+
return { width: null, height: Number(height[1]) };
|
|
182
390
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
height: height ? parseInt(height, 10) : null,
|
|
186
|
-
};
|
|
391
|
+
const width = /^(\d+)w?$/.exec(size);
|
|
392
|
+
return width ? { width: Number(width[1]), height: null } : { width: null, height: null };
|
|
187
393
|
};
|
|
188
|
-
const SOURCE_TYPES = ['image/avif', 'image/webp'
|
|
394
|
+
const SOURCE_TYPES = ['image/avif', 'image/webp'];
|
|
189
395
|
const generateContentfulImageSources = (data, srcsetSizes, backgroundColor, quality, focusArea, resizeBehavior) => {
|
|
190
|
-
const
|
|
191
|
-
|
|
396
|
+
const isGqlAsset = isContentfulGqlAsset(data);
|
|
397
|
+
const baseUrl = isGqlAsset ? data.url : data.fields.file.url;
|
|
398
|
+
if (!baseUrl) {
|
|
192
399
|
return [];
|
|
193
400
|
}
|
|
401
|
+
const imageDimensions = isGqlAsset ? data : data.fields.file.details.image;
|
|
194
402
|
const sources = [];
|
|
195
403
|
for (const type of SOURCE_TYPES) {
|
|
196
|
-
const baseUrl = isContentfulGqlAsset(assetData) ? assetData.url : assetData.fields.file.url;
|
|
197
404
|
const sourceSets = [];
|
|
198
405
|
const queryParams = [];
|
|
199
|
-
// Set the format (e.g. 'fm=webp')
|
|
200
406
|
queryParams.push(`fm=${type.split('/')[1]}`);
|
|
201
407
|
if (backgroundColor) {
|
|
202
408
|
queryParams.push(`bg=rgb:${backgroundColor}`);
|
|
203
409
|
}
|
|
204
|
-
if (quality !== null) {
|
|
410
|
+
if (quality !== null && Number.isFinite(quality)) {
|
|
205
411
|
queryParams.push(`q=${quality}`);
|
|
206
412
|
}
|
|
207
413
|
if (focusArea) {
|
|
@@ -211,7 +417,7 @@ const generateContentfulImageSources = (data, srcsetSizes, backgroundColor, qual
|
|
|
211
417
|
queryParams.push(`fit=${resizeBehavior}`);
|
|
212
418
|
}
|
|
213
419
|
if (srcsetSizes?.length) {
|
|
214
|
-
const urlWithParams = `${baseUrl}
|
|
420
|
+
const urlWithParams = `${baseUrl}${baseUrl.includes('?') ? '&' : '?'}${queryParams.join('&')}`;
|
|
215
421
|
for (const size of srcsetSizes) {
|
|
216
422
|
const { width, height } = parseContentfulImageSize(size);
|
|
217
423
|
if (width && height) {
|
|
@@ -220,29 +426,36 @@ const generateContentfulImageSources = (data, srcsetSizes, backgroundColor, qual
|
|
|
220
426
|
else if (width) {
|
|
221
427
|
sourceSets.push(`${urlWithParams}&w=${width} ${width}w`);
|
|
222
428
|
}
|
|
223
|
-
else if (height) {
|
|
224
|
-
|
|
429
|
+
else if (height && imageDimensions?.width && imageDimensions.height) {
|
|
430
|
+
const derivedWidth = Math.round((height * imageDimensions.width) / imageDimensions.height);
|
|
431
|
+
sourceSets.push(`${urlWithParams}&w=${derivedWidth}&h=${height} ${derivedWidth}w`);
|
|
225
432
|
}
|
|
226
433
|
}
|
|
227
434
|
}
|
|
228
435
|
sources.push({
|
|
229
436
|
type,
|
|
230
|
-
srcset: sourceSets.length
|
|
437
|
+
srcset: sourceSets.length
|
|
438
|
+
? sourceSets.join(', ')
|
|
439
|
+
: `${baseUrl}${baseUrl.includes('?') ? '&' : '?'}${queryParams.join('&')}`,
|
|
231
440
|
});
|
|
232
441
|
}
|
|
233
442
|
return sources;
|
|
234
443
|
};
|
|
235
444
|
|
|
445
|
+
const optionalNumberAttribute = (value) => {
|
|
446
|
+
const transformed = numberAttribute(value);
|
|
447
|
+
return Number.isFinite(transformed) ? transformed : null;
|
|
448
|
+
};
|
|
236
449
|
class ContentfulImageComponent {
|
|
237
450
|
constructor() {
|
|
238
|
-
this.contentfulConfig =
|
|
451
|
+
this.contentfulConfig = injectContentfulConfig();
|
|
239
452
|
this.asset = input.required(/* @ts-ignore */
|
|
240
453
|
...(ngDevMode ? [{ debugName: "asset" }] : /* istanbul ignore next */ []));
|
|
241
|
-
this.backgroundColor = input(
|
|
454
|
+
this.backgroundColor = input(this.contentfulConfig.imageOptions.backgroundColor, /* @ts-ignore */
|
|
242
455
|
...(ngDevMode ? [{ debugName: "backgroundColor" }] : /* istanbul ignore next */ []));
|
|
243
456
|
this.srcsetSizes = input(this.contentfulConfig.imageOptions.srcsetSizes, /* @ts-ignore */
|
|
244
457
|
...(ngDevMode ? [{ debugName: "srcsetSizes" }] : /* istanbul ignore next */ []));
|
|
245
|
-
this.quality = input(null, { ...(ngDevMode ? { debugName: "quality" } : /* istanbul ignore next */ {}), transform:
|
|
458
|
+
this.quality = input(null, { ...(ngDevMode ? { debugName: "quality" } : /* istanbul ignore next */ {}), transform: optionalNumberAttribute });
|
|
246
459
|
this.focusArea = input(null, /* @ts-ignore */
|
|
247
460
|
...(ngDevMode ? [{ debugName: "focusArea" }] : /* istanbul ignore next */ []));
|
|
248
461
|
this.resizeBehavior = input(null, /* @ts-ignore */
|
|
@@ -300,7 +513,7 @@ class ContentfulImageComponent {
|
|
|
300
513
|
[sizes]="sizes()"
|
|
301
514
|
[sources]="sourcesValue()"
|
|
302
515
|
/>
|
|
303
|
-
`, isInline: true, dependencies: [{ kind: "component", type: PictureComponent, selector: "et-picture", inputs: ["sources", "defaultSrc", "alt", "figcaption", "priority", "width", "height", "aspectRatio", "sizes"], outputs: ["imgLoad", "imgError"] }], encapsulation: i0.ViewEncapsulation.None }); }
|
|
516
|
+
`, isInline: true, dependencies: [{ kind: "component", type: PictureComponent, selector: "et-picture", inputs: ["sources", "defaultSrc", "alt", "figcaption", "priority", "width", "height", "aspectRatio", "sizes", "fit"], outputs: ["imgLoad", "imgError"] }], encapsulation: i0.ViewEncapsulation.None }); }
|
|
304
517
|
}
|
|
305
518
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ContentfulImageComponent, decorators: [{
|
|
306
519
|
type: Component,
|
|
@@ -326,206 +539,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
|
|
|
326
539
|
}]
|
|
327
540
|
}], 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 }] }] } });
|
|
328
541
|
|
|
329
|
-
class ContentfulVideoComponent {
|
|
330
|
-
constructor() {
|
|
331
|
-
this.asset = input.required(/* @ts-ignore */
|
|
332
|
-
...(ngDevMode ? [{ debugName: "asset" }] : /* istanbul ignore next */ []));
|
|
333
|
-
this.videoClass = input(null, /* @ts-ignore */
|
|
334
|
-
...(ngDevMode ? [{ debugName: "videoClass" }] : /* istanbul ignore next */ []));
|
|
335
|
-
this.data = computed(() => {
|
|
336
|
-
const asset = this.asset();
|
|
337
|
-
if (!asset) {
|
|
338
|
-
return null;
|
|
339
|
-
}
|
|
340
|
-
if (isContentfulGqlAsset(asset)) {
|
|
341
|
-
return {
|
|
342
|
-
url: asset.url,
|
|
343
|
-
contentType: asset.contentType,
|
|
344
|
-
};
|
|
345
|
-
}
|
|
346
|
-
return {
|
|
347
|
-
url: asset.fields.file.url,
|
|
348
|
-
contentType: asset.fields.file.contentType,
|
|
349
|
-
};
|
|
350
|
-
}, /* @ts-ignore */
|
|
351
|
-
...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
|
|
352
|
-
}
|
|
353
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ContentfulVideoComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
354
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", 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 } }, ngImport: i0, template: `
|
|
355
|
-
@if (data(); as data) {
|
|
356
|
-
<video [ngClass]="videoClass()" controls>
|
|
357
|
-
<source src="{{ data.url }}" type="{{ data.contentType }}" />
|
|
358
|
-
</video>
|
|
359
|
-
}
|
|
360
|
-
`, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], encapsulation: i0.ViewEncapsulation.None }); }
|
|
361
|
-
}
|
|
362
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ContentfulVideoComponent, decorators: [{
|
|
363
|
-
type: Component,
|
|
364
|
-
args: [{
|
|
365
|
-
selector: 'et-contentful-video',
|
|
366
|
-
template: `
|
|
367
|
-
@if (data(); as data) {
|
|
368
|
-
<video [ngClass]="videoClass()" controls>
|
|
369
|
-
<source src="{{ data.url }}" type="{{ data.contentType }}" />
|
|
370
|
-
</video>
|
|
371
|
-
}
|
|
372
|
-
`,
|
|
373
|
-
encapsulation: ViewEncapsulation.None,
|
|
374
|
-
imports: [NgClass],
|
|
375
|
-
}]
|
|
376
|
-
}], propDecorators: { asset: [{ type: i0.Input, args: [{ isSignal: true, alias: "asset", required: true }] }], videoClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "videoClass", required: false }] }] } });
|
|
377
|
-
|
|
378
|
-
/**
|
|
379
|
-
* The config used when no `provideContentfulConfig()` is in scope. It declares no
|
|
380
|
-
* `components`, so embedded assets are skipped and hyperlinks render as plain anchors
|
|
381
|
-
* until a config is provided.
|
|
382
|
-
*/
|
|
383
|
-
const CONTENTFUL_FALLBACK_CONFIG = {
|
|
384
|
-
internalHosts: [],
|
|
385
|
-
components: {},
|
|
386
|
-
customComponents: {},
|
|
387
|
-
imageOptions: {
|
|
388
|
-
srcsetSizes: ['375w', '1280w', '1920w', '2560w'],
|
|
389
|
-
sizes: [],
|
|
390
|
-
backgroundColor: null,
|
|
391
|
-
},
|
|
392
|
-
};
|
|
393
|
-
const createContentfulConfig = (config = {}) => ({
|
|
394
|
-
// This default config must be inline and not saved inside a const for some weird webpack reason
|
|
395
|
-
...{
|
|
396
|
-
internalHosts: [],
|
|
397
|
-
components: {
|
|
398
|
-
audio: ContentfulAudioComponent,
|
|
399
|
-
file: ContentfulFileComponent,
|
|
400
|
-
image: ContentfulImageComponent,
|
|
401
|
-
video: ContentfulVideoComponent,
|
|
402
|
-
link: ContentfulLinkComponent,
|
|
403
|
-
},
|
|
404
|
-
customComponents: {},
|
|
405
|
-
imageOptions: {
|
|
406
|
-
srcsetSizes: ['375w', '1280w', '1920w', '2560w'],
|
|
407
|
-
sizes: [],
|
|
408
|
-
backgroundColor: null,
|
|
409
|
-
},
|
|
410
|
-
},
|
|
411
|
-
...(config || {}),
|
|
412
|
-
});
|
|
413
|
-
/**
|
|
414
|
-
* Reads the contentful config from the current injector. Falls back to a config without
|
|
415
|
-
* any `components` when none was provided via `provideContentfulConfig()`.
|
|
416
|
-
*/
|
|
417
|
-
const injectContentfulConfig = () => inject(CONTENTFUL_CONFIG, { optional: true }) ?? CONTENTFUL_FALLBACK_CONFIG;
|
|
418
|
-
|
|
419
|
-
const getPrimaryDomain = (host) => {
|
|
420
|
-
const parts = host.split('.');
|
|
421
|
-
return parts.length > 2 ? parts.slice(-2).join('.') : host;
|
|
422
|
-
};
|
|
423
|
-
class ContentfulLinkComponent {
|
|
424
|
-
constructor() {
|
|
425
|
-
this.document = inject(DOCUMENT);
|
|
426
|
-
this.config = injectContentfulConfig();
|
|
427
|
-
this.href = input.required(/* @ts-ignore */
|
|
428
|
-
...(ngDevMode ? [{ debugName: "href" }] : /* istanbul ignore next */ []));
|
|
429
|
-
this.text = input.required(/* @ts-ignore */
|
|
430
|
-
...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
|
|
431
|
-
this.textClass = input('', /* @ts-ignore */
|
|
432
|
-
...(ngDevMode ? [{ debugName: "textClass" }] : /* istanbul ignore next */ []));
|
|
433
|
-
this.isExternal = computed(() => {
|
|
434
|
-
const href = this.href();
|
|
435
|
-
const isAbsolute = href.startsWith('http://') || href.startsWith('https://') || href.startsWith('//');
|
|
436
|
-
if (!isAbsolute)
|
|
437
|
-
return false;
|
|
438
|
-
try {
|
|
439
|
-
const parsed = new URL(href.startsWith('//') ? `https:${href}` : href);
|
|
440
|
-
const internalHosts = [this.document.location.host, ...this.config.internalHosts];
|
|
441
|
-
return !internalHosts.includes(parsed.host);
|
|
442
|
-
}
|
|
443
|
-
catch {
|
|
444
|
-
return true;
|
|
445
|
-
}
|
|
446
|
-
}, /* @ts-ignore */
|
|
447
|
-
...(ngDevMode ? [{ debugName: "isExternal" }] : /* istanbul ignore next */ []));
|
|
448
|
-
this.openInNewTab = computed(() => {
|
|
449
|
-
if (!this.isExternal())
|
|
450
|
-
return false;
|
|
451
|
-
const href = this.href();
|
|
452
|
-
try {
|
|
453
|
-
const parsed = new URL(href.startsWith('//') ? `https:${href}` : href);
|
|
454
|
-
const internalPrimaryDomains = [
|
|
455
|
-
getPrimaryDomain(this.document.location.host),
|
|
456
|
-
...this.config.internalHosts.map(getPrimaryDomain),
|
|
457
|
-
];
|
|
458
|
-
return !internalPrimaryDomains.includes(getPrimaryDomain(parsed.host));
|
|
459
|
-
}
|
|
460
|
-
catch {
|
|
461
|
-
return true;
|
|
462
|
-
}
|
|
463
|
-
}, /* @ts-ignore */
|
|
464
|
-
...(ngDevMode ? [{ debugName: "openInNewTab" }] : /* istanbul ignore next */ []));
|
|
465
|
-
this.internalPath = computed(() => {
|
|
466
|
-
const href = this.href();
|
|
467
|
-
if (!href.startsWith('http://') && !href.startsWith('https://') && !href.startsWith('//')) {
|
|
468
|
-
return href;
|
|
469
|
-
}
|
|
470
|
-
try {
|
|
471
|
-
const url = new URL(href.startsWith('//') ? `https:${href}` : href);
|
|
472
|
-
return url.pathname + url.search + url.hash;
|
|
473
|
-
}
|
|
474
|
-
catch {
|
|
475
|
-
return href;
|
|
476
|
-
}
|
|
477
|
-
}, /* @ts-ignore */
|
|
478
|
-
...(ngDevMode ? [{ debugName: "internalPath" }] : /* istanbul ignore next */ []));
|
|
479
|
-
this.linkClass = computed(() => {
|
|
480
|
-
const base = 'et-contentful-rich-text-default-element et-contentful-rich-text-default-a';
|
|
481
|
-
const extra = this.textClass();
|
|
482
|
-
return extra ? `${base} ${extra}` : base;
|
|
483
|
-
}, /* @ts-ignore */
|
|
484
|
-
...(ngDevMode ? [{ debugName: "linkClass" }] : /* istanbul ignore next */ []));
|
|
485
|
-
}
|
|
486
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ContentfulLinkComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
487
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", 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 } }, host: { styleAttribute: "display: contents" }, ngImport: i0, template: `
|
|
488
|
-
@if (isExternal()) {
|
|
489
|
-
<a
|
|
490
|
-
[class]="linkClass()"
|
|
491
|
-
[href]="href()"
|
|
492
|
-
[target]="openInNewTab() ? '_blank' : null"
|
|
493
|
-
[rel]="openInNewTab() ? 'noopener noreferrer' : null"
|
|
494
|
-
>{{ text() }}</a
|
|
495
|
-
>
|
|
496
|
-
} @else {
|
|
497
|
-
<a [class]="linkClass()" [routerLink]="internalPath()">{{ text() }}</a>
|
|
498
|
-
}
|
|
499
|
-
`, isInline: true, dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }], encapsulation: i0.ViewEncapsulation.None }); }
|
|
500
|
-
}
|
|
501
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ContentfulLinkComponent, decorators: [{
|
|
502
|
-
type: Component,
|
|
503
|
-
args: [{
|
|
504
|
-
selector: 'et-contentful-link',
|
|
505
|
-
template: `
|
|
506
|
-
@if (isExternal()) {
|
|
507
|
-
<a
|
|
508
|
-
[class]="linkClass()"
|
|
509
|
-
[href]="href()"
|
|
510
|
-
[target]="openInNewTab() ? '_blank' : null"
|
|
511
|
-
[rel]="openInNewTab() ? 'noopener noreferrer' : null"
|
|
512
|
-
>{{ text() }}</a
|
|
513
|
-
>
|
|
514
|
-
} @else {
|
|
515
|
-
<a [class]="linkClass()" [routerLink]="internalPath()">{{ text() }}</a>
|
|
516
|
-
}
|
|
517
|
-
`,
|
|
518
|
-
encapsulation: ViewEncapsulation.None,
|
|
519
|
-
imports: [RouterLink],
|
|
520
|
-
host: {
|
|
521
|
-
style: 'display: contents',
|
|
522
|
-
},
|
|
523
|
-
}]
|
|
524
|
-
}], 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 }] }] } });
|
|
525
|
-
|
|
526
542
|
/**
|
|
527
543
|
* The `BLOCKS` node type strings of `@contentful/rich-text-types`, as a plain literal map.
|
|
528
544
|
* Keep the values in sync with the enum - they are part of the Contentful Delivery API.
|
|
545
|
+
*
|
|
546
|
+
* These maps exist because `BLOCKS` and `INLINES` are TS enums: reading one in a *value* position
|
|
547
|
+
* anywhere in this lib emits a runtime import and costs every consumer ~2.4 kB gz. Use the enums for
|
|
548
|
+
* types only - the `contentful-deps` size golden fails if that slips.
|
|
529
549
|
*/
|
|
530
550
|
const CF_BLOCKS = {
|
|
531
551
|
DOCUMENT: 'document',
|
|
@@ -679,6 +699,18 @@ const marksToTags = (marks) => {
|
|
|
679
699
|
*/
|
|
680
700
|
const marksToClass = (marks) => marks.map((mark) => `et-contentful-rich-text-mark-${mark.type}`).join(' ');
|
|
681
701
|
const LINK_COMPONENT_TYPE = '$$$_et-link';
|
|
702
|
+
const normalizeHref = (href) => {
|
|
703
|
+
if (!href.trim()) {
|
|
704
|
+
return null;
|
|
705
|
+
}
|
|
706
|
+
try {
|
|
707
|
+
const url = new URL(href, 'https://contentful.invalid');
|
|
708
|
+
return ['http:', 'https:', 'mailto:', 'tel:', 'ftp:'].includes(url.protocol) ? href : null;
|
|
709
|
+
}
|
|
710
|
+
catch {
|
|
711
|
+
return null;
|
|
712
|
+
}
|
|
713
|
+
};
|
|
682
714
|
const isExecutedComponentCommandCacheItem = (cache) => {
|
|
683
715
|
return cache.command.kind === 'component';
|
|
684
716
|
};
|
|
@@ -752,26 +784,14 @@ class ContentfulRichTextRendererComponent {
|
|
|
752
784
|
*/
|
|
753
785
|
this.richTextPath = input.required(/* @ts-ignore */
|
|
754
786
|
...(ngDevMode ? [{ debugName: "richTextPath" }] : /* istanbul ignore next */ []));
|
|
755
|
-
/**
|
|
756
|
-
* A cache for all executed commands that are not deleted.
|
|
757
|
-
* This is used to keep track of all rendered elements and components.
|
|
758
|
-
* The key is the render command ID.
|
|
759
|
-
*/
|
|
760
787
|
this.executedCommandsCache = new Map();
|
|
761
|
-
/**
|
|
762
|
-
* A map of all includes in the contentful response.
|
|
763
|
-
* This is useful for looking up assets and entries that are referenced in the rich text without having to use loops.
|
|
764
|
-
*/
|
|
765
788
|
this.contentIncludesMap = computed(() => {
|
|
766
789
|
const content = this.content();
|
|
767
|
-
const assets = content?.includes
|
|
768
|
-
const entries = content?.includes
|
|
790
|
+
const assets = content?.includes?.Asset;
|
|
791
|
+
const entries = content?.includes?.Entry;
|
|
769
792
|
return createContentfulIncludeMap({ assets: assets ?? [], entries: entries ?? [] });
|
|
770
793
|
}, /* @ts-ignore */
|
|
771
794
|
...(ngDevMode ? [{ debugName: "contentIncludesMap" }] : /* istanbul ignore next */ []));
|
|
772
|
-
/**
|
|
773
|
-
* The rich text data that should be rendered.
|
|
774
|
-
*/
|
|
775
795
|
this.richTextData = computed(() => {
|
|
776
796
|
const content = this.content();
|
|
777
797
|
const richTextPath = this.richTextPath();
|
|
@@ -779,6 +799,9 @@ class ContentfulRichTextRendererComponent {
|
|
|
779
799
|
return null;
|
|
780
800
|
}
|
|
781
801
|
const richText = getObjectProperty(content, richTextPath);
|
|
802
|
+
if (richText === null || richText === undefined) {
|
|
803
|
+
return null;
|
|
804
|
+
}
|
|
782
805
|
if (!isObject(richText)) {
|
|
783
806
|
throw richTextRendererError('rich_text_undefined', { content, richTextPath });
|
|
784
807
|
}
|
|
@@ -796,25 +819,10 @@ class ContentfulRichTextRendererComponent {
|
|
|
796
819
|
return this.createRenderCommands(richTextData);
|
|
797
820
|
}, /* @ts-ignore */
|
|
798
821
|
...(ngDevMode ? [{ debugName: "renderCommands" }] : /* istanbul ignore next */ []));
|
|
799
|
-
this.renderCommandHistory = linkedSignal({ ...(ngDevMode ? { debugName: "renderCommandHistory" } : /* istanbul ignore next */ {}), source: this.renderCommands,
|
|
800
|
-
computation: (commands, previous) => [previous?.source ?? [], commands] });
|
|
801
|
-
this.previousRenderCommandMap = computed(() => {
|
|
802
|
-
const [prevCommands] = this.renderCommandHistory();
|
|
803
|
-
const map = new Map();
|
|
804
|
-
for (const command of prevCommands) {
|
|
805
|
-
map.set(command.id, command);
|
|
806
|
-
}
|
|
807
|
-
return map;
|
|
808
|
-
}, /* @ts-ignore */
|
|
809
|
-
...(ngDevMode ? [{ debugName: "previousRenderCommandMap" }] : /* istanbul ignore next */ []));
|
|
810
822
|
this.renderInstructions = computed(() => {
|
|
811
823
|
const commands = this.renderCommands();
|
|
812
|
-
const previousRenderCommandMap = this.
|
|
824
|
+
const previousRenderCommandMap = new Map([...this.executedCommandsCache.entries()].map(([id, item]) => [id, item.command]));
|
|
813
825
|
const instructions = [];
|
|
814
|
-
// Decide, in document order, which commands survive the change with their DOM intact.
|
|
815
|
-
// A plain element or text node survives when the same id renders the same output at the same
|
|
816
|
-
// spot inside a surviving parent. Ancestors are decided first because parents precede their
|
|
817
|
-
// children in command order. Components survive on id alone — their DOM can be reattached.
|
|
818
826
|
const preserved = new Set();
|
|
819
827
|
const needsReattach = new Set();
|
|
820
828
|
const parentIdOf = (command) => {
|
|
@@ -842,7 +850,6 @@ class ContentfulRichTextRendererComponent {
|
|
|
842
850
|
if (!previous || previous.kind !== command.kind) {
|
|
843
851
|
continue;
|
|
844
852
|
}
|
|
845
|
-
// The same key must render the same component class — a changed class means a fresh instance.
|
|
846
853
|
if (previous.kind === 'component' && command.kind === 'component' && previous.component !== command.component) {
|
|
847
854
|
continue;
|
|
848
855
|
}
|
|
@@ -871,15 +878,11 @@ class ContentfulRichTextRendererComponent {
|
|
|
871
878
|
preserved.add(command.id);
|
|
872
879
|
}
|
|
873
880
|
}
|
|
874
|
-
// Everything that did not survive is removed first, so the ordered pass below can rebuild
|
|
875
|
-
// into a clean tree. Close commands never render DOM and are skipped throughout.
|
|
876
881
|
for (const [id, command] of previousRenderCommandMap) {
|
|
877
882
|
if (command.kind !== 'htmlClose' && !preserved.has(id)) {
|
|
878
883
|
instructions.push({ type: 'delete', command });
|
|
879
884
|
}
|
|
880
885
|
}
|
|
881
|
-
// One pass in document order: parents are created before a child component is reattached.
|
|
882
|
-
// Surviving plain elements still get an update so the cache tracks their fresh command data.
|
|
883
886
|
for (const command of commands) {
|
|
884
887
|
if (command.kind === 'htmlClose') {
|
|
885
888
|
continue;
|
|
@@ -903,17 +906,11 @@ class ContentfulRichTextRendererComponent {
|
|
|
903
906
|
});
|
|
904
907
|
}
|
|
905
908
|
createRenderCommands(richTextData) {
|
|
906
|
-
/** List of all render commands */
|
|
907
909
|
const rootCommands = [];
|
|
908
|
-
/** Counter for generating unique html element IDs. */
|
|
909
910
|
let elementOpenId = 0;
|
|
910
|
-
/** Counter for generating unique html element IDs. */
|
|
911
911
|
let elementCloseId = 0;
|
|
912
|
-
/** Counter for generating unique component IDs. */
|
|
913
912
|
const componentIdMap = new Map();
|
|
914
|
-
/** The nesting level of the current node. */
|
|
915
913
|
let nestingLevel = 0;
|
|
916
|
-
/** The position (index) of the current node inside the parent node. */
|
|
917
914
|
let domPosition = 0;
|
|
918
915
|
let textId = 0;
|
|
919
916
|
let commandIndex = 0;
|
|
@@ -950,8 +947,6 @@ class ContentfulRichTextRendererComponent {
|
|
|
950
947
|
}
|
|
951
948
|
const contentType = asset.fields.file.contentType;
|
|
952
949
|
const assetComponents = this.config.components;
|
|
953
|
-
// Every property inside the asset will be null if no file was provided for a translation.
|
|
954
|
-
// In this case, we can assume that the asset is missing due to user error.
|
|
955
950
|
const isMissing = !contentType && !asset.fields.file.url;
|
|
956
951
|
if (isMissing) {
|
|
957
952
|
if (isDevMode()) {
|
|
@@ -991,9 +986,18 @@ class ContentfulRichTextRendererComponent {
|
|
|
991
986
|
domPosition++;
|
|
992
987
|
break;
|
|
993
988
|
}
|
|
994
|
-
case CF_INLINES.HYPERLINK:
|
|
995
|
-
|
|
996
|
-
|
|
989
|
+
case CF_INLINES.HYPERLINK:
|
|
990
|
+
case CF_INLINES.ASSET_HYPERLINK:
|
|
991
|
+
case CF_INLINES.ENTRY_HYPERLINK: {
|
|
992
|
+
let href = null;
|
|
993
|
+
if (node.nodeType === CF_INLINES.HYPERLINK) {
|
|
994
|
+
href = normalizeHref(node.data['uri'] ?? '');
|
|
995
|
+
}
|
|
996
|
+
else if (node.nodeType === CF_INLINES.ASSET_HYPERLINK) {
|
|
997
|
+
const assetId = node.data['target']?.sys?.id;
|
|
998
|
+
const asset = assetId ? this.contentIncludesMap().getAsset(assetId) : null;
|
|
999
|
+
href = asset?.fields.file.url ? normalizeHref(asset.fields.file.url) : null;
|
|
1000
|
+
}
|
|
997
1001
|
let linkText = '';
|
|
998
1002
|
const linkMarks = [];
|
|
999
1003
|
for (const child of node.content) {
|
|
@@ -1002,9 +1006,8 @@ class ContentfulRichTextRendererComponent {
|
|
|
1002
1006
|
linkMarks.push(...child.marks);
|
|
1003
1007
|
}
|
|
1004
1008
|
}
|
|
1005
|
-
const linkComponent = this.config.components.link;
|
|
1006
|
-
if (!linkComponent) {
|
|
1007
|
-
// Without a link component the hyperlink still renders - as a plain anchor.
|
|
1009
|
+
const linkComponent = href ? this.config.components.link : null;
|
|
1010
|
+
if (href && !linkComponent) {
|
|
1008
1011
|
rootCommands.push({
|
|
1009
1012
|
kind: 'htmlOpen',
|
|
1010
1013
|
nestingLevel,
|
|
@@ -1012,7 +1015,7 @@ class ContentfulRichTextRendererComponent {
|
|
|
1012
1015
|
index: commandIndex++,
|
|
1013
1016
|
attributes: {
|
|
1014
1017
|
class: 'et-contentful-rich-text-default-element et-contentful-rich-text-default-a',
|
|
1015
|
-
href
|
|
1018
|
+
href,
|
|
1016
1019
|
},
|
|
1017
1020
|
tagName: 'a',
|
|
1018
1021
|
id: 'e-o' + elementOpenId++,
|
|
@@ -1045,6 +1048,25 @@ class ContentfulRichTextRendererComponent {
|
|
|
1045
1048
|
domPosition++;
|
|
1046
1049
|
break;
|
|
1047
1050
|
}
|
|
1051
|
+
if (!href) {
|
|
1052
|
+
rootCommands.push({
|
|
1053
|
+
kind: 'text',
|
|
1054
|
+
nestingLevel,
|
|
1055
|
+
domPosition,
|
|
1056
|
+
index: commandIndex++,
|
|
1057
|
+
attributes: {
|
|
1058
|
+
class: 'et-contentful-rich-text-default-element et-contentful-rich-text-default-span',
|
|
1059
|
+
},
|
|
1060
|
+
markTags: marksToTags(linkMarks),
|
|
1061
|
+
text: linkText,
|
|
1062
|
+
id: 't' + textId++,
|
|
1063
|
+
});
|
|
1064
|
+
domPosition++;
|
|
1065
|
+
break;
|
|
1066
|
+
}
|
|
1067
|
+
if (!linkComponent) {
|
|
1068
|
+
break;
|
|
1069
|
+
}
|
|
1048
1070
|
const textClass = linkMarks.length ? marksToClass(linkMarks) : '';
|
|
1049
1071
|
let linkComponentId = componentIdMap.get(LINK_COMPONENT_TYPE) ?? -1;
|
|
1050
1072
|
const linkId = LINK_COMPONENT_TYPE + ++linkComponentId;
|
|
@@ -1055,7 +1077,7 @@ class ContentfulRichTextRendererComponent {
|
|
|
1055
1077
|
domPosition,
|
|
1056
1078
|
index: commandIndex++,
|
|
1057
1079
|
component: linkComponent,
|
|
1058
|
-
inputs: { href
|
|
1080
|
+
inputs: { href, text: linkText, textClass },
|
|
1059
1081
|
id: linkId,
|
|
1060
1082
|
});
|
|
1061
1083
|
domPosition++;
|
|
@@ -1116,17 +1138,18 @@ class ContentfulRichTextRendererComponent {
|
|
|
1116
1138
|
id: 'e-o' + elementOpenId++,
|
|
1117
1139
|
});
|
|
1118
1140
|
const domPositionAtThisLevel = domPosition;
|
|
1119
|
-
|
|
1141
|
+
nestingLevel++;
|
|
1142
|
+
domPosition = 0;
|
|
1120
1143
|
for (const child of node.content) {
|
|
1121
|
-
domPosition = 0;
|
|
1122
|
-
nestingLevel++;
|
|
1123
1144
|
traverse(child);
|
|
1124
|
-
nestingLevel--;
|
|
1125
|
-
domPosition = domPositionAtThisLevel;
|
|
1126
1145
|
}
|
|
1146
|
+
nestingLevel--;
|
|
1147
|
+
domPosition = domPositionAtThisLevel;
|
|
1127
1148
|
const lastCommand = rootCommands[rootCommands.length - 1];
|
|
1128
|
-
if (lastCommand?.kind === 'htmlOpen' &&
|
|
1129
|
-
|
|
1149
|
+
if (lastCommand?.kind === 'htmlOpen' &&
|
|
1150
|
+
lastCommand.tagName !== 'td' &&
|
|
1151
|
+
lastCommand.tagName !== 'th' &&
|
|
1152
|
+
lastCommand.tagName !== 'hr') {
|
|
1130
1153
|
rootCommands.pop();
|
|
1131
1154
|
elementOpenId--;
|
|
1132
1155
|
commandIndex--;
|
|
@@ -1175,7 +1198,6 @@ class ContentfulRichTextRendererComponent {
|
|
|
1175
1198
|
if (command.kind === 'component') {
|
|
1176
1199
|
const inputs = signal(command.inputs, /* @ts-ignore */
|
|
1177
1200
|
...(ngDevMode ? [{ debugName: "inputs" }] : /* istanbul ignore next */ []));
|
|
1178
|
-
// Components may declare any subset of the offered inputs — only bind the declared ones.
|
|
1179
1201
|
const declaredInputs = reflectComponentType(command.component)?.inputs ?? [];
|
|
1180
1202
|
const bindings = declaredInputs
|
|
1181
1203
|
.filter((declared) => declared.propName in command.inputs)
|
|
@@ -1192,15 +1214,10 @@ class ContentfulRichTextRendererComponent {
|
|
|
1192
1214
|
}
|
|
1193
1215
|
else if (command.kind === 'text') {
|
|
1194
1216
|
const span = this.renderer.createElement('span');
|
|
1195
|
-
const textSplitInLineBreaks = command.text.split('\n')
|
|
1217
|
+
const textSplitInLineBreaks = command.text.split('\n');
|
|
1196
1218
|
for (const [key, value] of Object.entries(command.attributes)) {
|
|
1197
1219
|
this.renderer.setAttribute(span, key, value);
|
|
1198
1220
|
}
|
|
1199
|
-
if (command.text.startsWith('\n')) {
|
|
1200
|
-
const brNode = this.renderer.createElement('br');
|
|
1201
|
-
this.renderer.appendChild(parentElement, brNode);
|
|
1202
|
-
}
|
|
1203
|
-
// Marks are rendered as nested semantic elements inside the span, outermost mark first.
|
|
1204
1221
|
let textContainer = span;
|
|
1205
1222
|
for (const tag of command.markTags) {
|
|
1206
1223
|
const markElement = this.renderer.createElement(tag);
|
|
@@ -1212,8 +1229,10 @@ class ContentfulRichTextRendererComponent {
|
|
|
1212
1229
|
const brNode = this.renderer.createElement('br');
|
|
1213
1230
|
this.renderer.appendChild(textContainer, brNode);
|
|
1214
1231
|
}
|
|
1215
|
-
|
|
1216
|
-
|
|
1232
|
+
if (textPart) {
|
|
1233
|
+
const textNode = this.renderer.createText(textPart);
|
|
1234
|
+
this.renderer.appendChild(textContainer, textNode);
|
|
1235
|
+
}
|
|
1217
1236
|
}
|
|
1218
1237
|
this.renderInsertOrAppend(span, parentElement, nextElement);
|
|
1219
1238
|
this.executedCommandsCache.set(command.id, {
|
|
@@ -1244,8 +1263,6 @@ class ContentfulRichTextRendererComponent {
|
|
|
1244
1263
|
}
|
|
1245
1264
|
cached.inputs.set(command.inputs);
|
|
1246
1265
|
}
|
|
1247
|
-
// `cached` and the fresh `command` share the same id and therefore the same union variant,
|
|
1248
|
-
// but TS can't correlate the two after the spread — assert the merged object as the union.
|
|
1249
1266
|
this.executedCommandsCache.set(command.id, {
|
|
1250
1267
|
...cached,
|
|
1251
1268
|
command,
|
|
@@ -1306,8 +1323,6 @@ class ContentfulRichTextRendererComponent {
|
|
|
1306
1323
|
parentElement = hostElement;
|
|
1307
1324
|
}
|
|
1308
1325
|
else {
|
|
1309
|
-
// Reverse search all render commands beginning from the current one.
|
|
1310
|
-
// The parent is the closest preceding html open command one nesting level up.
|
|
1311
1326
|
const allCommands = this.renderCommands();
|
|
1312
1327
|
let parentCommand = null;
|
|
1313
1328
|
for (let i = command.index - 1; i >= 0; i--) {
|
|
@@ -1321,24 +1336,24 @@ class ContentfulRichTextRendererComponent {
|
|
|
1321
1336
|
}
|
|
1322
1337
|
}
|
|
1323
1338
|
if (!parentCommand) {
|
|
1324
|
-
throw
|
|
1339
|
+
throw richTextRendererError('text_parent_not_found', { command });
|
|
1325
1340
|
}
|
|
1326
1341
|
parentElement = this.executedCommandsCache.get(parentCommand.id)?.element;
|
|
1327
1342
|
}
|
|
1328
1343
|
if (!parentElement) {
|
|
1329
|
-
throw
|
|
1344
|
+
throw richTextRendererError('text_parent_wrong_type', { command });
|
|
1330
1345
|
}
|
|
1331
1346
|
return parentElement;
|
|
1332
1347
|
}
|
|
1333
1348
|
findFollowingElement(command) {
|
|
1334
|
-
|
|
1335
|
-
// domPosition, to insert before. Without one the element is appended to the parent.
|
|
1349
|
+
const parentElement = this.findParent(command);
|
|
1336
1350
|
let nextElement;
|
|
1337
1351
|
let nextDomPosition = Infinity;
|
|
1338
1352
|
for (const cached of this.executedCommandsCache.values()) {
|
|
1339
1353
|
if (cached.command.domPosition > command.domPosition &&
|
|
1340
1354
|
cached.command.domPosition < nextDomPosition &&
|
|
1341
1355
|
cached.command.nestingLevel === command.nestingLevel &&
|
|
1356
|
+
cached.element.parentElement === parentElement &&
|
|
1342
1357
|
cached.command.id !== command.id) {
|
|
1343
1358
|
nextElement = cached.element;
|
|
1344
1359
|
nextDomPosition = cached.command.domPosition;
|
|
@@ -1377,7 +1392,7 @@ const ContentfulImports = [
|
|
|
1377
1392
|
ContentfulRichTextRendererComponent,
|
|
1378
1393
|
];
|
|
1379
1394
|
|
|
1380
|
-
const provideContentfulConfig = (contentfulConfig
|
|
1395
|
+
const provideContentfulConfig = (contentfulConfig) => {
|
|
1381
1396
|
return { provide: CONTENTFUL_CONFIG, useValue: createContentfulConfig(contentfulConfig) };
|
|
1382
1397
|
};
|
|
1383
1398
|
const isContentfulEntryType = (entry, type) => entry.sys.contentType.sys.id === type;
|