@ethlete/contentful 2.1.2 → 3.0.1

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.
Files changed (33) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/esm2022/index.mjs +2 -1
  3. package/esm2022/lib/components/audio/contentful-audio.component.mjs +18 -47
  4. package/esm2022/lib/components/file/contentful-file.component.mjs +14 -36
  5. package/esm2022/lib/components/image/contentful-image.component.mjs +64 -113
  6. package/esm2022/lib/components/image/contentful-image.component.utils.mjs +15 -19
  7. package/esm2022/lib/components/rich-text-renderer/index.mjs +1 -3
  8. package/esm2022/lib/components/rich-text-renderer/rich-text-renderer.component.mjs +683 -78
  9. package/esm2022/lib/components/rich-text-renderer/rich-text-renderer.errors.mjs +21 -0
  10. package/esm2022/lib/components/rich-text-renderer/rich-text-renderer.util.mjs +4 -133
  11. package/esm2022/lib/components/video/contentful-video.component.mjs +14 -39
  12. package/esm2022/lib/gql/asset.fragments.mjs +1 -17
  13. package/esm2022/lib/types/contentful.types.mjs +1 -1
  14. package/esm2022/lib/utils/contentful.util.mjs +1 -7
  15. package/fesm2022/ethlete-contentful.mjs +835 -484
  16. package/fesm2022/ethlete-contentful.mjs.map +1 -1
  17. package/lib/components/audio/contentful-audio.component.d.ts +6 -11
  18. package/lib/components/file/contentful-file.component.d.ts +4 -9
  19. package/lib/components/image/contentful-image.component.d.ts +19 -24
  20. package/lib/components/image/contentful-image.component.utils.d.ts +2 -2
  21. package/lib/components/rich-text-renderer/index.d.ts +0 -2
  22. package/lib/components/rich-text-renderer/rich-text-renderer.component.d.ts +160 -15
  23. package/lib/components/rich-text-renderer/rich-text-renderer.errors.d.ts +12 -0
  24. package/lib/components/rich-text-renderer/rich-text-renderer.util.d.ts +4 -17
  25. package/lib/components/video/contentful-video.component.d.ts +4 -9
  26. package/lib/gql/asset.fragments.d.ts +0 -1
  27. package/lib/types/contentful.types.d.ts +92 -48
  28. package/lib/utils/contentful.util.d.ts +1 -3
  29. package/package.json +4 -4
  30. package/esm2022/lib/components/rich-text-renderer/rich-text-renderer.constants.mjs +0 -3
  31. package/esm2022/lib/components/rich-text-renderer/rich-text-renderer.types.mjs +0 -2
  32. package/lib/components/rich-text-renderer/rich-text-renderer.constants.d.ts +0 -2
  33. package/lib/components/rich-text-renderer/rich-text-renderer.types.d.ts +0 -12
@@ -1,53 +1,71 @@
1
- import { NgClass, NgOptimizedImage, DOCUMENT } from '@angular/common';
1
+ import { NgClass, DOCUMENT } from '@angular/common';
2
2
  import * as i0 from '@angular/core';
3
- import { InjectionToken, inject, Component, ChangeDetectionStrategy, ViewEncapsulation, Input, ChangeDetectorRef, Renderer2, ElementRef, ComponentFactoryResolver, ApplicationRef, Injector } from '@angular/core';
4
- import { DomPortalOutlet, ComponentPortal } from '@angular/cdk/portal';
5
- import * as i1 from '@ethlete/cdk';
6
- import { PictureDataDirective, PictureComponent } from '@ethlete/cdk';
7
- import { BLOCKS, INLINES } from '@contentful/rich-text-types';
3
+ import { input, Component, ChangeDetectionStrategy, ViewEncapsulation, InjectionToken, inject, numberAttribute, booleanAttribute, computed, ViewContainerRef, Renderer2, ElementRef } from '@angular/core';
4
+ import { normalizePictureSizes, PictureComponent } from '@ethlete/cdk';
5
+ import { toSignal, toObservable, takeUntilDestroyed } from '@angular/core/rxjs-interop';
6
+ import { INLINES, BLOCKS } from '@contentful/rich-text-types';
7
+ import { RuntimeError, isObject, getObjectProperty } from '@ethlete/core';
8
+ import { startWith, pairwise, tap } from 'rxjs';
8
9
  import { gql } from '@ethlete/query';
9
10
 
10
- const CONTENTFUL_CONFIG = new InjectionToken('ContentfulConfig');
11
-
12
- class ContentfulFileComponent {
11
+ class ContentfulAudioComponent {
13
12
  constructor() {
14
- this._richTextData = inject(RICH_TEXT_RENDERER_COMPONENT_DATA, { optional: true });
15
- this._data = null;
16
- this.fileClass = null;
13
+ this.asset = input.required();
14
+ this.audioClass = input(null);
15
+ this.figureClass = input(null);
16
+ this.figcaptionClass = input(null);
17
17
  }
18
- get data() {
19
- return this._data;
18
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.3", ngImport: i0, type: ContentfulAudioComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
19
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.3", 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: `
20
+ @if (asset(); as data) {
21
+ <figure [ngClass]="figureClass()">
22
+ <figcaption [ngClass]="figcaptionClass()">{{ data.fields.title }}</figcaption>
23
+ <audio [ngClass]="audioClass()" controls src="{{ data.fields.file.url }}"></audio>
24
+ </figure>
20
25
  }
21
- set data(v) {
22
- if (v && !v.contentType) {
23
- this._data = null;
24
- console.warn('The provided asset is invalid', v);
25
- return;
26
- }
27
- this._data = v ?? null;
26
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
27
+ }
28
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.3", ngImport: i0, type: ContentfulAudioComponent, decorators: [{
29
+ type: Component,
30
+ args: [{
31
+ selector: 'et-contentful-audio',
32
+ template: `
33
+ @if (asset(); as data) {
34
+ <figure [ngClass]="figureClass()">
35
+ <figcaption [ngClass]="figcaptionClass()">{{ data.fields.title }}</figcaption>
36
+ <audio [ngClass]="audioClass()" controls src="{{ data.fields.file.url }}"></audio>
37
+ </figure>
28
38
  }
29
- ngOnInit() {
30
- if (this._richTextData && !this.data) {
31
- this.data = this._richTextData;
32
- }
39
+ `,
40
+ changeDetection: ChangeDetectionStrategy.OnPush,
41
+ encapsulation: ViewEncapsulation.None,
42
+ standalone: true,
43
+ imports: [NgClass],
44
+ }]
45
+ }] });
46
+
47
+ class ContentfulFileComponent {
48
+ constructor() {
49
+ this.asset = input.required();
50
+ this.fileClass = input(null);
33
51
  }
34
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.0", ngImport: i0, type: ContentfulFileComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
35
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.0", type: ContentfulFileComponent, isStandalone: true, selector: "et-contentful-file", inputs: { data: "data", fileClass: "fileClass" }, ngImport: i0, template: `
36
- @if (data) {
37
- <a [href]="data.url" [ngClass]="fileClass" class="underline" target="_blank">
38
- {{ data.title }} ({{ data.size }} Bytes)
52
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.3", ngImport: i0, type: ContentfulFileComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
53
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.3", 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: `
54
+ @if (asset(); as data) {
55
+ <a [href]="data.fields.file.url" [ngClass]="fileClass()" class="underline" target="_blank">
56
+ {{ data.fields.title }} ({{ data.fields.file.details.size }} Bytes)
39
57
  </a>
40
58
  }
41
59
  `, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
42
60
  }
43
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.0", ngImport: i0, type: ContentfulFileComponent, decorators: [{
61
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.3", ngImport: i0, type: ContentfulFileComponent, decorators: [{
44
62
  type: Component,
45
63
  args: [{
46
64
  selector: 'et-contentful-file',
47
65
  template: `
48
- @if (data) {
49
- <a [href]="data.url" [ngClass]="fileClass" class="underline" target="_blank">
50
- {{ data.title }} ({{ data.size }} Bytes)
66
+ @if (asset(); as data) {
67
+ <a [href]="data.fields.file.url" [ngClass]="fileClass()" class="underline" target="_blank">
68
+ {{ data.fields.title }} ({{ data.fields.file.details.size }} Bytes)
51
69
  </a>
52
70
  }
53
71
  `,
@@ -56,27 +74,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.0", ngImpor
56
74
  standalone: true,
57
75
  imports: [NgClass],
58
76
  }]
59
- }], propDecorators: { data: [{
60
- type: Input
61
- }], fileClass: [{
62
- type: Input
63
- }] } });
77
+ }] });
64
78
 
65
- const provideContentfulConfig = (contentfulConfig = {}) => {
66
- return { provide: CONTENTFUL_CONFIG, useValue: createContentfulConfig(contentfulConfig) };
67
- };
68
- const isContentfulImage = (v) => {
69
- return !!v && 'asset' in v && 'alt' in v && 'resizeBehavior' in v && 'focusArea' in v;
70
- };
71
- const isContentfulAsset = (v) => {
72
- return !!v && 'contentType' in v && 'description' in v && 'height' in v && 'url' in v && 'width' in v;
73
- };
79
+ const CONTENTFUL_CONFIG = new InjectionToken('ContentfulConfig');
74
80
 
75
81
  const generateDefaultContentfulImageSource = (data) => {
76
- const { url, contentType } = data;
82
+ const file = data.fields.file;
77
83
  return {
78
- type: contentType,
79
- srcset: url,
84
+ type: file.contentType,
85
+ srcset: file.url,
80
86
  };
81
87
  };
82
88
  /**
@@ -106,16 +112,15 @@ const parseContentfulImageSize = (size) => {
106
112
  height: height ? parseInt(height, 10) : null,
107
113
  };
108
114
  };
109
- const generateContentfulImageSources = (data, srcsetSizes, backgroundColor) => {
110
- const assetData = isContentfulImage(data) ? data.asset : data;
111
- const imageData = isContentfulImage(data) ? data : null;
115
+ const generateContentfulImageSources = (data, srcsetSizes, backgroundColor, quality, focusArea, resizeBehavior) => {
116
+ const assetData = data;
112
117
  if (!assetData) {
113
118
  return [];
114
119
  }
115
120
  const sources = [];
116
121
  const SOURCE_TYPES = ['image/avif', 'image/webp', 'image/png', 'image/jpg'];
117
122
  for (const type of SOURCE_TYPES) {
118
- const baseUrl = assetData.url;
123
+ const baseUrl = assetData.fields.file.url;
119
124
  const sourceSets = [];
120
125
  const queryParams = [];
121
126
  // Set the format (e.g. 'fm=webp')
@@ -123,16 +128,14 @@ const generateContentfulImageSources = (data, srcsetSizes, backgroundColor) => {
123
128
  if (backgroundColor) {
124
129
  queryParams.push(`bg=rgb:${backgroundColor}`);
125
130
  }
126
- if (imageData) {
127
- if (imageData.quality) {
128
- queryParams.push(`q=${imageData.quality}`);
129
- }
130
- if (imageData.focusArea) {
131
- queryParams.push(`f=${imageData.focusArea}`);
132
- }
133
- if (imageData.resizeBehavior) {
134
- queryParams.push(`fit=${imageData.resizeBehavior}`);
135
- }
131
+ if (quality !== null) {
132
+ queryParams.push(`q=${quality}`);
133
+ }
134
+ if (focusArea) {
135
+ queryParams.push(`f=${focusArea}`);
136
+ }
137
+ if (resizeBehavior) {
138
+ queryParams.push(`fit=${resizeBehavior}`);
136
139
  }
137
140
  if (srcsetSizes?.length) {
138
141
  const urlWithParams = `${baseUrl}?${queryParams.join('&')}`;
@@ -159,174 +162,108 @@ const generateContentfulImageSources = (data, srcsetSizes, backgroundColor) => {
159
162
 
160
163
  class ContentfulImageComponent {
161
164
  constructor() {
162
- this._richTextData = inject(RICH_TEXT_RENDERER_COMPONENT_DATA, {
163
- optional: true,
165
+ this._contentfulConfig = inject(CONTENTFUL_CONFIG);
166
+ this.asset = input.required();
167
+ this.backgroundColor = input(null);
168
+ this.srcsetSizes = input(this._contentfulConfig.imageOptions.srcsetSizes);
169
+ this.quality = input(null, { transform: numberAttribute });
170
+ this.focusArea = input(null);
171
+ this.resizeBehavior = input(null);
172
+ this.hasPriority = input(false, { transform: booleanAttribute });
173
+ this.imgClass = input(null);
174
+ this.figureClass = input(null);
175
+ this.figcaptionClass = input(null);
176
+ this.pictureClass = input(null);
177
+ this.sizes = input(normalizePictureSizes(this._contentfulConfig.imageOptions.sizes), {
178
+ transform: (v) => normalizePictureSizes(v),
179
+ });
180
+ this._sources = computed(() => {
181
+ const asset = this.asset();
182
+ const backgroundColor = this.backgroundColor();
183
+ const srcsetSizes = this.srcsetSizes();
184
+ const quality = this.quality();
185
+ const focusArea = this.focusArea();
186
+ const resizeBehavior = this.resizeBehavior();
187
+ if (!asset) {
188
+ return [];
189
+ }
190
+ return generateContentfulImageSources(asset, srcsetSizes, backgroundColor, quality, focusArea, resizeBehavior);
191
+ });
192
+ this._defaultSrc = computed(() => {
193
+ const asset = this.asset();
194
+ return asset ? generateDefaultContentfulImageSource(asset) : null;
164
195
  });
165
- this.pictureData = inject(PictureDataDirective);
166
- this._cdr = inject(ChangeDetectorRef);
167
- this._data = null;
168
- this._sizes = [];
169
- this._srcsetSizes = [];
170
- this._backgroundColor = null;
171
- this.sources = [];
172
- }
173
- get data() {
174
- return this._data;
175
- }
176
- set data(v) {
177
- const assetData = isContentfulImage(v) ? v.asset : v;
178
- const imageData = isContentfulImage(v) ? v : null;
179
- if (assetData && !assetData.contentType) {
180
- this._data = null;
181
- console.warn('The provided asset is invalid', v);
182
- return;
183
- }
184
- if (assetData && !assetData.contentType.startsWith('image/')) {
185
- throw new Error('The provided asset is not an image');
186
- }
187
- this._data = v ?? null;
188
- this.pictureData.defaultSrc = assetData ? generateDefaultContentfulImageSource(assetData) : null;
189
- this.pictureData.alt = imageData?.alt ?? assetData?.description ?? null;
190
- this.pictureData.width = assetData?.width ?? null;
191
- this.pictureData.height = assetData?.height ?? null;
192
- this.pictureData.figcaption = imageData?.caption ?? null;
193
- this.updateSizes();
194
- }
195
- get sizes() {
196
- return this._sizes;
197
- }
198
- set sizes(v) {
199
- this._sizes = v;
200
- this.pictureData.sizes = this.sizes?.join(', ') ?? null;
201
- }
202
- get srcsetSizes() {
203
- return this._srcsetSizes;
204
- }
205
- set srcsetSizes(v) {
206
- this._srcsetSizes = v;
207
- this.updateSizes();
208
- }
209
- get backgroundColor() {
210
- return this._backgroundColor;
211
- }
212
- set backgroundColor(v) {
213
- this._backgroundColor = v;
214
- this.updateSizes();
215
- }
216
- ngOnInit() {
217
- if (this._richTextData && !this.data) {
218
- this.data = this._richTextData;
219
- }
220
- }
221
- updateSizes() {
222
- this.sources = this._data ? generateContentfulImageSources(this._data, this.srcsetSizes, this.backgroundColor) : [];
223
- this._cdr.markForCheck();
224
- }
225
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.0", ngImport: i0, type: ContentfulImageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
226
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.0", type: ContentfulImageComponent, isStandalone: true, selector: "et-contentful-image", inputs: { data: "data", sizes: "sizes", srcsetSizes: "srcsetSizes", backgroundColor: "backgroundColor" }, host: { classAttribute: "et-contentful-image" }, hostDirectives: [{ directive: i1.PictureDataDirective, inputs: ["imgClass", "imgClass", "hasPriority", "hasPriority", "figureClass", "figureClass", "pictureClass", "pictureClass", "figcaptionClass", "figcaptionClass"] }], ngImport: i0, template: `
196
+ }
197
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.3", ngImport: i0, type: ContentfulImageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
198
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "17.3.3", 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 }, hasPriority: { classPropertyName: "hasPriority", publicName: "hasPriority", isSignal: true, isRequired: false, transformFunction: null }, imgClass: { classPropertyName: "imgClass", publicName: "imgClass", 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 }, pictureClass: { classPropertyName: "pictureClass", publicName: "pictureClass", 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: `
227
199
  <et-picture
228
- [imgClass]="pictureData.imgClass"
229
- [hasPriority]="pictureData.hasPriority"
230
- [figureClass]="pictureData.figureClass"
231
- [pictureClass]="pictureData.pictureClass"
232
- [figcaptionClass]="pictureData.figcaptionClass"
233
- [defaultSrc]="pictureData.defaultSrc"
234
- [alt]="pictureData.alt"
235
- [figcaption]="pictureData.figcaption"
236
- [width]="pictureData.width"
237
- [height]="pictureData.height"
238
- [sizes]="pictureData.sizes"
239
- [sources]="sources"
200
+ [imgClass]="imgClass()"
201
+ [hasPriority]="hasPriority()"
202
+ [figureClass]="figureClass()"
203
+ [pictureClass]="pictureClass()"
204
+ [figcaptionClass]="figcaptionClass()"
205
+ [defaultSrc]="_defaultSrc()"
206
+ [alt]="asset()?.fields?.title ?? null"
207
+ [figcaption]="asset()?.fields?.description ?? null"
208
+ [width]="asset()?.fields?.file?.details?.image?.width ?? null"
209
+ [height]="asset()?.fields?.file?.details?.image?.height ?? null"
210
+ [sizes]="sizes()"
211
+ [sources]="_sources()"
240
212
  />
241
- `, isInline: true, dependencies: [{ kind: "component", type: PictureComponent, selector: "et-picture", inputs: ["sources"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
213
+ `, isInline: true, dependencies: [{ kind: "component", type: PictureComponent, selector: "et-picture", inputs: ["sources", "hasPriority", "imgClass", "figureClass", "figcaptionClass", "pictureClass", "defaultSrc", "alt", "figcaption", "width", "height", "sizes"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
242
214
  }
243
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.0", ngImport: i0, type: ContentfulImageComponent, decorators: [{
215
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.3", ngImport: i0, type: ContentfulImageComponent, decorators: [{
244
216
  type: Component,
245
217
  args: [{
246
218
  selector: 'et-contentful-image',
247
219
  template: `
248
220
  <et-picture
249
- [imgClass]="pictureData.imgClass"
250
- [hasPriority]="pictureData.hasPriority"
251
- [figureClass]="pictureData.figureClass"
252
- [pictureClass]="pictureData.pictureClass"
253
- [figcaptionClass]="pictureData.figcaptionClass"
254
- [defaultSrc]="pictureData.defaultSrc"
255
- [alt]="pictureData.alt"
256
- [figcaption]="pictureData.figcaption"
257
- [width]="pictureData.width"
258
- [height]="pictureData.height"
259
- [sizes]="pictureData.sizes"
260
- [sources]="sources"
221
+ [imgClass]="imgClass()"
222
+ [hasPriority]="hasPriority()"
223
+ [figureClass]="figureClass()"
224
+ [pictureClass]="pictureClass()"
225
+ [figcaptionClass]="figcaptionClass()"
226
+ [defaultSrc]="_defaultSrc()"
227
+ [alt]="asset()?.fields?.title ?? null"
228
+ [figcaption]="asset()?.fields?.description ?? null"
229
+ [width]="asset()?.fields?.file?.details?.image?.width ?? null"
230
+ [height]="asset()?.fields?.file?.details?.image?.height ?? null"
231
+ [sizes]="sizes()"
232
+ [sources]="_sources()"
261
233
  />
262
234
  `,
263
235
  changeDetection: ChangeDetectionStrategy.OnPush,
264
236
  encapsulation: ViewEncapsulation.None,
265
237
  standalone: true,
266
- imports: [NgClass, NgOptimizedImage, PictureComponent],
267
- hostDirectives: [
268
- {
269
- directive: PictureDataDirective,
270
- // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
271
- inputs: ['imgClass', 'hasPriority', 'figureClass', 'pictureClass', 'figcaptionClass'],
272
- },
273
- ],
238
+ imports: [PictureComponent],
274
239
  host: {
275
240
  class: 'et-contentful-image',
276
241
  },
277
242
  }]
278
- }], propDecorators: { data: [{
279
- type: Input
280
- }], sizes: [{
281
- type: Input
282
- }], srcsetSizes: [{
283
- type: Input
284
- }], backgroundColor: [{
285
- type: Input
286
- }] } });
243
+ }] });
287
244
 
288
245
  class ContentfulVideoComponent {
289
246
  constructor() {
290
- this._richTextData = inject(RICH_TEXT_RENDERER_COMPONENT_DATA, { optional: true });
291
- this._data = null;
292
- this.videoClass = null;
293
- }
294
- get data() {
295
- return this._data;
296
- }
297
- set data(v) {
298
- if (v && !v.contentType) {
299
- this._data = null;
300
- console.warn('The provided asset is invalid', v);
301
- return;
302
- }
303
- if (v && !v.contentType.startsWith('video/')) {
304
- throw new Error('The provided asset is not an video');
305
- }
306
- this._data = v ?? null;
247
+ this.asset = input.required();
248
+ this.videoClass = input(null);
307
249
  }
308
- ngOnInit() {
309
- if (this._richTextData && !this.data) {
310
- this.data = this._richTextData;
311
- }
312
- }
313
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.0", ngImport: i0, type: ContentfulVideoComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
314
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.0", type: ContentfulVideoComponent, isStandalone: true, selector: "et-contentful-video", inputs: { data: "data", videoClass: "videoClass" }, ngImport: i0, template: `
315
- @if (data) {
316
- <video [ngClass]="videoClass" controls>
317
- <source src="{{ data.url }}" type="{{ data.contentType }}" />
250
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.3", ngImport: i0, type: ContentfulVideoComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
251
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.3", 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: `
252
+ @if (asset(); as data) {
253
+ <video [ngClass]="videoClass()" controls>
254
+ <source src="{{ data.fields.file.url }}" type="{{ data.fields.file.contentType }}" />
318
255
  </video>
319
256
  }
320
257
  `, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
321
258
  }
322
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.0", ngImport: i0, type: ContentfulVideoComponent, decorators: [{
259
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.3", ngImport: i0, type: ContentfulVideoComponent, decorators: [{
323
260
  type: Component,
324
261
  args: [{
325
262
  selector: 'et-contentful-video',
326
263
  template: `
327
- @if (data) {
328
- <video [ngClass]="videoClass" controls>
329
- <source src="{{ data.url }}" type="{{ data.contentType }}" />
264
+ @if (asset(); as data) {
265
+ <video [ngClass]="videoClass()" controls>
266
+ <source src="{{ data.fields.file.url }}" type="{{ data.fields.file.contentType }}" />
330
267
  </video>
331
268
  }
332
269
  `,
@@ -335,11 +272,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.0", ngImpor
335
272
  standalone: true,
336
273
  imports: [NgClass],
337
274
  }]
338
- }], propDecorators: { data: [{
339
- type: Input
340
- }], videoClass: [{
341
- type: Input
342
- }] } });
275
+ }] });
343
276
 
344
277
  const createContentfulConfig = (config = {}) => ({
345
278
  // This default config must be inline and not saved inside a const for some weird webpack reason
@@ -361,118 +294,28 @@ const createContentfulConfig = (config = {}) => ({
361
294
  ...(config || {}),
362
295
  });
363
296
 
364
- const RICH_TEXT_RENDERER_COMPONENT_DATA = new InjectionToken('RICH_TEXT_RENDERER_COMPONENT_DATA');
365
-
366
- const createRenderCommandsFromContentfulRichText = (options) => {
367
- const { data, config } = options;
368
- const commands = [];
369
- const resourceMap = createContentfulResourceMap(data.links);
370
- createContentfulRenderCommand(data.json, { children: commands, payload: data.json.nodeType }, resourceMap, config);
371
- return commands;
372
- };
373
- const createContentfulRenderCommand = (node, parent, resourceMap, config) => {
374
- const { nodeType, data: nodeData } = node;
375
- const { children, marks, value } = getContentfulNodeProps(node);
376
- const command = {
377
- payload: translateContentfulNodeTypeToHtmlTag(nodeType),
378
- data: value,
379
- attributes: {
380
- ...(marks
381
- ? {
382
- class: transformContentfulMarks(marks, config.useTailwindClasses),
383
- }
384
- : {}),
385
- ...(nodeData['uri'] ? { href: nodeData['uri'], target: '_blank' } : {}),
386
- },
387
- children: [],
388
- };
389
- const entryId = nodeData['target']?.sys?.id;
390
- if (nodeType === BLOCKS.EMBEDDED_ASSET) {
391
- const asset = resourceMap.assetsBlock.get(entryId);
392
- if (asset) {
393
- const isImage = asset.contentType.startsWith('image/');
394
- const isVideo = asset.contentType.startsWith('video/');
395
- const isAudio = asset.contentType.startsWith('audio/');
396
- command.data = asset;
397
- if (isImage) {
398
- command.payload = config.components.image;
399
- }
400
- else if (isVideo) {
401
- command.payload = config.components.video;
402
- }
403
- else if (isAudio) {
404
- command.payload = config.components.audio;
405
- }
406
- else {
407
- command.payload = config.components.file;
408
- }
409
- }
410
- else {
411
- console.warn(`No asset found for entry id "${entryId}"!`, nodeData);
412
- }
413
- }
414
- else if (nodeType === BLOCKS.EMBEDDED_ENTRY || nodeType === INLINES.EMBEDDED_ENTRY) {
415
- const entry = resourceMap.entriesBlock.get(entryId) ?? resourceMap.entriesInline.get(entryId);
416
- if (entry) {
417
- const type = entry?.__typename ?? 'ET_UNKNOWN';
418
- // "Image" is a special wrapper content model for contentful assets.
419
- // It must be created by the user.
420
- const component = type === 'Image' ? ContentfulImageComponent : config.customComponents?.[type];
421
- if (component) {
422
- command.payload = component;
423
- command.data = entry;
424
- }
425
- else {
426
- command.data = `ERROR: No component found for embedded entry "${type}" as "${nodeType}"!`;
427
- }
428
- }
429
- else {
430
- console.warn(`No entry found for entry id "${entryId}"!`, nodeData);
431
- }
432
- }
433
- if (children && (children.length !== 1 || children[0]?.nodeType !== 'text')) {
434
- children.forEach((child) => {
435
- createContentfulRenderCommand(child, command, resourceMap, config);
436
- });
437
- }
438
- parent.children.push(command);
439
- return command;
297
+ const RICH_TEXT_RENDERER_ERRORS = {
298
+ rich_text_undefined: 'The property value given at richTextPath is undefined. Use the richTextPath input to specify the path to the rich text object.',
299
+ rich_text_wrong_type: 'The rich text object does not satisfy the RichTextResponse interface. It should contain a property named "nodeType" with the value "document".',
300
+ asset_id_not_found: 'The asset ID was not found. This node is not supported.',
301
+ entry_id_not_found: 'The entry ID was not found. This node is not supported.',
302
+ asset_not_found: 'The asset was not found. This node is not supported.',
303
+ entry_not_found: 'The entry was not found. This node is not supported.',
304
+ custom_component_not_found: 'No custom component found for entry type. Please provide one for this type.',
305
+ text_parent_not_found: 'The parent node is not found. This structure is not supported.',
306
+ text_parent_wrong_type: 'The parent node neither a html element nor a custom component. This structure is not supported.',
440
307
  };
441
- const getContentfulNodeProps = (node) => {
442
- const children = 'content' in node ? node.content : null;
443
- let marks = null;
444
- let value = null;
445
- if (children && children.length === 1 && children[0]?.nodeType === 'text') {
446
- marks = children[0].marks?.length ? children[0].marks : null;
447
- value = children[0].value;
448
- }
449
- else {
450
- marks = 'marks' in node ? (node.marks?.length ? node.marks : null) : null;
451
- value = 'value' in node ? node.value : null;
452
- }
453
- return {
454
- children,
455
- marks,
456
- value,
457
- };
308
+ const RICH_TEXT_RENDERER_ERROR_CODES = Object.keys(RICH_TEXT_RENDERER_ERRORS).reduce((acc, key, index) => {
309
+ acc[key] = index;
310
+ return acc;
311
+ }, {});
312
+ const richTextRendererError = (code, devOnly, data) => {
313
+ const message = `<et-contentful-rich-text-renderer>: ${RICH_TEXT_RENDERER_ERRORS[code]}`;
314
+ throw new RuntimeError(RICH_TEXT_RENDERER_ERROR_CODES[code], message, devOnly, data);
458
315
  };
459
- const transformContentfulMarks = (marks, useTailwindClasses) => {
460
- return marks
461
- .map((mark) => {
462
- switch (mark.type) {
463
- case 'bold':
464
- return useTailwindClasses ? 'font-bold' : mark.type;
465
- case 'italic':
466
- return useTailwindClasses ? 'italic' : mark.type;
467
- case 'underline':
468
- return useTailwindClasses ? 'underline' : mark.type;
469
- case 'code':
470
- return useTailwindClasses ? 'font-mono' : mark.type;
471
- default:
472
- return mark.type;
473
- }
474
- })
475
- .join(' ');
316
+
317
+ const isRichTextRootNode = (node) => {
318
+ return isObject(node) && 'nodeType' in node && node['nodeType'] === 'document';
476
319
  };
477
320
  const translateContentfulNodeTypeToHtmlTag = (nodeType) => {
478
321
  switch (nodeType) {
@@ -529,116 +372,686 @@ const translateContentfulNodeTypeToHtmlTag = (nodeType) => {
529
372
  return 'div';
530
373
  }
531
374
  };
532
- const createContentfulResourceMap = (links) => {
533
- const resourceMap = {
534
- assetsBlock: createContentfulMap(links?.assets?.block),
535
- entriesInline: createContentfulMap(links?.entries?.inline),
536
- entriesBlock: createContentfulMap(links?.entries?.block),
537
- };
538
- return resourceMap;
375
+
376
+ const RENDER_COMMAND_TYPE = {
377
+ HTML_OPEN: 0,
378
+ HTML_CLOSE: 1,
379
+ TEXT: 2,
380
+ COMPONENT: 3,
539
381
  };
540
- const createContentfulMap = (links) => {
541
- const assetMap = new Map();
542
- if (links) {
543
- for (const link of links) {
544
- if (!link?.sys?.id) {
545
- console.warn('Link has no sys.id property. Please include it inside your query.', link);
546
- continue;
547
- }
548
- assetMap.set(link.sys.id, link);
382
+ const RENDER_COMMAND_POSITION = {
383
+ TYPE: 0,
384
+ NESTING_LEVEL: 1,
385
+ DOM_POSITION: 2,
386
+ INDEX: 3,
387
+ };
388
+ const HTML_OPEN_RENDER_COMMAND_POSITION = {
389
+ ATTRIBUTES: 4,
390
+ TAG_NAME: 5,
391
+ ELEMENT_ID: 6,
392
+ };
393
+ const HTML_CLOSE_RENDER_COMMAND_POSITION = {
394
+ TAG_NAME: 4,
395
+ ELEMENT_ID: 5,
396
+ };
397
+ const TEXT_RENDER_COMMAND_POSITION = {
398
+ TEXT: 4,
399
+ };
400
+ const COMPONENT_RENDER_COMMAND_POSITION = {
401
+ ATTRIBUTES: 4,
402
+ COMPONENT: 5,
403
+ INPUTS: 6,
404
+ COMPONENT_TYPE: 7,
405
+ };
406
+ const RENDER_INSTRUCTION_POSITION = {
407
+ TYPE: 0,
408
+ COMMAND: 1,
409
+ };
410
+ const RENDER_INSTRUCTION_TYPE = {
411
+ CREATE: 0,
412
+ UPDATE: 1,
413
+ DELETE: 2,
414
+ MOVE: 3,
415
+ };
416
+ const MARK_TAILWIND_MAP = {
417
+ bold: 'font-bold',
418
+ italic: 'italic',
419
+ underline: 'underline',
420
+ code: 'font-mono',
421
+ };
422
+ const marksToClass = (marks) => {
423
+ const classes = [];
424
+ for (const mark of marks) {
425
+ let klass = MARK_TAILWIND_MAP[mark.type];
426
+ if (!klass) {
427
+ console.warn(`No class found for mark type! Falling back to "${mark.type}".`, mark);
428
+ klass = mark.type;
549
429
  }
430
+ classes.push(klass);
550
431
  }
551
- return assetMap;
432
+ return classes.join(' ');
433
+ };
434
+ const isHtmlOpenRenderCommand = (command) => {
435
+ return command[RENDER_COMMAND_POSITION.TYPE] === RENDER_COMMAND_TYPE.HTML_OPEN;
436
+ };
437
+ const isHtmlCloseRenderCommand = (command) => {
438
+ return command[RENDER_COMMAND_POSITION.TYPE] === RENDER_COMMAND_TYPE.HTML_CLOSE;
439
+ };
440
+ const isTextRenderCommand = (command) => {
441
+ return command[RENDER_COMMAND_POSITION.TYPE] === RENDER_COMMAND_TYPE.TEXT;
442
+ };
443
+ const isComponentRenderCommand = (command) => {
444
+ return command[RENDER_COMMAND_POSITION.TYPE] === RENDER_COMMAND_TYPE.COMPONENT;
445
+ };
446
+ const getRenderCommandId = (command) => {
447
+ if (isHtmlOpenRenderCommand(command)) {
448
+ return command[HTML_OPEN_RENDER_COMMAND_POSITION.ELEMENT_ID];
449
+ }
450
+ else if (isHtmlCloseRenderCommand(command)) {
451
+ return command[HTML_CLOSE_RENDER_COMMAND_POSITION.ELEMENT_ID];
452
+ }
453
+ else if (isTextRenderCommand(command)) {
454
+ return command[TEXT_RENDER_COMMAND_POSITION.TEXT];
455
+ }
456
+ else {
457
+ return command[COMPONENT_RENDER_COMMAND_POSITION.COMPONENT_TYPE];
458
+ }
459
+ };
460
+ const CLASS_ATTR = 'class';
461
+ const DEFAULT_COMPONENT_TYPES = {
462
+ IMAGE: '$$$_et-image',
463
+ VIDEO: '$$$_et-video',
464
+ AUDIO: '$$$_et-audio',
465
+ FILE: '$$$_et-file',
466
+ };
467
+ const isExecutedComponentCommandCacheItem = (cache) => {
468
+ return isComponentRenderCommand(cache.command);
469
+ };
470
+ const isExecutedHtmlOrTextCommandCacheItem = (cache) => {
471
+ return isHtmlCloseRenderCommand(cache.command) || isHtmlOpenRenderCommand(cache.command);
472
+ };
473
+ const isExecutedTextCommandCacheItem = (cache) => {
474
+ return isTextRenderCommand(cache.command);
552
475
  };
553
-
554
476
  class ContentfulRichTextRendererComponent {
555
477
  constructor() {
556
- this._richText = null;
478
+ this._viewContainerRef = inject(ViewContainerRef);
557
479
  this._renderer = inject(Renderer2);
558
480
  this._elementRef = inject(ElementRef);
559
- this._componentFactoryResolver = inject(ComponentFactoryResolver);
560
481
  this._document = inject(DOCUMENT);
561
- this._appRef = inject(ApplicationRef);
562
- this._injector = inject(Injector);
563
482
  this._config = inject(CONTENTFUL_CONFIG, { optional: true }) ?? createContentfulConfig();
483
+ /**
484
+ * A cache for all executed commands that are not deleted.
485
+ * This is used to keep track of all rendered elements and components.
486
+ * The key is the render command element ID.
487
+ */
488
+ this._executedCommandsCache = new Map();
489
+ /**
490
+ * The contentful response gotten via their REST api.
491
+ * @see https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/entries/entries-collection
492
+ */
493
+ this.content = input.required();
494
+ /**
495
+ * The path to where the rich text field is inside the contentful response. Dot and array notation is allowed.
496
+ * @default "items[0].fields.html"
497
+ */
498
+ this.richTextPath = input.required();
499
+ /**
500
+ * A map of all includes in the contentful response.
501
+ * This is useful for looking up assets and entries that are referenced in the rich text without having to use loops.
502
+ */
503
+ this.contentIncludesMap = computed(() => {
504
+ const content = this.content();
505
+ const assets = content?.includes.Asset;
506
+ const entries = content?.includes.Entry;
507
+ return {
508
+ assets: assets?.reduce((acc, asset) => {
509
+ acc[asset.sys.id] = asset;
510
+ return acc;
511
+ }, {}) ?? {},
512
+ entries: entries?.reduce((acc, entry) => {
513
+ acc[entry.sys.id] = entry;
514
+ return acc;
515
+ }, {}) ?? {},
516
+ };
517
+ });
518
+ /**
519
+ * The rich text data that should be rendered.
520
+ */
521
+ this.richTextData = computed(() => {
522
+ const content = this.content();
523
+ const richTextPath = this.richTextPath();
524
+ if (!content) {
525
+ return null;
526
+ }
527
+ const richText = getObjectProperty(content, richTextPath);
528
+ if (!isObject(richText)) {
529
+ throw richTextRendererError('rich_text_undefined', false, { content, richTextPath });
530
+ }
531
+ if (!isRichTextRootNode(richText)) {
532
+ throw richTextRendererError('rich_text_wrong_type', false, { content, richTextPath });
533
+ }
534
+ return richText;
535
+ });
536
+ this.renderCommands = computed(() => {
537
+ const richTextData = this.richTextData();
538
+ if (!richTextData) {
539
+ return [];
540
+ }
541
+ return this._createRenderCommands(richTextData);
542
+ });
543
+ this.renderCommandHistory = toSignal(toObservable(this.renderCommands).pipe(startWith([]), pairwise()), { initialValue: [[], []] });
544
+ this.previousRenderCommandMap = computed(() => {
545
+ const [prevCommands] = this.renderCommandHistory();
546
+ const map = new Map();
547
+ for (const command of prevCommands) {
548
+ map.set(getRenderCommandId(command), command);
549
+ }
550
+ return map;
551
+ });
552
+ this.renderInstructions = computed(() => {
553
+ const commands = this.renderCommands();
554
+ const previousRenderCommandMap = new Map(this.previousRenderCommandMap());
555
+ const instructions = [];
556
+ // remove deleted components as well as all other elements
557
+ for (const [, command] of previousRenderCommandMap) {
558
+ if (isComponentRenderCommand(command)) {
559
+ // keep the component around if it's still in the new commands
560
+ if (commands.some((c) => getRenderCommandId(c) === getRenderCommandId(command))) {
561
+ continue;
562
+ }
563
+ }
564
+ instructions.push([RENDER_INSTRUCTION_TYPE.DELETE, command]);
565
+ // remove from map to mark as used
566
+ previousRenderCommandMap.delete(getRenderCommandId(command));
567
+ }
568
+ // Find the indexes of the commands remaining in the previousRenderCommandMap in the new commands array.
569
+ const lastComponentIndexes = [];
570
+ for (const [id, command] of previousRenderCommandMap) {
571
+ const index = commands.findIndex((c) => getRenderCommandId(c) === id);
572
+ const newCommand = commands[index];
573
+ if (index === -1) {
574
+ throw new Error('Command not found!');
575
+ }
576
+ lastComponentIndexes.push({
577
+ newIndex: index,
578
+ prevIndex: command[RENDER_COMMAND_POSITION.INDEX],
579
+ command: newCommand,
580
+ });
581
+ }
582
+ lastComponentIndexes.sort((a, b) => a.newIndex - b.newIndex);
583
+ // check if the prevIndex are in order ascending
584
+ // for indexes that are not in order, create a move instruction
585
+ // for indexes that are in order, create an update instruction
586
+ let moveCheckIndex = 0;
587
+ while (moveCheckIndex < lastComponentIndexes.length - 1) {
588
+ const moveCurr = lastComponentIndexes[moveCheckIndex];
589
+ const moveNext = lastComponentIndexes[moveCheckIndex + 1];
590
+ if (!moveCurr || !moveNext) {
591
+ break;
592
+ }
593
+ if (moveCurr.prevIndex > moveNext.prevIndex) {
594
+ lastComponentIndexes.splice(moveCheckIndex + 1, 1);
595
+ instructions.push([RENDER_INSTRUCTION_TYPE.MOVE, moveNext.command]);
596
+ }
597
+ else {
598
+ instructions.push([RENDER_INSTRUCTION_TYPE.UPDATE, moveNext.command]);
599
+ moveCheckIndex++;
600
+ }
601
+ }
602
+ for (const command of commands) {
603
+ const id = getRenderCommandId(command);
604
+ if (isComponentRenderCommand(command)) {
605
+ const previousCommand = previousRenderCommandMap.get(id);
606
+ if (!previousCommand) {
607
+ // create
608
+ instructions.push([RENDER_INSTRUCTION_TYPE.CREATE, command]);
609
+ }
610
+ }
611
+ else {
612
+ // create
613
+ instructions.push([RENDER_INSTRUCTION_TYPE.CREATE, command]);
614
+ }
615
+ }
616
+ return instructions;
617
+ });
618
+ toObservable(this.renderInstructions)
619
+ .pipe(takeUntilDestroyed(), tap((instructions) => {
620
+ this._execInstructions(instructions);
621
+ }))
622
+ .subscribe();
564
623
  }
565
- get richText() {
566
- return this._richText;
624
+ _createRenderCommands(richTextData) {
625
+ /** List of all render commands */
626
+ const rootCommands = [];
627
+ /** Counter for generating unique html element IDs. */
628
+ let elementId = 0;
629
+ /** Counter for generating unique component IDs. */
630
+ const componentIdMap = new Map();
631
+ /** The nesting level of the current node. */
632
+ let nestingLevel = 0;
633
+ /** The position (index) of the current node inside the parent node. */
634
+ let domPosition = 0;
635
+ let commandIndex = 0;
636
+ const traverse = (node, parent = null) => {
637
+ switch (node.nodeType) {
638
+ case 'text': {
639
+ // Render media
640
+ const type = RENDER_COMMAND_TYPE.TEXT;
641
+ const text = node.value;
642
+ if (!text)
643
+ break;
644
+ const command = [type, nestingLevel, domPosition, commandIndex++, text];
645
+ rootCommands.push(command);
646
+ domPosition++;
647
+ if (!parent) {
648
+ throw richTextRendererError('text_parent_not_found', false, { node });
649
+ }
650
+ if (!isHtmlOpenRenderCommand(parent) && !isComponentRenderCommand(parent)) {
651
+ throw richTextRendererError('text_parent_wrong_type', false, { node, parent });
652
+ }
653
+ const attributesPosition = parent[RENDER_COMMAND_POSITION.TYPE] === RENDER_COMMAND_TYPE.HTML_OPEN
654
+ ? HTML_OPEN_RENDER_COMMAND_POSITION.ATTRIBUTES
655
+ : COMPONENT_RENDER_COMMAND_POSITION.ATTRIBUTES;
656
+ const attributes = parent[attributesPosition];
657
+ if (node.marks.length) {
658
+ const markClasses = marksToClass(node.marks);
659
+ if (CLASS_ATTR in attributes) {
660
+ attributes[CLASS_ATTR] += ` ${markClasses}`;
661
+ }
662
+ else {
663
+ attributes[CLASS_ATTR] = markClasses;
664
+ }
665
+ }
666
+ break;
667
+ }
668
+ case BLOCKS.EMBEDDED_ASSET: {
669
+ // Render media
670
+ const type = RENDER_COMMAND_TYPE.COMPONENT;
671
+ const assetId = node.data['target']?.sys?.id;
672
+ if (!assetId) {
673
+ throw richTextRendererError('asset_id_not_found', false, { node });
674
+ }
675
+ const asset = this.contentIncludesMap().assets[assetId];
676
+ if (!asset) {
677
+ throw richTextRendererError('asset_not_found', false, { assetId, node });
678
+ }
679
+ const contentType = asset.fields.file.contentType;
680
+ const assetComponents = this._config.components;
681
+ const isImage = contentType.startsWith('image/');
682
+ const isVideo = contentType.startsWith('video/');
683
+ const isAudio = contentType.startsWith('audio/');
684
+ const component = isImage
685
+ ? assetComponents.image
686
+ : isVideo
687
+ ? assetComponents.video
688
+ : isAudio
689
+ ? assetComponents.audio
690
+ : assetComponents.file;
691
+ const componentType = isImage
692
+ ? DEFAULT_COMPONENT_TYPES.IMAGE
693
+ : isVideo
694
+ ? DEFAULT_COMPONENT_TYPES.VIDEO
695
+ : isAudio
696
+ ? DEFAULT_COMPONENT_TYPES.AUDIO
697
+ : DEFAULT_COMPONENT_TYPES.FILE;
698
+ let componentId = componentIdMap.get(componentType) ?? -1;
699
+ const id = componentType + ++componentId;
700
+ componentIdMap.set(componentType, componentId);
701
+ const attributes = {};
702
+ const inputs = {
703
+ asset,
704
+ };
705
+ const command = [
706
+ type,
707
+ nestingLevel,
708
+ domPosition,
709
+ commandIndex++,
710
+ attributes,
711
+ component,
712
+ inputs,
713
+ id,
714
+ ];
715
+ rootCommands.push(command);
716
+ domPosition++;
717
+ break;
718
+ }
719
+ case BLOCKS.EMBEDDED_ENTRY:
720
+ case INLINES.EMBEDDED_ENTRY: {
721
+ // Render component
722
+ const type = RENDER_COMMAND_TYPE.COMPONENT;
723
+ const entryId = node.data['target']?.sys?.id;
724
+ if (!entryId) {
725
+ throw richTextRendererError('entry_id_not_found', false, { node });
726
+ }
727
+ const entry = this.contentIncludesMap().entries[entryId];
728
+ if (!entry) {
729
+ throw richTextRendererError('entry_not_found', false, { entryId, node });
730
+ }
731
+ const componentType = entry.sys.contentType.sys.id;
732
+ const component = this._config.customComponents[componentType];
733
+ if (!component) {
734
+ throw richTextRendererError('custom_component_not_found', false, {
735
+ componentType,
736
+ customComponents: this._config.customComponents,
737
+ entry,
738
+ });
739
+ }
740
+ const attributes = {};
741
+ let componentId = componentIdMap.get(componentType) ?? -1;
742
+ const id = componentType + ++componentId;
743
+ componentIdMap.set(componentType, componentId);
744
+ const inputs = {
745
+ fields: entry.fields,
746
+ metadata: entry.metadata,
747
+ sys: entry.sys,
748
+ includes: this.contentIncludesMap(),
749
+ };
750
+ const command = [
751
+ type,
752
+ nestingLevel,
753
+ domPosition,
754
+ commandIndex++,
755
+ attributes,
756
+ component,
757
+ inputs,
758
+ id,
759
+ ];
760
+ rootCommands.push(command);
761
+ domPosition++;
762
+ break;
763
+ }
764
+ default: {
765
+ const type = RENDER_COMMAND_TYPE.HTML_OPEN;
766
+ const tag = translateContentfulNodeTypeToHtmlTag(node.nodeType);
767
+ const attributes = {};
768
+ const id = 'e' + elementId++;
769
+ const command = [type, nestingLevel, domPosition, commandIndex++, attributes, tag, id];
770
+ rootCommands.push(command);
771
+ const domPositionAtThisLevel = domPosition;
772
+ // Normal html elements can have children
773
+ for (const child of node.content) {
774
+ domPosition = 0;
775
+ nestingLevel++;
776
+ traverse(child, command);
777
+ nestingLevel--;
778
+ domPosition = domPositionAtThisLevel;
779
+ }
780
+ const lastCommand = rootCommands[rootCommands.length - 1];
781
+ if (lastCommand?.[RENDER_COMMAND_POSITION.TYPE] === RENDER_COMMAND_TYPE.HTML_OPEN) {
782
+ // If the last command is an open command, we can remove it since it's empty
783
+ rootCommands.pop();
784
+ }
785
+ else {
786
+ const closeId = 'e' + elementId++;
787
+ const closeCommand = [
788
+ RENDER_COMMAND_TYPE.HTML_CLOSE,
789
+ nestingLevel,
790
+ domPosition,
791
+ commandIndex++,
792
+ tag,
793
+ closeId,
794
+ ];
795
+ rootCommands.push(closeCommand);
796
+ domPosition++;
797
+ }
798
+ if (node.nodeType === INLINES.HYPERLINK) {
799
+ const uri = node.data['uri'];
800
+ attributes['href'] = uri;
801
+ const host = this._document.location.host;
802
+ const isExternal = !uri.includes(host);
803
+ if (isExternal) {
804
+ attributes['target'] = '_blank';
805
+ }
806
+ }
807
+ break;
808
+ }
809
+ }
810
+ };
811
+ for (const node of richTextData.content) {
812
+ traverse(node, null);
813
+ }
814
+ return rootCommands;
567
815
  }
568
- set richText(v) {
569
- this._richText = v ?? null;
570
- this.resetAndRender();
816
+ _execInstructions(instructions) {
817
+ for (const instruction of instructions) {
818
+ const type = instruction[RENDER_INSTRUCTION_POSITION.TYPE];
819
+ const command = instruction[RENDER_INSTRUCTION_POSITION.COMMAND];
820
+ switch (type) {
821
+ case RENDER_INSTRUCTION_TYPE.CREATE:
822
+ this._runCreateInstruction(command);
823
+ break;
824
+ case RENDER_INSTRUCTION_TYPE.UPDATE:
825
+ this._runUpdateInstruction(command);
826
+ break;
827
+ case RENDER_INSTRUCTION_TYPE.MOVE:
828
+ this._runMoveInstruction(command);
829
+ break;
830
+ case RENDER_INSTRUCTION_TYPE.DELETE:
831
+ this._runDeleteInstruction(command);
832
+ break;
833
+ }
834
+ }
571
835
  }
572
- resetAndRender() {
573
- // clear childNodes to prevent appending new richtext elements to existing one
574
- const childElements = this._elementRef.nativeElement.childNodes;
575
- for (const child of childElements) {
576
- this._renderer.removeChild(this._elementRef.nativeElement, child);
836
+ _runCreateInstruction(command) {
837
+ const id = getRenderCommandId(command);
838
+ const parentElement = this._findParent(command);
839
+ const nextElement = this._findFollowingElement(command);
840
+ if (isComponentRenderCommand(command)) {
841
+ const componentRef = this._viewContainerRef.createComponent(command[COMPONENT_RENDER_COMMAND_POSITION.COMPONENT]);
842
+ this._updateComponentInputs(command, componentRef);
843
+ const rootNode = this._getComponentRootNode(componentRef);
844
+ this._renderInsertOrAppend(rootNode, parentElement, nextElement);
845
+ this._executedCommandsCache.set(id, {
846
+ command,
847
+ componentRef,
848
+ element: rootNode,
849
+ });
577
850
  }
578
- if (!this._richText) {
579
- return;
851
+ else if (isTextRenderCommand(command)) {
852
+ const text = command[TEXT_RENDER_COMMAND_POSITION.TEXT];
853
+ const textNode = this._renderer.createText(text);
854
+ const span = this._renderer.createElement('span');
855
+ this._renderer.appendChild(span, textNode);
856
+ this._renderInsertOrAppend(span, parentElement, nextElement);
857
+ this._executedCommandsCache.set(id, {
858
+ command,
859
+ element: span,
860
+ textNode,
861
+ });
862
+ }
863
+ else if (isHtmlOpenRenderCommand(command)) {
864
+ const tag = command[HTML_OPEN_RENDER_COMMAND_POSITION.TAG_NAME];
865
+ const attributes = command[HTML_OPEN_RENDER_COMMAND_POSITION.ATTRIBUTES];
866
+ const element = this._renderer.createElement(tag);
867
+ for (const [key, value] of Object.entries(attributes)) {
868
+ this._renderer.setAttribute(element, key, value);
869
+ }
870
+ this._renderInsertOrAppend(element, parentElement, nextElement);
871
+ this._executedCommandsCache.set(id, {
872
+ command,
873
+ element,
874
+ });
580
875
  }
581
- const commands = createRenderCommandsFromContentfulRichText({ data: this._richText, config: this._config });
582
- this._render(commands);
583
876
  }
584
- _render(commands) {
585
- // create a document fragment to hold the elements while we create them
586
- const fragment = document.createDocumentFragment();
587
- const hostCommand = {
588
- payload: 'document',
589
- children: commands,
590
- };
591
- // loop over the commands
592
- this._executeCommand({ command: hostCommand, parent: fragment });
593
- // append the fragment to the host element
594
- this._renderer.appendChild(this._elementRef.nativeElement, fragment);
595
- }
596
- _executeCommand(data) {
597
- const { command, parent } = data;
598
- let element = null;
599
- if (command.payload !== 'document') {
600
- if (typeof command.payload === 'string') {
601
- element = this._renderer.createElement(command.payload);
602
- if (command.data) {
603
- this._renderer.setProperty(element, 'innerText', command.data);
877
+ _runUpdateInstruction(command) {
878
+ const id = getRenderCommandId(command);
879
+ const cached = this._executedCommandsCache.get(id);
880
+ if (!cached) {
881
+ throw new Error('Cached command not found!');
882
+ }
883
+ if (isComponentRenderCommand(command)) {
884
+ if (!isExecutedComponentCommandCacheItem(cached)) {
885
+ throw new Error('Cached command is not a component command!');
886
+ }
887
+ const componentRef = cached.componentRef;
888
+ if (!componentRef) {
889
+ throw new Error('Component ref not found!');
890
+ }
891
+ this._updateComponentInputs(command, componentRef);
892
+ }
893
+ this._executedCommandsCache.set(id, {
894
+ ...cached,
895
+ command: command,
896
+ });
897
+ }
898
+ _runMoveInstruction(command) {
899
+ const id = getRenderCommandId(command);
900
+ const cached = this._executedCommandsCache.get(id);
901
+ if (!cached) {
902
+ throw new Error('Cached command not found!');
903
+ }
904
+ if (isComponentRenderCommand(command)) {
905
+ if (!isExecutedComponentCommandCacheItem(cached)) {
906
+ throw new Error('Cached command is not a component command!');
907
+ }
908
+ const componentRef = cached.componentRef;
909
+ if (!componentRef) {
910
+ throw new Error('Component ref not found!');
911
+ }
912
+ const rootNode = cached.element;
913
+ const oldParentElement = cached.element.parentElement;
914
+ this._renderer.removeChild(oldParentElement, rootNode);
915
+ const newParentElement = this._findParent(command);
916
+ const nextElement = this._findFollowingElement(command);
917
+ this._renderInsertOrAppend(rootNode, newParentElement, nextElement);
918
+ this._updateComponentInputs(command, componentRef);
919
+ this._executedCommandsCache.set(id, {
920
+ ...cached,
921
+ command: command,
922
+ });
923
+ }
924
+ }
925
+ _runDeleteInstruction(command) {
926
+ const id = getRenderCommandId(command);
927
+ const cached = this._executedCommandsCache.get(id);
928
+ if (!cached) {
929
+ if (isHtmlCloseRenderCommand(command)) {
930
+ return;
931
+ }
932
+ throw new Error('Cached command not found!');
933
+ }
934
+ if (isComponentRenderCommand(command)) {
935
+ if (!isExecutedComponentCommandCacheItem(cached)) {
936
+ throw new Error('Cached command is not a component command!');
937
+ }
938
+ cached.componentRef?.destroy();
939
+ }
940
+ else if (isTextRenderCommand(command)) {
941
+ this._renderer.removeChild(cached.element?.parentElement, cached.element);
942
+ }
943
+ else if (isHtmlOpenRenderCommand(command)) {
944
+ this._renderer.removeChild(cached.element?.parentElement, cached.element);
945
+ }
946
+ this._executedCommandsCache.delete(id);
947
+ }
948
+ _getComponentRootNode(componentRef) {
949
+ return componentRef.hostView.rootNodes[0];
950
+ }
951
+ _updateComponentInputs(command, componentRef) {
952
+ for (const [key, value] of Object.entries(command[COMPONENT_RENDER_COMMAND_POSITION.INPUTS])) {
953
+ if (!componentRef.instance[key])
954
+ continue;
955
+ componentRef.setInput(key, value);
956
+ }
957
+ }
958
+ _findParent(command) {
959
+ const hostElement = this._elementRef.nativeElement;
960
+ const nestingLevel = command[RENDER_COMMAND_POSITION.NESTING_LEVEL];
961
+ let parentElement = undefined;
962
+ if (nestingLevel === 0) {
963
+ parentElement = hostElement;
964
+ }
965
+ else {
966
+ // Reverse search all render commands beginning from the current one using a for loop.
967
+ // The parent is found when the command's nesting level ist the same as the current nesting level minus 1.
968
+ // If the parent is not found, throw an error.
969
+ const allCommands = this.renderCommands();
970
+ const currentCommandIndex = command[RENDER_COMMAND_POSITION.INDEX];
971
+ let parentCommand = null;
972
+ for (let i = currentCommandIndex - 1; i >= 0; i--) {
973
+ const cmd = allCommands[i];
974
+ if (!cmd) {
975
+ throw new Error('Command not found!');
604
976
  }
605
- if (command.attributes) {
606
- for (const [name, value] of Object.entries(command.attributes)) {
607
- this._renderer.setAttribute(element, name, value);
608
- }
977
+ if (cmd[RENDER_COMMAND_POSITION.NESTING_LEVEL] === nestingLevel - 1) {
978
+ parentCommand = cmd;
979
+ break;
609
980
  }
610
- this._renderer.appendChild(parent, element);
611
981
  }
612
- else {
613
- const injector = Injector.create({
614
- providers: [
615
- {
616
- provide: RICH_TEXT_RENDERER_COMPONENT_DATA,
617
- useValue: command.data,
618
- },
619
- ],
620
- parent: this._injector,
621
- });
622
- const portal = new DomPortalOutlet(parent, this._componentFactoryResolver, this._appRef, injector, this._document);
623
- const comp = new ComponentPortal(command.payload);
624
- const ref = comp.attach(portal);
625
- if (ref.instance instanceof ContentfulImageComponent) {
626
- ref.instance.sizes = this._config.imageOptions.sizes;
627
- ref.instance.srcsetSizes = this._config.imageOptions.srcsetSizes;
628
- ref.instance.backgroundColor = this._config.imageOptions.backgroundColor;
629
- }
630
- element = ref.location.nativeElement;
982
+ if (!parentCommand) {
983
+ throw new Error('Parent command not found!');
984
+ }
985
+ parentElement = this._executedCommandsCache.get(getRenderCommandId(parentCommand))?.element;
986
+ }
987
+ if (!parentElement) {
988
+ throw new Error('Parent element not found!');
989
+ }
990
+ return parentElement;
991
+ }
992
+ _findFollowingElement(command) {
993
+ const cacheArray = Array.from(this._executedCommandsCache.values());
994
+ let nextElement = undefined;
995
+ const domPosition = command[RENDER_COMMAND_POSITION.DOM_POSITION];
996
+ const nestingLevel = command[RENDER_COMMAND_POSITION.NESTING_LEVEL];
997
+ const type = command[RENDER_COMMAND_POSITION.TYPE];
998
+ // Try to find an already rendered element with a domPosition that is greater than the current domPosition. If there are multiple, use the one with the smallest domPosition.
999
+ // If it does not exist, use .appendChild to simply append the element to the end of the parent element.
1000
+ // If it does exist, use .insertBefore to insert the element before the found element.
1001
+ for (const cached of cacheArray) {
1002
+ if (cached.command[RENDER_COMMAND_POSITION.DOM_POSITION] > domPosition &&
1003
+ cached.command[RENDER_COMMAND_POSITION.NESTING_LEVEL] === nestingLevel &&
1004
+ // Make sure the element does not find itself
1005
+ cached.command[RENDER_COMMAND_POSITION.TYPE] !== type) {
1006
+ nextElement = cached.element;
1007
+ break;
631
1008
  }
632
1009
  }
633
- // loop over the commands
634
- for (const childCommand of command.children) {
635
- this._executeCommand({ command: childCommand, parent: element ?? parent });
1010
+ return nextElement;
1011
+ }
1012
+ _renderInsertOrAppend(nodeToRender, parentElement, nextElement) {
1013
+ if (nextElement) {
1014
+ this._renderer.insertBefore(parentElement, nodeToRender, nextElement);
1015
+ }
1016
+ else {
1017
+ this._renderer.appendChild(parentElement, nodeToRender);
1018
+ }
1019
+ }
1020
+ _debugVisualizeRenderCommands(commands) {
1021
+ const debug = commands.map((command) => this._stringifyRenderCommand(command)).flat();
1022
+ const prettified = formatHTML(debug);
1023
+ return prettified;
1024
+ }
1025
+ _stringifyRenderCommand(command) {
1026
+ const nestAttr = `nest="${command[RENDER_COMMAND_POSITION.NESTING_LEVEL]}"`;
1027
+ const domAttr = `dom="${command[RENDER_COMMAND_POSITION.DOM_POSITION]}"`;
1028
+ if (isHtmlOpenRenderCommand(command)) {
1029
+ return `<${command[HTML_OPEN_RENDER_COMMAND_POSITION.TAG_NAME]} ${nestAttr} ${domAttr}>`;
1030
+ }
1031
+ else if (isHtmlCloseRenderCommand(command)) {
1032
+ return `</${command[HTML_CLOSE_RENDER_COMMAND_POSITION.TAG_NAME]} ${nestAttr} ${domAttr}>`;
1033
+ }
1034
+ else if (isTextRenderCommand(command)) {
1035
+ return [`<span ${nestAttr} ${domAttr}>`, command[TEXT_RENDER_COMMAND_POSITION.TEXT], '</span>'];
1036
+ }
1037
+ else if (isComponentRenderCommand(command)) {
1038
+ const selector = command[COMPONENT_RENDER_COMMAND_POSITION.COMPONENT_TYPE];
1039
+ return [`<${selector} ${nestAttr} ${domAttr}>`, `</${selector}>`];
1040
+ }
1041
+ else {
1042
+ return 'UNKNOWN';
636
1043
  }
637
1044
  }
638
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.0", ngImport: i0, type: ContentfulRichTextRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
639
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.0", type: ContentfulRichTextRendererComponent, isStandalone: true, selector: "et-contentful-rich-text-renderer", inputs: { richText: "richText" }, host: { classAttribute: "et-contentful-rich-text-renderer" }, ngImport: i0, template: ``, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
1045
+ _stringifyRenderInstruction(instruction) {
1046
+ const type = instruction[RENDER_INSTRUCTION_POSITION.TYPE];
1047
+ const translatedType = Object.entries(RENDER_INSTRUCTION_TYPE).find(([, value]) => value === type)?.[0];
1048
+ const command = this._stringifyRenderCommand(instruction[RENDER_INSTRUCTION_POSITION.COMMAND]);
1049
+ return `${translatedType}: ${command}`;
1050
+ }
1051
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.3", ngImport: i0, type: ContentfulRichTextRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1052
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "17.3.3", 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, changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
640
1053
  }
641
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.0", ngImport: i0, type: ContentfulRichTextRendererComponent, decorators: [{
1054
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.3", ngImport: i0, type: ContentfulRichTextRendererComponent, decorators: [{
642
1055
  type: Component,
643
1056
  args: [{
644
1057
  selector: 'et-contentful-rich-text-renderer',
@@ -650,73 +1063,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.0", ngImpor
650
1063
  class: 'et-contentful-rich-text-renderer',
651
1064
  },
652
1065
  }]
653
- }], propDecorators: { richText: [{
654
- type: Input
655
- }] } });
656
-
657
- class ContentfulAudioComponent {
658
- constructor() {
659
- this._richTextData = inject(RICH_TEXT_RENDERER_COMPONENT_DATA, { optional: true });
660
- this._data = null;
661
- this.audioClass = null;
662
- this.figureClass = null;
663
- this.figcaptionClass = null;
664
- }
665
- get data() {
666
- return this._data;
667
- }
668
- set data(v) {
669
- if (v && !v.contentType) {
670
- this._data = null;
671
- console.warn('The provided asset is invalid', v);
672
- return;
1066
+ }], ctorParameters: () => [] });
1067
+ function formatHTML(html) {
1068
+ const result = [];
1069
+ let indent = 0;
1070
+ for (const tag of html) {
1071
+ if (tag.startsWith('</')) {
1072
+ indent--;
673
1073
  }
674
- if (v && !v.contentType.startsWith('audio/')) {
675
- throw new Error('The provided asset is not an audio');
1074
+ result.push(' '.repeat(indent * 2) + tag);
1075
+ if (tag.startsWith('<') && !tag.startsWith('</')) {
1076
+ indent++;
676
1077
  }
677
- this._data = v ?? null;
678
1078
  }
679
- ngOnInit() {
680
- if (this._richTextData && !this.data) {
681
- this.data = this._richTextData;
682
- }
683
- }
684
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.0", ngImport: i0, type: ContentfulAudioComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
685
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.0", type: ContentfulAudioComponent, isStandalone: true, selector: "et-contentful-audio", inputs: { data: "data", audioClass: "audioClass", figureClass: "figureClass", figcaptionClass: "figcaptionClass" }, ngImport: i0, template: `
686
- @if (data) {
687
- <figure [ngClass]="figureClass">
688
- <figcaption [ngClass]="figcaptionClass">{{ data.title }}</figcaption>
689
- <audio [ngClass]="audioClass" controls src="{{ data.url }}"></audio>
690
- </figure>
691
- }
692
- `, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
1079
+ return result.join('\n');
693
1080
  }
694
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.0", ngImport: i0, type: ContentfulAudioComponent, decorators: [{
695
- type: Component,
696
- args: [{
697
- selector: 'et-contentful-audio',
698
- template: `
699
- @if (data) {
700
- <figure [ngClass]="figureClass">
701
- <figcaption [ngClass]="figcaptionClass">{{ data.title }}</figcaption>
702
- <audio [ngClass]="audioClass" controls src="{{ data.url }}"></audio>
703
- </figure>
704
- }
705
- `,
706
- changeDetection: ChangeDetectionStrategy.OnPush,
707
- encapsulation: ViewEncapsulation.None,
708
- standalone: true,
709
- imports: [NgClass],
710
- }]
711
- }], propDecorators: { data: [{
712
- type: Input
713
- }], audioClass: [{
714
- type: Input
715
- }], figureClass: [{
716
- type: Input
717
- }], figcaptionClass: [{
718
- type: Input
719
- }] } });
720
1081
 
721
1082
  const ContentfulImports = [
722
1083
  ContentfulAudioComponent,
@@ -740,26 +1101,16 @@ const GQL_FRAGMENT_CONTENTFUL_ASSET = gql `
740
1101
  size
741
1102
  }
742
1103
  `;
743
- const GQL_FRAGMENT_CONTENTFUL_IMAGE = gql `
744
- fragment ImageData on Image {
745
- sys {
746
- id
747
- }
748
- asset {
749
- ...AssetData
750
- }
751
- alt
752
- caption
753
- resizeBehavior
754
- focusArea
755
- quality
756
- __typename
757
- }
758
- `;
1104
+
1105
+ const provideContentfulConfig = (contentfulConfig = {}) => {
1106
+ return { provide: CONTENTFUL_CONFIG, useValue: createContentfulConfig(contentfulConfig) };
1107
+ };
1108
+
1109
+ // Burst cache
759
1110
 
760
1111
  /**
761
1112
  * Generated bundle index. Do not edit.
762
1113
  */
763
1114
 
764
- export { CONTENTFUL_CONFIG, ContentfulAudioComponent, ContentfulFileComponent, ContentfulImageComponent, ContentfulImports, ContentfulRichTextRendererComponent, ContentfulVideoComponent, GQL_FRAGMENT_CONTENTFUL_ASSET, GQL_FRAGMENT_CONTENTFUL_IMAGE, RICH_TEXT_RENDERER_COMPONENT_DATA, createContentfulConfig, createContentfulMap, createContentfulRenderCommand, createContentfulResourceMap, createRenderCommandsFromContentfulRichText, generateContentfulImageSources, generateDefaultContentfulImageSource, getContentfulNodeProps, isContentfulAsset, isContentfulImage, parseContentfulImageSize, provideContentfulConfig, transformContentfulMarks, translateContentfulNodeTypeToHtmlTag };
1115
+ export { COMPONENT_RENDER_COMMAND_POSITION, CONTENTFUL_CONFIG, ContentfulAudioComponent, ContentfulFileComponent, ContentfulImageComponent, ContentfulImports, ContentfulRichTextRendererComponent, ContentfulVideoComponent, GQL_FRAGMENT_CONTENTFUL_ASSET, HTML_CLOSE_RENDER_COMMAND_POSITION, HTML_OPEN_RENDER_COMMAND_POSITION, RENDER_COMMAND_POSITION, RENDER_COMMAND_TYPE, RENDER_INSTRUCTION_POSITION, RENDER_INSTRUCTION_TYPE, TEXT_RENDER_COMMAND_POSITION, createContentfulConfig, generateContentfulImageSources, generateDefaultContentfulImageSource, getRenderCommandId, isComponentRenderCommand, isExecutedComponentCommandCacheItem, isExecutedHtmlOrTextCommandCacheItem, isExecutedTextCommandCacheItem, isHtmlCloseRenderCommand, isHtmlOpenRenderCommand, isRichTextRootNode, isTextRenderCommand, marksToClass, parseContentfulImageSize, provideContentfulConfig, translateContentfulNodeTypeToHtmlTag };
765
1116
  //# sourceMappingURL=ethlete-contentful.mjs.map