@ethlete/contentful 2.1.1 → 3.0.0

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 (73) hide show
  1. package/CHANGELOG.md +235 -0
  2. package/esm2022/index.mjs +12 -2
  3. package/esm2022/lib/components/audio/contentful-audio.component.mjs +18 -47
  4. package/esm2022/lib/components/audio/index.mjs +2 -0
  5. package/esm2022/lib/components/file/contentful-file.component.mjs +14 -36
  6. package/esm2022/lib/components/file/index.mjs +2 -0
  7. package/esm2022/lib/components/image/contentful-image.component.mjs +64 -113
  8. package/esm2022/lib/components/image/contentful-image.component.utils.mjs +15 -19
  9. package/esm2022/lib/components/image/index.mjs +3 -2
  10. package/esm2022/lib/components/rich-text-renderer/index.mjs +3 -2
  11. package/esm2022/lib/components/rich-text-renderer/rich-text-renderer.component.mjs +683 -78
  12. package/esm2022/lib/components/rich-text-renderer/rich-text-renderer.errors.mjs +21 -0
  13. package/esm2022/lib/components/rich-text-renderer/rich-text-renderer.util.mjs +4 -133
  14. package/esm2022/lib/components/video/contentful-video.component.mjs +14 -39
  15. package/esm2022/lib/components/video/index.mjs +2 -0
  16. package/esm2022/lib/constants/index.mjs +2 -2
  17. package/esm2022/lib/contentful.imports.mjs +6 -2
  18. package/esm2022/lib/gql/asset.fragments.mjs +1 -17
  19. package/esm2022/lib/gql/index.mjs +2 -0
  20. package/esm2022/lib/types/contentful.types.mjs +1 -1
  21. package/esm2022/lib/types/index.mjs +2 -2
  22. package/esm2022/lib/utils/contentful-config.mjs +5 -2
  23. package/esm2022/lib/utils/contentful.util.mjs +1 -7
  24. package/esm2022/lib/utils/index.mjs +3 -2
  25. package/fesm2022/ethlete-contentful.mjs +887 -536
  26. package/fesm2022/ethlete-contentful.mjs.map +1 -1
  27. package/index.d.ts +10 -1
  28. package/lib/components/audio/contentful-audio.component.d.ts +6 -11
  29. package/lib/components/file/contentful-file.component.d.ts +4 -9
  30. package/lib/components/image/contentful-image.component.d.ts +19 -24
  31. package/lib/components/image/contentful-image.component.utils.d.ts +2 -2
  32. package/lib/components/image/index.d.ts +2 -1
  33. package/lib/components/rich-text-renderer/index.d.ts +2 -1
  34. package/lib/components/rich-text-renderer/rich-text-renderer.component.d.ts +160 -15
  35. package/lib/components/rich-text-renderer/rich-text-renderer.errors.d.ts +12 -0
  36. package/lib/components/rich-text-renderer/rich-text-renderer.util.d.ts +4 -17
  37. package/lib/components/video/contentful-video.component.d.ts +4 -9
  38. package/lib/constants/index.d.ts +1 -1
  39. package/lib/contentful.imports.d.ts +5 -1
  40. package/lib/gql/asset.fragments.d.ts +0 -1
  41. package/lib/types/contentful.types.d.ts +92 -48
  42. package/lib/types/index.d.ts +1 -1
  43. package/lib/utils/contentful.util.d.ts +1 -3
  44. package/lib/utils/index.d.ts +2 -1
  45. package/package.json +4 -4
  46. package/esm2022/lib/components/audio/public-api.mjs +0 -2
  47. package/esm2022/lib/components/file/public-api.mjs +0 -2
  48. package/esm2022/lib/components/image/public-api.mjs +0 -3
  49. package/esm2022/lib/components/index.mjs +0 -2
  50. package/esm2022/lib/components/public-api.mjs +0 -6
  51. package/esm2022/lib/components/rich-text-renderer/public-api.mjs +0 -5
  52. package/esm2022/lib/components/rich-text-renderer/rich-text-renderer.constants.mjs +0 -3
  53. package/esm2022/lib/components/rich-text-renderer/rich-text-renderer.types.mjs +0 -2
  54. package/esm2022/lib/components/video/public-api.mjs +0 -2
  55. package/esm2022/lib/constants/public-api.mjs +0 -2
  56. package/esm2022/lib/gql/public-api.mjs +0 -2
  57. package/esm2022/lib/public-api.mjs +0 -7
  58. package/esm2022/lib/types/public-api.mjs +0 -2
  59. package/esm2022/lib/utils/public-api.mjs +0 -3
  60. package/lib/components/image/public-api.d.ts +0 -2
  61. package/lib/components/index.d.ts +0 -1
  62. package/lib/components/public-api.d.ts +0 -5
  63. package/lib/components/rich-text-renderer/public-api.d.ts +0 -4
  64. package/lib/components/rich-text-renderer/rich-text-renderer.constants.d.ts +0 -2
  65. package/lib/components/rich-text-renderer/rich-text-renderer.types.d.ts +0 -12
  66. package/lib/constants/public-api.d.ts +0 -1
  67. package/lib/public-api.d.ts +0 -6
  68. package/lib/types/public-api.d.ts +0 -1
  69. package/lib/utils/public-api.d.ts +0 -2
  70. /package/lib/components/audio/{public-api.d.ts → index.d.ts} +0 -0
  71. /package/lib/components/file/{public-api.d.ts → index.d.ts} +0 -0
  72. /package/lib/components/video/{public-api.d.ts → index.d.ts} +0 -0
  73. /package/lib/gql/{public-api.d.ts → index.d.ts} +0 -0
@@ -1,51 +1,88 @@
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, ChangeDetectorRef, Component, ChangeDetectionStrategy, ViewEncapsulation, Input, 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, 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
- const createContentfulConfig = (config = {}) => ({
13
- // This default config must be inline and not saved inside a const for some weird webpack reason
14
- ...{
15
- useTailwindClasses: false,
16
- components: {
17
- audio: ContentfulAudioComponent,
18
- file: ContentfulFileComponent,
19
- image: ContentfulImageComponent,
20
- video: ContentfulVideoComponent,
21
- },
22
- customComponents: {},
23
- imageOptions: {
24
- srcsetSizes: ['375w', '1280w', '1920w', '2560w'],
25
- sizes: [],
26
- backgroundColor: null,
27
- },
28
- },
29
- ...(config || {}),
30
- });
11
+ class ContentfulAudioComponent {
12
+ constructor() {
13
+ this.asset = input.required();
14
+ this.audioClass = input(null);
15
+ this.figureClass = input(null);
16
+ this.figcaptionClass = input(null);
17
+ }
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>
25
+ }
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>
38
+ }
39
+ `,
40
+ changeDetection: ChangeDetectionStrategy.OnPush,
41
+ encapsulation: ViewEncapsulation.None,
42
+ standalone: true,
43
+ imports: [NgClass],
44
+ }]
45
+ }] });
31
46
 
32
- const RICH_TEXT_RENDERER_COMPONENT_DATA = new InjectionToken('RICH_TEXT_RENDERER_COMPONENT_DATA');
47
+ class ContentfulFileComponent {
48
+ constructor() {
49
+ this.asset = input.required();
50
+ this.fileClass = input(null);
51
+ }
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)
57
+ </a>
58
+ }
59
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
60
+ }
61
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.3", ngImport: i0, type: ContentfulFileComponent, decorators: [{
62
+ type: Component,
63
+ args: [{
64
+ selector: 'et-contentful-file',
65
+ template: `
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)
69
+ </a>
70
+ }
71
+ `,
72
+ changeDetection: ChangeDetectionStrategy.OnPush,
73
+ encapsulation: ViewEncapsulation.None,
74
+ standalone: true,
75
+ imports: [NgClass],
76
+ }]
77
+ }] });
33
78
 
34
- const provideContentfulConfig = (contentfulConfig = {}) => {
35
- return { provide: CONTENTFUL_CONFIG, useValue: createContentfulConfig(contentfulConfig) };
36
- };
37
- const isContentfulImage = (v) => {
38
- return !!v && 'asset' in v && 'alt' in v && 'resizeBehavior' in v && 'focusArea' in v;
39
- };
40
- const isContentfulAsset = (v) => {
41
- return !!v && 'contentType' in v && 'description' in v && 'height' in v && 'url' in v && 'width' in v;
42
- };
79
+ const CONTENTFUL_CONFIG = new InjectionToken('ContentfulConfig');
43
80
 
44
81
  const generateDefaultContentfulImageSource = (data) => {
45
- const { url, contentType } = data;
82
+ const file = data.fields.file;
46
83
  return {
47
- type: contentType,
48
- srcset: url,
84
+ type: file.contentType,
85
+ srcset: file.url,
49
86
  };
50
87
  };
51
88
  /**
@@ -75,16 +112,15 @@ const parseContentfulImageSize = (size) => {
75
112
  height: height ? parseInt(height, 10) : null,
76
113
  };
77
114
  };
78
- const generateContentfulImageSources = (data, srcsetSizes, backgroundColor) => {
79
- const assetData = isContentfulImage(data) ? data.asset : data;
80
- const imageData = isContentfulImage(data) ? data : null;
115
+ const generateContentfulImageSources = (data, srcsetSizes, backgroundColor, quality, focusArea, resizeBehavior) => {
116
+ const assetData = data;
81
117
  if (!assetData) {
82
118
  return [];
83
119
  }
84
120
  const sources = [];
85
121
  const SOURCE_TYPES = ['image/avif', 'image/webp', 'image/png', 'image/jpg'];
86
122
  for (const type of SOURCE_TYPES) {
87
- const baseUrl = assetData.url;
123
+ const baseUrl = assetData.fields.file.url;
88
124
  const sourceSets = [];
89
125
  const queryParams = [];
90
126
  // Set the format (e.g. 'fm=webp')
@@ -92,16 +128,14 @@ const generateContentfulImageSources = (data, srcsetSizes, backgroundColor) => {
92
128
  if (backgroundColor) {
93
129
  queryParams.push(`bg=rgb:${backgroundColor}`);
94
130
  }
95
- if (imageData) {
96
- if (imageData.quality) {
97
- queryParams.push(`q=${imageData.quality}`);
98
- }
99
- if (imageData.focusArea) {
100
- queryParams.push(`f=${imageData.focusArea}`);
101
- }
102
- if (imageData.resizeBehavior) {
103
- queryParams.push(`fit=${imageData.resizeBehavior}`);
104
- }
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}`);
105
139
  }
106
140
  if (srcsetSizes?.length) {
107
141
  const urlWithParams = `${baseUrl}?${queryParams.join('&')}`;
@@ -128,242 +162,160 @@ const generateContentfulImageSources = (data, srcsetSizes, backgroundColor) => {
128
162
 
129
163
  class ContentfulImageComponent {
130
164
  constructor() {
131
- this._richTextData = inject(RICH_TEXT_RENDERER_COMPONENT_DATA, {
132
- 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);
170
+ this.focusArea = input(null);
171
+ this.resizeBehavior = input(null);
172
+ this.hasPriority = input(false);
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;
133
195
  });
134
- this.pictureData = inject(PictureDataDirective);
135
- this._cdr = inject(ChangeDetectorRef);
136
- this._data = null;
137
- this._sizes = [];
138
- this._srcsetSizes = [];
139
- this._backgroundColor = null;
140
- this.sources = [];
141
- }
142
- get data() {
143
- return this._data;
144
- }
145
- set data(v) {
146
- const assetData = isContentfulImage(v) ? v.asset : v;
147
- const imageData = isContentfulImage(v) ? v : null;
148
- if (assetData && !assetData.contentType) {
149
- this._data = null;
150
- console.warn('The provided asset is invalid', v);
151
- return;
152
- }
153
- if (assetData && !assetData.contentType.startsWith('image/')) {
154
- throw new Error('The provided asset is not an image');
155
- }
156
- this._data = v ?? null;
157
- this.pictureData.defaultSrc = assetData ? generateDefaultContentfulImageSource(assetData) : null;
158
- this.pictureData.alt = imageData?.alt ?? assetData?.description ?? null;
159
- this.pictureData.width = assetData?.width ?? null;
160
- this.pictureData.height = assetData?.height ?? null;
161
- this.pictureData.figcaption = imageData?.caption ?? null;
162
- this.updateSizes();
163
- }
164
- get sizes() {
165
- return this._sizes;
166
- }
167
- set sizes(v) {
168
- this._sizes = v;
169
- this.pictureData.sizes = this.sizes?.join(', ') ?? null;
170
- }
171
- get srcsetSizes() {
172
- return this._srcsetSizes;
173
- }
174
- set srcsetSizes(v) {
175
- this._srcsetSizes = v;
176
- this.updateSizes();
177
- }
178
- get backgroundColor() {
179
- return this._backgroundColor;
180
- }
181
- set backgroundColor(v) {
182
- this._backgroundColor = v;
183
- this.updateSizes();
184
- }
185
- ngOnInit() {
186
- if (this._richTextData && !this.data) {
187
- this.data = this._richTextData;
188
- }
189
- }
190
- updateSizes() {
191
- this.sources = this._data ? generateContentfulImageSources(this._data, this.srcsetSizes, this.backgroundColor) : [];
192
- this._cdr.markForCheck();
193
- }
194
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.2", ngImport: i0, type: ContentfulImageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
195
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.0.2", 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: `
196
199
  <et-picture
197
- [imgClass]="pictureData.imgClass"
198
- [hasPriority]="pictureData.hasPriority"
199
- [figureClass]="pictureData.figureClass"
200
- [pictureClass]="pictureData.pictureClass"
201
- [figcaptionClass]="pictureData.figcaptionClass"
202
- [defaultSrc]="pictureData.defaultSrc"
203
- [alt]="pictureData.alt"
204
- [figcaption]="pictureData.figcaption"
205
- [width]="pictureData.width"
206
- [height]="pictureData.height"
207
- [sizes]="pictureData.sizes"
208
- [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()"
209
212
  />
210
- `, 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 }); }
211
214
  }
212
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.2", ngImport: i0, type: ContentfulImageComponent, decorators: [{
215
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.3", ngImport: i0, type: ContentfulImageComponent, decorators: [{
213
216
  type: Component,
214
217
  args: [{
215
218
  selector: 'et-contentful-image',
216
219
  template: `
217
220
  <et-picture
218
- [imgClass]="pictureData.imgClass"
219
- [hasPriority]="pictureData.hasPriority"
220
- [figureClass]="pictureData.figureClass"
221
- [pictureClass]="pictureData.pictureClass"
222
- [figcaptionClass]="pictureData.figcaptionClass"
223
- [defaultSrc]="pictureData.defaultSrc"
224
- [alt]="pictureData.alt"
225
- [figcaption]="pictureData.figcaption"
226
- [width]="pictureData.width"
227
- [height]="pictureData.height"
228
- [sizes]="pictureData.sizes"
229
- [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()"
230
233
  />
231
234
  `,
232
235
  changeDetection: ChangeDetectionStrategy.OnPush,
233
236
  encapsulation: ViewEncapsulation.None,
234
237
  standalone: true,
235
- imports: [NgClass, NgOptimizedImage, PictureComponent],
236
- hostDirectives: [
237
- {
238
- directive: PictureDataDirective,
239
- // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
240
- inputs: ['imgClass', 'hasPriority', 'figureClass', 'pictureClass', 'figcaptionClass'],
241
- },
242
- ],
238
+ imports: [PictureComponent],
243
239
  host: {
244
240
  class: 'et-contentful-image',
245
241
  },
246
242
  }]
247
- }], propDecorators: { data: [{
248
- type: Input
249
- }], sizes: [{
250
- type: Input
251
- }], srcsetSizes: [{
252
- type: Input
253
- }], backgroundColor: [{
254
- type: Input
255
- }] } });
243
+ }] });
256
244
 
257
- const createRenderCommandsFromContentfulRichText = (options) => {
258
- const { data, config } = options;
259
- const commands = [];
260
- const resourceMap = createContentfulResourceMap(data.links);
261
- createContentfulRenderCommand(data.json, { children: commands, payload: data.json.nodeType }, resourceMap, config);
262
- return commands;
263
- };
264
- const createContentfulRenderCommand = (node, parent, resourceMap, config) => {
265
- const { nodeType, data: nodeData } = node;
266
- const { children, marks, value } = getContentfulNodeProps(node);
267
- const command = {
268
- payload: translateContentfulNodeTypeToHtmlTag(nodeType),
269
- data: value,
270
- attributes: {
271
- ...(marks
272
- ? {
273
- class: transformContentfulMarks(marks, config.useTailwindClasses),
274
- }
275
- : {}),
276
- ...(nodeData['uri'] ? { href: nodeData['uri'], target: '_blank' } : {}),
277
- },
278
- children: [],
279
- };
280
- const entryId = nodeData['target']?.sys?.id;
281
- if (nodeType === BLOCKS.EMBEDDED_ASSET) {
282
- const asset = resourceMap.assetsBlock.get(entryId);
283
- if (asset) {
284
- const isImage = asset.contentType.startsWith('image/');
285
- const isVideo = asset.contentType.startsWith('video/');
286
- const isAudio = asset.contentType.startsWith('audio/');
287
- command.data = asset;
288
- if (isImage) {
289
- command.payload = config.components.image;
290
- }
291
- else if (isVideo) {
292
- command.payload = config.components.video;
293
- }
294
- else if (isAudio) {
295
- command.payload = config.components.audio;
296
- }
297
- else {
298
- command.payload = config.components.file;
299
- }
300
- }
301
- else {
302
- console.warn(`No asset found for entry id "${entryId}"!`, nodeData);
303
- }
304
- }
305
- else if (nodeType === BLOCKS.EMBEDDED_ENTRY || nodeType === INLINES.EMBEDDED_ENTRY) {
306
- const entry = resourceMap.entriesBlock.get(entryId) ?? resourceMap.entriesInline.get(entryId);
307
- if (entry) {
308
- const type = entry?.__typename ?? 'ET_UNKNOWN';
309
- // "Image" is a special wrapper content model for contentful assets.
310
- // It must be created by the user.
311
- const component = type === 'Image' ? ContentfulImageComponent : config.customComponents?.[type];
312
- if (component) {
313
- command.payload = component;
314
- command.data = entry;
315
- }
316
- else {
317
- command.data = `ERROR: No component found for embedded entry "${type}" as "${nodeType}"!`;
318
- }
319
- }
320
- else {
321
- console.warn(`No entry found for entry id "${entryId}"!`, nodeData);
322
- }
323
- }
324
- if (children && (children.length !== 1 || children[0]?.nodeType !== 'text')) {
325
- children.forEach((child) => {
326
- createContentfulRenderCommand(child, command, resourceMap, config);
327
- });
245
+ class ContentfulVideoComponent {
246
+ constructor() {
247
+ this.asset = input.required();
248
+ this.videoClass = input(null);
328
249
  }
329
- parent.children.push(command);
330
- return command;
331
- };
332
- const getContentfulNodeProps = (node) => {
333
- const children = 'content' in node ? node.content : null;
334
- let marks = null;
335
- let value = null;
336
- if (children && children.length === 1 && children[0]?.nodeType === 'text') {
337
- marks = children[0].marks?.length ? children[0].marks : null;
338
- value = children[0].value;
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 }}" />
255
+ </video>
339
256
  }
340
- else {
341
- marks = 'marks' in node ? (node.marks?.length ? node.marks : null) : null;
342
- value = 'value' in node ? node.value : null;
257
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
258
+ }
259
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.3", ngImport: i0, type: ContentfulVideoComponent, decorators: [{
260
+ type: Component,
261
+ args: [{
262
+ selector: 'et-contentful-video',
263
+ template: `
264
+ @if (asset(); as data) {
265
+ <video [ngClass]="videoClass()" controls>
266
+ <source src="{{ data.fields.file.url }}" type="{{ data.fields.file.contentType }}" />
267
+ </video>
343
268
  }
344
- return {
345
- children,
346
- marks,
347
- value,
348
- };
269
+ `,
270
+ changeDetection: ChangeDetectionStrategy.OnPush,
271
+ encapsulation: ViewEncapsulation.None,
272
+ standalone: true,
273
+ imports: [NgClass],
274
+ }]
275
+ }] });
276
+
277
+ const createContentfulConfig = (config = {}) => ({
278
+ // This default config must be inline and not saved inside a const for some weird webpack reason
279
+ ...{
280
+ useTailwindClasses: false,
281
+ components: {
282
+ audio: ContentfulAudioComponent,
283
+ file: ContentfulFileComponent,
284
+ image: ContentfulImageComponent,
285
+ video: ContentfulVideoComponent,
286
+ },
287
+ customComponents: {},
288
+ imageOptions: {
289
+ srcsetSizes: ['375w', '1280w', '1920w', '2560w'],
290
+ sizes: [],
291
+ backgroundColor: null,
292
+ },
293
+ },
294
+ ...(config || {}),
295
+ });
296
+
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.',
349
307
  };
350
- const transformContentfulMarks = (marks, useTailwindClasses) => {
351
- return marks
352
- .map((mark) => {
353
- switch (mark.type) {
354
- case 'bold':
355
- return useTailwindClasses ? 'font-bold' : mark.type;
356
- case 'italic':
357
- return useTailwindClasses ? 'italic' : mark.type;
358
- case 'underline':
359
- return useTailwindClasses ? 'underline' : mark.type;
360
- case 'code':
361
- return useTailwindClasses ? 'font-mono' : mark.type;
362
- default:
363
- return mark.type;
364
- }
365
- })
366
- .join(' ');
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);
315
+ };
316
+
317
+ const isRichTextRootNode = (node) => {
318
+ return isObject(node) && 'nodeType' in node && node['nodeType'] === 'document';
367
319
  };
368
320
  const translateContentfulNodeTypeToHtmlTag = (nodeType) => {
369
321
  switch (nodeType) {
@@ -420,303 +372,712 @@ const translateContentfulNodeTypeToHtmlTag = (nodeType) => {
420
372
  return 'div';
421
373
  }
422
374
  };
423
- const createContentfulResourceMap = (links) => {
424
- const resourceMap = {
425
- assetsBlock: createContentfulMap(links?.assets?.block),
426
- entriesInline: createContentfulMap(links?.entries?.inline),
427
- entriesBlock: createContentfulMap(links?.entries?.block),
428
- };
429
- return resourceMap;
375
+
376
+ const RENDER_COMMAND_TYPE = {
377
+ HTML_OPEN: 0,
378
+ HTML_CLOSE: 1,
379
+ TEXT: 2,
380
+ COMPONENT: 3,
430
381
  };
431
- const createContentfulMap = (links) => {
432
- const assetMap = new Map();
433
- if (links) {
434
- for (const link of links) {
435
- if (!link?.sys?.id) {
436
- console.warn('Link has no sys.id property. Please include it inside your query.', link);
437
- continue;
438
- }
439
- 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;
440
429
  }
430
+ classes.push(klass);
441
431
  }
442
- 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);
443
475
  };
444
-
445
476
  class ContentfulRichTextRendererComponent {
446
477
  constructor() {
447
- this._richText = null;
478
+ this._viewContainerRef = inject(ViewContainerRef);
448
479
  this._renderer = inject(Renderer2);
449
480
  this._elementRef = inject(ElementRef);
450
- this._componentFactoryResolver = inject(ComponentFactoryResolver);
451
481
  this._document = inject(DOCUMENT);
452
- this._appRef = inject(ApplicationRef);
453
- this._injector = inject(Injector);
454
482
  this._config = inject(CONTENTFUL_CONFIG, { optional: true }) ?? createContentfulConfig();
455
- }
456
- get richText() {
457
- return this._richText;
458
- }
459
- set richText(v) {
460
- this._richText = v ?? null;
461
- this.resetAndRender();
462
- }
463
- resetAndRender() {
464
- // clear childNodes to prevent appending new richtext elements to existing one
465
- const childElements = this._elementRef.nativeElement.childNodes;
466
- for (const child of childElements) {
467
- this._renderer.removeChild(this._elementRef.nativeElement, child);
468
- }
469
- if (!this._richText) {
470
- return;
471
- }
472
- const commands = createRenderCommandsFromContentfulRichText({ data: this._richText, config: this._config });
473
- this._render(commands);
474
- }
475
- _render(commands) {
476
- // create a document fragment to hold the elements while we create them
477
- const fragment = document.createDocumentFragment();
478
- const hostCommand = {
479
- payload: 'document',
480
- children: commands,
481
- };
482
- // loop over the commands
483
- this._executeCommand({ command: hostCommand, parent: fragment });
484
- // append the fragment to the host element
485
- this._renderer.appendChild(this._elementRef.nativeElement, fragment);
486
- }
487
- _executeCommand(data) {
488
- const { command, parent } = data;
489
- let element = null;
490
- if (command.payload !== 'document') {
491
- if (typeof command.payload === 'string') {
492
- element = this._renderer.createElement(command.payload);
493
- if (command.data) {
494
- this._renderer.setProperty(element, 'innerText', command.data);
495
- }
496
- if (command.attributes) {
497
- for (const [name, value] of Object.entries(command.attributes)) {
498
- this._renderer.setAttribute(element, name, value);
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;
499
562
  }
500
563
  }
501
- this._renderer.appendChild(parent, element);
564
+ instructions.push([RENDER_INSTRUCTION_TYPE.DELETE, command]);
565
+ // remove from map to mark as used
566
+ previousRenderCommandMap.delete(getRenderCommandId(command));
502
567
  }
503
- else {
504
- const injector = Injector.create({
505
- providers: [
506
- {
507
- provide: RICH_TEXT_RENDERER_COMPONENT_DATA,
508
- useValue: command.data,
509
- },
510
- ],
511
- parent: this._injector,
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,
512
580
  });
513
- const portal = new DomPortalOutlet(parent, this._componentFactoryResolver, this._appRef, injector, this._document);
514
- const comp = new ComponentPortal(command.payload);
515
- const ref = comp.attach(portal);
516
- if (ref.instance instanceof ContentfulImageComponent) {
517
- ref.instance.sizes = this._config.imageOptions.sizes;
518
- ref.instance.srcsetSizes = this._config.imageOptions.srcsetSizes;
519
- ref.instance.backgroundColor = this._config.imageOptions.backgroundColor;
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++;
520
600
  }
521
- element = ref.location.nativeElement;
522
601
  }
523
- }
524
- // loop over the commands
525
- for (const childCommand of command.children) {
526
- this._executeCommand({ command: childCommand, parent: element ?? parent });
527
- }
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();
528
623
  }
529
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.2", ngImport: i0, type: ContentfulRichTextRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
530
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.0.2", 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 }); }
531
- }
532
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.2", ngImport: i0, type: ContentfulRichTextRendererComponent, decorators: [{
533
- type: Component,
534
- args: [{
535
- selector: 'et-contentful-rich-text-renderer',
536
- template: ``,
537
- encapsulation: ViewEncapsulation.None,
538
- changeDetection: ChangeDetectionStrategy.OnPush,
539
- standalone: true,
540
- host: {
541
- class: 'et-contentful-rich-text-renderer',
542
- },
543
- }]
544
- }], propDecorators: { richText: [{
545
- type: Input
546
- }] } });
547
-
548
- class ContentfulAudioComponent {
549
- constructor() {
550
- this._richTextData = inject(RICH_TEXT_RENDERER_COMPONENT_DATA, { optional: true });
551
- this._data = null;
552
- this.audioClass = null;
553
- this.figureClass = null;
554
- this.figcaptionClass = null;
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;
555
815
  }
556
- get data() {
557
- return this._data;
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
+ }
558
835
  }
559
- set data(v) {
560
- if (v && !v.contentType) {
561
- this._data = null;
562
- console.warn('The provided asset is invalid', v);
563
- return;
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
+ });
564
850
  }
565
- if (v && !v.contentType.startsWith('audio/')) {
566
- throw new Error('The provided asset is not an audio');
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
+ });
567
862
  }
568
- this._data = v ?? null;
569
- }
570
- ngOnInit() {
571
- if (this._richTextData && !this.data) {
572
- this.data = this._richTextData;
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
+ });
573
875
  }
574
876
  }
575
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.2", ngImport: i0, type: ContentfulAudioComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
576
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.0.2", type: ContentfulAudioComponent, isStandalone: true, selector: "et-contentful-audio", inputs: { data: "data", audioClass: "audioClass", figureClass: "figureClass", figcaptionClass: "figcaptionClass" }, ngImport: i0, template: `
577
- @if (data) {
578
- <figure [ngClass]="figureClass">
579
- <figcaption [ngClass]="figcaptionClass">{{ data.title }}</figcaption>
580
- <audio [ngClass]="audioClass" controls src="{{ data.url }}"></audio>
581
- </figure>
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
+ });
582
897
  }
583
- `, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
584
- }
585
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.2", ngImport: i0, type: ContentfulAudioComponent, decorators: [{
586
- type: Component,
587
- args: [{
588
- selector: 'et-contentful-audio',
589
- template: `
590
- @if (data) {
591
- <figure [ngClass]="figureClass">
592
- <figcaption [ngClass]="figcaptionClass">{{ data.title }}</figcaption>
593
- <audio [ngClass]="audioClass" controls src="{{ data.url }}"></audio>
594
- </figure>
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
+ }
595
924
  }
596
- `,
597
- changeDetection: ChangeDetectionStrategy.OnPush,
598
- encapsulation: ViewEncapsulation.None,
599
- standalone: true,
600
- imports: [NgClass],
601
- }]
602
- }], propDecorators: { data: [{
603
- type: Input
604
- }], audioClass: [{
605
- type: Input
606
- }], figureClass: [{
607
- type: Input
608
- }], figcaptionClass: [{
609
- type: Input
610
- }] } });
611
-
612
- class ContentfulFileComponent {
613
- constructor() {
614
- this._richTextData = inject(RICH_TEXT_RENDERER_COMPONENT_DATA, { optional: true });
615
- this._data = null;
616
- this.fileClass = null;
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);
617
947
  }
618
- get data() {
619
- return this._data;
948
+ _getComponentRootNode(componentRef) {
949
+ return componentRef.hostView.rootNodes[0];
620
950
  }
621
- set data(v) {
622
- if (v && !v.contentType) {
623
- this._data = null;
624
- console.warn('The provided asset is invalid', v);
625
- return;
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);
626
956
  }
627
- this._data = v ?? null;
628
957
  }
629
- ngOnInit() {
630
- if (this._richTextData && !this.data) {
631
- this.data = this._richTextData;
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;
632
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!');
976
+ }
977
+ if (cmd[RENDER_COMMAND_POSITION.NESTING_LEVEL] === nestingLevel - 1) {
978
+ parentCommand = cmd;
979
+ break;
980
+ }
981
+ }
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;
633
991
  }
634
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.2", ngImport: i0, type: ContentfulFileComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
635
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.0.2", type: ContentfulFileComponent, isStandalone: true, selector: "et-contentful-file", inputs: { data: "data", fileClass: "fileClass" }, ngImport: i0, template: `
636
- @if (data) {
637
- <a [href]="data.url" [ngClass]="fileClass" class="underline" target="_blank">
638
- {{ data.title }} ({{ data.size }} Bytes)
639
- </a>
640
- }
641
- `, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
642
- }
643
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.2", ngImport: i0, type: ContentfulFileComponent, decorators: [{
644
- type: Component,
645
- args: [{
646
- selector: 'et-contentful-file',
647
- template: `
648
- @if (data) {
649
- <a [href]="data.url" [ngClass]="fileClass" class="underline" target="_blank">
650
- {{ data.title }} ({{ data.size }} Bytes)
651
- </a>
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;
1008
+ }
1009
+ }
1010
+ return nextElement;
652
1011
  }
653
- `,
654
- changeDetection: ChangeDetectionStrategy.OnPush,
655
- encapsulation: ViewEncapsulation.None,
656
- standalone: true,
657
- imports: [NgClass],
658
- }]
659
- }], propDecorators: { data: [{
660
- type: Input
661
- }], fileClass: [{
662
- type: Input
663
- }] } });
664
-
665
- class ContentfulVideoComponent {
666
- constructor() {
667
- this._richTextData = inject(RICH_TEXT_RENDERER_COMPONENT_DATA, { optional: true });
668
- this._data = null;
669
- this.videoClass = null;
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
+ }
670
1019
  }
671
- get data() {
672
- return this._data;
1020
+ _debugVisualizeRenderCommands(commands) {
1021
+ const debug = commands.map((command) => this._stringifyRenderCommand(command)).flat();
1022
+ const prettified = formatHTML(debug);
1023
+ return prettified;
673
1024
  }
674
- set data(v) {
675
- if (v && !v.contentType) {
676
- this._data = null;
677
- console.warn('The provided asset is invalid', v);
678
- return;
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}>`;
679
1030
  }
680
- if (v && !v.contentType.startsWith('video/')) {
681
- throw new Error('The provided asset is not an video');
1031
+ else if (isHtmlCloseRenderCommand(command)) {
1032
+ return `</${command[HTML_CLOSE_RENDER_COMMAND_POSITION.TAG_NAME]} ${nestAttr} ${domAttr}>`;
682
1033
  }
683
- this._data = v ?? null;
684
- }
685
- ngOnInit() {
686
- if (this._richTextData && !this.data) {
687
- this.data = this._richTextData;
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';
688
1043
  }
689
1044
  }
690
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.2", ngImport: i0, type: ContentfulVideoComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
691
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.0.2", type: ContentfulVideoComponent, isStandalone: true, selector: "et-contentful-video", inputs: { data: "data", videoClass: "videoClass" }, ngImport: i0, template: `
692
- @if (data) {
693
- <video [ngClass]="videoClass" controls>
694
- <source src="{{ data.url }}" type="{{ data.contentType }}" />
695
- </video>
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}`;
696
1050
  }
697
- `, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
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 }); }
698
1053
  }
699
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.2", ngImport: i0, type: ContentfulVideoComponent, decorators: [{
1054
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.3", ngImport: i0, type: ContentfulRichTextRendererComponent, decorators: [{
700
1055
  type: Component,
701
1056
  args: [{
702
- selector: 'et-contentful-video',
703
- template: `
704
- @if (data) {
705
- <video [ngClass]="videoClass" controls>
706
- <source src="{{ data.url }}" type="{{ data.contentType }}" />
707
- </video>
708
- }
709
- `,
710
- changeDetection: ChangeDetectionStrategy.OnPush,
1057
+ selector: 'et-contentful-rich-text-renderer',
1058
+ template: ``,
711
1059
  encapsulation: ViewEncapsulation.None,
1060
+ changeDetection: ChangeDetectionStrategy.OnPush,
712
1061
  standalone: true,
713
- imports: [NgClass],
1062
+ host: {
1063
+ class: 'et-contentful-rich-text-renderer',
1064
+ },
714
1065
  }]
715
- }], propDecorators: { data: [{
716
- type: Input
717
- }], videoClass: [{
718
- type: Input
719
- }] } });
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--;
1073
+ }
1074
+ result.push(' '.repeat(indent * 2) + tag);
1075
+ if (tag.startsWith('<') && !tag.startsWith('</')) {
1076
+ indent++;
1077
+ }
1078
+ }
1079
+ return result.join('\n');
1080
+ }
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