@ethlete/contentful 4.0.0-next.10 → 4.0.0-next.12

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,9 +1,9 @@
1
1
  import { NgClass, DOCUMENT } from '@angular/common';
2
2
  import * as i0 from '@angular/core';
3
- import { input, computed, ViewEncapsulation, Component, inject, InjectionToken, numberAttribute, booleanAttribute, isDevMode, ViewContainerRef, ElementRef, effect, untracked, signal, reflectComponentType, inputBinding } from '@angular/core';
4
- import { normalizePictureSizes, PictureComponent } from '@ethlete/components';
3
+ import { input, computed, ViewEncapsulation, Component, InjectionToken, inject, numberAttribute, booleanAttribute, isDevMode, ViewContainerRef, ElementRef, effect, untracked, signal, reflectComponentType, inputBinding } from '@angular/core';
4
+ import { formatFileSize, normalizePictureSizes, PictureComponent } from '@ethlete/components';
5
+ import { defineLabels, toProvideFn, toInjectFn, toToken, injectUrl, RuntimeError, isObject, injectRenderer, getObjectProperty } from '@ethlete/core';
5
6
  import { Router, RouterLink } from '@angular/router';
6
- import { RuntimeError, isObject, injectRenderer, getObjectProperty } from '@ethlete/core';
7
7
 
8
8
  const GQL_FRAGMENT_CONTENTFUL_ASSET = `
9
9
  fragment ContentfulAssetData on Asset {
@@ -12,6 +12,7 @@ const GQL_FRAGMENT_CONTENTFUL_ASSET = `
12
12
  }
13
13
  url
14
14
  title
15
+ fileName
15
16
  width
16
17
  height
17
18
  description
@@ -42,13 +43,13 @@ class ContentfulAudioComponent {
42
43
  if (isContentfulGqlAsset(asset) && asset.url) {
43
44
  return {
44
45
  url: asset.url,
45
- title: asset.title,
46
+ title: asset.title || asset.fileName || null,
46
47
  };
47
48
  }
48
- if (!isContentfulGqlAsset(asset) && asset.fields.file.url) {
49
+ if (!isContentfulGqlAsset(asset) && asset.fields.file?.url) {
49
50
  return {
50
51
  url: asset.fields.file.url,
51
- title: asset.fields.title,
52
+ title: asset.fields.title || asset.fields.file.fileName || null,
52
53
  };
53
54
  }
54
55
  return null;
@@ -85,8 +86,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
85
86
  }]
86
87
  }], propDecorators: { asset: [{ type: i0.Input, args: [{ isSignal: true, alias: "asset", required: true }] }], audioClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "audioClass", required: false }] }], figureClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "figureClass", required: false }] }], figcaptionClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "figcaptionClass", required: false }] }] } });
87
88
 
89
+ /** The built-in English labels. */
90
+ const DEFAULT_CONTENTFUL_FILE_LABELS = {
91
+ fileSize: (bytes) => `(${formatFileSize(bytes)})`,
92
+ };
93
+ const CONTENTFUL_FILE_LABELS_DEF = /* @__PURE__ */ defineLabels('CONTENTFUL_FILE_LABELS', DEFAULT_CONTENTFUL_FILE_LABELS);
94
+ /**
95
+ * Localize `et-contentful-file` for everything below this injector, and read the set in effect here as a signal.
96
+ * Partial - whatever you leave out keeps its {@link DEFAULT_CONTENTFUL_FILE_LABELS} value.
97
+ *
98
+ * @example
99
+ * provideContentfulFileLabels({ fileSize: (bytes) => `(${new Intl.NumberFormat('de').format(bytes)} Byte)` });
100
+ */
101
+ const provideContentfulFileLabels = /* @__PURE__ */ toProvideFn(CONTENTFUL_FILE_LABELS_DEF);
102
+ const injectContentfulFileLabels = /* @__PURE__ */ toInjectFn(CONTENTFUL_FILE_LABELS_DEF);
103
+ const CONTENTFUL_FILE_LABELS = /* @__PURE__ */ toToken(CONTENTFUL_FILE_LABELS_DEF);
104
+
88
105
  class ContentfulFileComponent {
89
106
  constructor() {
107
+ this.labels = injectContentfulFileLabels();
90
108
  this.asset = input.required(/* @ts-ignore */
91
109
  ...(ngDevMode ? [{ debugName: "asset" }] : /* istanbul ignore next */ []));
92
110
  this.fileClass = input(null, /* @ts-ignore */
@@ -100,14 +118,14 @@ class ContentfulFileComponent {
100
118
  return {
101
119
  url: asset.url,
102
120
  size: asset.size,
103
- title: asset.title,
121
+ title: asset.title || asset.fileName || null,
104
122
  };
105
123
  }
106
- if (!isContentfulGqlAsset(asset) && asset.fields.file.url) {
124
+ if (!isContentfulGqlAsset(asset) && asset.fields.file?.url) {
107
125
  return {
108
126
  url: asset.fields.file.url,
109
127
  size: asset.fields.file.details.size,
110
- title: asset.fields.title,
128
+ title: asset.fields.title || asset.fields.file.fileName || null,
111
129
  };
112
130
  }
113
131
  return null;
@@ -120,7 +138,7 @@ class ContentfulFileComponent {
120
138
  <a [href]="data.url" [ngClass]="fileClass()" target="_blank" rel="noopener noreferrer">
121
139
  {{ data.title }}
122
140
  @if (data.size !== null) {
123
- ({{ data.size }} Bytes)
141
+ {{ labels().fileSize(data.size) }}
124
142
  }
125
143
  </a>
126
144
  }
@@ -135,7 +153,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
135
153
  <a [href]="data.url" [ngClass]="fileClass()" target="_blank" rel="noopener noreferrer">
136
154
  {{ data.title }}
137
155
  @if (data.size !== null) {
138
- ({{ data.size }} Bytes)
156
+ {{ labels().fileSize(data.size) }}
139
157
  }
140
158
  </a>
141
159
  }
@@ -148,160 +166,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
148
166
  }]
149
167
  }], propDecorators: { asset: [{ type: i0.Input, args: [{ isSignal: true, alias: "asset", required: true }] }], fileClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "fileClass", required: false }] }] } });
150
168
 
151
- const parseWebUrl = (href) => {
152
- if (!/^(?:https?:)?\/\//i.test(href)) {
153
- return null;
154
- }
155
- try {
156
- return new URL(href.startsWith('//') ? 'https:' + href : href);
157
- }
158
- catch {
159
- return null;
160
- }
161
- };
162
- const normalizeHostname = (host) => {
163
- const value = host.trim().toLowerCase();
164
- try {
165
- return new URL(value.includes('://') ? value : 'https://' + value).hostname;
166
- }
167
- catch {
168
- return value.split(':')[0] ?? value;
169
- }
170
- };
171
- const matchesHostname = (hostname, configuredHost) => {
172
- const normalizedHost = normalizeHostname(configuredHost);
173
- return hostname === normalizedHost || hostname.endsWith('.' + normalizedHost);
174
- };
175
- class ContentfulLinkComponent {
176
- constructor() {
177
- this.document = inject(DOCUMENT);
178
- this.router = inject(Router);
179
- this.config = injectContentfulConfig();
180
- this.href = input.required(/* @ts-ignore */
181
- ...(ngDevMode ? [{ debugName: "href" }] : /* istanbul ignore next */ []));
182
- this.text = input.required(/* @ts-ignore */
183
- ...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
184
- this.textClass = input('', /* @ts-ignore */
185
- ...(ngDevMode ? [{ debugName: "textClass" }] : /* istanbul ignore next */ []));
186
- this.usesRouterLink = computed(() => {
187
- const href = this.href();
188
- const absoluteUrl = parseWebUrl(href);
189
- if (absoluteUrl) {
190
- const internalHosts = [this.document.location.hostname, ...this.config.internalHosts];
191
- return internalHosts.some((host) => matchesHostname(absoluteUrl.hostname, host));
192
- }
193
- return !href.startsWith('#') && !/^[a-z][a-z\d+.-]*:/i.test(href);
194
- }, /* @ts-ignore */
195
- ...(ngDevMode ? [{ debugName: "usesRouterLink" }] : /* istanbul ignore next */ []));
196
- this.openInNewTab = computed(() => Boolean(parseWebUrl(this.href())) && !this.usesRouterLink(), /* @ts-ignore */
197
- ...(ngDevMode ? [{ debugName: "openInNewTab" }] : /* istanbul ignore next */ []));
198
- this.internalPath = computed(() => {
199
- const href = this.href();
200
- const url = parseWebUrl(href);
201
- if (!url) {
202
- return href;
203
- }
204
- return url.pathname + url.search + url.hash;
205
- }, /* @ts-ignore */
206
- ...(ngDevMode ? [{ debugName: "internalPath" }] : /* istanbul ignore next */ []));
207
- this.internalUrlTree = computed(() => this.router.parseUrl(this.internalPath()), /* @ts-ignore */
208
- ...(ngDevMode ? [{ debugName: "internalUrlTree" }] : /* istanbul ignore next */ []));
209
- this.linkClass = computed(() => {
210
- const base = 'et-contentful-rich-text-default-element et-contentful-rich-text-default-a';
211
- const extra = this.textClass();
212
- return extra ? `${base} ${extra}` : base;
213
- }, /* @ts-ignore */
214
- ...(ngDevMode ? [{ debugName: "linkClass" }] : /* istanbul ignore next */ []));
215
- }
216
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulLinkComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
217
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: ContentfulLinkComponent, isStandalone: true, selector: "et-contentful-link", inputs: { href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: true, transformFunction: null }, text: { classPropertyName: "text", publicName: "text", isSignal: true, isRequired: true, transformFunction: null }, textClass: { classPropertyName: "textClass", publicName: "textClass", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "et-contentful-link" }, ngImport: i0, template: `
218
- @if (usesRouterLink()) {
219
- <a [class]="linkClass()" [routerLink]="internalUrlTree()">{{ text() }}</a>
220
- } @else {
221
- <a
222
- [class]="linkClass()"
223
- [href]="href()"
224
- [target]="openInNewTab() ? '_blank' : null"
225
- [rel]="openInNewTab() ? 'noopener noreferrer' : null"
226
- >{{ text() }}</a
227
- >
228
- }
229
- `, isInline: true, styles: ["@layer components{.et-contentful-link{display:contents}}\n"], dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }], encapsulation: i0.ViewEncapsulation.None }); }
230
- }
231
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulLinkComponent, decorators: [{
232
- type: Component,
233
- args: [{ selector: 'et-contentful-link', template: `
234
- @if (usesRouterLink()) {
235
- <a [class]="linkClass()" [routerLink]="internalUrlTree()">{{ text() }}</a>
236
- } @else {
237
- <a
238
- [class]="linkClass()"
239
- [href]="href()"
240
- [target]="openInNewTab() ? '_blank' : null"
241
- [rel]="openInNewTab() ? 'noopener noreferrer' : null"
242
- >{{ text() }}</a
243
- >
244
- }
245
- `, encapsulation: ViewEncapsulation.None, imports: [RouterLink], host: {
246
- class: 'et-contentful-link',
247
- }, styles: ["@layer components{.et-contentful-link{display:contents}}\n"] }]
248
- }], propDecorators: { href: [{ type: i0.Input, args: [{ isSignal: true, alias: "href", required: true }] }], text: [{ type: i0.Input, args: [{ isSignal: true, alias: "text", required: true }] }], textClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "textClass", required: false }] }] } });
249
-
250
- class ContentfulVideoComponent {
251
- constructor() {
252
- this.asset = input.required(/* @ts-ignore */
253
- ...(ngDevMode ? [{ debugName: "asset" }] : /* istanbul ignore next */ []));
254
- this.videoClass = input(null, /* @ts-ignore */
255
- ...(ngDevMode ? [{ debugName: "videoClass" }] : /* istanbul ignore next */ []));
256
- this.data = computed(() => {
257
- const asset = this.asset();
258
- if (!asset) {
259
- return null;
260
- }
261
- if (isContentfulGqlAsset(asset) && asset.url) {
262
- return {
263
- url: asset.url,
264
- contentType: asset.contentType,
265
- };
266
- }
267
- if (!isContentfulGqlAsset(asset) && asset.fields.file.url) {
268
- return {
269
- url: asset.fields.file.url,
270
- contentType: asset.fields.file.contentType,
271
- };
272
- }
273
- return null;
274
- }, /* @ts-ignore */
275
- ...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
276
- }
277
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulVideoComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
278
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: ContentfulVideoComponent, isStandalone: true, selector: "et-contentful-video", inputs: { asset: { classPropertyName: "asset", publicName: "asset", isSignal: true, isRequired: true, transformFunction: null }, videoClass: { classPropertyName: "videoClass", publicName: "videoClass", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "et-contentful-video" }, ngImport: i0, template: `
279
- @if (data(); as data) {
280
- <video [ngClass]="videoClass()" controls>
281
- <source [src]="data.url" [type]="data.contentType" />
282
- </video>
283
- }
284
- `, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], encapsulation: i0.ViewEncapsulation.None }); }
285
- }
286
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulVideoComponent, decorators: [{
287
- type: Component,
288
- args: [{
289
- selector: 'et-contentful-video',
290
- template: `
291
- @if (data(); as data) {
292
- <video [ngClass]="videoClass()" controls>
293
- <source [src]="data.url" [type]="data.contentType" />
294
- </video>
295
- }
296
- `,
297
- encapsulation: ViewEncapsulation.None,
298
- imports: [NgClass],
299
- host: {
300
- class: 'et-contentful-video',
301
- },
302
- }]
303
- }], propDecorators: { asset: [{ type: i0.Input, args: [{ isSignal: true, alias: "asset", required: true }] }], videoClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "videoClass", required: false }] }] } });
304
-
305
169
  const CONTENTFUL_CONFIG = new InjectionToken('ContentfulConfig');
306
170
 
307
171
  /**
@@ -319,25 +183,15 @@ const CONTENTFUL_FALLBACK_CONFIG = {
319
183
  backgroundColor: null,
320
184
  },
321
185
  };
322
- const createContentfulConfig = (config) => ({
323
- ...{
324
- internalHosts: [],
325
- components: {
326
- audio: ContentfulAudioComponent,
327
- file: ContentfulFileComponent,
328
- image: ContentfulImageComponent,
329
- video: ContentfulVideoComponent,
330
- link: ContentfulLinkComponent,
331
- },
332
- customComponents: {},
333
- imageOptions: {
334
- srcsetSizes: ['375w', '1280w', '1920w', '2560w'],
335
- sizes: ['100vw'],
336
- backgroundColor: null,
337
- },
338
- },
339
- ...(config ?? {}),
340
- });
186
+ const createContentfulConfig = (options) => {
187
+ const config = options ?? {};
188
+ return {
189
+ ...CONTENTFUL_FALLBACK_CONFIG,
190
+ ...config,
191
+ components: config.components ?? CONTENTFUL_FALLBACK_CONFIG.components,
192
+ imageOptions: { ...CONTENTFUL_FALLBACK_CONFIG.imageOptions, ...config.imageOptions },
193
+ };
194
+ };
341
195
  /**
342
196
  * Reads the contentful config from the current injector. Falls back to a config without
343
197
  * any `components` when none was provided via `provideContentfulConfig()`.
@@ -357,15 +211,16 @@ const generateDefaultContentfulImageSource = (data) => {
357
211
  srcset: data.url,
358
212
  };
359
213
  }
360
- if (!data.fields.file.contentType || !data.fields.file.url) {
214
+ const file = data.fields.file;
215
+ if (!file?.contentType || !file.url) {
361
216
  return {
362
217
  type: '',
363
218
  srcset: '',
364
219
  };
365
220
  }
366
221
  return {
367
- type: data.fields.file.contentType,
368
- srcset: data.fields.file.url,
222
+ type: file.contentType,
223
+ srcset: file.url,
369
224
  };
370
225
  };
371
226
  /**
@@ -392,13 +247,20 @@ const parseContentfulImageSize = (size) => {
392
247
  return width ? { width: Number(width[1]), height: null } : { width: null, height: null };
393
248
  };
394
249
  const SOURCE_TYPES = ['image/avif', 'image/webp'];
395
- const generateContentfulImageSources = (data, srcsetSizes, backgroundColor, quality, focusArea, resizeBehavior) => {
250
+ /**
251
+ * Builds AVIF and WebP `<source>` entries for a Contentful asset through the Contentful Images API.
252
+ *
253
+ * @example
254
+ * generateContentfulImageSources(asset, { srcsetSizes: ['400w', '800w'], quality: 80 });
255
+ */
256
+ const generateContentfulImageSources = (data, options = {}) => {
257
+ const { srcsetSizes = [], backgroundColor = null, quality = null, focusArea = null, resizeBehavior = null } = options;
396
258
  const isGqlAsset = isContentfulGqlAsset(data);
397
- const baseUrl = isGqlAsset ? data.url : data.fields.file.url;
259
+ const baseUrl = isGqlAsset ? data.url : data.fields.file?.url;
398
260
  if (!baseUrl) {
399
261
  return [];
400
262
  }
401
- const imageDimensions = isGqlAsset ? data : data.fields.file.details.image;
263
+ const imageDimensions = isGqlAsset ? data : data.fields.file?.details.image;
402
264
  const sources = [];
403
265
  for (const type of SOURCE_TYPES) {
404
266
  const sourceSets = [];
@@ -416,7 +278,7 @@ const generateContentfulImageSources = (data, srcsetSizes, backgroundColor, qual
416
278
  if (resizeBehavior) {
417
279
  queryParams.push(`fit=${resizeBehavior}`);
418
280
  }
419
- if (srcsetSizes?.length) {
281
+ if (srcsetSizes.length) {
420
282
  const urlWithParams = `${baseUrl}${baseUrl.includes('?') ? '&' : '?'}${queryParams.join('&')}`;
421
283
  for (const size of srcsetSizes) {
422
284
  const { width, height } = parseContentfulImageSize(size);
@@ -484,15 +346,17 @@ class ContentfulImageComponent {
484
346
  ...(ngDevMode ? [{ debugName: "normalizedAsset" }] : /* istanbul ignore next */ []));
485
347
  this.sourcesValue = computed(() => {
486
348
  const asset = this.asset();
487
- const backgroundColor = this.backgroundColor();
488
- const srcsetSizes = this.srcsetSizes();
489
- const quality = this.quality();
490
- const focusArea = this.focusArea();
491
- const resizeBehavior = this.resizeBehavior();
349
+ const options = {
350
+ srcsetSizes: this.srcsetSizes(),
351
+ backgroundColor: this.backgroundColor(),
352
+ quality: this.quality(),
353
+ focusArea: this.focusArea(),
354
+ resizeBehavior: this.resizeBehavior(),
355
+ };
492
356
  if (!asset) {
493
357
  return [];
494
358
  }
495
- return generateContentfulImageSources(asset, srcsetSizes, backgroundColor, quality, focusArea, resizeBehavior);
359
+ return generateContentfulImageSources(asset, options);
496
360
  }, /* @ts-ignore */
497
361
  ...(ngDevMode ? [{ debugName: "sourcesValue" }] : /* istanbul ignore next */ []));
498
362
  this.defaultSrcValue = computed(() => {
@@ -539,6 +403,117 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
539
403
  }]
540
404
  }], 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 }] }] } });
541
405
 
406
+ const parseWebUrl = (href) => {
407
+ if (!/^(?:https?:)?\/\//i.test(href)) {
408
+ return null;
409
+ }
410
+ try {
411
+ return new URL(href.startsWith('//') ? 'https:' + href : href);
412
+ }
413
+ catch {
414
+ return null;
415
+ }
416
+ };
417
+ const normalizeHostname = (host) => {
418
+ const value = host.trim().toLowerCase();
419
+ try {
420
+ return new URL(value.includes('://') ? value : 'https://' + value).hostname;
421
+ }
422
+ catch {
423
+ return value.split(':')[0] ?? value;
424
+ }
425
+ };
426
+ const matchesHostname = (hostname, configuredHost) => {
427
+ const normalizedHost = normalizeHostname(configuredHost);
428
+ return hostname === normalizedHost || hostname.endsWith('.' + normalizedHost);
429
+ };
430
+ const isInternalWebUrl = (url, { location, internalHosts }) => (url.hostname === location.hostname && url.port === location.port) ||
431
+ internalHosts.some((host) => matchesHostname(url.hostname, host));
432
+ const isExternalWebHref = (href, context) => {
433
+ const url = parseWebUrl(href);
434
+ return url !== null && !isInternalWebUrl(url, context);
435
+ };
436
+
437
+ class ContentfulLinkComponent {
438
+ constructor() {
439
+ this.document = inject(DOCUMENT);
440
+ this.router = inject(Router);
441
+ this.config = injectContentfulConfig();
442
+ this.url = injectUrl();
443
+ this.href = input.required(/* @ts-ignore */
444
+ ...(ngDevMode ? [{ debugName: "href" }] : /* istanbul ignore next */ []));
445
+ this.text = input.required(/* @ts-ignore */
446
+ ...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
447
+ this.textClass = input('', /* @ts-ignore */
448
+ ...(ngDevMode ? [{ debugName: "textClass" }] : /* istanbul ignore next */ []));
449
+ this.anchorClass = input('', /* @ts-ignore */
450
+ ...(ngDevMode ? [{ debugName: "anchorClass" }] : /* istanbul ignore next */ []));
451
+ this.usesRouterLink = computed(() => {
452
+ const href = this.href();
453
+ const absoluteUrl = parseWebUrl(href);
454
+ if (absoluteUrl) {
455
+ return isInternalWebUrl(absoluteUrl, {
456
+ location: this.document.location,
457
+ internalHosts: this.config.internalHosts,
458
+ });
459
+ }
460
+ return !href.startsWith('#') && !/^[a-z][a-z\d+.-]*:/i.test(href);
461
+ }, /* @ts-ignore */
462
+ ...(ngDevMode ? [{ debugName: "usesRouterLink" }] : /* istanbul ignore next */ []));
463
+ this.openInNewTab = computed(() => Boolean(parseWebUrl(this.href())) && !this.usesRouterLink(), /* @ts-ignore */
464
+ ...(ngDevMode ? [{ debugName: "openInNewTab" }] : /* istanbul ignore next */ []));
465
+ this.internalPath = computed(() => {
466
+ const href = this.href();
467
+ const url = parseWebUrl(href);
468
+ if (url) {
469
+ return url.pathname + url.search + url.hash;
470
+ }
471
+ if (href.startsWith('/')) {
472
+ return href;
473
+ }
474
+ const resolved = new URL(href, 'https://contentful.invalid' + this.url());
475
+ return resolved.pathname + resolved.search + resolved.hash;
476
+ }, /* @ts-ignore */
477
+ ...(ngDevMode ? [{ debugName: "internalPath" }] : /* istanbul ignore next */ []));
478
+ this.internalUrlTree = computed(() => this.router.parseUrl(this.internalPath()), /* @ts-ignore */
479
+ ...(ngDevMode ? [{ debugName: "internalUrlTree" }] : /* istanbul ignore next */ []));
480
+ this.linkClass = computed(() => [this.anchorClass(), this.textClass()].filter(Boolean).join(' ') || null, /* @ts-ignore */
481
+ ...(ngDevMode ? [{ debugName: "linkClass" }] : /* istanbul ignore next */ []));
482
+ }
483
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulLinkComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
484
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", 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 }, anchorClass: { classPropertyName: "anchorClass", publicName: "anchorClass", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "et-contentful-link" }, ngImport: i0, template: `
485
+ @if (usesRouterLink()) {
486
+ <a [class]="linkClass()" [routerLink]="internalUrlTree()">{{ text() }}</a>
487
+ } @else {
488
+ <a
489
+ [class]="linkClass()"
490
+ [href]="href()"
491
+ [target]="openInNewTab() ? '_blank' : null"
492
+ [rel]="openInNewTab() ? 'noopener noreferrer' : null"
493
+ >{{ text() }}</a
494
+ >
495
+ }
496
+ `, isInline: true, styles: ["@layer components{.et-contentful-link{display:contents}}\n"], dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }], encapsulation: i0.ViewEncapsulation.None }); }
497
+ }
498
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulLinkComponent, decorators: [{
499
+ type: Component,
500
+ args: [{ selector: 'et-contentful-link', template: `
501
+ @if (usesRouterLink()) {
502
+ <a [class]="linkClass()" [routerLink]="internalUrlTree()">{{ text() }}</a>
503
+ } @else {
504
+ <a
505
+ [class]="linkClass()"
506
+ [href]="href()"
507
+ [target]="openInNewTab() ? '_blank' : null"
508
+ [rel]="openInNewTab() ? 'noopener noreferrer' : null"
509
+ >{{ text() }}</a
510
+ >
511
+ }
512
+ `, encapsulation: ViewEncapsulation.None, imports: [RouterLink], host: {
513
+ class: 'et-contentful-link',
514
+ }, styles: ["@layer components{.et-contentful-link{display:contents}}\n"] }]
515
+ }], 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 }] }], anchorClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "anchorClass", required: false }] }] } });
516
+
542
517
  /**
543
518
  * The `BLOCKS` node type strings of `@contentful/rich-text-types`, as a plain literal map.
544
519
  * Keep the values in sync with the enum - they are part of the Contentful Delivery API.
@@ -587,22 +562,18 @@ const RICH_TEXT_RENDERER_ERRORS = {
587
562
  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".',
588
563
  asset_id_not_found: 'The asset ID was not found. This node is not supported.',
589
564
  entry_id_not_found: 'The entry ID was not found. This node is not supported.',
590
- asset_not_found: 'The asset was not found. This node is not supported.',
591
- entry_not_found: 'The entry was not found. This node is not supported.',
592
- custom_component_not_found: 'No custom component found for entry type. Please provide one for this type.',
593
565
  text_parent_not_found: 'The parent node is not found. This structure is not supported.',
594
- text_parent_wrong_type: 'The parent node neither a html element nor a custom component. This structure is not supported.',
566
+ cached_command_not_found: 'No rendered node was found for a render command. The renderer state is out of sync.',
567
+ cached_command_not_component: 'The rendered node for a component command is not a component. The renderer state is out of sync.',
595
568
  };
596
569
  const RICH_TEXT_RENDERER_ERROR_CODES = {
597
570
  rich_text_undefined: 0,
598
571
  rich_text_wrong_type: 1,
599
572
  asset_id_not_found: 2,
600
573
  entry_id_not_found: 3,
601
- asset_not_found: 4,
602
- entry_not_found: 5,
603
- custom_component_not_found: 6,
604
574
  text_parent_not_found: 7,
605
- text_parent_wrong_type: 8,
575
+ cached_command_not_found: 9,
576
+ cached_command_not_component: 10,
606
577
  };
607
578
  const richTextRendererError = (code, data) => {
608
579
  const message = `<et-contentful-rich-text-renderer>: ${RICH_TEXT_RENDERER_ERRORS[code]}`;
@@ -612,60 +583,43 @@ const richTextRendererError = (code, data) => {
612
583
  const isRichTextRootNode = (node) => {
613
584
  return isObject(node) && 'nodeType' in node && node['nodeType'] === 'document';
614
585
  };
586
+ const NODE_TYPE_TAGS = /* @__PURE__ */ (() => ({
587
+ [CF_BLOCKS.HEADING_1]: 'h1',
588
+ [CF_BLOCKS.HEADING_2]: 'h2',
589
+ [CF_BLOCKS.HEADING_3]: 'h3',
590
+ [CF_BLOCKS.HEADING_4]: 'h4',
591
+ [CF_BLOCKS.HEADING_5]: 'h5',
592
+ [CF_BLOCKS.HEADING_6]: 'h6',
593
+ [CF_BLOCKS.PARAGRAPH]: 'p',
594
+ [CF_BLOCKS.UL_LIST]: 'ul',
595
+ [CF_BLOCKS.OL_LIST]: 'ol',
596
+ [CF_BLOCKS.LIST_ITEM]: 'li',
597
+ [CF_BLOCKS.HR]: 'hr',
598
+ [CF_BLOCKS.QUOTE]: 'blockquote',
599
+ [CF_BLOCKS.TABLE]: 'table',
600
+ [CF_BLOCKS.TABLE_ROW]: 'tr',
601
+ [CF_BLOCKS.TABLE_CELL]: 'td',
602
+ [CF_BLOCKS.TABLE_HEADER_CELL]: 'th',
603
+ }))();
604
+ const INLINE_NODE_TYPES = /* @__PURE__ */ (() => new Set(Object.values(CF_INLINES)))();
605
+ /**
606
+ * The html element a structural rich text node renders as. A node type without an element
607
+ * (an unsupported inline or block) falls back to `span` for inlines and `div` otherwise,
608
+ * with a dev-mode warning.
609
+ */
615
610
  const translateContentfulNodeTypeToHtmlTag = (nodeType) => {
616
- switch (nodeType) {
617
- case CF_BLOCKS.HEADING_1:
618
- return 'h1';
619
- case CF_BLOCKS.HEADING_2:
620
- return 'h2';
621
- case CF_BLOCKS.HEADING_3:
622
- return 'h3';
623
- case CF_BLOCKS.HEADING_4:
624
- return 'h4';
625
- case CF_BLOCKS.HEADING_5:
626
- return 'h5';
627
- case CF_BLOCKS.HEADING_6:
628
- return 'h6';
629
- case CF_BLOCKS.PARAGRAPH:
630
- return 'p';
631
- case CF_BLOCKS.UL_LIST:
632
- return 'ul';
633
- case CF_BLOCKS.OL_LIST:
634
- return 'ol';
635
- case CF_BLOCKS.LIST_ITEM:
636
- return 'li';
637
- case CF_BLOCKS.HR:
638
- return 'hr';
639
- case CF_BLOCKS.QUOTE:
640
- return 'blockquote';
641
- case CF_BLOCKS.TABLE:
642
- return 'table';
643
- case CF_BLOCKS.TABLE_ROW:
644
- return 'tr';
645
- case CF_BLOCKS.TABLE_CELL:
646
- return 'td';
647
- case CF_BLOCKS.TABLE_HEADER_CELL:
648
- return 'th';
649
- case CF_BLOCKS.EMBEDDED_ASSET:
650
- return 'div';
651
- case CF_INLINES.ENTRY_HYPERLINK:
652
- return 'a';
653
- case CF_INLINES.ASSET_HYPERLINK:
654
- return 'a';
655
- case 'text':
656
- return 'span';
657
- // Will be ignored by the renderer
658
- case 'document':
659
- return 'div';
660
- case CF_BLOCKS.EMBEDDED_ENTRY:
661
- return 'div';
662
- case CF_INLINES.EMBEDDED_ENTRY:
663
- return 'div';
664
- default:
665
- return 'div';
611
+ const tag = NODE_TYPE_TAGS[nodeType];
612
+ if (tag) {
613
+ return tag;
614
+ }
615
+ const fallback = INLINE_NODE_TYPES.has(nodeType) ? 'span' : 'div';
616
+ if (isDevMode()) {
617
+ console.warn(`Unsupported rich text node type "${nodeType}"! Its content is rendered inside a <${fallback}>.`);
666
618
  }
619
+ return fallback;
667
620
  };
668
621
 
622
+ const DEFAULT_ANCHOR_CLASS = 'et-contentful-rich-text-default-element et-contentful-rich-text-default-a';
669
623
  const MARK_TAG_MAP = {
670
624
  bold: 'strong',
671
625
  italic: 'em',
@@ -727,7 +681,7 @@ const createContentfulIncludeMap = (config) => {
727
681
  const entry = entryMap.get(id);
728
682
  if (!entry) {
729
683
  if (isDevMode()) {
730
- console.warn('Entry not found! Will return null. Is the include query param to low?', { id, entryMap });
684
+ console.warn('Entry not found! Will return null. Is the include query param too low?', { id, entryMap });
731
685
  }
732
686
  return null;
733
687
  }
@@ -772,6 +726,7 @@ class ContentfulRichTextRendererComponent {
772
726
  this.renderer = injectRenderer();
773
727
  this.elementRef = inject(ElementRef);
774
728
  this.config = injectContentfulConfig();
729
+ this.document = inject(DOCUMENT);
775
730
  /**
776
731
  * The contentful response gotten via their REST api.
777
732
  * @see https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/entries/entries-collection
@@ -825,18 +780,6 @@ class ContentfulRichTextRendererComponent {
825
780
  const instructions = [];
826
781
  const preserved = new Set();
827
782
  const needsReattach = new Set();
828
- const parentIdOf = (command) => {
829
- if (command.nestingLevel === 0) {
830
- return null;
831
- }
832
- for (let i = command.index - 1; i >= 0; i--) {
833
- const cmd = commands[i];
834
- if (cmd && cmd.nestingLevel === command.nestingLevel - 1 && cmd.kind === 'htmlOpen') {
835
- return cmd.id;
836
- }
837
- }
838
- return null;
839
- };
840
783
  const markTagsEqual = (a, b) => a.length === b.length && a.every((tag, index) => tag === b[index]);
841
784
  const attributesEqual = (a, b) => {
842
785
  const aKeys = Object.keys(a);
@@ -853,8 +796,7 @@ class ContentfulRichTextRendererComponent {
853
796
  if (previous.kind === 'component' && command.kind === 'component' && previous.component !== command.component) {
854
797
  continue;
855
798
  }
856
- const parentId = parentIdOf(command);
857
- const parentPreserved = parentId === null || preserved.has(parentId);
799
+ const parentPreserved = previous.parentId === command.parentId && (command.parentId === null || preserved.has(command.parentId));
858
800
  if (command.kind === 'component') {
859
801
  preserved.add(command.id);
860
802
  const samePlace = parentPreserved &&
@@ -913,7 +855,7 @@ class ContentfulRichTextRendererComponent {
913
855
  let nestingLevel = 0;
914
856
  let domPosition = 0;
915
857
  let textId = 0;
916
- let commandIndex = 0;
858
+ const openElementIds = [];
917
859
  const traverse = (node) => {
918
860
  switch (node.nodeType) {
919
861
  case 'text': {
@@ -927,7 +869,7 @@ class ContentfulRichTextRendererComponent {
927
869
  kind: 'text',
928
870
  nestingLevel,
929
871
  domPosition,
930
- index: commandIndex++,
872
+ parentId: openElementIds.at(-1) ?? null,
931
873
  attributes,
932
874
  markTags: node.marks.length ? marksToTags(node.marks) : [],
933
875
  text,
@@ -943,11 +885,14 @@ class ContentfulRichTextRendererComponent {
943
885
  }
944
886
  const asset = this.contentIncludesMap().getAsset(assetId);
945
887
  if (!asset) {
946
- throw richTextRendererError('asset_not_found', { assetId, node });
888
+ if (isDevMode()) {
889
+ console.warn('Embedded asset is missing from the includes! The asset will be skipped. Is it unpublished or deleted?', { assetId, node });
890
+ }
891
+ break;
947
892
  }
948
- const contentType = asset.fields.file.contentType;
893
+ const contentType = asset.fields.file?.contentType;
949
894
  const assetComponents = this.config.components;
950
- const isMissing = !contentType && !asset.fields.file.url;
895
+ const isMissing = !contentType && !asset.fields.file?.url;
951
896
  if (isMissing) {
952
897
  if (isDevMode()) {
953
898
  console.warn('Asset is missing file data! Asset will be skipped. Did you forget to upload a file for the current translation in Contentful?', { asset });
@@ -978,7 +923,7 @@ class ContentfulRichTextRendererComponent {
978
923
  kind: 'component',
979
924
  nestingLevel,
980
925
  domPosition,
981
- index: commandIndex++,
926
+ parentId: openElementIds.at(-1) ?? null,
982
927
  component,
983
928
  inputs: { asset },
984
929
  id,
@@ -996,52 +941,42 @@ class ContentfulRichTextRendererComponent {
996
941
  else if (node.nodeType === CF_INLINES.ASSET_HYPERLINK) {
997
942
  const assetId = node.data['target']?.sys?.id;
998
943
  const asset = assetId ? this.contentIncludesMap().getAsset(assetId) : null;
999
- href = asset?.fields.file.url ? normalizeHref(asset.fields.file.url) : null;
1000
- }
1001
- let linkText = '';
1002
- const linkMarks = [];
1003
- for (const child of node.content) {
1004
- if (child.nodeType === 'text') {
1005
- linkText += child.value;
1006
- linkMarks.push(...child.marks);
1007
- }
944
+ const url = asset?.fields.file?.url;
945
+ href = url ? normalizeHref(url) : null;
1008
946
  }
947
+ const linkTexts = node.content.filter((child) => child.nodeType === 'text' && Boolean(child.value));
948
+ const linkText = linkTexts.map((child) => child.value).join('');
1009
949
  const linkComponent = href ? this.config.components.link : null;
1010
950
  if (href && !linkComponent) {
951
+ const attributes = { class: DEFAULT_ANCHOR_CLASS, href };
952
+ if (isExternalWebHref(href, { location: this.document.location, internalHosts: this.config.internalHosts })) {
953
+ attributes['target'] = '_blank';
954
+ attributes['rel'] = 'noopener noreferrer';
955
+ }
1011
956
  rootCommands.push({
1012
957
  kind: 'htmlOpen',
1013
958
  nestingLevel,
1014
959
  domPosition,
1015
- index: commandIndex++,
1016
- attributes: {
1017
- class: 'et-contentful-rich-text-default-element et-contentful-rich-text-default-a',
1018
- href,
1019
- },
960
+ parentId: openElementIds.at(-1) ?? null,
961
+ attributes,
1020
962
  tagName: 'a',
1021
963
  id: 'e-o' + elementOpenId++,
1022
964
  });
1023
965
  const anchorDomPosition = domPosition;
966
+ openElementIds.push('e-o' + (elementOpenId - 1));
1024
967
  nestingLevel++;
1025
968
  domPosition = 0;
1026
- rootCommands.push({
1027
- kind: 'text',
1028
- nestingLevel,
1029
- domPosition,
1030
- index: commandIndex++,
1031
- attributes: {
1032
- class: 'et-contentful-rich-text-default-element et-contentful-rich-text-default-span',
1033
- },
1034
- markTags: marksToTags(linkMarks),
1035
- text: linkText,
1036
- id: 't' + textId++,
1037
- });
969
+ for (const child of linkTexts) {
970
+ traverse(child);
971
+ }
972
+ openElementIds.pop();
1038
973
  nestingLevel--;
1039
974
  domPosition = anchorDomPosition;
1040
975
  rootCommands.push({
1041
976
  kind: 'htmlClose',
1042
977
  nestingLevel,
1043
978
  domPosition,
1044
- index: commandIndex++,
979
+ parentId: openElementIds.at(-1) ?? null,
1045
980
  tagName: 'a',
1046
981
  id: 'e-c' + elementCloseId++,
1047
982
  });
@@ -1049,25 +984,16 @@ class ContentfulRichTextRendererComponent {
1049
984
  break;
1050
985
  }
1051
986
  if (!href) {
1052
- rootCommands.push({
1053
- kind: 'text',
1054
- nestingLevel,
1055
- domPosition,
1056
- index: commandIndex++,
1057
- attributes: {
1058
- class: 'et-contentful-rich-text-default-element et-contentful-rich-text-default-span',
1059
- },
1060
- markTags: marksToTags(linkMarks),
1061
- text: linkText,
1062
- id: 't' + textId++,
1063
- });
1064
- domPosition++;
987
+ for (const child of linkTexts) {
988
+ traverse(child);
989
+ }
1065
990
  break;
1066
991
  }
1067
992
  if (!linkComponent) {
1068
993
  break;
1069
994
  }
1070
- const textClass = linkMarks.length ? marksToClass(linkMarks) : '';
995
+ const sharedMarks = linkTexts[0]?.marks.filter((mark) => linkTexts.every((child) => child.marks.some((other) => other.type === mark.type))) ?? [];
996
+ const textClass = marksToClass(sharedMarks);
1071
997
  let linkComponentId = componentIdMap.get(LINK_COMPONENT_TYPE) ?? -1;
1072
998
  const linkId = LINK_COMPONENT_TYPE + ++linkComponentId;
1073
999
  componentIdMap.set(LINK_COMPONENT_TYPE, linkComponentId);
@@ -1075,9 +1001,9 @@ class ContentfulRichTextRendererComponent {
1075
1001
  kind: 'component',
1076
1002
  nestingLevel,
1077
1003
  domPosition,
1078
- index: commandIndex++,
1004
+ parentId: openElementIds.at(-1) ?? null,
1079
1005
  component: linkComponent,
1080
- inputs: { href, text: linkText, textClass },
1006
+ inputs: { href, text: linkText, textClass, anchorClass: DEFAULT_ANCHOR_CLASS },
1081
1007
  id: linkId,
1082
1008
  });
1083
1009
  domPosition++;
@@ -1091,16 +1017,18 @@ class ContentfulRichTextRendererComponent {
1091
1017
  }
1092
1018
  const entry = this.contentIncludesMap().getEntry(entryId, ET_CONTENTFUL_ANY_ENTRY_CONTENT_TYPE_SYS_ID);
1093
1019
  if (!entry) {
1094
- throw richTextRendererError('entry_not_found', { entryId, node });
1020
+ if (isDevMode()) {
1021
+ console.warn('Embedded entry is missing from the includes! The entry will be skipped. Is it unpublished or deleted?', { entryId, node });
1022
+ }
1023
+ break;
1095
1024
  }
1096
1025
  const componentType = entry.sys.contentType.sys.id;
1097
1026
  const component = this.config.customComponents[componentType];
1098
1027
  if (!component) {
1099
- throw richTextRendererError('custom_component_not_found', {
1100
- componentType,
1101
- customComponents: this.config.customComponents,
1102
- entry,
1103
- });
1028
+ if (isDevMode()) {
1029
+ console.warn('No custom component registered for this embedded entry type! The entry will be skipped. Provide one via provideContentfulConfig({ customComponents: … }).', { componentType, customComponents: this.config.customComponents, entry });
1030
+ }
1031
+ break;
1104
1032
  }
1105
1033
  const occurrenceKey = 'entry:' + entryId;
1106
1034
  const occurrence = (componentIdMap.get(occurrenceKey) ?? -1) + 1;
@@ -1110,7 +1038,7 @@ class ContentfulRichTextRendererComponent {
1110
1038
  kind: 'component',
1111
1039
  nestingLevel,
1112
1040
  domPosition,
1113
- index: commandIndex++,
1041
+ parentId: openElementIds.at(-1) ?? null,
1114
1042
  component,
1115
1043
  inputs: {
1116
1044
  fields: entry.fields,
@@ -1132,17 +1060,19 @@ class ContentfulRichTextRendererComponent {
1132
1060
  kind: 'htmlOpen',
1133
1061
  nestingLevel,
1134
1062
  domPosition,
1135
- index: commandIndex++,
1063
+ parentId: openElementIds.at(-1) ?? null,
1136
1064
  attributes,
1137
1065
  tagName: tag,
1138
1066
  id: 'e-o' + elementOpenId++,
1139
1067
  });
1140
1068
  const domPositionAtThisLevel = domPosition;
1069
+ openElementIds.push('e-o' + (elementOpenId - 1));
1141
1070
  nestingLevel++;
1142
1071
  domPosition = 0;
1143
1072
  for (const child of node.content) {
1144
1073
  traverse(child);
1145
1074
  }
1075
+ openElementIds.pop();
1146
1076
  nestingLevel--;
1147
1077
  domPosition = domPositionAtThisLevel;
1148
1078
  const lastCommand = rootCommands[rootCommands.length - 1];
@@ -1152,14 +1082,13 @@ class ContentfulRichTextRendererComponent {
1152
1082
  lastCommand.tagName !== 'hr') {
1153
1083
  rootCommands.pop();
1154
1084
  elementOpenId--;
1155
- commandIndex--;
1156
1085
  }
1157
1086
  else {
1158
1087
  rootCommands.push({
1159
1088
  kind: 'htmlClose',
1160
1089
  nestingLevel,
1161
1090
  domPosition,
1162
- index: commandIndex++,
1091
+ parentId: openElementIds.at(-1) ?? null,
1163
1092
  tagName: tag,
1164
1093
  id: 'e-c' + elementCloseId++,
1165
1094
  });
@@ -1175,26 +1104,32 @@ class ContentfulRichTextRendererComponent {
1175
1104
  return rootCommands;
1176
1105
  }
1177
1106
  execInstructions(instructions) {
1178
- for (const instruction of instructions) {
1179
- switch (instruction.type) {
1107
+ const lastPlacedChild = new Map();
1108
+ for (const { type, command } of instructions) {
1109
+ if (type === 'delete') {
1110
+ this.runDeleteInstruction(command);
1111
+ continue;
1112
+ }
1113
+ const previousSibling = lastPlacedChild.get(command.parentId) ?? null;
1114
+ switch (type) {
1180
1115
  case 'create':
1181
- this.runCreateInstruction(instruction.command);
1116
+ this.runCreateInstruction(command, previousSibling);
1182
1117
  break;
1183
1118
  case 'update':
1184
- this.runUpdateInstruction(instruction.command);
1119
+ this.runUpdateInstruction(command);
1185
1120
  break;
1186
1121
  case 'move':
1187
- this.runMoveInstruction(instruction.command);
1188
- break;
1189
- case 'delete':
1190
- this.runDeleteInstruction(instruction.command);
1122
+ this.runMoveInstruction(command, previousSibling);
1191
1123
  break;
1192
1124
  }
1125
+ const element = this.executedCommandsCache.get(command.id)?.element;
1126
+ if (element) {
1127
+ lastPlacedChild.set(command.parentId, element);
1128
+ }
1193
1129
  }
1194
1130
  }
1195
- runCreateInstruction(command) {
1131
+ runCreateInstruction(command, previousSibling) {
1196
1132
  const parentElement = this.findParent(command);
1197
- const nextElement = this.findFollowingElement(command);
1198
1133
  if (command.kind === 'component') {
1199
1134
  const inputs = signal(command.inputs, /* @ts-ignore */
1200
1135
  ...(ngDevMode ? [{ debugName: "inputs" }] : /* istanbul ignore next */ []));
@@ -1204,7 +1139,7 @@ class ContentfulRichTextRendererComponent {
1204
1139
  .map((declared) => inputBinding(declared.templateName, () => inputs()[declared.propName]));
1205
1140
  const componentRef = this.viewContainerRef.createComponent(command.component, { bindings });
1206
1141
  const rootNode = this.getComponentRootNode(componentRef);
1207
- this.renderInsertOrAppend(rootNode, parentElement, nextElement);
1142
+ this.renderInsertAfter(rootNode, { parentElement, previousSibling });
1208
1143
  this.executedCommandsCache.set(command.id, {
1209
1144
  command,
1210
1145
  componentRef,
@@ -1234,7 +1169,7 @@ class ContentfulRichTextRendererComponent {
1234
1169
  this.renderer.appendChild(textContainer, textNode);
1235
1170
  }
1236
1171
  }
1237
- this.renderInsertOrAppend(span, parentElement, nextElement);
1172
+ this.renderInsertAfter(span, { parentElement, previousSibling });
1238
1173
  this.executedCommandsCache.set(command.id, {
1239
1174
  command,
1240
1175
  element: span,
@@ -1245,7 +1180,7 @@ class ContentfulRichTextRendererComponent {
1245
1180
  for (const [key, value] of Object.entries(command.attributes)) {
1246
1181
  this.renderer.setAttribute(element, key, value);
1247
1182
  }
1248
- this.renderInsertOrAppend(element, parentElement, nextElement);
1183
+ this.renderInsertAfter(element, { parentElement, previousSibling });
1249
1184
  this.executedCommandsCache.set(command.id, {
1250
1185
  command,
1251
1186
  element,
@@ -1255,11 +1190,11 @@ class ContentfulRichTextRendererComponent {
1255
1190
  runUpdateInstruction(command) {
1256
1191
  const cached = this.executedCommandsCache.get(command.id);
1257
1192
  if (!cached) {
1258
- throw new Error('Cached command not found!');
1193
+ throw richTextRendererError('cached_command_not_found', { command });
1259
1194
  }
1260
1195
  if (command.kind === 'component') {
1261
1196
  if (!isExecutedComponentCommandCacheItem(cached)) {
1262
- throw new Error('Cached command is not a component command!');
1197
+ throw richTextRendererError('cached_command_not_component', { command });
1263
1198
  }
1264
1199
  cached.inputs.set(command.inputs);
1265
1200
  }
@@ -1268,23 +1203,21 @@ class ContentfulRichTextRendererComponent {
1268
1203
  command,
1269
1204
  });
1270
1205
  }
1271
- runMoveInstruction(command) {
1206
+ runMoveInstruction(command, previousSibling) {
1272
1207
  const cached = this.executedCommandsCache.get(command.id);
1273
1208
  if (!cached) {
1274
- throw new Error('Cached command not found!');
1209
+ throw richTextRendererError('cached_command_not_found', { command });
1275
1210
  }
1276
1211
  if (command.kind === 'component') {
1277
1212
  if (!isExecutedComponentCommandCacheItem(cached)) {
1278
- throw new Error('Cached command is not a component command!');
1213
+ throw richTextRendererError('cached_command_not_component', { command });
1279
1214
  }
1280
1215
  const rootNode = cached.element;
1281
1216
  const oldParentElement = cached.element.parentElement;
1282
1217
  if (oldParentElement) {
1283
1218
  this.renderer.removeChild(oldParentElement, rootNode);
1284
1219
  }
1285
- const newParentElement = this.findParent(command);
1286
- const nextElement = this.findFollowingElement(command);
1287
- this.renderInsertOrAppend(rootNode, newParentElement, nextElement);
1220
+ this.renderInsertAfter(rootNode, { parentElement: this.findParent(command), previousSibling });
1288
1221
  cached.inputs.set(command.inputs);
1289
1222
  this.executedCommandsCache.set(command.id, {
1290
1223
  ...cached,
@@ -1298,11 +1231,11 @@ class ContentfulRichTextRendererComponent {
1298
1231
  if (command.kind === 'htmlClose') {
1299
1232
  return;
1300
1233
  }
1301
- throw new Error('Cached command not found!');
1234
+ throw richTextRendererError('cached_command_not_found', { command });
1302
1235
  }
1303
1236
  if (command.kind === 'component') {
1304
1237
  if (!isExecutedComponentCommandCacheItem(cached)) {
1305
- throw new Error('Cached command is not a component command!');
1238
+ throw richTextRendererError('cached_command_not_component', { command });
1306
1239
  }
1307
1240
  cached.componentRef.destroy();
1308
1241
  }
@@ -1317,57 +1250,17 @@ class ContentfulRichTextRendererComponent {
1317
1250
  return componentRef.hostView.rootNodes[0];
1318
1251
  }
1319
1252
  findParent(command) {
1320
- const hostElement = this.elementRef.nativeElement;
1321
- let parentElement;
1322
- if (command.nestingLevel === 0) {
1323
- parentElement = hostElement;
1324
- }
1325
- else {
1326
- const allCommands = this.renderCommands();
1327
- let parentCommand = null;
1328
- for (let i = command.index - 1; i >= 0; i--) {
1329
- const cmd = allCommands[i];
1330
- if (!cmd) {
1331
- throw new Error('Command not found!');
1332
- }
1333
- if (cmd.nestingLevel === command.nestingLevel - 1 && cmd.kind === 'htmlOpen') {
1334
- parentCommand = cmd;
1335
- break;
1336
- }
1337
- }
1338
- if (!parentCommand) {
1339
- throw richTextRendererError('text_parent_not_found', { command });
1340
- }
1341
- parentElement = this.executedCommandsCache.get(parentCommand.id)?.element;
1253
+ if (command.parentId === null) {
1254
+ return this.elementRef.nativeElement;
1342
1255
  }
1256
+ const parentElement = this.executedCommandsCache.get(command.parentId)?.element;
1343
1257
  if (!parentElement) {
1344
- throw richTextRendererError('text_parent_wrong_type', { command });
1258
+ throw richTextRendererError('text_parent_not_found', { command });
1345
1259
  }
1346
1260
  return parentElement;
1347
1261
  }
1348
- findFollowingElement(command) {
1349
- const parentElement = this.findParent(command);
1350
- let nextElement;
1351
- let nextDomPosition = Infinity;
1352
- for (const cached of this.executedCommandsCache.values()) {
1353
- if (cached.command.domPosition > command.domPosition &&
1354
- cached.command.domPosition < nextDomPosition &&
1355
- cached.command.nestingLevel === command.nestingLevel &&
1356
- cached.element.parentElement === parentElement &&
1357
- cached.command.id !== command.id) {
1358
- nextElement = cached.element;
1359
- nextDomPosition = cached.command.domPosition;
1360
- }
1361
- }
1362
- return nextElement;
1363
- }
1364
- renderInsertOrAppend(nodeToRender, parentElement, nextElement) {
1365
- if (nextElement) {
1366
- this.renderer.insertBefore(parentElement, nodeToRender, nextElement);
1367
- }
1368
- else {
1369
- this.renderer.appendChild(parentElement, nodeToRender);
1370
- }
1262
+ renderInsertAfter(node, { parentElement, previousSibling }) {
1263
+ this.renderer.insertBefore(parentElement, node, previousSibling ? previousSibling.nextSibling : parentElement.firstChild);
1371
1264
  }
1372
1265
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulRichTextRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1373
1266
  static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.6", 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 }); }
@@ -1384,6 +1277,61 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
1384
1277
  }]
1385
1278
  }], ctorParameters: () => [], propDecorators: { content: [{ type: i0.Input, args: [{ isSignal: true, alias: "content", required: true }] }], richTextPath: [{ type: i0.Input, args: [{ isSignal: true, alias: "richTextPath", required: true }] }] } });
1386
1279
 
1280
+ class ContentfulVideoComponent {
1281
+ constructor() {
1282
+ this.asset = input.required(/* @ts-ignore */
1283
+ ...(ngDevMode ? [{ debugName: "asset" }] : /* istanbul ignore next */ []));
1284
+ this.videoClass = input(null, /* @ts-ignore */
1285
+ ...(ngDevMode ? [{ debugName: "videoClass" }] : /* istanbul ignore next */ []));
1286
+ this.data = computed(() => {
1287
+ const asset = this.asset();
1288
+ if (!asset) {
1289
+ return null;
1290
+ }
1291
+ if (isContentfulGqlAsset(asset) && asset.url) {
1292
+ return {
1293
+ url: asset.url,
1294
+ contentType: asset.contentType,
1295
+ };
1296
+ }
1297
+ if (!isContentfulGqlAsset(asset) && asset.fields.file?.url) {
1298
+ return {
1299
+ url: asset.fields.file.url,
1300
+ contentType: asset.fields.file.contentType,
1301
+ };
1302
+ }
1303
+ return null;
1304
+ }, /* @ts-ignore */
1305
+ ...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
1306
+ }
1307
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulVideoComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1308
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: ContentfulVideoComponent, isStandalone: true, selector: "et-contentful-video", inputs: { asset: { classPropertyName: "asset", publicName: "asset", isSignal: true, isRequired: true, transformFunction: null }, videoClass: { classPropertyName: "videoClass", publicName: "videoClass", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "et-contentful-video" }, ngImport: i0, template: `
1309
+ @if (data(); as data) {
1310
+ <video [ngClass]="videoClass()" controls>
1311
+ <source [src]="data.url" [type]="data.contentType" />
1312
+ </video>
1313
+ }
1314
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], encapsulation: i0.ViewEncapsulation.None }); }
1315
+ }
1316
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContentfulVideoComponent, decorators: [{
1317
+ type: Component,
1318
+ args: [{
1319
+ selector: 'et-contentful-video',
1320
+ template: `
1321
+ @if (data(); as data) {
1322
+ <video [ngClass]="videoClass()" controls>
1323
+ <source [src]="data.url" [type]="data.contentType" />
1324
+ </video>
1325
+ }
1326
+ `,
1327
+ encapsulation: ViewEncapsulation.None,
1328
+ imports: [NgClass],
1329
+ host: {
1330
+ class: 'et-contentful-video',
1331
+ },
1332
+ }]
1333
+ }], propDecorators: { asset: [{ type: i0.Input, args: [{ isSignal: true, alias: "asset", required: true }] }], videoClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "videoClass", required: false }] }] } });
1334
+
1387
1335
  const ContentfulImports = [
1388
1336
  ContentfulAudioComponent,
1389
1337
  ContentfulFileComponent,
@@ -1392,6 +1340,24 @@ const ContentfulImports = [
1392
1340
  ContentfulRichTextRendererComponent,
1393
1341
  ];
1394
1342
 
1343
+ /**
1344
+ * The shipped audio, file, image, video and link components, to spread into `provideContentfulConfig`.
1345
+ * An app that brings its own components, or renders text only, leaves this out and bundles none of them.
1346
+ * To replace one, spread the map instead: `components: { ...CONTENTFUL_DEFAULT_COMPONENTS.components, image: MyImage }`.
1347
+ *
1348
+ * @example
1349
+ * provideContentfulConfig({ ...CONTENTFUL_DEFAULT_COMPONENTS });
1350
+ */
1351
+ const CONTENTFUL_DEFAULT_COMPONENTS = {
1352
+ components: {
1353
+ audio: ContentfulAudioComponent,
1354
+ file: ContentfulFileComponent,
1355
+ image: ContentfulImageComponent,
1356
+ video: ContentfulVideoComponent,
1357
+ link: ContentfulLinkComponent,
1358
+ },
1359
+ };
1360
+
1395
1361
  const provideContentfulConfig = (contentfulConfig) => {
1396
1362
  return { provide: CONTENTFUL_CONFIG, useValue: createContentfulConfig(contentfulConfig) };
1397
1363
  };
@@ -1401,5 +1367,5 @@ const isContentfulEntryType = (entry, type) => entry.sys.contentType.sys.id ===
1401
1367
  * Generated bundle index. Do not edit.
1402
1368
  */
1403
1369
 
1404
- export { CONTENTFUL_CONFIG, ContentfulAudioComponent, ContentfulFileComponent, ContentfulImageComponent, ContentfulImports, ContentfulLinkComponent, ContentfulRichTextRendererComponent, ContentfulVideoComponent, ET_CONTENTFUL_ANY_ENTRY_CONTENT_TYPE_SYS_ID, GQL_FRAGMENT_CONTENTFUL_ASSET, createContentfulConfig, createContentfulIncludeMap, generateContentfulImageSources, generateDefaultContentfulImageSource, injectContentfulConfig, isContentfulEntryType, isContentfulGqlAsset, isRichTextRootNode, marksToClass, marksToTags, parseContentfulImageSize, provideContentfulConfig, translateContentfulNodeTypeToHtmlTag };
1370
+ export { CONTENTFUL_CONFIG, CONTENTFUL_DEFAULT_COMPONENTS, CONTENTFUL_FILE_LABELS, ContentfulAudioComponent, ContentfulFileComponent, ContentfulImageComponent, ContentfulImports, ContentfulLinkComponent, ContentfulRichTextRendererComponent, ContentfulVideoComponent, DEFAULT_CONTENTFUL_FILE_LABELS, ET_CONTENTFUL_ANY_ENTRY_CONTENT_TYPE_SYS_ID, GQL_FRAGMENT_CONTENTFUL_ASSET, createContentfulConfig, createContentfulIncludeMap, generateContentfulImageSources, generateDefaultContentfulImageSource, injectContentfulConfig, injectContentfulFileLabels, isContentfulEntryType, isContentfulGqlAsset, isRichTextRootNode, marksToClass, marksToTags, parseContentfulImageSize, provideContentfulConfig, provideContentfulFileLabels, translateContentfulNodeTypeToHtmlTag };
1405
1371
  //# sourceMappingURL=ethlete-contentful.mjs.map