@ethlete/contentful 4.0.0-next.6 → 4.0.0-next.7

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.
@@ -1,15 +1,11 @@
1
1
  import { NgClass, DOCUMENT } from '@angular/common';
2
2
  import * as i0 from '@angular/core';
3
- import { input, computed, ViewEncapsulation, Component, InjectionToken, inject, numberAttribute, booleanAttribute, isDevMode, ViewContainerRef, ElementRef, reflectComponentType } from '@angular/core';
4
- import { gql } from '@ethlete/query';
5
- import { normalizePictureSizes, PictureComponent } from '@ethlete/cdk';
3
+ import { input, computed, ViewEncapsulation, Component, InjectionToken, inject, numberAttribute, booleanAttribute, isDevMode, ViewContainerRef, ElementRef, linkedSignal, effect, untracked, signal, reflectComponentType, inputBinding } from '@angular/core';
4
+ import { normalizePictureSizes, PictureComponent } from '@ethlete/components';
6
5
  import { RouterLink } from '@angular/router';
7
- import { toSignal, toObservable, takeUntilDestroyed } from '@angular/core/rxjs-interop';
8
- import { INLINES, BLOCKS } from '@contentful/rich-text-types';
9
6
  import { RuntimeError, isObject, injectRenderer, getObjectProperty } from '@ethlete/core';
10
- import { startWith, pairwise, tap } from 'rxjs';
11
7
 
12
- const GQL_FRAGMENT_CONTENTFUL_ASSET = gql `
8
+ const GQL_FRAGMENT_CONTENTFUL_ASSET = `
13
9
  fragment AssetData on Asset {
14
10
  sys {
15
11
  id
@@ -56,8 +52,8 @@ class ContentfulAudioComponent {
56
52
  }, /* @ts-ignore */
57
53
  ...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
58
54
  }
59
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ContentfulAudioComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
60
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", 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: `
55
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ContentfulAudioComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
56
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: ContentfulAudioComponent, isStandalone: true, selector: "et-contentful-audio", inputs: { asset: { classPropertyName: "asset", publicName: "asset", isSignal: true, isRequired: true, transformFunction: null }, audioClass: { classPropertyName: "audioClass", publicName: "audioClass", isSignal: true, isRequired: false, transformFunction: null }, figureClass: { classPropertyName: "figureClass", publicName: "figureClass", isSignal: true, isRequired: false, transformFunction: null }, figcaptionClass: { classPropertyName: "figcaptionClass", publicName: "figcaptionClass", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
61
57
  @if (data(); as data) {
62
58
  <figure [ngClass]="figureClass()">
63
59
  <figcaption [ngClass]="figcaptionClass()">{{ data.title }}</figcaption>
@@ -66,7 +62,7 @@ class ContentfulAudioComponent {
66
62
  }
67
63
  `, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], encapsulation: i0.ViewEncapsulation.None }); }
68
64
  }
69
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ContentfulAudioComponent, decorators: [{
65
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ContentfulAudioComponent, decorators: [{
70
66
  type: Component,
71
67
  args: [{
72
68
  selector: 'et-contentful-audio',
@@ -111,8 +107,8 @@ class ContentfulFileComponent {
111
107
  }, /* @ts-ignore */
112
108
  ...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
113
109
  }
114
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ContentfulFileComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
115
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", 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: `
110
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ContentfulFileComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
111
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: ContentfulFileComponent, isStandalone: true, selector: "et-contentful-file", inputs: { asset: { classPropertyName: "asset", publicName: "asset", isSignal: true, isRequired: true, transformFunction: null }, fileClass: { classPropertyName: "fileClass", publicName: "fileClass", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
116
112
  @if (data(); as data) {
117
113
  <a [href]="data.url" [ngClass]="fileClass()" class="underline" target="_blank">
118
114
  {{ data.title }} ({{ data.size }} Bytes)
@@ -120,7 +116,7 @@ class ContentfulFileComponent {
120
116
  }
121
117
  `, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], encapsulation: i0.ViewEncapsulation.None }); }
122
118
  }
123
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ContentfulFileComponent, decorators: [{
119
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ContentfulFileComponent, decorators: [{
124
120
  type: Component,
125
121
  args: [{
126
122
  selector: 'et-contentful-file',
@@ -189,13 +185,13 @@ const parseContentfulImageSize = (size) => {
189
185
  height: height ? parseInt(height, 10) : null,
190
186
  };
191
187
  };
188
+ const SOURCE_TYPES = ['image/avif', 'image/webp', 'image/png', 'image/jpg'];
192
189
  const generateContentfulImageSources = (data, srcsetSizes, backgroundColor, quality, focusArea, resizeBehavior) => {
193
190
  const assetData = data;
194
191
  if (!assetData) {
195
192
  return [];
196
193
  }
197
194
  const sources = [];
198
- const SOURCE_TYPES = ['image/avif', 'image/webp', 'image/png', 'image/jpg'];
199
195
  for (const type of SOURCE_TYPES) {
200
196
  const baseUrl = isContentfulGqlAsset(assetData) ? assetData.url : assetData.fields.file.url;
201
197
  const sourceSets = [];
@@ -239,28 +235,20 @@ const generateContentfulImageSources = (data, srcsetSizes, backgroundColor, qual
239
235
 
240
236
  class ContentfulImageComponent {
241
237
  constructor() {
242
- this._contentfulConfig = inject(CONTENTFUL_CONFIG);
238
+ this.contentfulConfig = inject(CONTENTFUL_CONFIG);
243
239
  this.asset = input.required(/* @ts-ignore */
244
240
  ...(ngDevMode ? [{ debugName: "asset" }] : /* istanbul ignore next */ []));
245
241
  this.backgroundColor = input(null, /* @ts-ignore */
246
242
  ...(ngDevMode ? [{ debugName: "backgroundColor" }] : /* istanbul ignore next */ []));
247
- this.srcsetSizes = input(this._contentfulConfig.imageOptions.srcsetSizes, /* @ts-ignore */
243
+ this.srcsetSizes = input(this.contentfulConfig.imageOptions.srcsetSizes, /* @ts-ignore */
248
244
  ...(ngDevMode ? [{ debugName: "srcsetSizes" }] : /* istanbul ignore next */ []));
249
245
  this.quality = input(null, { ...(ngDevMode ? { debugName: "quality" } : /* istanbul ignore next */ {}), transform: numberAttribute });
250
246
  this.focusArea = input(null, /* @ts-ignore */
251
247
  ...(ngDevMode ? [{ debugName: "focusArea" }] : /* istanbul ignore next */ []));
252
248
  this.resizeBehavior = input(null, /* @ts-ignore */
253
249
  ...(ngDevMode ? [{ debugName: "resizeBehavior" }] : /* istanbul ignore next */ []));
254
- this.hasPriority = input(false, { ...(ngDevMode ? { debugName: "hasPriority" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
255
- this.imgClass = input(null, /* @ts-ignore */
256
- ...(ngDevMode ? [{ debugName: "imgClass" }] : /* istanbul ignore next */ []));
257
- this.figureClass = input(null, /* @ts-ignore */
258
- ...(ngDevMode ? [{ debugName: "figureClass" }] : /* istanbul ignore next */ []));
259
- this.figcaptionClass = input(null, /* @ts-ignore */
260
- ...(ngDevMode ? [{ debugName: "figcaptionClass" }] : /* istanbul ignore next */ []));
261
- this.pictureClass = input(null, /* @ts-ignore */
262
- ...(ngDevMode ? [{ debugName: "pictureClass" }] : /* istanbul ignore next */ []));
263
- this.sizes = input(normalizePictureSizes(this._contentfulConfig.imageOptions.sizes), { ...(ngDevMode ? { debugName: "sizes" } : /* istanbul ignore next */ {}), transform: (v) => normalizePictureSizes(v) });
250
+ this.priority = input(false, { ...(ngDevMode ? { debugName: "priority" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
251
+ this.sizes = input(normalizePictureSizes(this.contentfulConfig.imageOptions.sizes), { ...(ngDevMode ? { debugName: "sizes" } : /* istanbul ignore next */ {}), transform: (v) => normalizePictureSizes(v) });
264
252
  this.normalizedAsset = computed(() => {
265
253
  const asset = this.asset();
266
254
  if (isContentfulGqlAsset(asset)) {
@@ -281,7 +269,7 @@ class ContentfulImageComponent {
281
269
  }
282
270
  }, /* @ts-ignore */
283
271
  ...(ngDevMode ? [{ debugName: "normalizedAsset" }] : /* istanbul ignore next */ []));
284
- this._sources = computed(() => {
272
+ this.sourcesValue = computed(() => {
285
273
  const asset = this.asset();
286
274
  const backgroundColor = this.backgroundColor();
287
275
  const srcsetSizes = this.srcsetSizes();
@@ -293,49 +281,41 @@ class ContentfulImageComponent {
293
281
  }
294
282
  return generateContentfulImageSources(asset, srcsetSizes, backgroundColor, quality, focusArea, resizeBehavior);
295
283
  }, /* @ts-ignore */
296
- ...(ngDevMode ? [{ debugName: "_sources" }] : /* istanbul ignore next */ []));
297
- this._defaultSrc = computed(() => {
284
+ ...(ngDevMode ? [{ debugName: "sourcesValue" }] : /* istanbul ignore next */ []));
285
+ this.defaultSrcValue = computed(() => {
298
286
  const asset = this.asset();
299
287
  return asset ? generateDefaultContentfulImageSource(asset) : null;
300
288
  }, /* @ts-ignore */
301
- ...(ngDevMode ? [{ debugName: "_defaultSrc" }] : /* istanbul ignore next */ []));
289
+ ...(ngDevMode ? [{ debugName: "defaultSrcValue" }] : /* istanbul ignore next */ []));
302
290
  }
303
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ContentfulImageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
304
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.5", 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: `
291
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ContentfulImageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
292
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.7", type: ContentfulImageComponent, isStandalone: true, selector: "et-contentful-image", inputs: { asset: { classPropertyName: "asset", publicName: "asset", isSignal: true, isRequired: true, transformFunction: null }, backgroundColor: { classPropertyName: "backgroundColor", publicName: "backgroundColor", isSignal: true, isRequired: false, transformFunction: null }, srcsetSizes: { classPropertyName: "srcsetSizes", publicName: "srcsetSizes", isSignal: true, isRequired: false, transformFunction: null }, quality: { classPropertyName: "quality", publicName: "quality", isSignal: true, isRequired: false, transformFunction: null }, focusArea: { classPropertyName: "focusArea", publicName: "focusArea", isSignal: true, isRequired: false, transformFunction: null }, resizeBehavior: { classPropertyName: "resizeBehavior", publicName: "resizeBehavior", isSignal: true, isRequired: false, transformFunction: null }, priority: { classPropertyName: "priority", publicName: "priority", isSignal: true, isRequired: false, transformFunction: null }, sizes: { classPropertyName: "sizes", publicName: "sizes", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "et-contentful-image" }, ngImport: i0, template: `
305
293
  <et-picture
306
- [imgClass]="imgClass()"
307
- [hasPriority]="hasPriority()"
308
- [figureClass]="figureClass()"
309
- [pictureClass]="pictureClass()"
310
- [figcaptionClass]="figcaptionClass()"
311
- [defaultSrc]="_defaultSrc()"
312
- [alt]="normalizedAsset().alt ?? null"
294
+ [priority]="priority()"
295
+ [defaultSrc]="defaultSrcValue()"
296
+ [alt]="normalizedAsset().alt ?? ''"
313
297
  [figcaption]="normalizedAsset().figcaption ?? null"
314
298
  [width]="normalizedAsset().width ?? null"
315
299
  [height]="normalizedAsset().height ?? null"
316
300
  [sizes]="sizes()"
317
- [sources]="_sources()"
301
+ [sources]="sourcesValue()"
318
302
  />
319
- `, isInline: true, dependencies: [{ kind: "component", type: PictureComponent, selector: "et-picture", inputs: ["sources", "hasPriority", "imgClass", "figureClass", "figcaptionClass", "pictureClass", "defaultSrc", "alt", "figcaption", "width", "height", "sizes"], outputs: ["imgLoaded", "imgError"] }], encapsulation: i0.ViewEncapsulation.None }); }
303
+ `, isInline: true, dependencies: [{ kind: "component", type: PictureComponent, selector: "et-picture", inputs: ["sources", "defaultSrc", "alt", "figcaption", "priority", "width", "height", "aspectRatio", "sizes"], outputs: ["imgLoad", "imgError"] }], encapsulation: i0.ViewEncapsulation.None }); }
320
304
  }
321
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ContentfulImageComponent, decorators: [{
305
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ContentfulImageComponent, decorators: [{
322
306
  type: Component,
323
307
  args: [{
324
308
  selector: 'et-contentful-image',
325
309
  template: `
326
310
  <et-picture
327
- [imgClass]="imgClass()"
328
- [hasPriority]="hasPriority()"
329
- [figureClass]="figureClass()"
330
- [pictureClass]="pictureClass()"
331
- [figcaptionClass]="figcaptionClass()"
332
- [defaultSrc]="_defaultSrc()"
333
- [alt]="normalizedAsset().alt ?? null"
311
+ [priority]="priority()"
312
+ [defaultSrc]="defaultSrcValue()"
313
+ [alt]="normalizedAsset().alt ?? ''"
334
314
  [figcaption]="normalizedAsset().figcaption ?? null"
335
315
  [width]="normalizedAsset().width ?? null"
336
316
  [height]="normalizedAsset().height ?? null"
337
317
  [sizes]="sizes()"
338
- [sources]="_sources()"
318
+ [sources]="sourcesValue()"
339
319
  />
340
320
  `,
341
321
  encapsulation: ViewEncapsulation.None,
@@ -344,7 +324,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
344
324
  class: 'et-contentful-image',
345
325
  },
346
326
  }]
347
- }], propDecorators: { asset: [{ type: i0.Input, args: [{ isSignal: true, alias: "asset", required: true }] }], backgroundColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "backgroundColor", required: false }] }], srcsetSizes: [{ type: i0.Input, args: [{ isSignal: true, alias: "srcsetSizes", required: false }] }], quality: [{ type: i0.Input, args: [{ isSignal: true, alias: "quality", required: false }] }], focusArea: [{ type: i0.Input, args: [{ isSignal: true, alias: "focusArea", required: false }] }], resizeBehavior: [{ type: i0.Input, args: [{ isSignal: true, alias: "resizeBehavior", required: false }] }], hasPriority: [{ type: i0.Input, args: [{ isSignal: true, alias: "hasPriority", required: false }] }], imgClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "imgClass", required: false }] }], figureClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "figureClass", required: false }] }], figcaptionClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "figcaptionClass", required: false }] }], pictureClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "pictureClass", required: false }] }], sizes: [{ type: i0.Input, args: [{ isSignal: true, alias: "sizes", required: false }] }] } });
327
+ }], propDecorators: { asset: [{ type: i0.Input, args: [{ isSignal: true, alias: "asset", required: true }] }], backgroundColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "backgroundColor", required: false }] }], srcsetSizes: [{ type: i0.Input, args: [{ isSignal: true, alias: "srcsetSizes", required: false }] }], quality: [{ type: i0.Input, args: [{ isSignal: true, alias: "quality", required: false }] }], focusArea: [{ type: i0.Input, args: [{ isSignal: true, alias: "focusArea", required: false }] }], resizeBehavior: [{ type: i0.Input, args: [{ isSignal: true, alias: "resizeBehavior", required: false }] }], priority: [{ type: i0.Input, args: [{ isSignal: true, alias: "priority", required: false }] }], sizes: [{ type: i0.Input, args: [{ isSignal: true, alias: "sizes", required: false }] }] } });
348
328
 
349
329
  class ContentfulVideoComponent {
350
330
  constructor() {
@@ -370,8 +350,8 @@ class ContentfulVideoComponent {
370
350
  }, /* @ts-ignore */
371
351
  ...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
372
352
  }
373
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ContentfulVideoComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
374
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: ContentfulVideoComponent, isStandalone: true, selector: "et-contentful-video", inputs: { asset: { classPropertyName: "asset", publicName: "asset", isSignal: true, isRequired: true, transformFunction: null }, videoClass: { classPropertyName: "videoClass", publicName: "videoClass", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
353
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ContentfulVideoComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
354
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: ContentfulVideoComponent, isStandalone: true, selector: "et-contentful-video", inputs: { asset: { classPropertyName: "asset", publicName: "asset", isSignal: true, isRequired: true, transformFunction: null }, videoClass: { classPropertyName: "videoClass", publicName: "videoClass", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
375
355
  @if (data(); as data) {
376
356
  <video [ngClass]="videoClass()" controls>
377
357
  <source src="{{ data.url }}" type="{{ data.contentType }}" />
@@ -379,7 +359,7 @@ class ContentfulVideoComponent {
379
359
  }
380
360
  `, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], encapsulation: i0.ViewEncapsulation.None }); }
381
361
  }
382
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ContentfulVideoComponent, decorators: [{
362
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ContentfulVideoComponent, decorators: [{
383
363
  type: Component,
384
364
  args: [{
385
365
  selector: 'et-contentful-video',
@@ -395,10 +375,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
395
375
  }]
396
376
  }], propDecorators: { asset: [{ type: i0.Input, args: [{ isSignal: true, alias: "asset", required: true }] }], videoClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "videoClass", required: false }] }] } });
397
377
 
378
+ /**
379
+ * The config used when no `provideContentfulConfig()` is in scope. It declares no
380
+ * `components`, so embedded assets are skipped and hyperlinks render as plain anchors
381
+ * until a config is provided.
382
+ */
383
+ const CONTENTFUL_FALLBACK_CONFIG = {
384
+ internalHosts: [],
385
+ components: {},
386
+ customComponents: {},
387
+ imageOptions: {
388
+ srcsetSizes: ['375w', '1280w', '1920w', '2560w'],
389
+ sizes: [],
390
+ backgroundColor: null,
391
+ },
392
+ };
398
393
  const createContentfulConfig = (config = {}) => ({
399
394
  // This default config must be inline and not saved inside a const for some weird webpack reason
400
395
  ...{
401
- useTailwindClasses: false,
402
396
  internalHosts: [],
403
397
  components: {
404
398
  audio: ContentfulAudioComponent,
@@ -416,6 +410,11 @@ const createContentfulConfig = (config = {}) => ({
416
410
  },
417
411
  ...(config || {}),
418
412
  });
413
+ /**
414
+ * Reads the contentful config from the current injector. Falls back to a config without
415
+ * any `components` when none was provided via `provideContentfulConfig()`.
416
+ */
417
+ const injectContentfulConfig = () => inject(CONTENTFUL_CONFIG, { optional: true }) ?? CONTENTFUL_FALLBACK_CONFIG;
419
418
 
420
419
  const getPrimaryDomain = (host) => {
421
420
  const parts = host.split('.');
@@ -423,8 +422,8 @@ const getPrimaryDomain = (host) => {
423
422
  };
424
423
  class ContentfulLinkComponent {
425
424
  constructor() {
426
- this._document = inject(DOCUMENT);
427
- this._config = inject(CONTENTFUL_CONFIG, { optional: true }) ?? createContentfulConfig();
425
+ this.document = inject(DOCUMENT);
426
+ this.config = injectContentfulConfig();
428
427
  this.href = input.required(/* @ts-ignore */
429
428
  ...(ngDevMode ? [{ debugName: "href" }] : /* istanbul ignore next */ []));
430
429
  this.text = input.required(/* @ts-ignore */
@@ -438,7 +437,7 @@ class ContentfulLinkComponent {
438
437
  return false;
439
438
  try {
440
439
  const parsed = new URL(href.startsWith('//') ? `https:${href}` : href);
441
- const internalHosts = [this._document.location.host, ...this._config.internalHosts];
440
+ const internalHosts = [this.document.location.host, ...this.config.internalHosts];
442
441
  return !internalHosts.includes(parsed.host);
443
442
  }
444
443
  catch {
@@ -453,8 +452,8 @@ class ContentfulLinkComponent {
453
452
  try {
454
453
  const parsed = new URL(href.startsWith('//') ? `https:${href}` : href);
455
454
  const internalPrimaryDomains = [
456
- getPrimaryDomain(this._document.location.host),
457
- ...this._config.internalHosts.map(getPrimaryDomain),
455
+ getPrimaryDomain(this.document.location.host),
456
+ ...this.config.internalHosts.map(getPrimaryDomain),
458
457
  ];
459
458
  return !internalPrimaryDomains.includes(getPrimaryDomain(parsed.host));
460
459
  }
@@ -484,8 +483,8 @@ class ContentfulLinkComponent {
484
483
  }, /* @ts-ignore */
485
484
  ...(ngDevMode ? [{ debugName: "linkClass" }] : /* istanbul ignore next */ []));
486
485
  }
487
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ContentfulLinkComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
488
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: ContentfulLinkComponent, isStandalone: true, selector: "et-contentful-link", inputs: { href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: true, transformFunction: null }, text: { classPropertyName: "text", publicName: "text", isSignal: true, isRequired: true, transformFunction: null }, textClass: { classPropertyName: "textClass", publicName: "textClass", isSignal: true, isRequired: false, transformFunction: null } }, host: { styleAttribute: "display: contents" }, ngImport: i0, template: `
486
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ContentfulLinkComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
487
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: ContentfulLinkComponent, isStandalone: true, selector: "et-contentful-link", inputs: { href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: true, transformFunction: null }, text: { classPropertyName: "text", publicName: "text", isSignal: true, isRequired: true, transformFunction: null }, textClass: { classPropertyName: "textClass", publicName: "textClass", isSignal: true, isRequired: false, transformFunction: null } }, host: { styleAttribute: "display: contents" }, ngImport: i0, template: `
489
488
  @if (isExternal()) {
490
489
  <a
491
490
  [class]="linkClass()"
@@ -499,7 +498,7 @@ class ContentfulLinkComponent {
499
498
  }
500
499
  `, isInline: true, dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }], encapsulation: i0.ViewEncapsulation.None }); }
501
500
  }
502
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ContentfulLinkComponent, decorators: [{
501
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ContentfulLinkComponent, decorators: [{
503
502
  type: Component,
504
503
  args: [{
505
504
  selector: 'et-contentful-link',
@@ -516,14 +515,53 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
516
515
  <a [class]="linkClass()" [routerLink]="internalPath()">{{ text() }}</a>
517
516
  }
518
517
  `,
518
+ encapsulation: ViewEncapsulation.None,
519
+ imports: [RouterLink],
519
520
  host: {
520
521
  style: 'display: contents',
521
522
  },
522
- imports: [RouterLink],
523
- encapsulation: ViewEncapsulation.None,
524
523
  }]
525
524
  }], propDecorators: { href: [{ type: i0.Input, args: [{ isSignal: true, alias: "href", required: true }] }], text: [{ type: i0.Input, args: [{ isSignal: true, alias: "text", required: true }] }], textClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "textClass", required: false }] }] } });
526
525
 
526
+ /**
527
+ * The `BLOCKS` node type strings of `@contentful/rich-text-types`, as a plain literal map.
528
+ * Keep the values in sync with the enum - they are part of the Contentful Delivery API.
529
+ */
530
+ const CF_BLOCKS = {
531
+ DOCUMENT: 'document',
532
+ PARAGRAPH: 'paragraph',
533
+ HEADING_1: 'heading-1',
534
+ HEADING_2: 'heading-2',
535
+ HEADING_3: 'heading-3',
536
+ HEADING_4: 'heading-4',
537
+ HEADING_5: 'heading-5',
538
+ HEADING_6: 'heading-6',
539
+ OL_LIST: 'ordered-list',
540
+ UL_LIST: 'unordered-list',
541
+ LIST_ITEM: 'list-item',
542
+ HR: 'hr',
543
+ QUOTE: 'blockquote',
544
+ EMBEDDED_ENTRY: 'embedded-entry-block',
545
+ EMBEDDED_ASSET: 'embedded-asset-block',
546
+ EMBEDDED_RESOURCE: 'embedded-resource-block',
547
+ TABLE: 'table',
548
+ TABLE_ROW: 'table-row',
549
+ TABLE_CELL: 'table-cell',
550
+ TABLE_HEADER_CELL: 'table-header-cell',
551
+ };
552
+ /**
553
+ * The `INLINES` node type strings of `@contentful/rich-text-types`, as a plain literal map.
554
+ * Keep the values in sync with the enum - they are part of the Contentful Delivery API.
555
+ */
556
+ const CF_INLINES = {
557
+ ASSET_HYPERLINK: 'asset-hyperlink',
558
+ EMBEDDED_ENTRY: 'embedded-entry-inline',
559
+ EMBEDDED_RESOURCE: 'embedded-resource-inline',
560
+ ENTRY_HYPERLINK: 'entry-hyperlink',
561
+ HYPERLINK: 'hyperlink',
562
+ RESOURCE_HYPERLINK: 'resource-hyperlink',
563
+ };
564
+
527
565
  const RICH_TEXT_RENDERER_ERRORS = {
528
566
  rich_text_undefined: 'The property value given at richTextPath is undefined. Use the richTextPath input to specify the path to the rich text object.',
529
567
  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".',
@@ -535,10 +573,17 @@ const RICH_TEXT_RENDERER_ERRORS = {
535
573
  text_parent_not_found: 'The parent node is not found. This structure is not supported.',
536
574
  text_parent_wrong_type: 'The parent node neither a html element nor a custom component. This structure is not supported.',
537
575
  };
538
- const RICH_TEXT_RENDERER_ERROR_CODES = Object.keys(RICH_TEXT_RENDERER_ERRORS).reduce((acc, key, index) => {
539
- acc[key] = index;
540
- return acc;
541
- }, {});
576
+ const RICH_TEXT_RENDERER_ERROR_CODES = {
577
+ rich_text_undefined: 0,
578
+ rich_text_wrong_type: 1,
579
+ asset_id_not_found: 2,
580
+ entry_id_not_found: 3,
581
+ asset_not_found: 4,
582
+ entry_not_found: 5,
583
+ custom_component_not_found: 6,
584
+ text_parent_not_found: 7,
585
+ text_parent_wrong_type: 8,
586
+ };
542
587
  const richTextRendererError = (code, data) => {
543
588
  const message = `<et-contentful-rich-text-renderer>: ${RICH_TEXT_RENDERER_ERRORS[code]}`;
544
589
  throw new RuntimeError(RICH_TEXT_RENDERER_ERROR_CODES[code], message, data);
@@ -549,160 +594,93 @@ const isRichTextRootNode = (node) => {
549
594
  };
550
595
  const translateContentfulNodeTypeToHtmlTag = (nodeType) => {
551
596
  switch (nodeType) {
552
- case BLOCKS.HEADING_1:
597
+ case CF_BLOCKS.HEADING_1:
553
598
  return 'h1';
554
- case BLOCKS.HEADING_2:
599
+ case CF_BLOCKS.HEADING_2:
555
600
  return 'h2';
556
- case BLOCKS.HEADING_3:
601
+ case CF_BLOCKS.HEADING_3:
557
602
  return 'h3';
558
- case BLOCKS.HEADING_4:
603
+ case CF_BLOCKS.HEADING_4:
559
604
  return 'h4';
560
- case BLOCKS.HEADING_5:
605
+ case CF_BLOCKS.HEADING_5:
561
606
  return 'h5';
562
- case BLOCKS.HEADING_6:
607
+ case CF_BLOCKS.HEADING_6:
563
608
  return 'h6';
564
- case BLOCKS.PARAGRAPH:
609
+ case CF_BLOCKS.PARAGRAPH:
565
610
  return 'p';
566
- case BLOCKS.UL_LIST:
611
+ case CF_BLOCKS.UL_LIST:
567
612
  return 'ul';
568
- case BLOCKS.OL_LIST:
613
+ case CF_BLOCKS.OL_LIST:
569
614
  return 'ol';
570
- case BLOCKS.LIST_ITEM:
615
+ case CF_BLOCKS.LIST_ITEM:
571
616
  return 'li';
572
- case BLOCKS.HR:
617
+ case CF_BLOCKS.HR:
573
618
  return 'hr';
574
- case BLOCKS.QUOTE:
619
+ case CF_BLOCKS.QUOTE:
575
620
  return 'blockquote';
576
- case BLOCKS.TABLE:
621
+ case CF_BLOCKS.TABLE:
577
622
  return 'table';
578
- case BLOCKS.TABLE_ROW:
623
+ case CF_BLOCKS.TABLE_ROW:
579
624
  return 'tr';
580
- case BLOCKS.TABLE_CELL:
625
+ case CF_BLOCKS.TABLE_CELL:
581
626
  return 'td';
582
- case BLOCKS.TABLE_HEADER_CELL:
627
+ case CF_BLOCKS.TABLE_HEADER_CELL:
583
628
  return 'th';
584
- case BLOCKS.EMBEDDED_ASSET:
629
+ case CF_BLOCKS.EMBEDDED_ASSET:
585
630
  return 'div';
586
- case INLINES.ENTRY_HYPERLINK:
631
+ case CF_INLINES.ENTRY_HYPERLINK:
587
632
  return 'a';
588
- case INLINES.ASSET_HYPERLINK:
633
+ case CF_INLINES.ASSET_HYPERLINK:
589
634
  return 'a';
590
635
  case 'text':
591
636
  return 'span';
592
637
  // Will be ignored by the renderer
593
638
  case 'document':
594
639
  return 'div';
595
- case BLOCKS.EMBEDDED_ENTRY:
640
+ case CF_BLOCKS.EMBEDDED_ENTRY:
596
641
  return 'div';
597
- case INLINES.EMBEDDED_ENTRY:
642
+ case CF_INLINES.EMBEDDED_ENTRY:
598
643
  return 'div';
599
644
  default:
600
645
  return 'div';
601
646
  }
602
647
  };
603
648
 
604
- const RENDER_COMMAND_TYPE = {
605
- HTML_OPEN: 0,
606
- HTML_CLOSE: 1,
607
- TEXT: 2,
608
- COMPONENT: 3,
609
- };
610
- const RENDER_COMMAND_POSITION = {
611
- TYPE: 0,
612
- NESTING_LEVEL: 1,
613
- DOM_POSITION: 2,
614
- INDEX: 3,
615
- };
616
- const HTML_OPEN_RENDER_COMMAND_POSITION = {
617
- ATTRIBUTES: 4,
618
- TAG_NAME: 5,
619
- ELEMENT_ID: 6,
649
+ const MARK_TAG_MAP = {
650
+ bold: 'strong',
651
+ italic: 'em',
652
+ underline: 'u',
653
+ code: 'code',
654
+ strikethrough: 's',
655
+ subscript: 'sub',
656
+ superscript: 'sup',
620
657
  };
621
- const HTML_CLOSE_RENDER_COMMAND_POSITION = {
622
- TAG_NAME: 4,
623
- ELEMENT_ID: 5,
624
- };
625
- const TEXT_RENDER_COMMAND_POSITION = {
626
- ATTRIBUTES: 4,
627
- TEXT: 5,
628
- TEXT_ID: 6,
629
- };
630
- const COMPONENT_RENDER_COMMAND_POSITION = {
631
- ATTRIBUTES: 4,
632
- COMPONENT: 5,
633
- INPUTS: 6,
634
- COMPONENT_ID: 7,
635
- };
636
- const RENDER_INSTRUCTION_POSITION = {
637
- TYPE: 0,
638
- COMMAND: 1,
639
- };
640
- const RENDER_INSTRUCTION_TYPE = {
641
- CREATE: 0,
642
- UPDATE: 1,
643
- DELETE: 2,
644
- MOVE: 3,
645
- };
646
- const MARK_TAILWIND_MAP = {
647
- bold: 'font-bold',
648
- italic: 'italic',
649
- underline: 'underline',
650
- code: 'font-mono',
651
- };
652
- const marksToClass = (marks) => {
653
- const classes = [];
658
+ /**
659
+ * Translates contentful text marks into the semantic html elements the marked text is
660
+ * wrapped in, outermost first. Unknown marks are skipped (with a dev-mode warning).
661
+ */
662
+ const marksToTags = (marks) => {
663
+ const tags = [];
654
664
  for (const mark of marks) {
655
- let klass = MARK_TAILWIND_MAP[mark.type];
656
- if (!klass) {
657
- console.warn(`No class found for mark type! Falling back to "${mark.type}".`, mark);
658
- klass = mark.type;
665
+ const tag = MARK_TAG_MAP[mark.type];
666
+ if (!tag) {
667
+ if (isDevMode()) {
668
+ console.warn(`No element found for mark type "${mark.type}"! The mark is ignored.`, mark);
669
+ }
670
+ continue;
659
671
  }
660
- classes.push(klass);
661
- }
662
- return classes.join(' ');
663
- };
664
- const isHtmlOpenRenderCommand = (command) => {
665
- return command[RENDER_COMMAND_POSITION.TYPE] === RENDER_COMMAND_TYPE.HTML_OPEN;
666
- };
667
- const isHtmlCloseRenderCommand = (command) => {
668
- return command[RENDER_COMMAND_POSITION.TYPE] === RENDER_COMMAND_TYPE.HTML_CLOSE;
669
- };
670
- const isTextRenderCommand = (command) => {
671
- return command[RENDER_COMMAND_POSITION.TYPE] === RENDER_COMMAND_TYPE.TEXT;
672
- };
673
- const isComponentRenderCommand = (command) => {
674
- return command[RENDER_COMMAND_POSITION.TYPE] === RENDER_COMMAND_TYPE.COMPONENT;
675
- };
676
- const getRenderCommandId = (command) => {
677
- if (isHtmlOpenRenderCommand(command)) {
678
- return command[HTML_OPEN_RENDER_COMMAND_POSITION.ELEMENT_ID];
679
- }
680
- else if (isHtmlCloseRenderCommand(command)) {
681
- return command[HTML_CLOSE_RENDER_COMMAND_POSITION.ELEMENT_ID];
682
- }
683
- else if (isTextRenderCommand(command)) {
684
- return command[TEXT_RENDER_COMMAND_POSITION.TEXT_ID];
685
- }
686
- else {
687
- return command[COMPONENT_RENDER_COMMAND_POSITION.COMPONENT_ID];
672
+ tags.push(tag);
688
673
  }
674
+ return tags;
689
675
  };
690
- const CLASS_ATTR = 'class';
691
- const DEFAULT_COMPONENT_TYPES = {
692
- IMAGE: '$$$_et-image',
693
- VIDEO: '$$$_et-video',
694
- AUDIO: '$$$_et-audio',
695
- FILE: '$$$_et-file',
696
- LINK: '$$$_et-link',
697
- };
676
+ /**
677
+ * The classes marked text inside a hyperlink is rendered with, since the link component
678
+ * receives its text as a plain string.
679
+ */
680
+ const marksToClass = (marks) => marks.map((mark) => `et-contentful-rich-text-mark-${mark.type}`).join(' ');
681
+ const LINK_COMPONENT_TYPE = '$$$_et-link';
698
682
  const isExecutedComponentCommandCacheItem = (cache) => {
699
- return isComponentRenderCommand(cache.command);
700
- };
701
- const isExecutedHtmlOrTextCommandCacheItem = (cache) => {
702
- return isHtmlCloseRenderCommand(cache.command) || isHtmlOpenRenderCommand(cache.command);
703
- };
704
- const isExecutedTextCommandCacheItem = (cache) => {
705
- return isTextRenderCommand(cache.command);
683
+ return cache.command.kind === 'component';
706
684
  };
707
685
  const ET_CONTENTFUL_ANY_ENTRY_CONTENT_TYPE_SYS_ID = '$$$_et-contentful-any-entry-content-type-sys-id';
708
686
  /**
@@ -758,17 +736,10 @@ const createContentfulIncludeMap = (config) => {
758
736
  };
759
737
  class ContentfulRichTextRendererComponent {
760
738
  constructor() {
761
- this._viewContainerRef = inject(ViewContainerRef);
762
- this._renderer = injectRenderer();
763
- this._elementRef = inject(ElementRef);
764
- this._document = inject(DOCUMENT);
765
- this._config = inject(CONTENTFUL_CONFIG, { optional: true }) ?? createContentfulConfig();
766
- /**
767
- * A cache for all executed commands that are not deleted.
768
- * This is used to keep track of all rendered elements and components.
769
- * The key is the render command element ID.
770
- */
771
- this._executedCommandsCache = new Map();
739
+ this.viewContainerRef = inject(ViewContainerRef);
740
+ this.renderer = injectRenderer();
741
+ this.elementRef = inject(ElementRef);
742
+ this.config = injectContentfulConfig();
772
743
  /**
773
744
  * The contentful response gotten via their REST api.
774
745
  * @see https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/entries/entries-collection
@@ -781,6 +752,12 @@ class ContentfulRichTextRendererComponent {
781
752
  */
782
753
  this.richTextPath = input.required(/* @ts-ignore */
783
754
  ...(ngDevMode ? [{ debugName: "richTextPath" }] : /* istanbul ignore next */ []));
755
+ /**
756
+ * A cache for all executed commands that are not deleted.
757
+ * This is used to keep track of all rendered elements and components.
758
+ * The key is the render command ID.
759
+ */
760
+ this.executedCommandsCache = new Map();
784
761
  /**
785
762
  * A map of all includes in the contentful response.
786
763
  * This is useful for looking up assets and entries that are referenced in the rich text without having to use loops.
@@ -816,93 +793,116 @@ class ContentfulRichTextRendererComponent {
816
793
  if (!richTextData) {
817
794
  return [];
818
795
  }
819
- return this._createRenderCommands(richTextData);
796
+ return this.createRenderCommands(richTextData);
820
797
  }, /* @ts-ignore */
821
798
  ...(ngDevMode ? [{ debugName: "renderCommands" }] : /* istanbul ignore next */ []));
822
- this.renderCommandHistory = toSignal(toObservable(this.renderCommands).pipe(startWith([]), pairwise()), { initialValue: [[], []] });
799
+ this.renderCommandHistory = linkedSignal({ ...(ngDevMode ? { debugName: "renderCommandHistory" } : /* istanbul ignore next */ {}), source: this.renderCommands,
800
+ computation: (commands, previous) => [previous?.source ?? [], commands] });
823
801
  this.previousRenderCommandMap = computed(() => {
824
802
  const [prevCommands] = this.renderCommandHistory();
825
803
  const map = new Map();
826
804
  for (const command of prevCommands) {
827
- map.set(getRenderCommandId(command), command);
805
+ map.set(command.id, command);
828
806
  }
829
807
  return map;
830
808
  }, /* @ts-ignore */
831
809
  ...(ngDevMode ? [{ debugName: "previousRenderCommandMap" }] : /* istanbul ignore next */ []));
832
810
  this.renderInstructions = computed(() => {
833
811
  const commands = this.renderCommands();
834
- const previousRenderCommandMap = new Map(this.previousRenderCommandMap());
812
+ const previousRenderCommandMap = this.previousRenderCommandMap();
835
813
  const instructions = [];
836
- // remove deleted components as well as all other elements
837
- for (const [, command] of previousRenderCommandMap) {
838
- if (isComponentRenderCommand(command)) {
839
- // keep the component around if it's still in the new commands
840
- if (commands.some((c) => getRenderCommandId(c) === getRenderCommandId(command))) {
841
- continue;
814
+ // Decide, in document order, which commands survive the change with their DOM intact.
815
+ // A plain element or text node survives when the same id renders the same output at the same
816
+ // spot inside a surviving parent. Ancestors are decided first because parents precede their
817
+ // children in command order. Components survive on id alone — their DOM can be reattached.
818
+ const preserved = new Set();
819
+ const needsReattach = new Set();
820
+ const parentIdOf = (command) => {
821
+ if (command.nestingLevel === 0) {
822
+ return null;
823
+ }
824
+ for (let i = command.index - 1; i >= 0; i--) {
825
+ const cmd = commands[i];
826
+ if (cmd && cmd.nestingLevel === command.nestingLevel - 1 && cmd.kind === 'htmlOpen') {
827
+ return cmd.id;
842
828
  }
843
829
  }
844
- instructions.push([RENDER_INSTRUCTION_TYPE.DELETE, command]);
845
- // remove from map to mark as used
846
- previousRenderCommandMap.delete(getRenderCommandId(command));
847
- }
848
- // Find the indexes of the commands remaining in the previousRenderCommandMap in the new commands array.
849
- const lastComponentIndexes = [];
850
- for (const [id, command] of previousRenderCommandMap) {
851
- const index = commands.findIndex((c) => getRenderCommandId(c) === id);
852
- const newCommand = commands[index];
853
- if (index === -1) {
854
- throw new Error('Command not found!');
830
+ return null;
831
+ };
832
+ const markTagsEqual = (a, b) => a.length === b.length && a.every((tag, index) => tag === b[index]);
833
+ const attributesEqual = (a, b) => {
834
+ const aKeys = Object.keys(a);
835
+ return aKeys.length === Object.keys(b).length && aKeys.every((key) => a[key] === b[key]);
836
+ };
837
+ for (const command of commands) {
838
+ if (command.kind === 'htmlClose') {
839
+ continue;
855
840
  }
856
- lastComponentIndexes.push({
857
- newIndex: index,
858
- prevIndex: command[RENDER_COMMAND_POSITION.INDEX],
859
- command: newCommand,
860
- });
861
- }
862
- lastComponentIndexes.sort((a, b) => a.newIndex - b.newIndex);
863
- // check if the prevIndex are in order ascending
864
- // for indexes that are not in order, create a move instruction
865
- // for indexes that are in order, create an update instruction
866
- let moveCheckIndex = 0;
867
- while (moveCheckIndex < lastComponentIndexes.length - 1) {
868
- const moveCurr = lastComponentIndexes[moveCheckIndex];
869
- const moveNext = lastComponentIndexes[moveCheckIndex + 1];
870
- if (!moveCurr || !moveNext) {
871
- break;
841
+ const previous = previousRenderCommandMap.get(command.id);
842
+ if (!previous || previous.kind !== command.kind) {
843
+ continue;
872
844
  }
873
- if (moveCurr.prevIndex > moveNext.prevIndex) {
874
- lastComponentIndexes.splice(moveCheckIndex + 1, 1);
875
- instructions.push([RENDER_INSTRUCTION_TYPE.MOVE, moveNext.command]);
845
+ // The same key must render the same component class — a changed class means a fresh instance.
846
+ if (previous.kind === 'component' && command.kind === 'component' && previous.component !== command.component) {
847
+ continue;
876
848
  }
877
- else {
878
- instructions.push([RENDER_INSTRUCTION_TYPE.UPDATE, moveNext.command]);
879
- moveCheckIndex++;
849
+ const parentId = parentIdOf(command);
850
+ const parentPreserved = parentId === null || preserved.has(parentId);
851
+ if (command.kind === 'component') {
852
+ preserved.add(command.id);
853
+ const samePlace = parentPreserved &&
854
+ previous.nestingLevel === command.nestingLevel &&
855
+ previous.domPosition === command.domPosition;
856
+ if (!samePlace) {
857
+ needsReattach.add(command.id);
858
+ }
859
+ continue;
860
+ }
861
+ const sameOutput = previous.kind === command.kind &&
862
+ (previous.kind !== 'htmlOpen' || previous.tagName === command.tagName) &&
863
+ (previous.kind !== 'text' ||
864
+ (previous.text === command.text &&
865
+ markTagsEqual(previous.markTags, command.markTags)));
866
+ if (parentPreserved &&
867
+ sameOutput &&
868
+ previous.nestingLevel === command.nestingLevel &&
869
+ previous.domPosition === command.domPosition &&
870
+ attributesEqual(previous.attributes, command.attributes)) {
871
+ preserved.add(command.id);
872
+ }
873
+ }
874
+ // Everything that did not survive is removed first, so the ordered pass below can rebuild
875
+ // into a clean tree. Close commands never render DOM and are skipped throughout.
876
+ for (const [id, command] of previousRenderCommandMap) {
877
+ if (command.kind !== 'htmlClose' && !preserved.has(id)) {
878
+ instructions.push({ type: 'delete', command });
880
879
  }
881
880
  }
881
+ // One pass in document order: parents are created before a child component is reattached.
882
+ // Surviving plain elements still get an update so the cache tracks their fresh command data.
882
883
  for (const command of commands) {
883
- const id = getRenderCommandId(command);
884
- if (isComponentRenderCommand(command)) {
885
- const previousCommand = previousRenderCommandMap.get(id);
886
- if (!previousCommand) {
887
- // create
888
- instructions.push([RENDER_INSTRUCTION_TYPE.CREATE, command]);
889
- }
884
+ if (command.kind === 'htmlClose') {
885
+ continue;
886
+ }
887
+ if (!preserved.has(command.id)) {
888
+ instructions.push({ type: 'create', command });
889
+ }
890
+ else if (command.kind === 'component' && needsReattach.has(command.id)) {
891
+ instructions.push({ type: 'move', command });
890
892
  }
891
893
  else {
892
- // create
893
- instructions.push([RENDER_INSTRUCTION_TYPE.CREATE, command]);
894
+ instructions.push({ type: 'update', command });
894
895
  }
895
896
  }
896
897
  return instructions;
897
898
  }, /* @ts-ignore */
898
899
  ...(ngDevMode ? [{ debugName: "renderInstructions" }] : /* istanbul ignore next */ []));
899
- toObservable(this.renderInstructions)
900
- .pipe(takeUntilDestroyed(), tap((instructions) => {
901
- this._execInstructions(instructions);
902
- }))
903
- .subscribe();
900
+ effect(() => {
901
+ const instructions = this.renderInstructions();
902
+ untracked(() => this.execInstructions(instructions));
903
+ });
904
904
  }
905
- _createRenderCommands(richTextData) {
905
+ createRenderCommands(richTextData) {
906
906
  /** List of all render commands */
907
907
  const rootCommands = [];
908
908
  /** Counter for generating unique html element IDs. */
@@ -920,32 +920,26 @@ class ContentfulRichTextRendererComponent {
920
920
  const traverse = (node) => {
921
921
  switch (node.nodeType) {
922
922
  case 'text': {
923
- // Render media
924
- const type = RENDER_COMMAND_TYPE.TEXT;
925
923
  const text = node.value;
926
924
  if (!text)
927
925
  break;
928
926
  const attributes = {
929
927
  class: 'et-contentful-rich-text-default-element et-contentful-rich-text-default-span',
930
928
  };
931
- if (node.marks.length) {
932
- const markClasses = marksToClass(node.marks);
933
- if (CLASS_ATTR in attributes) {
934
- attributes[CLASS_ATTR] += ` ${markClasses}`;
935
- }
936
- else {
937
- attributes[CLASS_ATTR] = markClasses;
938
- }
939
- }
940
- const id = 't' + textId++;
941
- const command = [type, nestingLevel, domPosition, commandIndex++, attributes, text, id];
942
- rootCommands.push(command);
929
+ rootCommands.push({
930
+ kind: 'text',
931
+ nestingLevel,
932
+ domPosition,
933
+ index: commandIndex++,
934
+ attributes,
935
+ markTags: node.marks.length ? marksToTags(node.marks) : [],
936
+ text,
937
+ id: 't' + textId++,
938
+ });
943
939
  domPosition++;
944
940
  break;
945
941
  }
946
- case BLOCKS.EMBEDDED_ASSET: {
947
- // Render media
948
- const type = RENDER_COMMAND_TYPE.COMPONENT;
942
+ case CF_BLOCKS.EMBEDDED_ASSET: {
949
943
  const assetId = node.data['target']?.sys?.id;
950
944
  if (!assetId) {
951
945
  throw richTextRendererError('asset_id_not_found', { node });
@@ -955,7 +949,7 @@ class ContentfulRichTextRendererComponent {
955
949
  throw richTextRendererError('asset_not_found', { assetId, node });
956
950
  }
957
951
  const contentType = asset.fields.file.contentType;
958
- const assetComponents = this._config.components;
952
+ const assetComponents = this.config.components;
959
953
  // Every property inside the asset will be null if no file was provided for a translation.
960
954
  // In this case, we can assume that the asset is missing due to user error.
961
955
  const isMissing = !contentType && !asset.fields.file.url;
@@ -975,35 +969,29 @@ class ContentfulRichTextRendererComponent {
975
969
  : isAudio
976
970
  ? assetComponents.audio
977
971
  : assetComponents.file;
978
- const componentType = isImage
979
- ? DEFAULT_COMPONENT_TYPES.IMAGE
980
- : isVideo
981
- ? DEFAULT_COMPONENT_TYPES.VIDEO
982
- : isAudio
983
- ? DEFAULT_COMPONENT_TYPES.AUDIO
984
- : DEFAULT_COMPONENT_TYPES.FILE;
985
- let componentId = componentIdMap.get(componentType) ?? -1;
986
- const id = componentType + ++componentId;
987
- componentIdMap.set(componentType, componentId);
988
- const attributes = {};
989
- const inputs = {
990
- asset,
991
- };
992
- const command = [
993
- type,
972
+ if (!component) {
973
+ if (isDevMode()) {
974
+ console.warn('No component registered for this embedded asset! The asset will be skipped. Provide one via provideContentfulConfig({ components: … }).', { asset });
975
+ }
976
+ break;
977
+ }
978
+ const occurrenceKey = 'asset:' + assetId;
979
+ const occurrence = (componentIdMap.get(occurrenceKey) ?? -1) + 1;
980
+ componentIdMap.set(occurrenceKey, occurrence);
981
+ const id = occurrence === 0 ? occurrenceKey : `${occurrenceKey}:${occurrence}`;
982
+ rootCommands.push({
983
+ kind: 'component',
994
984
  nestingLevel,
995
985
  domPosition,
996
- commandIndex++,
997
- attributes,
986
+ index: commandIndex++,
998
987
  component,
999
- inputs,
988
+ inputs: { asset },
1000
989
  id,
1001
- ];
1002
- rootCommands.push(command);
990
+ });
1003
991
  domPosition++;
1004
992
  break;
1005
993
  }
1006
- case INLINES.HYPERLINK: {
994
+ case CF_INLINES.HYPERLINK: {
1007
995
  const uri = node.data['uri'] ?? '';
1008
996
  // Collect all text and marks from children (hyperlink children are text nodes)
1009
997
  let linkText = '';
@@ -1014,28 +1002,67 @@ class ContentfulRichTextRendererComponent {
1014
1002
  linkMarks.push(...child.marks);
1015
1003
  }
1016
1004
  }
1005
+ const linkComponent = this.config.components.link;
1006
+ if (!linkComponent) {
1007
+ // Without a link component the hyperlink still renders - as a plain anchor.
1008
+ rootCommands.push({
1009
+ kind: 'htmlOpen',
1010
+ nestingLevel,
1011
+ domPosition,
1012
+ index: commandIndex++,
1013
+ attributes: {
1014
+ class: 'et-contentful-rich-text-default-element et-contentful-rich-text-default-a',
1015
+ href: uri,
1016
+ },
1017
+ tagName: 'a',
1018
+ id: 'e-o' + elementOpenId++,
1019
+ });
1020
+ const anchorDomPosition = domPosition;
1021
+ nestingLevel++;
1022
+ domPosition = 0;
1023
+ rootCommands.push({
1024
+ kind: 'text',
1025
+ nestingLevel,
1026
+ domPosition,
1027
+ index: commandIndex++,
1028
+ attributes: {
1029
+ class: 'et-contentful-rich-text-default-element et-contentful-rich-text-default-span',
1030
+ },
1031
+ markTags: marksToTags(linkMarks),
1032
+ text: linkText,
1033
+ id: 't' + textId++,
1034
+ });
1035
+ nestingLevel--;
1036
+ domPosition = anchorDomPosition;
1037
+ rootCommands.push({
1038
+ kind: 'htmlClose',
1039
+ nestingLevel,
1040
+ domPosition,
1041
+ index: commandIndex++,
1042
+ tagName: 'a',
1043
+ id: 'e-c' + elementCloseId++,
1044
+ });
1045
+ domPosition++;
1046
+ break;
1047
+ }
1017
1048
  const textClass = linkMarks.length ? marksToClass(linkMarks) : '';
1018
- let linkComponentId = componentIdMap.get(DEFAULT_COMPONENT_TYPES.LINK) ?? -1;
1019
- const linkId = DEFAULT_COMPONENT_TYPES.LINK + ++linkComponentId;
1020
- componentIdMap.set(DEFAULT_COMPONENT_TYPES.LINK, linkComponentId);
1021
- const linkCommand = [
1022
- RENDER_COMMAND_TYPE.COMPONENT,
1049
+ let linkComponentId = componentIdMap.get(LINK_COMPONENT_TYPE) ?? -1;
1050
+ const linkId = LINK_COMPONENT_TYPE + ++linkComponentId;
1051
+ componentIdMap.set(LINK_COMPONENT_TYPE, linkComponentId);
1052
+ rootCommands.push({
1053
+ kind: 'component',
1023
1054
  nestingLevel,
1024
1055
  domPosition,
1025
- commandIndex++,
1026
- {},
1027
- this._config.components.link,
1028
- { href: uri, text: linkText, textClass },
1029
- linkId,
1030
- ];
1031
- rootCommands.push(linkCommand);
1056
+ index: commandIndex++,
1057
+ component: linkComponent,
1058
+ inputs: { href: uri, text: linkText, textClass },
1059
+ id: linkId,
1060
+ });
1032
1061
  domPosition++;
1033
1062
  break;
1034
1063
  }
1035
- case BLOCKS.EMBEDDED_ENTRY:
1036
- case INLINES.EMBEDDED_ENTRY: {
1037
- // Render component
1038
- const type = RENDER_COMMAND_TYPE.COMPONENT;
1064
+ case CF_BLOCKS.EMBEDDED_ENTRY:
1065
+ case CF_INLINES.EMBEDDED_ENTRY: {
1039
1066
  const entryId = node.data['target']?.sys?.id;
1040
1067
  if (!entryId) {
1041
1068
  throw richTextRendererError('entry_id_not_found', { node });
@@ -1045,47 +1072,49 @@ class ContentfulRichTextRendererComponent {
1045
1072
  throw richTextRendererError('entry_not_found', { entryId, node });
1046
1073
  }
1047
1074
  const componentType = entry.sys.contentType.sys.id;
1048
- const component = this._config.customComponents[componentType];
1075
+ const component = this.config.customComponents[componentType];
1049
1076
  if (!component) {
1050
1077
  throw richTextRendererError('custom_component_not_found', {
1051
1078
  componentType,
1052
- customComponents: this._config.customComponents,
1079
+ customComponents: this.config.customComponents,
1053
1080
  entry,
1054
1081
  });
1055
1082
  }
1056
- const attributes = {};
1057
- let componentId = componentIdMap.get(componentType) ?? -1;
1058
- const id = componentType + ++componentId;
1059
- componentIdMap.set(componentType, componentId);
1060
- const inputs = {
1061
- fields: entry.fields,
1062
- metadata: entry.metadata,
1063
- sys: entry.sys,
1064
- includes: this.contentIncludesMap(),
1065
- };
1066
- const command = [
1067
- type,
1083
+ const occurrenceKey = 'entry:' + entryId;
1084
+ const occurrence = (componentIdMap.get(occurrenceKey) ?? -1) + 1;
1085
+ componentIdMap.set(occurrenceKey, occurrence);
1086
+ const id = occurrence === 0 ? occurrenceKey : `${occurrenceKey}:${occurrence}`;
1087
+ rootCommands.push({
1088
+ kind: 'component',
1068
1089
  nestingLevel,
1069
1090
  domPosition,
1070
- commandIndex++,
1071
- attributes,
1091
+ index: commandIndex++,
1072
1092
  component,
1073
- inputs,
1093
+ inputs: {
1094
+ fields: entry.fields,
1095
+ metadata: entry.metadata,
1096
+ sys: entry.sys,
1097
+ includes: this.contentIncludesMap(),
1098
+ },
1074
1099
  id,
1075
- ];
1076
- rootCommands.push(command);
1100
+ });
1077
1101
  domPosition++;
1078
1102
  break;
1079
1103
  }
1080
1104
  default: {
1081
- const type = RENDER_COMMAND_TYPE.HTML_OPEN;
1082
1105
  const tag = translateContentfulNodeTypeToHtmlTag(node.nodeType);
1083
1106
  const attributes = {
1084
1107
  class: `et-contentful-rich-text-default-element et-contentful-rich-text-default-${tag}`,
1085
1108
  };
1086
- const id = 'e-o' + elementOpenId++;
1087
- const command = [type, nestingLevel, domPosition, commandIndex++, attributes, tag, id];
1088
- rootCommands.push(command);
1109
+ rootCommands.push({
1110
+ kind: 'htmlOpen',
1111
+ nestingLevel,
1112
+ domPosition,
1113
+ index: commandIndex++,
1114
+ attributes,
1115
+ tagName: tag,
1116
+ id: 'e-o' + elementOpenId++,
1117
+ });
1089
1118
  const domPositionAtThisLevel = domPosition;
1090
1119
  // Normal html elements can have children
1091
1120
  for (const child of node.content) {
@@ -1096,25 +1125,21 @@ class ContentfulRichTextRendererComponent {
1096
1125
  domPosition = domPositionAtThisLevel;
1097
1126
  }
1098
1127
  const lastCommand = rootCommands[rootCommands.length - 1];
1099
- if (lastCommand?.[RENDER_COMMAND_POSITION.TYPE] === RENDER_COMMAND_TYPE.HTML_OPEN &&
1100
- lastCommand[HTML_OPEN_RENDER_COMMAND_POSITION.TAG_NAME] !== 'td' &&
1101
- lastCommand[HTML_OPEN_RENDER_COMMAND_POSITION.TAG_NAME] !== 'hr') {
1128
+ if (lastCommand?.kind === 'htmlOpen' && lastCommand.tagName !== 'td' && lastCommand.tagName !== 'hr') {
1102
1129
  // If the last command is an open command, we can remove it since it's empty
1103
1130
  rootCommands.pop();
1104
1131
  elementOpenId--;
1105
1132
  commandIndex--;
1106
1133
  }
1107
1134
  else {
1108
- const closeId = 'e-c' + elementCloseId++;
1109
- const closeCommand = [
1110
- RENDER_COMMAND_TYPE.HTML_CLOSE,
1135
+ rootCommands.push({
1136
+ kind: 'htmlClose',
1111
1137
  nestingLevel,
1112
1138
  domPosition,
1113
- commandIndex++,
1114
- tag,
1115
- closeId,
1116
- ];
1117
- rootCommands.push(closeCommand);
1139
+ index: commandIndex++,
1140
+ tagName: tag,
1141
+ id: 'e-c' + elementCloseId++,
1142
+ });
1118
1143
  domPosition++;
1119
1144
  }
1120
1145
  break;
@@ -1126,194 +1151,171 @@ class ContentfulRichTextRendererComponent {
1126
1151
  }
1127
1152
  return rootCommands;
1128
1153
  }
1129
- _execInstructions(instructions) {
1154
+ execInstructions(instructions) {
1130
1155
  for (const instruction of instructions) {
1131
- const type = instruction[RENDER_INSTRUCTION_POSITION.TYPE];
1132
- const command = instruction[RENDER_INSTRUCTION_POSITION.COMMAND];
1133
- switch (type) {
1134
- case RENDER_INSTRUCTION_TYPE.CREATE:
1135
- this._runCreateInstruction(command);
1156
+ switch (instruction.type) {
1157
+ case 'create':
1158
+ this.runCreateInstruction(instruction.command);
1136
1159
  break;
1137
- case RENDER_INSTRUCTION_TYPE.UPDATE:
1138
- this._runUpdateInstruction(command);
1160
+ case 'update':
1161
+ this.runUpdateInstruction(instruction.command);
1139
1162
  break;
1140
- case RENDER_INSTRUCTION_TYPE.MOVE:
1141
- this._runMoveInstruction(command);
1163
+ case 'move':
1164
+ this.runMoveInstruction(instruction.command);
1142
1165
  break;
1143
- case RENDER_INSTRUCTION_TYPE.DELETE:
1144
- this._runDeleteInstruction(command);
1166
+ case 'delete':
1167
+ this.runDeleteInstruction(instruction.command);
1145
1168
  break;
1146
1169
  }
1147
1170
  }
1148
1171
  }
1149
- _runCreateInstruction(command) {
1150
- const id = getRenderCommandId(command);
1151
- const parentElement = this._findParent(command);
1152
- const nextElement = this._findFollowingElement(command);
1153
- if (isComponentRenderCommand(command)) {
1154
- const componentRef = this._viewContainerRef.createComponent(command[COMPONENT_RENDER_COMMAND_POSITION.COMPONENT]);
1155
- this._updateComponentInputs(command, componentRef);
1156
- const rootNode = this._getComponentRootNode(componentRef);
1157
- this._renderInsertOrAppend(rootNode, parentElement, nextElement);
1158
- this._executedCommandsCache.set(id, {
1172
+ runCreateInstruction(command) {
1173
+ const parentElement = this.findParent(command);
1174
+ const nextElement = this.findFollowingElement(command);
1175
+ if (command.kind === 'component') {
1176
+ const inputs = signal(command.inputs, /* @ts-ignore */
1177
+ ...(ngDevMode ? [{ debugName: "inputs" }] : /* istanbul ignore next */ []));
1178
+ // Components may declare any subset of the offered inputs — only bind the declared ones.
1179
+ const declaredInputs = reflectComponentType(command.component)?.inputs ?? [];
1180
+ const bindings = declaredInputs
1181
+ .filter((declared) => declared.propName in command.inputs)
1182
+ .map((declared) => inputBinding(declared.templateName, () => inputs()[declared.propName]));
1183
+ const componentRef = this.viewContainerRef.createComponent(command.component, { bindings });
1184
+ const rootNode = this.getComponentRootNode(componentRef);
1185
+ this.renderInsertOrAppend(rootNode, parentElement, nextElement);
1186
+ this.executedCommandsCache.set(command.id, {
1159
1187
  command,
1160
1188
  componentRef,
1189
+ inputs,
1161
1190
  element: rootNode,
1162
1191
  });
1163
1192
  }
1164
- else if (isTextRenderCommand(command)) {
1165
- const text = command[TEXT_RENDER_COMMAND_POSITION.TEXT];
1166
- const span = this._renderer.createElement('span');
1167
- const attributes = command[TEXT_RENDER_COMMAND_POSITION.ATTRIBUTES];
1168
- const textSplitInLineBreaks = text.split('\n').filter((t) => t.trim().length > 0);
1169
- const textNodes = [];
1170
- for (const [key, value] of Object.entries(attributes)) {
1171
- this._renderer.setAttribute(span, key, value);
1193
+ else if (command.kind === 'text') {
1194
+ const span = this.renderer.createElement('span');
1195
+ const textSplitInLineBreaks = command.text.split('\n').filter((t) => t.trim().length > 0);
1196
+ for (const [key, value] of Object.entries(command.attributes)) {
1197
+ this.renderer.setAttribute(span, key, value);
1172
1198
  }
1173
- if (text.startsWith('\n')) {
1174
- const brNode = this._renderer.createElement('br');
1175
- this._renderer.appendChild(parentElement, brNode);
1199
+ if (command.text.startsWith('\n')) {
1200
+ const brNode = this.renderer.createElement('br');
1201
+ this.renderer.appendChild(parentElement, brNode);
1202
+ }
1203
+ // Marks are rendered as nested semantic elements inside the span, outermost mark first.
1204
+ let textContainer = span;
1205
+ for (const tag of command.markTags) {
1206
+ const markElement = this.renderer.createElement(tag);
1207
+ this.renderer.appendChild(textContainer, markElement);
1208
+ textContainer = markElement;
1176
1209
  }
1177
1210
  for (const [textPartIndex, textPart] of textSplitInLineBreaks.entries()) {
1178
1211
  if (textPartIndex > 0) {
1179
- const brNode = this._renderer.createElement('br');
1180
- this._renderer.appendChild(span, brNode);
1212
+ const brNode = this.renderer.createElement('br');
1213
+ this.renderer.appendChild(textContainer, brNode);
1181
1214
  }
1182
- const textNode = this._renderer.createText(textPart);
1183
- this._renderer.appendChild(span, textNode);
1184
- textNodes.push(textNode);
1215
+ const textNode = this.renderer.createText(textPart);
1216
+ this.renderer.appendChild(textContainer, textNode);
1185
1217
  }
1186
- this._renderInsertOrAppend(span, parentElement, nextElement);
1187
- this._executedCommandsCache.set(id, {
1218
+ this.renderInsertOrAppend(span, parentElement, nextElement);
1219
+ this.executedCommandsCache.set(command.id, {
1188
1220
  command,
1189
1221
  element: span,
1190
- textNode: textNodes,
1191
1222
  });
1192
1223
  }
1193
- else if (isHtmlOpenRenderCommand(command)) {
1194
- const tag = command[HTML_OPEN_RENDER_COMMAND_POSITION.TAG_NAME];
1195
- const attributes = command[HTML_OPEN_RENDER_COMMAND_POSITION.ATTRIBUTES];
1196
- const element = this._renderer.createElement(tag);
1197
- for (const [key, value] of Object.entries(attributes)) {
1198
- this._renderer.setAttribute(element, key, value);
1224
+ else if (command.kind === 'htmlOpen') {
1225
+ const element = this.renderer.createElement(command.tagName);
1226
+ for (const [key, value] of Object.entries(command.attributes)) {
1227
+ this.renderer.setAttribute(element, key, value);
1199
1228
  }
1200
- this._renderInsertOrAppend(element, parentElement, nextElement);
1201
- this._executedCommandsCache.set(id, {
1229
+ this.renderInsertOrAppend(element, parentElement, nextElement);
1230
+ this.executedCommandsCache.set(command.id, {
1202
1231
  command,
1203
1232
  element,
1204
1233
  });
1205
1234
  }
1206
1235
  }
1207
- _runUpdateInstruction(command) {
1208
- const id = getRenderCommandId(command);
1209
- const cached = this._executedCommandsCache.get(id);
1236
+ runUpdateInstruction(command) {
1237
+ const cached = this.executedCommandsCache.get(command.id);
1210
1238
  if (!cached) {
1211
1239
  throw new Error('Cached command not found!');
1212
1240
  }
1213
- if (isComponentRenderCommand(command)) {
1241
+ if (command.kind === 'component') {
1214
1242
  if (!isExecutedComponentCommandCacheItem(cached)) {
1215
1243
  throw new Error('Cached command is not a component command!');
1216
1244
  }
1217
- const componentRef = cached.componentRef;
1218
- if (!componentRef) {
1219
- throw new Error('Component ref not found!');
1220
- }
1221
- this._updateComponentInputs(command, componentRef);
1245
+ cached.inputs.set(command.inputs);
1222
1246
  }
1223
- this._executedCommandsCache.set(id, {
1247
+ // `cached` and the fresh `command` share the same id and therefore the same union variant,
1248
+ // but TS can't correlate the two after the spread — assert the merged object as the union.
1249
+ this.executedCommandsCache.set(command.id, {
1224
1250
  ...cached,
1225
- command: command,
1251
+ command,
1226
1252
  });
1227
1253
  }
1228
- _runMoveInstruction(command) {
1229
- const id = getRenderCommandId(command);
1230
- const cached = this._executedCommandsCache.get(id);
1254
+ runMoveInstruction(command) {
1255
+ const cached = this.executedCommandsCache.get(command.id);
1231
1256
  if (!cached) {
1232
1257
  throw new Error('Cached command not found!');
1233
1258
  }
1234
- if (isComponentRenderCommand(command)) {
1259
+ if (command.kind === 'component') {
1235
1260
  if (!isExecutedComponentCommandCacheItem(cached)) {
1236
1261
  throw new Error('Cached command is not a component command!');
1237
1262
  }
1238
- const componentRef = cached.componentRef;
1239
- if (!componentRef) {
1240
- throw new Error('Component ref not found!');
1241
- }
1242
1263
  const rootNode = cached.element;
1243
1264
  const oldParentElement = cached.element.parentElement;
1244
1265
  if (oldParentElement) {
1245
- this._renderer.removeChild(oldParentElement, rootNode);
1266
+ this.renderer.removeChild(oldParentElement, rootNode);
1246
1267
  }
1247
- const newParentElement = this._findParent(command);
1248
- const nextElement = this._findFollowingElement(command);
1249
- this._renderInsertOrAppend(rootNode, newParentElement, nextElement);
1250
- this._updateComponentInputs(command, componentRef);
1251
- this._executedCommandsCache.set(id, {
1268
+ const newParentElement = this.findParent(command);
1269
+ const nextElement = this.findFollowingElement(command);
1270
+ this.renderInsertOrAppend(rootNode, newParentElement, nextElement);
1271
+ cached.inputs.set(command.inputs);
1272
+ this.executedCommandsCache.set(command.id, {
1252
1273
  ...cached,
1253
- command: command,
1274
+ command,
1254
1275
  });
1255
1276
  }
1256
1277
  }
1257
- _runDeleteInstruction(command) {
1258
- const id = getRenderCommandId(command);
1259
- const cached = this._executedCommandsCache.get(id);
1278
+ runDeleteInstruction(command) {
1279
+ const cached = this.executedCommandsCache.get(command.id);
1260
1280
  if (!cached) {
1261
- if (isHtmlCloseRenderCommand(command)) {
1281
+ if (command.kind === 'htmlClose') {
1262
1282
  return;
1263
1283
  }
1264
1284
  throw new Error('Cached command not found!');
1265
1285
  }
1266
- if (isComponentRenderCommand(command)) {
1286
+ if (command.kind === 'component') {
1267
1287
  if (!isExecutedComponentCommandCacheItem(cached)) {
1268
1288
  throw new Error('Cached command is not a component command!');
1269
1289
  }
1270
- cached.componentRef?.destroy();
1271
- }
1272
- else if (isTextRenderCommand(command)) {
1273
- if (cached.element?.parentElement) {
1274
- this._renderer.removeChild(cached.element.parentElement, cached.element);
1275
- }
1290
+ cached.componentRef.destroy();
1276
1291
  }
1277
- else if (isHtmlOpenRenderCommand(command)) {
1278
- if (cached.element?.parentElement) {
1279
- this._renderer.removeChild(cached.element.parentElement, cached.element);
1292
+ else if (command.kind === 'text' || command.kind === 'htmlOpen') {
1293
+ if (cached.element.parentElement) {
1294
+ this.renderer.removeChild(cached.element.parentElement, cached.element);
1280
1295
  }
1281
1296
  }
1282
- this._executedCommandsCache.delete(id);
1297
+ this.executedCommandsCache.delete(command.id);
1283
1298
  }
1284
- _getComponentRootNode(componentRef) {
1299
+ getComponentRootNode(componentRef) {
1285
1300
  return componentRef.hostView.rootNodes[0];
1286
1301
  }
1287
- _updateComponentInputs(command, componentRef) {
1288
- const inputMeta = reflectComponentType(command[COMPONENT_RENDER_COMMAND_POSITION.COMPONENT])?.inputs ?? [];
1289
- const inputsByProp = new Map(inputMeta.map((i) => [i.propName, i.templateName]));
1290
- for (const [key, value] of Object.entries(command[COMPONENT_RENDER_COMMAND_POSITION.INPUTS])) {
1291
- const publicName = inputsByProp.get(key);
1292
- if (publicName === undefined)
1293
- continue;
1294
- componentRef.setInput(publicName, value);
1295
- }
1296
- }
1297
- _findParent(command) {
1298
- const hostElement = this._elementRef.nativeElement;
1299
- const nestingLevel = command[RENDER_COMMAND_POSITION.NESTING_LEVEL];
1302
+ findParent(command) {
1303
+ const hostElement = this.elementRef.nativeElement;
1300
1304
  let parentElement;
1301
- if (nestingLevel === 0) {
1305
+ if (command.nestingLevel === 0) {
1302
1306
  parentElement = hostElement;
1303
1307
  }
1304
1308
  else {
1305
- // Reverse search all render commands beginning from the current one using a for loop.
1306
- // The parent is found when the command's nesting level ist the same as the current nesting level minus 1.
1307
- // If the parent is not found, throw an error.
1309
+ // Reverse search all render commands beginning from the current one.
1310
+ // The parent is the closest preceding html open command one nesting level up.
1308
1311
  const allCommands = this.renderCommands();
1309
- const currentCommandIndex = command[RENDER_COMMAND_POSITION.INDEX];
1310
1312
  let parentCommand = null;
1311
- for (let i = currentCommandIndex - 1; i >= 0; i--) {
1313
+ for (let i = command.index - 1; i >= 0; i--) {
1312
1314
  const cmd = allCommands[i];
1313
1315
  if (!cmd) {
1314
1316
  throw new Error('Command not found!');
1315
1317
  }
1316
- if (cmd[RENDER_COMMAND_POSITION.NESTING_LEVEL] === nestingLevel - 1 && isHtmlOpenRenderCommand(cmd)) {
1318
+ if (cmd.nestingLevel === command.nestingLevel - 1 && cmd.kind === 'htmlOpen') {
1317
1319
  parentCommand = cmd;
1318
1320
  break;
1319
1321
  }
@@ -1321,45 +1323,41 @@ class ContentfulRichTextRendererComponent {
1321
1323
  if (!parentCommand) {
1322
1324
  throw new Error('Parent command not found!');
1323
1325
  }
1324
- parentElement = this._executedCommandsCache.get(getRenderCommandId(parentCommand))?.element;
1326
+ parentElement = this.executedCommandsCache.get(parentCommand.id)?.element;
1325
1327
  }
1326
1328
  if (!parentElement) {
1327
1329
  throw new Error('Parent element not found!');
1328
1330
  }
1329
1331
  return parentElement;
1330
1332
  }
1331
- _findFollowingElement(command) {
1332
- const cacheArray = Array.from(this._executedCommandsCache.values());
1333
- let nextElement = undefined;
1334
- const domPosition = command[RENDER_COMMAND_POSITION.DOM_POSITION];
1335
- const nestingLevel = command[RENDER_COMMAND_POSITION.NESTING_LEVEL];
1336
- const type = command[RENDER_COMMAND_POSITION.TYPE];
1337
- // 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.
1338
- // If it does not exist, use .appendChild to simply append the element to the end of the parent element.
1339
- // If it does exist, use .insertBefore to insert the element before the found element.
1340
- for (const cached of cacheArray) {
1341
- if (cached.command[RENDER_COMMAND_POSITION.DOM_POSITION] > domPosition &&
1342
- cached.command[RENDER_COMMAND_POSITION.NESTING_LEVEL] === nestingLevel &&
1343
- // Make sure the element does not find itself
1344
- cached.command[RENDER_COMMAND_POSITION.TYPE] !== type) {
1333
+ findFollowingElement(command) {
1334
+ // Find the closest already rendered element at the same nesting level with a greater
1335
+ // domPosition, to insert before. Without one the element is appended to the parent.
1336
+ let nextElement;
1337
+ let nextDomPosition = Infinity;
1338
+ for (const cached of this.executedCommandsCache.values()) {
1339
+ if (cached.command.domPosition > command.domPosition &&
1340
+ cached.command.domPosition < nextDomPosition &&
1341
+ cached.command.nestingLevel === command.nestingLevel &&
1342
+ cached.command.id !== command.id) {
1345
1343
  nextElement = cached.element;
1346
- break;
1344
+ nextDomPosition = cached.command.domPosition;
1347
1345
  }
1348
1346
  }
1349
1347
  return nextElement;
1350
1348
  }
1351
- _renderInsertOrAppend(nodeToRender, parentElement, nextElement) {
1349
+ renderInsertOrAppend(nodeToRender, parentElement, nextElement) {
1352
1350
  if (nextElement) {
1353
- this._renderer.insertBefore(parentElement, nodeToRender, nextElement);
1351
+ this.renderer.insertBefore(parentElement, nodeToRender, nextElement);
1354
1352
  }
1355
1353
  else {
1356
- this._renderer.appendChild(parentElement, nodeToRender);
1354
+ this.renderer.appendChild(parentElement, nodeToRender);
1357
1355
  }
1358
1356
  }
1359
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ContentfulRichTextRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1360
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.5", type: ContentfulRichTextRendererComponent, isStandalone: true, selector: "et-contentful-rich-text-renderer", inputs: { content: { classPropertyName: "content", publicName: "content", isSignal: true, isRequired: true, transformFunction: null }, richTextPath: { classPropertyName: "richTextPath", publicName: "richTextPath", isSignal: true, isRequired: true, transformFunction: null } }, host: { classAttribute: "et-contentful-rich-text-renderer" }, ngImport: i0, template: ``, isInline: true, encapsulation: i0.ViewEncapsulation.None }); }
1357
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ContentfulRichTextRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1358
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.7", type: ContentfulRichTextRendererComponent, isStandalone: true, selector: "et-contentful-rich-text-renderer", inputs: { content: { classPropertyName: "content", publicName: "content", isSignal: true, isRequired: true, transformFunction: null }, richTextPath: { classPropertyName: "richTextPath", publicName: "richTextPath", isSignal: true, isRequired: true, transformFunction: null } }, host: { classAttribute: "et-contentful-rich-text-renderer" }, ngImport: i0, template: ``, isInline: true, encapsulation: i0.ViewEncapsulation.None }); }
1361
1359
  }
1362
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ContentfulRichTextRendererComponent, decorators: [{
1360
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ContentfulRichTextRendererComponent, decorators: [{
1363
1361
  type: Component,
1364
1362
  args: [{
1365
1363
  selector: 'et-contentful-rich-text-renderer',
@@ -1384,11 +1382,9 @@ const provideContentfulConfig = (contentfulConfig = {}) => {
1384
1382
  };
1385
1383
  const isContentfulEntryType = (entry, type) => entry.sys.contentType.sys.id === type;
1386
1384
 
1387
- // Burst cache
1388
-
1389
1385
  /**
1390
1386
  * Generated bundle index. Do not edit.
1391
1387
  */
1392
1388
 
1393
- export { COMPONENT_RENDER_COMMAND_POSITION, CONTENTFUL_CONFIG, ContentfulAudioComponent, ContentfulFileComponent, ContentfulImageComponent, ContentfulImports, ContentfulLinkComponent, ContentfulRichTextRendererComponent, ContentfulVideoComponent, ET_CONTENTFUL_ANY_ENTRY_CONTENT_TYPE_SYS_ID, 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, createContentfulIncludeMap, generateContentfulImageSources, generateDefaultContentfulImageSource, getRenderCommandId, isComponentRenderCommand, isContentfulEntryType, isContentfulGqlAsset, isExecutedComponentCommandCacheItem, isExecutedHtmlOrTextCommandCacheItem, isExecutedTextCommandCacheItem, isHtmlCloseRenderCommand, isHtmlOpenRenderCommand, isRichTextRootNode, isTextRenderCommand, marksToClass, parseContentfulImageSize, provideContentfulConfig, translateContentfulNodeTypeToHtmlTag };
1389
+ export { CONTENTFUL_CONFIG, ContentfulAudioComponent, ContentfulFileComponent, ContentfulImageComponent, ContentfulImports, ContentfulLinkComponent, ContentfulRichTextRendererComponent, ContentfulVideoComponent, ET_CONTENTFUL_ANY_ENTRY_CONTENT_TYPE_SYS_ID, GQL_FRAGMENT_CONTENTFUL_ASSET, createContentfulConfig, createContentfulIncludeMap, generateContentfulImageSources, generateDefaultContentfulImageSource, injectContentfulConfig, isContentfulEntryType, isContentfulGqlAsset, isRichTextRootNode, marksToClass, marksToTags, parseContentfulImageSize, provideContentfulConfig, translateContentfulNodeTypeToHtmlTag };
1394
1390
  //# sourceMappingURL=ethlete-contentful.mjs.map