@ethlete/contentful 4.0.0-next.6 → 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 +37 -0
- package/README.md +23 -4
- package/fesm2022/ethlete-contentful.mjs +734 -723
- package/fesm2022/ethlete-contentful.mjs.map +1 -1
- package/generators/generators.json +9 -0
- package/generators/migrate-to-contentful-v5/migration-scope.js +41 -0
- package/generators/migrate-to-contentful-v5/migration.js +256 -0
- package/generators/migrate-to-contentful-v5/schema.json +26 -0
- package/package.json +12 -10
- package/types/ethlete-contentful.d.ts +78 -231
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
import { NgClass, DOCUMENT } from '@angular/common';
|
|
2
2
|
import * as i0 from '@angular/core';
|
|
3
|
-
import { input, computed, ViewEncapsulation, Component,
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { RouterLink } from '@angular/router';
|
|
7
|
-
import { toSignal, toObservable, takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
8
|
-
import { INLINES, BLOCKS } from '@contentful/rich-text-types';
|
|
3
|
+
import { input, computed, ViewEncapsulation, Component, inject, InjectionToken, numberAttribute, booleanAttribute, isDevMode, ViewContainerRef, ElementRef, effect, untracked, signal, reflectComponentType, inputBinding } from '@angular/core';
|
|
4
|
+
import { normalizePictureSizes, PictureComponent } from '@ethlete/components';
|
|
5
|
+
import { Router, RouterLink } from '@angular/router';
|
|
9
6
|
import { RuntimeError, isObject, injectRenderer, getObjectProperty } from '@ethlete/core';
|
|
10
|
-
import { startWith, pairwise, tap } from 'rxjs';
|
|
11
7
|
|
|
12
|
-
const GQL_FRAGMENT_CONTENTFUL_ASSET =
|
|
13
|
-
fragment
|
|
8
|
+
const GQL_FRAGMENT_CONTENTFUL_ASSET = `
|
|
9
|
+
fragment ContentfulAssetData on Asset {
|
|
14
10
|
sys {
|
|
15
11
|
id
|
|
16
12
|
}
|
|
@@ -25,7 +21,7 @@ const GQL_FRAGMENT_CONTENTFUL_ASSET = gql `
|
|
|
25
21
|
`;
|
|
26
22
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
27
23
|
const isContentfulGqlAsset = (asset) => {
|
|
28
|
-
return asset
|
|
24
|
+
return Boolean(asset && typeof asset === 'object' && asset.sys?.id && !('fields' in asset));
|
|
29
25
|
};
|
|
30
26
|
|
|
31
27
|
class ContentfulAudioComponent {
|
|
@@ -43,30 +39,33 @@ class ContentfulAudioComponent {
|
|
|
43
39
|
if (!asset) {
|
|
44
40
|
return null;
|
|
45
41
|
}
|
|
46
|
-
if (isContentfulGqlAsset(asset)) {
|
|
42
|
+
if (isContentfulGqlAsset(asset) && asset.url) {
|
|
47
43
|
return {
|
|
48
44
|
url: asset.url,
|
|
49
45
|
title: asset.title,
|
|
50
46
|
};
|
|
51
47
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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;
|
|
56
55
|
}, /* @ts-ignore */
|
|
57
56
|
...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
|
|
58
57
|
}
|
|
59
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.
|
|
60
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.
|
|
58
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ContentfulAudioComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
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: `
|
|
61
60
|
@if (data(); as data) {
|
|
62
61
|
<figure [ngClass]="figureClass()">
|
|
63
62
|
<figcaption [ngClass]="figcaptionClass()">{{ data.title }}</figcaption>
|
|
64
|
-
<audio [ngClass]="audioClass()"
|
|
63
|
+
<audio [ngClass]="audioClass()" [src]="data.url" controls></audio>
|
|
65
64
|
</figure>
|
|
66
65
|
}
|
|
67
66
|
`, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], encapsulation: i0.ViewEncapsulation.None }); }
|
|
68
67
|
}
|
|
69
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.
|
|
68
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ContentfulAudioComponent, decorators: [{
|
|
70
69
|
type: Component,
|
|
71
70
|
args: [{
|
|
72
71
|
selector: 'et-contentful-audio',
|
|
@@ -74,12 +73,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
74
73
|
@if (data(); as data) {
|
|
75
74
|
<figure [ngClass]="figureClass()">
|
|
76
75
|
<figcaption [ngClass]="figcaptionClass()">{{ data.title }}</figcaption>
|
|
77
|
-
<audio [ngClass]="audioClass()"
|
|
76
|
+
<audio [ngClass]="audioClass()" [src]="data.url" controls></audio>
|
|
78
77
|
</figure>
|
|
79
78
|
}
|
|
80
79
|
`,
|
|
81
80
|
encapsulation: ViewEncapsulation.None,
|
|
82
81
|
imports: [NgClass],
|
|
82
|
+
host: {
|
|
83
|
+
class: 'et-contentful-audio',
|
|
84
|
+
},
|
|
83
85
|
}]
|
|
84
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 }] }] } });
|
|
85
87
|
|
|
@@ -94,50 +96,254 @@ class ContentfulFileComponent {
|
|
|
94
96
|
if (!asset) {
|
|
95
97
|
return null;
|
|
96
98
|
}
|
|
97
|
-
if (isContentfulGqlAsset(asset)) {
|
|
99
|
+
if (isContentfulGqlAsset(asset) && asset.url) {
|
|
98
100
|
return {
|
|
99
101
|
url: asset.url,
|
|
100
|
-
contentType: asset.contentType,
|
|
101
102
|
size: asset.size,
|
|
102
103
|
title: asset.title,
|
|
103
104
|
};
|
|
104
105
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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;
|
|
111
114
|
}, /* @ts-ignore */
|
|
112
115
|
...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
|
|
113
116
|
}
|
|
114
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.
|
|
115
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.
|
|
117
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ContentfulFileComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
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: `
|
|
116
119
|
@if (data(); as data) {
|
|
117
|
-
<a [href]="data.url" [ngClass]="fileClass()"
|
|
118
|
-
{{ 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
|
+
}
|
|
119
125
|
</a>
|
|
120
126
|
}
|
|
121
127
|
`, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], encapsulation: i0.ViewEncapsulation.None }); }
|
|
122
128
|
}
|
|
123
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.
|
|
129
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ContentfulFileComponent, decorators: [{
|
|
124
130
|
type: Component,
|
|
125
131
|
args: [{
|
|
126
132
|
selector: 'et-contentful-file',
|
|
127
133
|
template: `
|
|
128
134
|
@if (data(); as data) {
|
|
129
|
-
<a [href]="data.url" [ngClass]="fileClass()"
|
|
130
|
-
{{ 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
|
+
}
|
|
131
140
|
</a>
|
|
132
141
|
}
|
|
133
142
|
`,
|
|
134
143
|
encapsulation: ViewEncapsulation.None,
|
|
135
144
|
imports: [NgClass],
|
|
145
|
+
host: {
|
|
146
|
+
class: 'et-contentful-file',
|
|
147
|
+
},
|
|
136
148
|
}]
|
|
137
149
|
}], propDecorators: { asset: [{ type: i0.Input, args: [{ isSignal: true, alias: "asset", required: true }] }], fileClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "fileClass", required: false }] }] } });
|
|
138
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
|
+
|
|
139
305
|
const CONTENTFUL_CONFIG = new InjectionToken('ContentfulConfig');
|
|
140
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
|
+
|
|
141
347
|
const generateDefaultContentfulImageSource = (data) => {
|
|
142
348
|
if (isContentfulGqlAsset(data)) {
|
|
143
349
|
if (!data.contentType || !data.url) {
|
|
@@ -171,41 +377,37 @@ const generateDefaultContentfulImageSource = (data) => {
|
|
|
171
377
|
* - `"400wx300h"` - 400px width and 300px height
|
|
172
378
|
**/
|
|
173
379
|
const parseContentfulImageSize = (size) => {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
181
|
-
else if (size.includes('h')) {
|
|
182
|
-
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
|
+
};
|
|
183
386
|
}
|
|
184
|
-
|
|
185
|
-
|
|
387
|
+
const height = /^(\d+)h$/.exec(size);
|
|
388
|
+
if (height) {
|
|
389
|
+
return { width: null, height: Number(height[1]) };
|
|
186
390
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
height: height ? parseInt(height, 10) : null,
|
|
190
|
-
};
|
|
391
|
+
const width = /^(\d+)w?$/.exec(size);
|
|
392
|
+
return width ? { width: Number(width[1]), height: null } : { width: null, height: null };
|
|
191
393
|
};
|
|
394
|
+
const SOURCE_TYPES = ['image/avif', 'image/webp'];
|
|
192
395
|
const generateContentfulImageSources = (data, srcsetSizes, backgroundColor, quality, focusArea, resizeBehavior) => {
|
|
193
|
-
const
|
|
194
|
-
|
|
396
|
+
const isGqlAsset = isContentfulGqlAsset(data);
|
|
397
|
+
const baseUrl = isGqlAsset ? data.url : data.fields.file.url;
|
|
398
|
+
if (!baseUrl) {
|
|
195
399
|
return [];
|
|
196
400
|
}
|
|
401
|
+
const imageDimensions = isGqlAsset ? data : data.fields.file.details.image;
|
|
197
402
|
const sources = [];
|
|
198
|
-
const SOURCE_TYPES = ['image/avif', 'image/webp', 'image/png', 'image/jpg'];
|
|
199
403
|
for (const type of SOURCE_TYPES) {
|
|
200
|
-
const baseUrl = isContentfulGqlAsset(assetData) ? assetData.url : assetData.fields.file.url;
|
|
201
404
|
const sourceSets = [];
|
|
202
405
|
const queryParams = [];
|
|
203
|
-
// Set the format (e.g. 'fm=webp')
|
|
204
406
|
queryParams.push(`fm=${type.split('/')[1]}`);
|
|
205
407
|
if (backgroundColor) {
|
|
206
408
|
queryParams.push(`bg=rgb:${backgroundColor}`);
|
|
207
409
|
}
|
|
208
|
-
if (quality !== null) {
|
|
410
|
+
if (quality !== null && Number.isFinite(quality)) {
|
|
209
411
|
queryParams.push(`q=${quality}`);
|
|
210
412
|
}
|
|
211
413
|
if (focusArea) {
|
|
@@ -215,7 +417,7 @@ const generateContentfulImageSources = (data, srcsetSizes, backgroundColor, qual
|
|
|
215
417
|
queryParams.push(`fit=${resizeBehavior}`);
|
|
216
418
|
}
|
|
217
419
|
if (srcsetSizes?.length) {
|
|
218
|
-
const urlWithParams = `${baseUrl}
|
|
420
|
+
const urlWithParams = `${baseUrl}${baseUrl.includes('?') ? '&' : '?'}${queryParams.join('&')}`;
|
|
219
421
|
for (const size of srcsetSizes) {
|
|
220
422
|
const { width, height } = parseContentfulImageSize(size);
|
|
221
423
|
if (width && height) {
|
|
@@ -224,43 +426,42 @@ const generateContentfulImageSources = (data, srcsetSizes, backgroundColor, qual
|
|
|
224
426
|
else if (width) {
|
|
225
427
|
sourceSets.push(`${urlWithParams}&w=${width} ${width}w`);
|
|
226
428
|
}
|
|
227
|
-
else if (height) {
|
|
228
|
-
|
|
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`);
|
|
229
432
|
}
|
|
230
433
|
}
|
|
231
434
|
}
|
|
232
435
|
sources.push({
|
|
233
436
|
type,
|
|
234
|
-
srcset: sourceSets.length
|
|
437
|
+
srcset: sourceSets.length
|
|
438
|
+
? sourceSets.join(', ')
|
|
439
|
+
: `${baseUrl}${baseUrl.includes('?') ? '&' : '?'}${queryParams.join('&')}`,
|
|
235
440
|
});
|
|
236
441
|
}
|
|
237
442
|
return sources;
|
|
238
443
|
};
|
|
239
444
|
|
|
445
|
+
const optionalNumberAttribute = (value) => {
|
|
446
|
+
const transformed = numberAttribute(value);
|
|
447
|
+
return Number.isFinite(transformed) ? transformed : null;
|
|
448
|
+
};
|
|
240
449
|
class ContentfulImageComponent {
|
|
241
450
|
constructor() {
|
|
242
|
-
this.
|
|
451
|
+
this.contentfulConfig = injectContentfulConfig();
|
|
243
452
|
this.asset = input.required(/* @ts-ignore */
|
|
244
453
|
...(ngDevMode ? [{ debugName: "asset" }] : /* istanbul ignore next */ []));
|
|
245
|
-
this.backgroundColor = input(
|
|
454
|
+
this.backgroundColor = input(this.contentfulConfig.imageOptions.backgroundColor, /* @ts-ignore */
|
|
246
455
|
...(ngDevMode ? [{ debugName: "backgroundColor" }] : /* istanbul ignore next */ []));
|
|
247
|
-
this.srcsetSizes = input(this.
|
|
456
|
+
this.srcsetSizes = input(this.contentfulConfig.imageOptions.srcsetSizes, /* @ts-ignore */
|
|
248
457
|
...(ngDevMode ? [{ debugName: "srcsetSizes" }] : /* istanbul ignore next */ []));
|
|
249
|
-
this.quality = input(null, { ...(ngDevMode ? { debugName: "quality" } : /* istanbul ignore next */ {}), transform:
|
|
458
|
+
this.quality = input(null, { ...(ngDevMode ? { debugName: "quality" } : /* istanbul ignore next */ {}), transform: optionalNumberAttribute });
|
|
250
459
|
this.focusArea = input(null, /* @ts-ignore */
|
|
251
460
|
...(ngDevMode ? [{ debugName: "focusArea" }] : /* istanbul ignore next */ []));
|
|
252
461
|
this.resizeBehavior = input(null, /* @ts-ignore */
|
|
253
462
|
...(ngDevMode ? [{ debugName: "resizeBehavior" }] : /* istanbul ignore next */ []));
|
|
254
|
-
this.
|
|
255
|
-
this.
|
|
256
|
-
...(ngDevMode ? [{ debugName: "imgClass" }] : /* istanbul ignore next */ []));
|
|
257
|
-
this.figureClass = input(null, /* @ts-ignore */
|
|
258
|
-
...(ngDevMode ? [{ debugName: "figureClass" }] : /* istanbul ignore next */ []));
|
|
259
|
-
this.figcaptionClass = input(null, /* @ts-ignore */
|
|
260
|
-
...(ngDevMode ? [{ debugName: "figcaptionClass" }] : /* istanbul ignore next */ []));
|
|
261
|
-
this.pictureClass = input(null, /* @ts-ignore */
|
|
262
|
-
...(ngDevMode ? [{ debugName: "pictureClass" }] : /* istanbul ignore next */ []));
|
|
263
|
-
this.sizes = input(normalizePictureSizes(this._contentfulConfig.imageOptions.sizes), { ...(ngDevMode ? { debugName: "sizes" } : /* istanbul ignore next */ {}), transform: (v) => normalizePictureSizes(v) });
|
|
463
|
+
this.priority = input(false, { ...(ngDevMode ? { debugName: "priority" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
464
|
+
this.sizes = input(normalizePictureSizes(this.contentfulConfig.imageOptions.sizes), { ...(ngDevMode ? { debugName: "sizes" } : /* istanbul ignore next */ {}), transform: (v) => normalizePictureSizes(v) });
|
|
264
465
|
this.normalizedAsset = computed(() => {
|
|
265
466
|
const asset = this.asset();
|
|
266
467
|
if (isContentfulGqlAsset(asset)) {
|
|
@@ -281,7 +482,7 @@ class ContentfulImageComponent {
|
|
|
281
482
|
}
|
|
282
483
|
}, /* @ts-ignore */
|
|
283
484
|
...(ngDevMode ? [{ debugName: "normalizedAsset" }] : /* istanbul ignore next */ []));
|
|
284
|
-
this.
|
|
485
|
+
this.sourcesValue = computed(() => {
|
|
285
486
|
const asset = this.asset();
|
|
286
487
|
const backgroundColor = this.backgroundColor();
|
|
287
488
|
const srcsetSizes = this.srcsetSizes();
|
|
@@ -293,49 +494,41 @@ class ContentfulImageComponent {
|
|
|
293
494
|
}
|
|
294
495
|
return generateContentfulImageSources(asset, srcsetSizes, backgroundColor, quality, focusArea, resizeBehavior);
|
|
295
496
|
}, /* @ts-ignore */
|
|
296
|
-
...(ngDevMode ? [{ debugName: "
|
|
297
|
-
this.
|
|
497
|
+
...(ngDevMode ? [{ debugName: "sourcesValue" }] : /* istanbul ignore next */ []));
|
|
498
|
+
this.defaultSrcValue = computed(() => {
|
|
298
499
|
const asset = this.asset();
|
|
299
500
|
return asset ? generateDefaultContentfulImageSource(asset) : null;
|
|
300
501
|
}, /* @ts-ignore */
|
|
301
|
-
...(ngDevMode ? [{ debugName: "
|
|
502
|
+
...(ngDevMode ? [{ debugName: "defaultSrcValue" }] : /* istanbul ignore next */ []));
|
|
302
503
|
}
|
|
303
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.
|
|
304
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.
|
|
504
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ContentfulImageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
505
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.7", type: ContentfulImageComponent, isStandalone: true, selector: "et-contentful-image", inputs: { asset: { classPropertyName: "asset", publicName: "asset", isSignal: true, isRequired: true, transformFunction: null }, backgroundColor: { classPropertyName: "backgroundColor", publicName: "backgroundColor", isSignal: true, isRequired: false, transformFunction: null }, srcsetSizes: { classPropertyName: "srcsetSizes", publicName: "srcsetSizes", isSignal: true, isRequired: false, transformFunction: null }, quality: { classPropertyName: "quality", publicName: "quality", isSignal: true, isRequired: false, transformFunction: null }, focusArea: { classPropertyName: "focusArea", publicName: "focusArea", isSignal: true, isRequired: false, transformFunction: null }, resizeBehavior: { classPropertyName: "resizeBehavior", publicName: "resizeBehavior", isSignal: true, isRequired: false, transformFunction: null }, priority: { classPropertyName: "priority", publicName: "priority", isSignal: true, isRequired: false, transformFunction: null }, sizes: { classPropertyName: "sizes", publicName: "sizes", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "et-contentful-image" }, ngImport: i0, template: `
|
|
305
506
|
<et-picture
|
|
306
|
-
[
|
|
307
|
-
[
|
|
308
|
-
[
|
|
309
|
-
[pictureClass]="pictureClass()"
|
|
310
|
-
[figcaptionClass]="figcaptionClass()"
|
|
311
|
-
[defaultSrc]="_defaultSrc()"
|
|
312
|
-
[alt]="normalizedAsset().alt ?? null"
|
|
507
|
+
[priority]="priority()"
|
|
508
|
+
[defaultSrc]="defaultSrcValue()"
|
|
509
|
+
[alt]="normalizedAsset().alt ?? ''"
|
|
313
510
|
[figcaption]="normalizedAsset().figcaption ?? null"
|
|
314
511
|
[width]="normalizedAsset().width ?? null"
|
|
315
512
|
[height]="normalizedAsset().height ?? null"
|
|
316
513
|
[sizes]="sizes()"
|
|
317
|
-
[sources]="
|
|
514
|
+
[sources]="sourcesValue()"
|
|
318
515
|
/>
|
|
319
|
-
`, isInline: true, dependencies: [{ kind: "component", type: PictureComponent, selector: "et-picture", inputs: ["sources", "
|
|
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 }); }
|
|
320
517
|
}
|
|
321
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.
|
|
518
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ContentfulImageComponent, decorators: [{
|
|
322
519
|
type: Component,
|
|
323
520
|
args: [{
|
|
324
521
|
selector: 'et-contentful-image',
|
|
325
522
|
template: `
|
|
326
523
|
<et-picture
|
|
327
|
-
[
|
|
328
|
-
[
|
|
329
|
-
[
|
|
330
|
-
[pictureClass]="pictureClass()"
|
|
331
|
-
[figcaptionClass]="figcaptionClass()"
|
|
332
|
-
[defaultSrc]="_defaultSrc()"
|
|
333
|
-
[alt]="normalizedAsset().alt ?? null"
|
|
524
|
+
[priority]="priority()"
|
|
525
|
+
[defaultSrc]="defaultSrcValue()"
|
|
526
|
+
[alt]="normalizedAsset().alt ?? ''"
|
|
334
527
|
[figcaption]="normalizedAsset().figcaption ?? null"
|
|
335
528
|
[width]="normalizedAsset().width ?? null"
|
|
336
529
|
[height]="normalizedAsset().height ?? null"
|
|
337
530
|
[sizes]="sizes()"
|
|
338
|
-
[sources]="
|
|
531
|
+
[sources]="sourcesValue()"
|
|
339
532
|
/>
|
|
340
533
|
`,
|
|
341
534
|
encapsulation: ViewEncapsulation.None,
|
|
@@ -344,185 +537,50 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
344
537
|
class: 'et-contentful-image',
|
|
345
538
|
},
|
|
346
539
|
}]
|
|
347
|
-
}], 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 }] }],
|
|
348
|
-
|
|
349
|
-
class ContentfulVideoComponent {
|
|
350
|
-
constructor() {
|
|
351
|
-
this.asset = input.required(/* @ts-ignore */
|
|
352
|
-
...(ngDevMode ? [{ debugName: "asset" }] : /* istanbul ignore next */ []));
|
|
353
|
-
this.videoClass = input(null, /* @ts-ignore */
|
|
354
|
-
...(ngDevMode ? [{ debugName: "videoClass" }] : /* istanbul ignore next */ []));
|
|
355
|
-
this.data = computed(() => {
|
|
356
|
-
const asset = this.asset();
|
|
357
|
-
if (!asset) {
|
|
358
|
-
return null;
|
|
359
|
-
}
|
|
360
|
-
if (isContentfulGqlAsset(asset)) {
|
|
361
|
-
return {
|
|
362
|
-
url: asset.url,
|
|
363
|
-
contentType: asset.contentType,
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
return {
|
|
367
|
-
url: asset.fields.file.url,
|
|
368
|
-
contentType: asset.fields.file.contentType,
|
|
369
|
-
};
|
|
370
|
-
}, /* @ts-ignore */
|
|
371
|
-
...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
|
|
372
|
-
}
|
|
373
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ContentfulVideoComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
374
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", 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: `
|
|
375
|
-
@if (data(); as data) {
|
|
376
|
-
<video [ngClass]="videoClass()" controls>
|
|
377
|
-
<source src="{{ data.url }}" type="{{ data.contentType }}" />
|
|
378
|
-
</video>
|
|
379
|
-
}
|
|
380
|
-
`, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], encapsulation: i0.ViewEncapsulation.None }); }
|
|
381
|
-
}
|
|
382
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ContentfulVideoComponent, decorators: [{
|
|
383
|
-
type: Component,
|
|
384
|
-
args: [{
|
|
385
|
-
selector: 'et-contentful-video',
|
|
386
|
-
template: `
|
|
387
|
-
@if (data(); as data) {
|
|
388
|
-
<video [ngClass]="videoClass()" controls>
|
|
389
|
-
<source src="{{ data.url }}" type="{{ data.contentType }}" />
|
|
390
|
-
</video>
|
|
391
|
-
}
|
|
392
|
-
`,
|
|
393
|
-
encapsulation: ViewEncapsulation.None,
|
|
394
|
-
imports: [NgClass],
|
|
395
|
-
}]
|
|
396
|
-
}], propDecorators: { asset: [{ type: i0.Input, args: [{ isSignal: true, alias: "asset", required: true }] }], videoClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "videoClass", required: false }] }] } });
|
|
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 }] }] } });
|
|
397
541
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
542
|
+
/**
|
|
543
|
+
* The `BLOCKS` node type strings of `@contentful/rich-text-types`, as a plain literal map.
|
|
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.
|
|
549
|
+
*/
|
|
550
|
+
const CF_BLOCKS = {
|
|
551
|
+
DOCUMENT: 'document',
|
|
552
|
+
PARAGRAPH: 'paragraph',
|
|
553
|
+
HEADING_1: 'heading-1',
|
|
554
|
+
HEADING_2: 'heading-2',
|
|
555
|
+
HEADING_3: 'heading-3',
|
|
556
|
+
HEADING_4: 'heading-4',
|
|
557
|
+
HEADING_5: 'heading-5',
|
|
558
|
+
HEADING_6: 'heading-6',
|
|
559
|
+
OL_LIST: 'ordered-list',
|
|
560
|
+
UL_LIST: 'unordered-list',
|
|
561
|
+
LIST_ITEM: 'list-item',
|
|
562
|
+
HR: 'hr',
|
|
563
|
+
QUOTE: 'blockquote',
|
|
564
|
+
EMBEDDED_ENTRY: 'embedded-entry-block',
|
|
565
|
+
EMBEDDED_ASSET: 'embedded-asset-block',
|
|
566
|
+
EMBEDDED_RESOURCE: 'embedded-resource-block',
|
|
567
|
+
TABLE: 'table',
|
|
568
|
+
TABLE_ROW: 'table-row',
|
|
569
|
+
TABLE_CELL: 'table-cell',
|
|
570
|
+
TABLE_HEADER_CELL: 'table-header-cell',
|
|
571
|
+
};
|
|
572
|
+
/**
|
|
573
|
+
* The `INLINES` node type strings of `@contentful/rich-text-types`, as a plain literal map.
|
|
574
|
+
* Keep the values in sync with the enum - they are part of the Contentful Delivery API.
|
|
575
|
+
*/
|
|
576
|
+
const CF_INLINES = {
|
|
577
|
+
ASSET_HYPERLINK: 'asset-hyperlink',
|
|
578
|
+
EMBEDDED_ENTRY: 'embedded-entry-inline',
|
|
579
|
+
EMBEDDED_RESOURCE: 'embedded-resource-inline',
|
|
580
|
+
ENTRY_HYPERLINK: 'entry-hyperlink',
|
|
581
|
+
HYPERLINK: 'hyperlink',
|
|
582
|
+
RESOURCE_HYPERLINK: 'resource-hyperlink',
|
|
423
583
|
};
|
|
424
|
-
class ContentfulLinkComponent {
|
|
425
|
-
constructor() {
|
|
426
|
-
this._document = inject(DOCUMENT);
|
|
427
|
-
this._config = inject(CONTENTFUL_CONFIG, { optional: true }) ?? createContentfulConfig();
|
|
428
|
-
this.href = input.required(/* @ts-ignore */
|
|
429
|
-
...(ngDevMode ? [{ debugName: "href" }] : /* istanbul ignore next */ []));
|
|
430
|
-
this.text = input.required(/* @ts-ignore */
|
|
431
|
-
...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
|
|
432
|
-
this.textClass = input('', /* @ts-ignore */
|
|
433
|
-
...(ngDevMode ? [{ debugName: "textClass" }] : /* istanbul ignore next */ []));
|
|
434
|
-
this.isExternal = computed(() => {
|
|
435
|
-
const href = this.href();
|
|
436
|
-
const isAbsolute = href.startsWith('http://') || href.startsWith('https://') || href.startsWith('//');
|
|
437
|
-
if (!isAbsolute)
|
|
438
|
-
return false;
|
|
439
|
-
try {
|
|
440
|
-
const parsed = new URL(href.startsWith('//') ? `https:${href}` : href);
|
|
441
|
-
const internalHosts = [this._document.location.host, ...this._config.internalHosts];
|
|
442
|
-
return !internalHosts.includes(parsed.host);
|
|
443
|
-
}
|
|
444
|
-
catch {
|
|
445
|
-
return true;
|
|
446
|
-
}
|
|
447
|
-
}, /* @ts-ignore */
|
|
448
|
-
...(ngDevMode ? [{ debugName: "isExternal" }] : /* istanbul ignore next */ []));
|
|
449
|
-
this.openInNewTab = computed(() => {
|
|
450
|
-
if (!this.isExternal())
|
|
451
|
-
return false;
|
|
452
|
-
const href = this.href();
|
|
453
|
-
try {
|
|
454
|
-
const parsed = new URL(href.startsWith('//') ? `https:${href}` : href);
|
|
455
|
-
const internalPrimaryDomains = [
|
|
456
|
-
getPrimaryDomain(this._document.location.host),
|
|
457
|
-
...this._config.internalHosts.map(getPrimaryDomain),
|
|
458
|
-
];
|
|
459
|
-
return !internalPrimaryDomains.includes(getPrimaryDomain(parsed.host));
|
|
460
|
-
}
|
|
461
|
-
catch {
|
|
462
|
-
return true;
|
|
463
|
-
}
|
|
464
|
-
}, /* @ts-ignore */
|
|
465
|
-
...(ngDevMode ? [{ debugName: "openInNewTab" }] : /* istanbul ignore next */ []));
|
|
466
|
-
this.internalPath = computed(() => {
|
|
467
|
-
const href = this.href();
|
|
468
|
-
if (!href.startsWith('http://') && !href.startsWith('https://') && !href.startsWith('//')) {
|
|
469
|
-
return href;
|
|
470
|
-
}
|
|
471
|
-
try {
|
|
472
|
-
const url = new URL(href.startsWith('//') ? `https:${href}` : href);
|
|
473
|
-
return url.pathname + url.search + url.hash;
|
|
474
|
-
}
|
|
475
|
-
catch {
|
|
476
|
-
return href;
|
|
477
|
-
}
|
|
478
|
-
}, /* @ts-ignore */
|
|
479
|
-
...(ngDevMode ? [{ debugName: "internalPath" }] : /* istanbul ignore next */ []));
|
|
480
|
-
this.linkClass = computed(() => {
|
|
481
|
-
const base = 'et-contentful-rich-text-default-element et-contentful-rich-text-default-a';
|
|
482
|
-
const extra = this.textClass();
|
|
483
|
-
return extra ? `${base} ${extra}` : base;
|
|
484
|
-
}, /* @ts-ignore */
|
|
485
|
-
...(ngDevMode ? [{ debugName: "linkClass" }] : /* istanbul ignore next */ []));
|
|
486
|
-
}
|
|
487
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ContentfulLinkComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
488
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", 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: `
|
|
489
|
-
@if (isExternal()) {
|
|
490
|
-
<a
|
|
491
|
-
[class]="linkClass()"
|
|
492
|
-
[href]="href()"
|
|
493
|
-
[target]="openInNewTab() ? '_blank' : null"
|
|
494
|
-
[rel]="openInNewTab() ? 'noopener noreferrer' : null"
|
|
495
|
-
>{{ text() }}</a
|
|
496
|
-
>
|
|
497
|
-
} @else {
|
|
498
|
-
<a [class]="linkClass()" [routerLink]="internalPath()">{{ text() }}</a>
|
|
499
|
-
}
|
|
500
|
-
`, 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 }); }
|
|
501
|
-
}
|
|
502
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ContentfulLinkComponent, decorators: [{
|
|
503
|
-
type: Component,
|
|
504
|
-
args: [{
|
|
505
|
-
selector: 'et-contentful-link',
|
|
506
|
-
template: `
|
|
507
|
-
@if (isExternal()) {
|
|
508
|
-
<a
|
|
509
|
-
[class]="linkClass()"
|
|
510
|
-
[href]="href()"
|
|
511
|
-
[target]="openInNewTab() ? '_blank' : null"
|
|
512
|
-
[rel]="openInNewTab() ? 'noopener noreferrer' : null"
|
|
513
|
-
>{{ text() }}</a
|
|
514
|
-
>
|
|
515
|
-
} @else {
|
|
516
|
-
<a [class]="linkClass()" [routerLink]="internalPath()">{{ text() }}</a>
|
|
517
|
-
}
|
|
518
|
-
`,
|
|
519
|
-
host: {
|
|
520
|
-
style: 'display: contents',
|
|
521
|
-
},
|
|
522
|
-
imports: [RouterLink],
|
|
523
|
-
encapsulation: ViewEncapsulation.None,
|
|
524
|
-
}]
|
|
525
|
-
}], 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 }] }] } });
|
|
526
584
|
|
|
527
585
|
const RICH_TEXT_RENDERER_ERRORS = {
|
|
528
586
|
rich_text_undefined: 'The property value given at richTextPath is undefined. Use the richTextPath input to specify the path to the rich text object.',
|
|
@@ -535,10 +593,17 @@ const RICH_TEXT_RENDERER_ERRORS = {
|
|
|
535
593
|
text_parent_not_found: 'The parent node is not found. This structure is not supported.',
|
|
536
594
|
text_parent_wrong_type: 'The parent node neither a html element nor a custom component. This structure is not supported.',
|
|
537
595
|
};
|
|
538
|
-
const RICH_TEXT_RENDERER_ERROR_CODES =
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
596
|
+
const RICH_TEXT_RENDERER_ERROR_CODES = {
|
|
597
|
+
rich_text_undefined: 0,
|
|
598
|
+
rich_text_wrong_type: 1,
|
|
599
|
+
asset_id_not_found: 2,
|
|
600
|
+
entry_id_not_found: 3,
|
|
601
|
+
asset_not_found: 4,
|
|
602
|
+
entry_not_found: 5,
|
|
603
|
+
custom_component_not_found: 6,
|
|
604
|
+
text_parent_not_found: 7,
|
|
605
|
+
text_parent_wrong_type: 8,
|
|
606
|
+
};
|
|
542
607
|
const richTextRendererError = (code, data) => {
|
|
543
608
|
const message = `<et-contentful-rich-text-renderer>: ${RICH_TEXT_RENDERER_ERRORS[code]}`;
|
|
544
609
|
throw new RuntimeError(RICH_TEXT_RENDERER_ERROR_CODES[code], message, data);
|
|
@@ -549,160 +614,105 @@ const isRichTextRootNode = (node) => {
|
|
|
549
614
|
};
|
|
550
615
|
const translateContentfulNodeTypeToHtmlTag = (nodeType) => {
|
|
551
616
|
switch (nodeType) {
|
|
552
|
-
case
|
|
617
|
+
case CF_BLOCKS.HEADING_1:
|
|
553
618
|
return 'h1';
|
|
554
|
-
case
|
|
619
|
+
case CF_BLOCKS.HEADING_2:
|
|
555
620
|
return 'h2';
|
|
556
|
-
case
|
|
621
|
+
case CF_BLOCKS.HEADING_3:
|
|
557
622
|
return 'h3';
|
|
558
|
-
case
|
|
623
|
+
case CF_BLOCKS.HEADING_4:
|
|
559
624
|
return 'h4';
|
|
560
|
-
case
|
|
625
|
+
case CF_BLOCKS.HEADING_5:
|
|
561
626
|
return 'h5';
|
|
562
|
-
case
|
|
627
|
+
case CF_BLOCKS.HEADING_6:
|
|
563
628
|
return 'h6';
|
|
564
|
-
case
|
|
629
|
+
case CF_BLOCKS.PARAGRAPH:
|
|
565
630
|
return 'p';
|
|
566
|
-
case
|
|
631
|
+
case CF_BLOCKS.UL_LIST:
|
|
567
632
|
return 'ul';
|
|
568
|
-
case
|
|
633
|
+
case CF_BLOCKS.OL_LIST:
|
|
569
634
|
return 'ol';
|
|
570
|
-
case
|
|
635
|
+
case CF_BLOCKS.LIST_ITEM:
|
|
571
636
|
return 'li';
|
|
572
|
-
case
|
|
637
|
+
case CF_BLOCKS.HR:
|
|
573
638
|
return 'hr';
|
|
574
|
-
case
|
|
639
|
+
case CF_BLOCKS.QUOTE:
|
|
575
640
|
return 'blockquote';
|
|
576
|
-
case
|
|
641
|
+
case CF_BLOCKS.TABLE:
|
|
577
642
|
return 'table';
|
|
578
|
-
case
|
|
643
|
+
case CF_BLOCKS.TABLE_ROW:
|
|
579
644
|
return 'tr';
|
|
580
|
-
case
|
|
645
|
+
case CF_BLOCKS.TABLE_CELL:
|
|
581
646
|
return 'td';
|
|
582
|
-
case
|
|
647
|
+
case CF_BLOCKS.TABLE_HEADER_CELL:
|
|
583
648
|
return 'th';
|
|
584
|
-
case
|
|
649
|
+
case CF_BLOCKS.EMBEDDED_ASSET:
|
|
585
650
|
return 'div';
|
|
586
|
-
case
|
|
651
|
+
case CF_INLINES.ENTRY_HYPERLINK:
|
|
587
652
|
return 'a';
|
|
588
|
-
case
|
|
653
|
+
case CF_INLINES.ASSET_HYPERLINK:
|
|
589
654
|
return 'a';
|
|
590
655
|
case 'text':
|
|
591
656
|
return 'span';
|
|
592
657
|
// Will be ignored by the renderer
|
|
593
658
|
case 'document':
|
|
594
659
|
return 'div';
|
|
595
|
-
case
|
|
660
|
+
case CF_BLOCKS.EMBEDDED_ENTRY:
|
|
596
661
|
return 'div';
|
|
597
|
-
case
|
|
662
|
+
case CF_INLINES.EMBEDDED_ENTRY:
|
|
598
663
|
return 'div';
|
|
599
664
|
default:
|
|
600
665
|
return 'div';
|
|
601
666
|
}
|
|
602
667
|
};
|
|
603
668
|
|
|
604
|
-
const
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
NESTING_LEVEL: 1,
|
|
613
|
-
DOM_POSITION: 2,
|
|
614
|
-
INDEX: 3,
|
|
615
|
-
};
|
|
616
|
-
const HTML_OPEN_RENDER_COMMAND_POSITION = {
|
|
617
|
-
ATTRIBUTES: 4,
|
|
618
|
-
TAG_NAME: 5,
|
|
619
|
-
ELEMENT_ID: 6,
|
|
669
|
+
const MARK_TAG_MAP = {
|
|
670
|
+
bold: 'strong',
|
|
671
|
+
italic: 'em',
|
|
672
|
+
underline: 'u',
|
|
673
|
+
code: 'code',
|
|
674
|
+
strikethrough: 's',
|
|
675
|
+
subscript: 'sub',
|
|
676
|
+
superscript: 'sup',
|
|
620
677
|
};
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
const
|
|
626
|
-
|
|
627
|
-
TEXT: 5,
|
|
628
|
-
TEXT_ID: 6,
|
|
629
|
-
};
|
|
630
|
-
const COMPONENT_RENDER_COMMAND_POSITION = {
|
|
631
|
-
ATTRIBUTES: 4,
|
|
632
|
-
COMPONENT: 5,
|
|
633
|
-
INPUTS: 6,
|
|
634
|
-
COMPONENT_ID: 7,
|
|
635
|
-
};
|
|
636
|
-
const RENDER_INSTRUCTION_POSITION = {
|
|
637
|
-
TYPE: 0,
|
|
638
|
-
COMMAND: 1,
|
|
639
|
-
};
|
|
640
|
-
const RENDER_INSTRUCTION_TYPE = {
|
|
641
|
-
CREATE: 0,
|
|
642
|
-
UPDATE: 1,
|
|
643
|
-
DELETE: 2,
|
|
644
|
-
MOVE: 3,
|
|
645
|
-
};
|
|
646
|
-
const MARK_TAILWIND_MAP = {
|
|
647
|
-
bold: 'font-bold',
|
|
648
|
-
italic: 'italic',
|
|
649
|
-
underline: 'underline',
|
|
650
|
-
code: 'font-mono',
|
|
651
|
-
};
|
|
652
|
-
const marksToClass = (marks) => {
|
|
653
|
-
const classes = [];
|
|
678
|
+
/**
|
|
679
|
+
* Translates contentful text marks into the semantic html elements the marked text is
|
|
680
|
+
* wrapped in, outermost first. Unknown marks are skipped (with a dev-mode warning).
|
|
681
|
+
*/
|
|
682
|
+
const marksToTags = (marks) => {
|
|
683
|
+
const tags = [];
|
|
654
684
|
for (const mark of marks) {
|
|
655
|
-
|
|
656
|
-
if (!
|
|
657
|
-
|
|
658
|
-
|
|
685
|
+
const tag = MARK_TAG_MAP[mark.type];
|
|
686
|
+
if (!tag) {
|
|
687
|
+
if (isDevMode()) {
|
|
688
|
+
console.warn(`No element found for mark type "${mark.type}"! The mark is ignored.`, mark);
|
|
689
|
+
}
|
|
690
|
+
continue;
|
|
659
691
|
}
|
|
660
|
-
|
|
692
|
+
tags.push(tag);
|
|
661
693
|
}
|
|
662
|
-
return
|
|
663
|
-
};
|
|
664
|
-
const isHtmlOpenRenderCommand = (command) => {
|
|
665
|
-
return command[RENDER_COMMAND_POSITION.TYPE] === RENDER_COMMAND_TYPE.HTML_OPEN;
|
|
666
|
-
};
|
|
667
|
-
const isHtmlCloseRenderCommand = (command) => {
|
|
668
|
-
return command[RENDER_COMMAND_POSITION.TYPE] === RENDER_COMMAND_TYPE.HTML_CLOSE;
|
|
694
|
+
return tags;
|
|
669
695
|
};
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
const
|
|
677
|
-
if (
|
|
678
|
-
return
|
|
679
|
-
}
|
|
680
|
-
else if (isHtmlCloseRenderCommand(command)) {
|
|
681
|
-
return command[HTML_CLOSE_RENDER_COMMAND_POSITION.ELEMENT_ID];
|
|
696
|
+
/**
|
|
697
|
+
* The classes marked text inside a hyperlink is rendered with, since the link component
|
|
698
|
+
* receives its text as a plain string.
|
|
699
|
+
*/
|
|
700
|
+
const marksToClass = (marks) => marks.map((mark) => `et-contentful-rich-text-mark-${mark.type}`).join(' ');
|
|
701
|
+
const LINK_COMPONENT_TYPE = '$$$_et-link';
|
|
702
|
+
const normalizeHref = (href) => {
|
|
703
|
+
if (!href.trim()) {
|
|
704
|
+
return null;
|
|
682
705
|
}
|
|
683
|
-
|
|
684
|
-
|
|
706
|
+
try {
|
|
707
|
+
const url = new URL(href, 'https://contentful.invalid');
|
|
708
|
+
return ['http:', 'https:', 'mailto:', 'tel:', 'ftp:'].includes(url.protocol) ? href : null;
|
|
685
709
|
}
|
|
686
|
-
|
|
687
|
-
return
|
|
710
|
+
catch {
|
|
711
|
+
return null;
|
|
688
712
|
}
|
|
689
713
|
};
|
|
690
|
-
const CLASS_ATTR = 'class';
|
|
691
|
-
const DEFAULT_COMPONENT_TYPES = {
|
|
692
|
-
IMAGE: '$$$_et-image',
|
|
693
|
-
VIDEO: '$$$_et-video',
|
|
694
|
-
AUDIO: '$$$_et-audio',
|
|
695
|
-
FILE: '$$$_et-file',
|
|
696
|
-
LINK: '$$$_et-link',
|
|
697
|
-
};
|
|
698
714
|
const isExecutedComponentCommandCacheItem = (cache) => {
|
|
699
|
-
return
|
|
700
|
-
};
|
|
701
|
-
const isExecutedHtmlOrTextCommandCacheItem = (cache) => {
|
|
702
|
-
return isHtmlCloseRenderCommand(cache.command) || isHtmlOpenRenderCommand(cache.command);
|
|
703
|
-
};
|
|
704
|
-
const isExecutedTextCommandCacheItem = (cache) => {
|
|
705
|
-
return isTextRenderCommand(cache.command);
|
|
715
|
+
return cache.command.kind === 'component';
|
|
706
716
|
};
|
|
707
717
|
const ET_CONTENTFUL_ANY_ENTRY_CONTENT_TYPE_SYS_ID = '$$$_et-contentful-any-entry-content-type-sys-id';
|
|
708
718
|
/**
|
|
@@ -758,17 +768,10 @@ const createContentfulIncludeMap = (config) => {
|
|
|
758
768
|
};
|
|
759
769
|
class ContentfulRichTextRendererComponent {
|
|
760
770
|
constructor() {
|
|
761
|
-
this.
|
|
762
|
-
this.
|
|
763
|
-
this.
|
|
764
|
-
this.
|
|
765
|
-
this._config = inject(CONTENTFUL_CONFIG, { optional: true }) ?? createContentfulConfig();
|
|
766
|
-
/**
|
|
767
|
-
* A cache for all executed commands that are not deleted.
|
|
768
|
-
* This is used to keep track of all rendered elements and components.
|
|
769
|
-
* The key is the render command element ID.
|
|
770
|
-
*/
|
|
771
|
-
this._executedCommandsCache = new Map();
|
|
771
|
+
this.viewContainerRef = inject(ViewContainerRef);
|
|
772
|
+
this.renderer = injectRenderer();
|
|
773
|
+
this.elementRef = inject(ElementRef);
|
|
774
|
+
this.config = injectContentfulConfig();
|
|
772
775
|
/**
|
|
773
776
|
* The contentful response gotten via their REST api.
|
|
774
777
|
* @see https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/entries/entries-collection
|
|
@@ -781,20 +784,14 @@ class ContentfulRichTextRendererComponent {
|
|
|
781
784
|
*/
|
|
782
785
|
this.richTextPath = input.required(/* @ts-ignore */
|
|
783
786
|
...(ngDevMode ? [{ debugName: "richTextPath" }] : /* istanbul ignore next */ []));
|
|
784
|
-
|
|
785
|
-
* A map of all includes in the contentful response.
|
|
786
|
-
* This is useful for looking up assets and entries that are referenced in the rich text without having to use loops.
|
|
787
|
-
*/
|
|
787
|
+
this.executedCommandsCache = new Map();
|
|
788
788
|
this.contentIncludesMap = computed(() => {
|
|
789
789
|
const content = this.content();
|
|
790
|
-
const assets = content?.includes
|
|
791
|
-
const entries = content?.includes
|
|
790
|
+
const assets = content?.includes?.Asset;
|
|
791
|
+
const entries = content?.includes?.Entry;
|
|
792
792
|
return createContentfulIncludeMap({ assets: assets ?? [], entries: entries ?? [] });
|
|
793
793
|
}, /* @ts-ignore */
|
|
794
794
|
...(ngDevMode ? [{ debugName: "contentIncludesMap" }] : /* istanbul ignore next */ []));
|
|
795
|
-
/**
|
|
796
|
-
* The rich text data that should be rendered.
|
|
797
|
-
*/
|
|
798
795
|
this.richTextData = computed(() => {
|
|
799
796
|
const content = this.content();
|
|
800
797
|
const richTextPath = this.richTextPath();
|
|
@@ -802,6 +799,9 @@ class ContentfulRichTextRendererComponent {
|
|
|
802
799
|
return null;
|
|
803
800
|
}
|
|
804
801
|
const richText = getObjectProperty(content, richTextPath);
|
|
802
|
+
if (richText === null || richText === undefined) {
|
|
803
|
+
return null;
|
|
804
|
+
}
|
|
805
805
|
if (!isObject(richText)) {
|
|
806
806
|
throw richTextRendererError('rich_text_undefined', { content, richTextPath });
|
|
807
807
|
}
|
|
@@ -816,136 +816,127 @@ class ContentfulRichTextRendererComponent {
|
|
|
816
816
|
if (!richTextData) {
|
|
817
817
|
return [];
|
|
818
818
|
}
|
|
819
|
-
return this.
|
|
819
|
+
return this.createRenderCommands(richTextData);
|
|
820
820
|
}, /* @ts-ignore */
|
|
821
821
|
...(ngDevMode ? [{ debugName: "renderCommands" }] : /* istanbul ignore next */ []));
|
|
822
|
-
this.renderCommandHistory = toSignal(toObservable(this.renderCommands).pipe(startWith([]), pairwise()), { initialValue: [[], []] });
|
|
823
|
-
this.previousRenderCommandMap = computed(() => {
|
|
824
|
-
const [prevCommands] = this.renderCommandHistory();
|
|
825
|
-
const map = new Map();
|
|
826
|
-
for (const command of prevCommands) {
|
|
827
|
-
map.set(getRenderCommandId(command), command);
|
|
828
|
-
}
|
|
829
|
-
return map;
|
|
830
|
-
}, /* @ts-ignore */
|
|
831
|
-
...(ngDevMode ? [{ debugName: "previousRenderCommandMap" }] : /* istanbul ignore next */ []));
|
|
832
822
|
this.renderInstructions = computed(() => {
|
|
833
823
|
const commands = this.renderCommands();
|
|
834
|
-
const previousRenderCommandMap = new Map(this.
|
|
824
|
+
const previousRenderCommandMap = new Map([...this.executedCommandsCache.entries()].map(([id, item]) => [id, item.command]));
|
|
835
825
|
const instructions = [];
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
826
|
+
const preserved = new Set();
|
|
827
|
+
const needsReattach = new Set();
|
|
828
|
+
const parentIdOf = (command) => {
|
|
829
|
+
if (command.nestingLevel === 0) {
|
|
830
|
+
return null;
|
|
831
|
+
}
|
|
832
|
+
for (let i = command.index - 1; i >= 0; i--) {
|
|
833
|
+
const cmd = commands[i];
|
|
834
|
+
if (cmd && cmd.nestingLevel === command.nestingLevel - 1 && cmd.kind === 'htmlOpen') {
|
|
835
|
+
return cmd.id;
|
|
842
836
|
}
|
|
843
837
|
}
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
throw new Error('Command not found!');
|
|
838
|
+
return null;
|
|
839
|
+
};
|
|
840
|
+
const markTagsEqual = (a, b) => a.length === b.length && a.every((tag, index) => tag === b[index]);
|
|
841
|
+
const attributesEqual = (a, b) => {
|
|
842
|
+
const aKeys = Object.keys(a);
|
|
843
|
+
return aKeys.length === Object.keys(b).length && aKeys.every((key) => a[key] === b[key]);
|
|
844
|
+
};
|
|
845
|
+
for (const command of commands) {
|
|
846
|
+
if (command.kind === 'htmlClose') {
|
|
847
|
+
continue;
|
|
855
848
|
}
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
command: newCommand,
|
|
860
|
-
});
|
|
861
|
-
}
|
|
862
|
-
lastComponentIndexes.sort((a, b) => a.newIndex - b.newIndex);
|
|
863
|
-
// check if the prevIndex are in order ascending
|
|
864
|
-
// for indexes that are not in order, create a move instruction
|
|
865
|
-
// for indexes that are in order, create an update instruction
|
|
866
|
-
let moveCheckIndex = 0;
|
|
867
|
-
while (moveCheckIndex < lastComponentIndexes.length - 1) {
|
|
868
|
-
const moveCurr = lastComponentIndexes[moveCheckIndex];
|
|
869
|
-
const moveNext = lastComponentIndexes[moveCheckIndex + 1];
|
|
870
|
-
if (!moveCurr || !moveNext) {
|
|
871
|
-
break;
|
|
849
|
+
const previous = previousRenderCommandMap.get(command.id);
|
|
850
|
+
if (!previous || previous.kind !== command.kind) {
|
|
851
|
+
continue;
|
|
872
852
|
}
|
|
873
|
-
if (
|
|
874
|
-
|
|
875
|
-
instructions.push([RENDER_INSTRUCTION_TYPE.MOVE, moveNext.command]);
|
|
853
|
+
if (previous.kind === 'component' && command.kind === 'component' && previous.component !== command.component) {
|
|
854
|
+
continue;
|
|
876
855
|
}
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
856
|
+
const parentId = parentIdOf(command);
|
|
857
|
+
const parentPreserved = parentId === null || preserved.has(parentId);
|
|
858
|
+
if (command.kind === 'component') {
|
|
859
|
+
preserved.add(command.id);
|
|
860
|
+
const samePlace = parentPreserved &&
|
|
861
|
+
previous.nestingLevel === command.nestingLevel &&
|
|
862
|
+
previous.domPosition === command.domPosition;
|
|
863
|
+
if (!samePlace) {
|
|
864
|
+
needsReattach.add(command.id);
|
|
865
|
+
}
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
const sameOutput = previous.kind === command.kind &&
|
|
869
|
+
(previous.kind !== 'htmlOpen' || previous.tagName === command.tagName) &&
|
|
870
|
+
(previous.kind !== 'text' ||
|
|
871
|
+
(previous.text === command.text &&
|
|
872
|
+
markTagsEqual(previous.markTags, command.markTags)));
|
|
873
|
+
if (parentPreserved &&
|
|
874
|
+
sameOutput &&
|
|
875
|
+
previous.nestingLevel === command.nestingLevel &&
|
|
876
|
+
previous.domPosition === command.domPosition &&
|
|
877
|
+
attributesEqual(previous.attributes, command.attributes)) {
|
|
878
|
+
preserved.add(command.id);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
for (const [id, command] of previousRenderCommandMap) {
|
|
882
|
+
if (command.kind !== 'htmlClose' && !preserved.has(id)) {
|
|
883
|
+
instructions.push({ type: 'delete', command });
|
|
880
884
|
}
|
|
881
885
|
}
|
|
882
886
|
for (const command of commands) {
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
887
|
+
if (command.kind === 'htmlClose') {
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
890
|
+
if (!preserved.has(command.id)) {
|
|
891
|
+
instructions.push({ type: 'create', command });
|
|
892
|
+
}
|
|
893
|
+
else if (command.kind === 'component' && needsReattach.has(command.id)) {
|
|
894
|
+
instructions.push({ type: 'move', command });
|
|
890
895
|
}
|
|
891
896
|
else {
|
|
892
|
-
|
|
893
|
-
instructions.push([RENDER_INSTRUCTION_TYPE.CREATE, command]);
|
|
897
|
+
instructions.push({ type: 'update', command });
|
|
894
898
|
}
|
|
895
899
|
}
|
|
896
900
|
return instructions;
|
|
897
901
|
}, /* @ts-ignore */
|
|
898
902
|
...(ngDevMode ? [{ debugName: "renderInstructions" }] : /* istanbul ignore next */ []));
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
this.
|
|
902
|
-
})
|
|
903
|
-
.subscribe();
|
|
903
|
+
effect(() => {
|
|
904
|
+
const instructions = this.renderInstructions();
|
|
905
|
+
untracked(() => this.execInstructions(instructions));
|
|
906
|
+
});
|
|
904
907
|
}
|
|
905
|
-
|
|
906
|
-
/** List of all render commands */
|
|
908
|
+
createRenderCommands(richTextData) {
|
|
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;
|
|
920
917
|
const traverse = (node) => {
|
|
921
918
|
switch (node.nodeType) {
|
|
922
919
|
case 'text': {
|
|
923
|
-
// Render media
|
|
924
|
-
const type = RENDER_COMMAND_TYPE.TEXT;
|
|
925
920
|
const text = node.value;
|
|
926
921
|
if (!text)
|
|
927
922
|
break;
|
|
928
923
|
const attributes = {
|
|
929
924
|
class: 'et-contentful-rich-text-default-element et-contentful-rich-text-default-span',
|
|
930
925
|
};
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
const command = [type, nestingLevel, domPosition, commandIndex++, attributes, text, id];
|
|
942
|
-
rootCommands.push(command);
|
|
926
|
+
rootCommands.push({
|
|
927
|
+
kind: 'text',
|
|
928
|
+
nestingLevel,
|
|
929
|
+
domPosition,
|
|
930
|
+
index: commandIndex++,
|
|
931
|
+
attributes,
|
|
932
|
+
markTags: node.marks.length ? marksToTags(node.marks) : [],
|
|
933
|
+
text,
|
|
934
|
+
id: 't' + textId++,
|
|
935
|
+
});
|
|
943
936
|
domPosition++;
|
|
944
937
|
break;
|
|
945
938
|
}
|
|
946
|
-
case
|
|
947
|
-
// Render media
|
|
948
|
-
const type = RENDER_COMMAND_TYPE.COMPONENT;
|
|
939
|
+
case CF_BLOCKS.EMBEDDED_ASSET: {
|
|
949
940
|
const assetId = node.data['target']?.sys?.id;
|
|
950
941
|
if (!assetId) {
|
|
951
942
|
throw richTextRendererError('asset_id_not_found', { node });
|
|
@@ -955,9 +946,7 @@ class ContentfulRichTextRendererComponent {
|
|
|
955
946
|
throw richTextRendererError('asset_not_found', { assetId, node });
|
|
956
947
|
}
|
|
957
948
|
const contentType = asset.fields.file.contentType;
|
|
958
|
-
const assetComponents = this.
|
|
959
|
-
// Every property inside the asset will be null if no file was provided for a translation.
|
|
960
|
-
// In this case, we can assume that the asset is missing due to user error.
|
|
949
|
+
const assetComponents = this.config.components;
|
|
961
950
|
const isMissing = !contentType && !asset.fields.file.url;
|
|
962
951
|
if (isMissing) {
|
|
963
952
|
if (isDevMode()) {
|
|
@@ -975,37 +964,40 @@ class ContentfulRichTextRendererComponent {
|
|
|
975
964
|
: isAudio
|
|
976
965
|
? assetComponents.audio
|
|
977
966
|
: assetComponents.file;
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
asset,
|
|
991
|
-
};
|
|
992
|
-
const command = [
|
|
993
|
-
type,
|
|
967
|
+
if (!component) {
|
|
968
|
+
if (isDevMode()) {
|
|
969
|
+
console.warn('No component registered for this embedded asset! The asset will be skipped. Provide one via provideContentfulConfig({ components: … }).', { asset });
|
|
970
|
+
}
|
|
971
|
+
break;
|
|
972
|
+
}
|
|
973
|
+
const occurrenceKey = 'asset:' + assetId;
|
|
974
|
+
const occurrence = (componentIdMap.get(occurrenceKey) ?? -1) + 1;
|
|
975
|
+
componentIdMap.set(occurrenceKey, occurrence);
|
|
976
|
+
const id = occurrence === 0 ? occurrenceKey : `${occurrenceKey}:${occurrence}`;
|
|
977
|
+
rootCommands.push({
|
|
978
|
+
kind: 'component',
|
|
994
979
|
nestingLevel,
|
|
995
980
|
domPosition,
|
|
996
|
-
commandIndex++,
|
|
997
|
-
attributes,
|
|
981
|
+
index: commandIndex++,
|
|
998
982
|
component,
|
|
999
|
-
inputs,
|
|
983
|
+
inputs: { asset },
|
|
1000
984
|
id,
|
|
1001
|
-
|
|
1002
|
-
rootCommands.push(command);
|
|
985
|
+
});
|
|
1003
986
|
domPosition++;
|
|
1004
987
|
break;
|
|
1005
988
|
}
|
|
1006
|
-
case
|
|
1007
|
-
|
|
1008
|
-
|
|
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
|
+
}
|
|
1009
1001
|
let linkText = '';
|
|
1010
1002
|
const linkMarks = [];
|
|
1011
1003
|
for (const child of node.content) {
|
|
@@ -1014,28 +1006,85 @@ class ContentfulRichTextRendererComponent {
|
|
|
1014
1006
|
linkMarks.push(...child.marks);
|
|
1015
1007
|
}
|
|
1016
1008
|
}
|
|
1009
|
+
const linkComponent = href ? this.config.components.link : null;
|
|
1010
|
+
if (href && !linkComponent) {
|
|
1011
|
+
rootCommands.push({
|
|
1012
|
+
kind: 'htmlOpen',
|
|
1013
|
+
nestingLevel,
|
|
1014
|
+
domPosition,
|
|
1015
|
+
index: commandIndex++,
|
|
1016
|
+
attributes: {
|
|
1017
|
+
class: 'et-contentful-rich-text-default-element et-contentful-rich-text-default-a',
|
|
1018
|
+
href,
|
|
1019
|
+
},
|
|
1020
|
+
tagName: 'a',
|
|
1021
|
+
id: 'e-o' + elementOpenId++,
|
|
1022
|
+
});
|
|
1023
|
+
const anchorDomPosition = domPosition;
|
|
1024
|
+
nestingLevel++;
|
|
1025
|
+
domPosition = 0;
|
|
1026
|
+
rootCommands.push({
|
|
1027
|
+
kind: 'text',
|
|
1028
|
+
nestingLevel,
|
|
1029
|
+
domPosition,
|
|
1030
|
+
index: commandIndex++,
|
|
1031
|
+
attributes: {
|
|
1032
|
+
class: 'et-contentful-rich-text-default-element et-contentful-rich-text-default-span',
|
|
1033
|
+
},
|
|
1034
|
+
markTags: marksToTags(linkMarks),
|
|
1035
|
+
text: linkText,
|
|
1036
|
+
id: 't' + textId++,
|
|
1037
|
+
});
|
|
1038
|
+
nestingLevel--;
|
|
1039
|
+
domPosition = anchorDomPosition;
|
|
1040
|
+
rootCommands.push({
|
|
1041
|
+
kind: 'htmlClose',
|
|
1042
|
+
nestingLevel,
|
|
1043
|
+
domPosition,
|
|
1044
|
+
index: commandIndex++,
|
|
1045
|
+
tagName: 'a',
|
|
1046
|
+
id: 'e-c' + elementCloseId++,
|
|
1047
|
+
});
|
|
1048
|
+
domPosition++;
|
|
1049
|
+
break;
|
|
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
|
+
}
|
|
1017
1070
|
const textClass = linkMarks.length ? marksToClass(linkMarks) : '';
|
|
1018
|
-
let linkComponentId = componentIdMap.get(
|
|
1019
|
-
const linkId =
|
|
1020
|
-
componentIdMap.set(
|
|
1021
|
-
|
|
1022
|
-
|
|
1071
|
+
let linkComponentId = componentIdMap.get(LINK_COMPONENT_TYPE) ?? -1;
|
|
1072
|
+
const linkId = LINK_COMPONENT_TYPE + ++linkComponentId;
|
|
1073
|
+
componentIdMap.set(LINK_COMPONENT_TYPE, linkComponentId);
|
|
1074
|
+
rootCommands.push({
|
|
1075
|
+
kind: 'component',
|
|
1023
1076
|
nestingLevel,
|
|
1024
1077
|
domPosition,
|
|
1025
|
-
commandIndex++,
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
];
|
|
1031
|
-
rootCommands.push(linkCommand);
|
|
1078
|
+
index: commandIndex++,
|
|
1079
|
+
component: linkComponent,
|
|
1080
|
+
inputs: { href, text: linkText, textClass },
|
|
1081
|
+
id: linkId,
|
|
1082
|
+
});
|
|
1032
1083
|
domPosition++;
|
|
1033
1084
|
break;
|
|
1034
1085
|
}
|
|
1035
|
-
case
|
|
1036
|
-
case
|
|
1037
|
-
// Render component
|
|
1038
|
-
const type = RENDER_COMMAND_TYPE.COMPONENT;
|
|
1086
|
+
case CF_BLOCKS.EMBEDDED_ENTRY:
|
|
1087
|
+
case CF_INLINES.EMBEDDED_ENTRY: {
|
|
1039
1088
|
const entryId = node.data['target']?.sys?.id;
|
|
1040
1089
|
if (!entryId) {
|
|
1041
1090
|
throw richTextRendererError('entry_id_not_found', { node });
|
|
@@ -1045,76 +1094,75 @@ class ContentfulRichTextRendererComponent {
|
|
|
1045
1094
|
throw richTextRendererError('entry_not_found', { entryId, node });
|
|
1046
1095
|
}
|
|
1047
1096
|
const componentType = entry.sys.contentType.sys.id;
|
|
1048
|
-
const component = this.
|
|
1097
|
+
const component = this.config.customComponents[componentType];
|
|
1049
1098
|
if (!component) {
|
|
1050
1099
|
throw richTextRendererError('custom_component_not_found', {
|
|
1051
1100
|
componentType,
|
|
1052
|
-
customComponents: this.
|
|
1101
|
+
customComponents: this.config.customComponents,
|
|
1053
1102
|
entry,
|
|
1054
1103
|
});
|
|
1055
1104
|
}
|
|
1056
|
-
const
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
metadata: entry.metadata,
|
|
1063
|
-
sys: entry.sys,
|
|
1064
|
-
includes: this.contentIncludesMap(),
|
|
1065
|
-
};
|
|
1066
|
-
const command = [
|
|
1067
|
-
type,
|
|
1105
|
+
const occurrenceKey = 'entry:' + entryId;
|
|
1106
|
+
const occurrence = (componentIdMap.get(occurrenceKey) ?? -1) + 1;
|
|
1107
|
+
componentIdMap.set(occurrenceKey, occurrence);
|
|
1108
|
+
const id = occurrence === 0 ? occurrenceKey : `${occurrenceKey}:${occurrence}`;
|
|
1109
|
+
rootCommands.push({
|
|
1110
|
+
kind: 'component',
|
|
1068
1111
|
nestingLevel,
|
|
1069
1112
|
domPosition,
|
|
1070
|
-
commandIndex++,
|
|
1071
|
-
attributes,
|
|
1113
|
+
index: commandIndex++,
|
|
1072
1114
|
component,
|
|
1073
|
-
inputs
|
|
1115
|
+
inputs: {
|
|
1116
|
+
fields: entry.fields,
|
|
1117
|
+
metadata: entry.metadata,
|
|
1118
|
+
sys: entry.sys,
|
|
1119
|
+
includes: this.contentIncludesMap(),
|
|
1120
|
+
},
|
|
1074
1121
|
id,
|
|
1075
|
-
|
|
1076
|
-
rootCommands.push(command);
|
|
1122
|
+
});
|
|
1077
1123
|
domPosition++;
|
|
1078
1124
|
break;
|
|
1079
1125
|
}
|
|
1080
1126
|
default: {
|
|
1081
|
-
const type = RENDER_COMMAND_TYPE.HTML_OPEN;
|
|
1082
1127
|
const tag = translateContentfulNodeTypeToHtmlTag(node.nodeType);
|
|
1083
1128
|
const attributes = {
|
|
1084
1129
|
class: `et-contentful-rich-text-default-element et-contentful-rich-text-default-${tag}`,
|
|
1085
1130
|
};
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1131
|
+
rootCommands.push({
|
|
1132
|
+
kind: 'htmlOpen',
|
|
1133
|
+
nestingLevel,
|
|
1134
|
+
domPosition,
|
|
1135
|
+
index: commandIndex++,
|
|
1136
|
+
attributes,
|
|
1137
|
+
tagName: tag,
|
|
1138
|
+
id: 'e-o' + elementOpenId++,
|
|
1139
|
+
});
|
|
1089
1140
|
const domPositionAtThisLevel = domPosition;
|
|
1090
|
-
|
|
1141
|
+
nestingLevel++;
|
|
1142
|
+
domPosition = 0;
|
|
1091
1143
|
for (const child of node.content) {
|
|
1092
|
-
domPosition = 0;
|
|
1093
|
-
nestingLevel++;
|
|
1094
1144
|
traverse(child);
|
|
1095
|
-
nestingLevel--;
|
|
1096
|
-
domPosition = domPositionAtThisLevel;
|
|
1097
1145
|
}
|
|
1146
|
+
nestingLevel--;
|
|
1147
|
+
domPosition = domPositionAtThisLevel;
|
|
1098
1148
|
const lastCommand = rootCommands[rootCommands.length - 1];
|
|
1099
|
-
if (lastCommand?.
|
|
1100
|
-
lastCommand
|
|
1101
|
-
lastCommand
|
|
1102
|
-
|
|
1149
|
+
if (lastCommand?.kind === 'htmlOpen' &&
|
|
1150
|
+
lastCommand.tagName !== 'td' &&
|
|
1151
|
+
lastCommand.tagName !== 'th' &&
|
|
1152
|
+
lastCommand.tagName !== 'hr') {
|
|
1103
1153
|
rootCommands.pop();
|
|
1104
1154
|
elementOpenId--;
|
|
1105
1155
|
commandIndex--;
|
|
1106
1156
|
}
|
|
1107
1157
|
else {
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
RENDER_COMMAND_TYPE.HTML_CLOSE,
|
|
1158
|
+
rootCommands.push({
|
|
1159
|
+
kind: 'htmlClose',
|
|
1111
1160
|
nestingLevel,
|
|
1112
1161
|
domPosition,
|
|
1113
|
-
commandIndex++,
|
|
1114
|
-
tag,
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
rootCommands.push(closeCommand);
|
|
1162
|
+
index: commandIndex++,
|
|
1163
|
+
tagName: tag,
|
|
1164
|
+
id: 'e-c' + elementCloseId++,
|
|
1165
|
+
});
|
|
1118
1166
|
domPosition++;
|
|
1119
1167
|
}
|
|
1120
1168
|
break;
|
|
@@ -1126,240 +1174,205 @@ class ContentfulRichTextRendererComponent {
|
|
|
1126
1174
|
}
|
|
1127
1175
|
return rootCommands;
|
|
1128
1176
|
}
|
|
1129
|
-
|
|
1177
|
+
execInstructions(instructions) {
|
|
1130
1178
|
for (const instruction of instructions) {
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
case RENDER_INSTRUCTION_TYPE.CREATE:
|
|
1135
|
-
this._runCreateInstruction(command);
|
|
1179
|
+
switch (instruction.type) {
|
|
1180
|
+
case 'create':
|
|
1181
|
+
this.runCreateInstruction(instruction.command);
|
|
1136
1182
|
break;
|
|
1137
|
-
case
|
|
1138
|
-
this.
|
|
1183
|
+
case 'update':
|
|
1184
|
+
this.runUpdateInstruction(instruction.command);
|
|
1139
1185
|
break;
|
|
1140
|
-
case
|
|
1141
|
-
this.
|
|
1186
|
+
case 'move':
|
|
1187
|
+
this.runMoveInstruction(instruction.command);
|
|
1142
1188
|
break;
|
|
1143
|
-
case
|
|
1144
|
-
this.
|
|
1189
|
+
case 'delete':
|
|
1190
|
+
this.runDeleteInstruction(instruction.command);
|
|
1145
1191
|
break;
|
|
1146
1192
|
}
|
|
1147
1193
|
}
|
|
1148
1194
|
}
|
|
1149
|
-
|
|
1150
|
-
const
|
|
1151
|
-
const
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
const
|
|
1157
|
-
|
|
1158
|
-
|
|
1195
|
+
runCreateInstruction(command) {
|
|
1196
|
+
const parentElement = this.findParent(command);
|
|
1197
|
+
const nextElement = this.findFollowingElement(command);
|
|
1198
|
+
if (command.kind === 'component') {
|
|
1199
|
+
const inputs = signal(command.inputs, /* @ts-ignore */
|
|
1200
|
+
...(ngDevMode ? [{ debugName: "inputs" }] : /* istanbul ignore next */ []));
|
|
1201
|
+
const declaredInputs = reflectComponentType(command.component)?.inputs ?? [];
|
|
1202
|
+
const bindings = declaredInputs
|
|
1203
|
+
.filter((declared) => declared.propName in command.inputs)
|
|
1204
|
+
.map((declared) => inputBinding(declared.templateName, () => inputs()[declared.propName]));
|
|
1205
|
+
const componentRef = this.viewContainerRef.createComponent(command.component, { bindings });
|
|
1206
|
+
const rootNode = this.getComponentRootNode(componentRef);
|
|
1207
|
+
this.renderInsertOrAppend(rootNode, parentElement, nextElement);
|
|
1208
|
+
this.executedCommandsCache.set(command.id, {
|
|
1159
1209
|
command,
|
|
1160
1210
|
componentRef,
|
|
1211
|
+
inputs,
|
|
1161
1212
|
element: rootNode,
|
|
1162
1213
|
});
|
|
1163
1214
|
}
|
|
1164
|
-
else if (
|
|
1165
|
-
const
|
|
1166
|
-
const
|
|
1167
|
-
const
|
|
1168
|
-
|
|
1169
|
-
const textNodes = [];
|
|
1170
|
-
for (const [key, value] of Object.entries(attributes)) {
|
|
1171
|
-
this._renderer.setAttribute(span, key, value);
|
|
1215
|
+
else if (command.kind === 'text') {
|
|
1216
|
+
const span = this.renderer.createElement('span');
|
|
1217
|
+
const textSplitInLineBreaks = command.text.split('\n');
|
|
1218
|
+
for (const [key, value] of Object.entries(command.attributes)) {
|
|
1219
|
+
this.renderer.setAttribute(span, key, value);
|
|
1172
1220
|
}
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
this.
|
|
1221
|
+
let textContainer = span;
|
|
1222
|
+
for (const tag of command.markTags) {
|
|
1223
|
+
const markElement = this.renderer.createElement(tag);
|
|
1224
|
+
this.renderer.appendChild(textContainer, markElement);
|
|
1225
|
+
textContainer = markElement;
|
|
1176
1226
|
}
|
|
1177
1227
|
for (const [textPartIndex, textPart] of textSplitInLineBreaks.entries()) {
|
|
1178
1228
|
if (textPartIndex > 0) {
|
|
1179
|
-
const brNode = this.
|
|
1180
|
-
this.
|
|
1229
|
+
const brNode = this.renderer.createElement('br');
|
|
1230
|
+
this.renderer.appendChild(textContainer, brNode);
|
|
1231
|
+
}
|
|
1232
|
+
if (textPart) {
|
|
1233
|
+
const textNode = this.renderer.createText(textPart);
|
|
1234
|
+
this.renderer.appendChild(textContainer, textNode);
|
|
1181
1235
|
}
|
|
1182
|
-
const textNode = this._renderer.createText(textPart);
|
|
1183
|
-
this._renderer.appendChild(span, textNode);
|
|
1184
|
-
textNodes.push(textNode);
|
|
1185
1236
|
}
|
|
1186
|
-
this.
|
|
1187
|
-
this.
|
|
1237
|
+
this.renderInsertOrAppend(span, parentElement, nextElement);
|
|
1238
|
+
this.executedCommandsCache.set(command.id, {
|
|
1188
1239
|
command,
|
|
1189
1240
|
element: span,
|
|
1190
|
-
textNode: textNodes,
|
|
1191
1241
|
});
|
|
1192
1242
|
}
|
|
1193
|
-
else if (
|
|
1194
|
-
const
|
|
1195
|
-
const
|
|
1196
|
-
|
|
1197
|
-
for (const [key, value] of Object.entries(attributes)) {
|
|
1198
|
-
this._renderer.setAttribute(element, key, value);
|
|
1243
|
+
else if (command.kind === 'htmlOpen') {
|
|
1244
|
+
const element = this.renderer.createElement(command.tagName);
|
|
1245
|
+
for (const [key, value] of Object.entries(command.attributes)) {
|
|
1246
|
+
this.renderer.setAttribute(element, key, value);
|
|
1199
1247
|
}
|
|
1200
|
-
this.
|
|
1201
|
-
this.
|
|
1248
|
+
this.renderInsertOrAppend(element, parentElement, nextElement);
|
|
1249
|
+
this.executedCommandsCache.set(command.id, {
|
|
1202
1250
|
command,
|
|
1203
1251
|
element,
|
|
1204
1252
|
});
|
|
1205
1253
|
}
|
|
1206
1254
|
}
|
|
1207
|
-
|
|
1208
|
-
const
|
|
1209
|
-
const cached = this._executedCommandsCache.get(id);
|
|
1255
|
+
runUpdateInstruction(command) {
|
|
1256
|
+
const cached = this.executedCommandsCache.get(command.id);
|
|
1210
1257
|
if (!cached) {
|
|
1211
1258
|
throw new Error('Cached command not found!');
|
|
1212
1259
|
}
|
|
1213
|
-
if (
|
|
1260
|
+
if (command.kind === 'component') {
|
|
1214
1261
|
if (!isExecutedComponentCommandCacheItem(cached)) {
|
|
1215
1262
|
throw new Error('Cached command is not a component command!');
|
|
1216
1263
|
}
|
|
1217
|
-
|
|
1218
|
-
if (!componentRef) {
|
|
1219
|
-
throw new Error('Component ref not found!');
|
|
1220
|
-
}
|
|
1221
|
-
this._updateComponentInputs(command, componentRef);
|
|
1264
|
+
cached.inputs.set(command.inputs);
|
|
1222
1265
|
}
|
|
1223
|
-
this.
|
|
1266
|
+
this.executedCommandsCache.set(command.id, {
|
|
1224
1267
|
...cached,
|
|
1225
|
-
command
|
|
1268
|
+
command,
|
|
1226
1269
|
});
|
|
1227
1270
|
}
|
|
1228
|
-
|
|
1229
|
-
const
|
|
1230
|
-
const cached = this._executedCommandsCache.get(id);
|
|
1271
|
+
runMoveInstruction(command) {
|
|
1272
|
+
const cached = this.executedCommandsCache.get(command.id);
|
|
1231
1273
|
if (!cached) {
|
|
1232
1274
|
throw new Error('Cached command not found!');
|
|
1233
1275
|
}
|
|
1234
|
-
if (
|
|
1276
|
+
if (command.kind === 'component') {
|
|
1235
1277
|
if (!isExecutedComponentCommandCacheItem(cached)) {
|
|
1236
1278
|
throw new Error('Cached command is not a component command!');
|
|
1237
1279
|
}
|
|
1238
|
-
const componentRef = cached.componentRef;
|
|
1239
|
-
if (!componentRef) {
|
|
1240
|
-
throw new Error('Component ref not found!');
|
|
1241
|
-
}
|
|
1242
1280
|
const rootNode = cached.element;
|
|
1243
1281
|
const oldParentElement = cached.element.parentElement;
|
|
1244
1282
|
if (oldParentElement) {
|
|
1245
|
-
this.
|
|
1283
|
+
this.renderer.removeChild(oldParentElement, rootNode);
|
|
1246
1284
|
}
|
|
1247
|
-
const newParentElement = this.
|
|
1248
|
-
const nextElement = this.
|
|
1249
|
-
this.
|
|
1250
|
-
|
|
1251
|
-
this.
|
|
1285
|
+
const newParentElement = this.findParent(command);
|
|
1286
|
+
const nextElement = this.findFollowingElement(command);
|
|
1287
|
+
this.renderInsertOrAppend(rootNode, newParentElement, nextElement);
|
|
1288
|
+
cached.inputs.set(command.inputs);
|
|
1289
|
+
this.executedCommandsCache.set(command.id, {
|
|
1252
1290
|
...cached,
|
|
1253
|
-
command
|
|
1291
|
+
command,
|
|
1254
1292
|
});
|
|
1255
1293
|
}
|
|
1256
1294
|
}
|
|
1257
|
-
|
|
1258
|
-
const
|
|
1259
|
-
const cached = this._executedCommandsCache.get(id);
|
|
1295
|
+
runDeleteInstruction(command) {
|
|
1296
|
+
const cached = this.executedCommandsCache.get(command.id);
|
|
1260
1297
|
if (!cached) {
|
|
1261
|
-
if (
|
|
1298
|
+
if (command.kind === 'htmlClose') {
|
|
1262
1299
|
return;
|
|
1263
1300
|
}
|
|
1264
1301
|
throw new Error('Cached command not found!');
|
|
1265
1302
|
}
|
|
1266
|
-
if (
|
|
1303
|
+
if (command.kind === 'component') {
|
|
1267
1304
|
if (!isExecutedComponentCommandCacheItem(cached)) {
|
|
1268
1305
|
throw new Error('Cached command is not a component command!');
|
|
1269
1306
|
}
|
|
1270
|
-
cached.componentRef
|
|
1271
|
-
}
|
|
1272
|
-
else if (isTextRenderCommand(command)) {
|
|
1273
|
-
if (cached.element?.parentElement) {
|
|
1274
|
-
this._renderer.removeChild(cached.element.parentElement, cached.element);
|
|
1275
|
-
}
|
|
1307
|
+
cached.componentRef.destroy();
|
|
1276
1308
|
}
|
|
1277
|
-
else if (
|
|
1278
|
-
if (cached.element
|
|
1279
|
-
this.
|
|
1309
|
+
else if (command.kind === 'text' || command.kind === 'htmlOpen') {
|
|
1310
|
+
if (cached.element.parentElement) {
|
|
1311
|
+
this.renderer.removeChild(cached.element.parentElement, cached.element);
|
|
1280
1312
|
}
|
|
1281
1313
|
}
|
|
1282
|
-
this.
|
|
1314
|
+
this.executedCommandsCache.delete(command.id);
|
|
1283
1315
|
}
|
|
1284
|
-
|
|
1316
|
+
getComponentRootNode(componentRef) {
|
|
1285
1317
|
return componentRef.hostView.rootNodes[0];
|
|
1286
1318
|
}
|
|
1287
|
-
|
|
1288
|
-
const
|
|
1289
|
-
const inputsByProp = new Map(inputMeta.map((i) => [i.propName, i.templateName]));
|
|
1290
|
-
for (const [key, value] of Object.entries(command[COMPONENT_RENDER_COMMAND_POSITION.INPUTS])) {
|
|
1291
|
-
const publicName = inputsByProp.get(key);
|
|
1292
|
-
if (publicName === undefined)
|
|
1293
|
-
continue;
|
|
1294
|
-
componentRef.setInput(publicName, value);
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
1297
|
-
_findParent(command) {
|
|
1298
|
-
const hostElement = this._elementRef.nativeElement;
|
|
1299
|
-
const nestingLevel = command[RENDER_COMMAND_POSITION.NESTING_LEVEL];
|
|
1319
|
+
findParent(command) {
|
|
1320
|
+
const hostElement = this.elementRef.nativeElement;
|
|
1300
1321
|
let parentElement;
|
|
1301
|
-
if (nestingLevel === 0) {
|
|
1322
|
+
if (command.nestingLevel === 0) {
|
|
1302
1323
|
parentElement = hostElement;
|
|
1303
1324
|
}
|
|
1304
1325
|
else {
|
|
1305
|
-
// Reverse search all render commands beginning from the current one using a for loop.
|
|
1306
|
-
// The parent is found when the command's nesting level ist the same as the current nesting level minus 1.
|
|
1307
|
-
// If the parent is not found, throw an error.
|
|
1308
1326
|
const allCommands = this.renderCommands();
|
|
1309
|
-
const currentCommandIndex = command[RENDER_COMMAND_POSITION.INDEX];
|
|
1310
1327
|
let parentCommand = null;
|
|
1311
|
-
for (let i =
|
|
1328
|
+
for (let i = command.index - 1; i >= 0; i--) {
|
|
1312
1329
|
const cmd = allCommands[i];
|
|
1313
1330
|
if (!cmd) {
|
|
1314
1331
|
throw new Error('Command not found!');
|
|
1315
1332
|
}
|
|
1316
|
-
if (cmd
|
|
1333
|
+
if (cmd.nestingLevel === command.nestingLevel - 1 && cmd.kind === 'htmlOpen') {
|
|
1317
1334
|
parentCommand = cmd;
|
|
1318
1335
|
break;
|
|
1319
1336
|
}
|
|
1320
1337
|
}
|
|
1321
1338
|
if (!parentCommand) {
|
|
1322
|
-
throw
|
|
1339
|
+
throw richTextRendererError('text_parent_not_found', { command });
|
|
1323
1340
|
}
|
|
1324
|
-
parentElement = this.
|
|
1341
|
+
parentElement = this.executedCommandsCache.get(parentCommand.id)?.element;
|
|
1325
1342
|
}
|
|
1326
1343
|
if (!parentElement) {
|
|
1327
|
-
throw
|
|
1344
|
+
throw richTextRendererError('text_parent_wrong_type', { command });
|
|
1328
1345
|
}
|
|
1329
1346
|
return parentElement;
|
|
1330
1347
|
}
|
|
1331
|
-
|
|
1332
|
-
const
|
|
1333
|
-
let nextElement
|
|
1334
|
-
|
|
1335
|
-
const
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
if (cached.command[RENDER_COMMAND_POSITION.DOM_POSITION] > domPosition &&
|
|
1342
|
-
cached.command[RENDER_COMMAND_POSITION.NESTING_LEVEL] === nestingLevel &&
|
|
1343
|
-
// Make sure the element does not find itself
|
|
1344
|
-
cached.command[RENDER_COMMAND_POSITION.TYPE] !== type) {
|
|
1348
|
+
findFollowingElement(command) {
|
|
1349
|
+
const parentElement = this.findParent(command);
|
|
1350
|
+
let nextElement;
|
|
1351
|
+
let nextDomPosition = Infinity;
|
|
1352
|
+
for (const cached of this.executedCommandsCache.values()) {
|
|
1353
|
+
if (cached.command.domPosition > command.domPosition &&
|
|
1354
|
+
cached.command.domPosition < nextDomPosition &&
|
|
1355
|
+
cached.command.nestingLevel === command.nestingLevel &&
|
|
1356
|
+
cached.element.parentElement === parentElement &&
|
|
1357
|
+
cached.command.id !== command.id) {
|
|
1345
1358
|
nextElement = cached.element;
|
|
1346
|
-
|
|
1359
|
+
nextDomPosition = cached.command.domPosition;
|
|
1347
1360
|
}
|
|
1348
1361
|
}
|
|
1349
1362
|
return nextElement;
|
|
1350
1363
|
}
|
|
1351
|
-
|
|
1364
|
+
renderInsertOrAppend(nodeToRender, parentElement, nextElement) {
|
|
1352
1365
|
if (nextElement) {
|
|
1353
|
-
this.
|
|
1366
|
+
this.renderer.insertBefore(parentElement, nodeToRender, nextElement);
|
|
1354
1367
|
}
|
|
1355
1368
|
else {
|
|
1356
|
-
this.
|
|
1369
|
+
this.renderer.appendChild(parentElement, nodeToRender);
|
|
1357
1370
|
}
|
|
1358
1371
|
}
|
|
1359
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.
|
|
1360
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.
|
|
1372
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ContentfulRichTextRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1373
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.7", type: ContentfulRichTextRendererComponent, isStandalone: true, selector: "et-contentful-rich-text-renderer", inputs: { content: { classPropertyName: "content", publicName: "content", isSignal: true, isRequired: true, transformFunction: null }, richTextPath: { classPropertyName: "richTextPath", publicName: "richTextPath", isSignal: true, isRequired: true, transformFunction: null } }, host: { classAttribute: "et-contentful-rich-text-renderer" }, ngImport: i0, template: ``, isInline: true, encapsulation: i0.ViewEncapsulation.None }); }
|
|
1361
1374
|
}
|
|
1362
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.
|
|
1375
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ContentfulRichTextRendererComponent, decorators: [{
|
|
1363
1376
|
type: Component,
|
|
1364
1377
|
args: [{
|
|
1365
1378
|
selector: 'et-contentful-rich-text-renderer',
|
|
@@ -1379,16 +1392,14 @@ const ContentfulImports = [
|
|
|
1379
1392
|
ContentfulRichTextRendererComponent,
|
|
1380
1393
|
];
|
|
1381
1394
|
|
|
1382
|
-
const provideContentfulConfig = (contentfulConfig
|
|
1395
|
+
const provideContentfulConfig = (contentfulConfig) => {
|
|
1383
1396
|
return { provide: CONTENTFUL_CONFIG, useValue: createContentfulConfig(contentfulConfig) };
|
|
1384
1397
|
};
|
|
1385
1398
|
const isContentfulEntryType = (entry, type) => entry.sys.contentType.sys.id === type;
|
|
1386
1399
|
|
|
1387
|
-
// Burst cache
|
|
1388
|
-
|
|
1389
1400
|
/**
|
|
1390
1401
|
* Generated bundle index. Do not edit.
|
|
1391
1402
|
*/
|
|
1392
1403
|
|
|
1393
|
-
export {
|
|
1404
|
+
export { CONTENTFUL_CONFIG, ContentfulAudioComponent, ContentfulFileComponent, ContentfulImageComponent, ContentfulImports, ContentfulLinkComponent, ContentfulRichTextRendererComponent, ContentfulVideoComponent, ET_CONTENTFUL_ANY_ENTRY_CONTENT_TYPE_SYS_ID, GQL_FRAGMENT_CONTENTFUL_ASSET, createContentfulConfig, createContentfulIncludeMap, generateContentfulImageSources, generateDefaultContentfulImageSource, injectContentfulConfig, isContentfulEntryType, isContentfulGqlAsset, isRichTextRootNode, marksToClass, marksToTags, parseContentfulImageSize, provideContentfulConfig, translateContentfulNodeTypeToHtmlTag };
|
|
1394
1405
|
//# sourceMappingURL=ethlete-contentful.mjs.map
|