@praxisui/rich-content 8.0.0-beta.0 → 8.0.0-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -0
- package/fesm2022/praxisui-rich-content.mjs +3156 -71
- package/index.d.ts +103 -4
- package/package.json +2 -2
|
@@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common';
|
|
|
3
3
|
import * as i0 from '@angular/core';
|
|
4
4
|
import { InjectionToken, inject, Injectable, input, computed, Input, ChangeDetectionStrategy, Component, ChangeDetectorRef, ENVIRONMENT_INITIALIZER } from '@angular/core';
|
|
5
5
|
import { PraxisJsonLogicService, providePraxisI18n, PraxisI18nService, SETTINGS_PANEL_DATA, ComponentMetadataRegistry } from '@praxisui/core';
|
|
6
|
-
import * as
|
|
6
|
+
import * as i2 from '@angular/forms';
|
|
7
7
|
import { FormsModule } from '@angular/forms';
|
|
8
8
|
import { BehaviorSubject } from 'rxjs';
|
|
9
9
|
|
|
@@ -79,6 +79,15 @@ class PraxisRichContent {
|
|
|
79
79
|
resolveImageAlt(node) {
|
|
80
80
|
return this.resolveValue(node.altExpr) ?? node.alt ?? '';
|
|
81
81
|
}
|
|
82
|
+
resolveLinkLabel(node) {
|
|
83
|
+
return this.resolveValue(node.labelExpr) ?? node.label ?? node.href;
|
|
84
|
+
}
|
|
85
|
+
resolveLinkRel(node) {
|
|
86
|
+
if (node.rel) {
|
|
87
|
+
return node.rel;
|
|
88
|
+
}
|
|
89
|
+
return node.target === '_blank' ? 'noopener noreferrer' : null;
|
|
90
|
+
}
|
|
82
91
|
resolveBadgeLabel(node) {
|
|
83
92
|
return this.resolveValue(node.labelExpr) ?? node.label ?? '';
|
|
84
93
|
}
|
|
@@ -171,12 +180,42 @@ class PraxisRichContent {
|
|
|
171
180
|
return this.resolveValue(item.badgeExpr) ?? item.badge ?? null;
|
|
172
181
|
}
|
|
173
182
|
resolvePresetNodes(node) {
|
|
183
|
+
return this.expandPresetReference(node, new Set(), 0);
|
|
184
|
+
}
|
|
185
|
+
expandPresetReference(node, stack, depth) {
|
|
186
|
+
const key = this.getPresetKey(node.ref);
|
|
187
|
+
if (stack.has(key) || depth > 8) {
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
const document = this.resolvePresetDocument(node);
|
|
191
|
+
if (!document) {
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
const nextStack = new Set(stack);
|
|
195
|
+
nextStack.add(key);
|
|
196
|
+
return document.nodes.flatMap((child) => {
|
|
197
|
+
if (child.type === 'preset') {
|
|
198
|
+
return this.expandPresetReference(child, nextStack, depth + 1);
|
|
199
|
+
}
|
|
200
|
+
return [child];
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
resolvePresetDocument(node) {
|
|
174
204
|
const localPreset = this.presetRegistry.get(node.ref);
|
|
175
205
|
if (localPreset) {
|
|
176
|
-
return localPreset.document
|
|
206
|
+
return localPreset.document;
|
|
177
207
|
}
|
|
178
208
|
const hostResolved = this.hostCapabilities()?.resolvePreset?.(node.ref);
|
|
179
|
-
|
|
209
|
+
if (hostResolved &&
|
|
210
|
+
typeof hostResolved === 'object' &&
|
|
211
|
+
hostResolved.kind === 'praxis.rich-content' &&
|
|
212
|
+
Array.isArray(hostResolved.nodes)) {
|
|
213
|
+
return hostResolved;
|
|
214
|
+
}
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
getPresetKey(ref) {
|
|
218
|
+
return `${ref.kind}:${ref.namespace}:${ref.presetId}:${ref.version ?? ''}`;
|
|
180
219
|
}
|
|
181
220
|
buildEvaluationContext() {
|
|
182
221
|
return this.context() ?? {};
|
|
@@ -238,6 +277,8 @@ class PraxisRichContent {
|
|
|
238
277
|
<span
|
|
239
278
|
class="prx-rich-icon material-symbols-outlined"
|
|
240
279
|
[attr.aria-label]="node.ariaLabel || null"
|
|
280
|
+
[attr.aria-hidden]="node.ariaLabel ? null : 'true'"
|
|
281
|
+
[attr.role]="node.ariaLabel ? 'img' : null"
|
|
241
282
|
>
|
|
242
283
|
{{ node.icon }}
|
|
243
284
|
</span>
|
|
@@ -249,6 +290,16 @@ class PraxisRichContent {
|
|
|
249
290
|
[alt]="resolveImageAlt(node)"
|
|
250
291
|
/>
|
|
251
292
|
}
|
|
293
|
+
@case ('link') {
|
|
294
|
+
<a
|
|
295
|
+
class="prx-rich-link"
|
|
296
|
+
[href]="node.href"
|
|
297
|
+
[attr.target]="node.target || null"
|
|
298
|
+
[attr.rel]="resolveLinkRel(node)"
|
|
299
|
+
>
|
|
300
|
+
{{ resolveLinkLabel(node) }}
|
|
301
|
+
</a>
|
|
302
|
+
}
|
|
252
303
|
@case ('badge') {
|
|
253
304
|
<span class="prx-rich-badge">{{ resolveBadgeLabel(node) }}</span>
|
|
254
305
|
}
|
|
@@ -279,6 +330,7 @@ class PraxisRichContent {
|
|
|
279
330
|
class="prx-rich-progress-bar"
|
|
280
331
|
[value]="resolveProgressValue(node)"
|
|
281
332
|
[max]="node.max ?? 100"
|
|
333
|
+
[attr.aria-label]="resolveProgressLabel(node) || null"
|
|
282
334
|
></progress>
|
|
283
335
|
</div>
|
|
284
336
|
}
|
|
@@ -369,7 +421,10 @@ class PraxisRichContent {
|
|
|
369
421
|
<article class="prx-rich-timeline__item">
|
|
370
422
|
<div class="prx-rich-timeline__item-marker">
|
|
371
423
|
@if (resolveTimelineItemIcon(item); as icon) {
|
|
372
|
-
<span
|
|
424
|
+
<span
|
|
425
|
+
class="prx-rich-timeline__item-icon material-symbols-outlined"
|
|
426
|
+
aria-hidden="true"
|
|
427
|
+
>
|
|
373
428
|
{{ icon }}
|
|
374
429
|
</span>
|
|
375
430
|
} @else {
|
|
@@ -412,7 +467,7 @@ class PraxisRichContent {
|
|
|
412
467
|
}
|
|
413
468
|
}
|
|
414
469
|
</ng-template>
|
|
415
|
-
`, isInline: true, styles: [".prx-rich-content-root{display:block}.prx-rich-content-root--inline{display:inline-flex;align-items:var(--prx-rich-content-inline-align-items, center);flex-wrap:var(--prx-rich-content-inline-flex-wrap, nowrap);gap:var(--prx-rich-content-inline-gap, 6px);min-width:0;max-width:100%;vertical-align:middle}.prx-rich-node{min-width:0}.prx-rich-node--inline{display:inline-flex;align-items:center;min-width:0}.prx-rich-compose{display:flex;align-items:center;gap:8px}.prx-rich-compose.direction-column{flex-direction:column;align-items:stretch}.prx-rich-compose.wrap{flex-wrap:wrap}.prx-rich-badge{display:inline-flex;align-items:center;padding:2px 10px;border-radius:999px;background:var(--md-sys-color-primary-container, #e8def8);color:var(--md-sys-color-on-primary-container, #21005d);font-size:12px;line-height:20px}.prx-rich-avatar{display:inline-flex;align-items:center;justify-content:center;width:40px;height:40px;border-radius:50%;background:var(--md-sys-color-surface-container-high, #ece6f0);overflow:hidden}.prx-rich-avatar-image{width:100%;height:100%;object-fit:cover}.prx-rich-avatar-fallback{font-weight:600}.prx-rich-card{display:flex;flex-direction:column;gap:8px;padding:16px;border:1px solid var(--md-sys-color-outline-variant, #cac4d0);border-radius:16px;background:var(--md-sys-color-surface, #fff)}.prx-rich-card-title{font-weight:600}.prx-rich-card-subtitle,.prx-rich-metric-caption{color:var(--md-sys-color-on-surface-variant, #49454f);font-size:12px}.prx-rich-metric{display:flex;flex-direction:column;gap:4px}.prx-rich-metric-value{font-size:20px;font-weight:700}.prx-rich-progress{display:flex;flex-direction:column;gap:4px}.prx-rich-progress-bar{width:100%}.prx-rich-media-block{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:12px;align-items:center;min-width:0}.prx-rich-media-block__avatar,.prx-rich-media-block__body,.prx-rich-media-block__trailing{min-width:0}.prx-rich-media-block__body{display:flex;flex-direction:column;gap:6px}.prx-rich-media-block__title{font-weight:700}.prx-rich-media-block__subtitle{color:var(--md-sys-color-on-surface-variant, #49454f);font-size:13px}.prx-rich-media-block__meta{color:var(--md-sys-color-on-surface-variant, #49454f);font-size:12px}.prx-rich-media-block__trailing{display:flex;align-items:center;justify-content:flex-end}.prx-rich-timeline{display:flex;flex-direction:column;gap:12px;min-width:0}.prx-rich-timeline__title{font-weight:700}.prx-rich-timeline__items{display:flex;flex-direction:column;gap:12px}.prx-rich-timeline__item{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:12px;align-items:flex-start;min-width:0}.prx-rich-timeline__item-marker{display:inline-flex;align-items:center;justify-content:center;width:24px;min-height:24px}.prx-rich-timeline__item-icon{font-size:18px;line-height:18px;color:var(--md-sys-color-primary, #6750a4)}.prx-rich-timeline__item-dot{width:10px;height:10px;border-radius:50%;background:var(--md-sys-color-primary, #6750a4);display:inline-block;margin-top:4px}.prx-rich-timeline__item-body{display:flex;flex-direction:column;gap:4px;min-width:0}.prx-rich-timeline__item-title{font-weight:600}.prx-rich-timeline__item-subtitle,.prx-rich-timeline__item-meta,.prx-rich-timeline__empty{color:var(--md-sys-color-on-surface-variant, #49454f);font-size:12px}.prx-rich-timeline__item-badge{display:inline-flex;align-items:flex-start;justify-content:flex-end}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
470
|
+
`, isInline: true, styles: [".prx-rich-content-root{display:block}.prx-rich-content-root--inline{display:inline-flex;align-items:var(--prx-rich-content-inline-align-items, center);flex-wrap:var(--prx-rich-content-inline-flex-wrap, nowrap);gap:var(--prx-rich-content-inline-gap, 6px);min-width:0;max-width:100%;vertical-align:middle}.prx-rich-node{min-width:0}.prx-rich-node--inline{display:inline-flex;align-items:center;min-width:0}.prx-rich-compose{display:flex;align-items:center;gap:8px}.prx-rich-compose.direction-column{flex-direction:column;align-items:stretch}.prx-rich-compose.wrap{flex-wrap:wrap}.prx-rich-badge{display:inline-flex;align-items:center;padding:2px 10px;border-radius:999px;background:var(--md-sys-color-primary-container, #e8def8);color:var(--md-sys-color-on-primary-container, #21005d);font-size:12px;line-height:20px}.prx-rich-link{color:var(--md-sys-color-primary, #6750a4);text-decoration:underline;text-underline-offset:2px}.prx-rich-avatar{display:inline-flex;align-items:center;justify-content:center;width:40px;height:40px;border-radius:50%;background:var(--md-sys-color-surface-container-high, #ece6f0);overflow:hidden}.prx-rich-avatar-image{width:100%;height:100%;object-fit:cover}.prx-rich-avatar-fallback{font-weight:600}.prx-rich-card{display:flex;flex-direction:column;gap:8px;padding:16px;border:1px solid var(--md-sys-color-outline-variant, #cac4d0);border-radius:16px;background:var(--md-sys-color-surface, #fff)}.prx-rich-card-title{font-weight:600}.prx-rich-card-subtitle,.prx-rich-metric-caption{color:var(--md-sys-color-on-surface-variant, #49454f);font-size:12px}.prx-rich-metric{display:flex;flex-direction:column;gap:4px}.prx-rich-metric-value{font-size:20px;font-weight:700}.prx-rich-progress{display:flex;flex-direction:column;gap:4px}.prx-rich-progress-bar{width:100%}.prx-rich-media-block{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:12px;align-items:center;min-width:0}.prx-rich-media-block__avatar,.prx-rich-media-block__body,.prx-rich-media-block__trailing{min-width:0}.prx-rich-media-block__body{display:flex;flex-direction:column;gap:6px}.prx-rich-media-block__title{font-weight:700}.prx-rich-media-block__subtitle{color:var(--md-sys-color-on-surface-variant, #49454f);font-size:13px}.prx-rich-media-block__meta{color:var(--md-sys-color-on-surface-variant, #49454f);font-size:12px}.prx-rich-media-block__trailing{display:flex;align-items:center;justify-content:flex-end}.prx-rich-timeline{display:flex;flex-direction:column;gap:12px;min-width:0}.prx-rich-timeline__title{font-weight:700}.prx-rich-timeline__items{display:flex;flex-direction:column;gap:12px}.prx-rich-timeline__item{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:12px;align-items:flex-start;min-width:0}.prx-rich-timeline__item-marker{display:inline-flex;align-items:center;justify-content:center;width:24px;min-height:24px}.prx-rich-timeline__item-icon{font-size:18px;line-height:18px;color:var(--md-sys-color-primary, #6750a4)}.prx-rich-timeline__item-dot{width:10px;height:10px;border-radius:50%;background:var(--md-sys-color-primary, #6750a4);display:inline-block;margin-top:4px}.prx-rich-timeline__item-body{display:flex;flex-direction:column;gap:4px;min-width:0}.prx-rich-timeline__item-title{font-weight:600}.prx-rich-timeline__item-subtitle,.prx-rich-timeline__item-meta,.prx-rich-timeline__empty{color:var(--md-sys-color-on-surface-variant, #49454f);font-size:12px}.prx-rich-timeline__item-badge{display:inline-flex;align-items:flex-start;justify-content:flex-end}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
416
471
|
}
|
|
417
472
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisRichContent, decorators: [{
|
|
418
473
|
type: Component,
|
|
@@ -447,6 +502,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
447
502
|
<span
|
|
448
503
|
class="prx-rich-icon material-symbols-outlined"
|
|
449
504
|
[attr.aria-label]="node.ariaLabel || null"
|
|
505
|
+
[attr.aria-hidden]="node.ariaLabel ? null : 'true'"
|
|
506
|
+
[attr.role]="node.ariaLabel ? 'img' : null"
|
|
450
507
|
>
|
|
451
508
|
{{ node.icon }}
|
|
452
509
|
</span>
|
|
@@ -458,6 +515,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
458
515
|
[alt]="resolveImageAlt(node)"
|
|
459
516
|
/>
|
|
460
517
|
}
|
|
518
|
+
@case ('link') {
|
|
519
|
+
<a
|
|
520
|
+
class="prx-rich-link"
|
|
521
|
+
[href]="node.href"
|
|
522
|
+
[attr.target]="node.target || null"
|
|
523
|
+
[attr.rel]="resolveLinkRel(node)"
|
|
524
|
+
>
|
|
525
|
+
{{ resolveLinkLabel(node) }}
|
|
526
|
+
</a>
|
|
527
|
+
}
|
|
461
528
|
@case ('badge') {
|
|
462
529
|
<span class="prx-rich-badge">{{ resolveBadgeLabel(node) }}</span>
|
|
463
530
|
}
|
|
@@ -488,6 +555,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
488
555
|
class="prx-rich-progress-bar"
|
|
489
556
|
[value]="resolveProgressValue(node)"
|
|
490
557
|
[max]="node.max ?? 100"
|
|
558
|
+
[attr.aria-label]="resolveProgressLabel(node) || null"
|
|
491
559
|
></progress>
|
|
492
560
|
</div>
|
|
493
561
|
}
|
|
@@ -578,7 +646,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
578
646
|
<article class="prx-rich-timeline__item">
|
|
579
647
|
<div class="prx-rich-timeline__item-marker">
|
|
580
648
|
@if (resolveTimelineItemIcon(item); as icon) {
|
|
581
|
-
<span
|
|
649
|
+
<span
|
|
650
|
+
class="prx-rich-timeline__item-icon material-symbols-outlined"
|
|
651
|
+
aria-hidden="true"
|
|
652
|
+
>
|
|
582
653
|
{{ icon }}
|
|
583
654
|
</span>
|
|
584
655
|
} @else {
|
|
@@ -621,7 +692,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
621
692
|
}
|
|
622
693
|
}
|
|
623
694
|
</ng-template>
|
|
624
|
-
`, styles: [".prx-rich-content-root{display:block}.prx-rich-content-root--inline{display:inline-flex;align-items:var(--prx-rich-content-inline-align-items, center);flex-wrap:var(--prx-rich-content-inline-flex-wrap, nowrap);gap:var(--prx-rich-content-inline-gap, 6px);min-width:0;max-width:100%;vertical-align:middle}.prx-rich-node{min-width:0}.prx-rich-node--inline{display:inline-flex;align-items:center;min-width:0}.prx-rich-compose{display:flex;align-items:center;gap:8px}.prx-rich-compose.direction-column{flex-direction:column;align-items:stretch}.prx-rich-compose.wrap{flex-wrap:wrap}.prx-rich-badge{display:inline-flex;align-items:center;padding:2px 10px;border-radius:999px;background:var(--md-sys-color-primary-container, #e8def8);color:var(--md-sys-color-on-primary-container, #21005d);font-size:12px;line-height:20px}.prx-rich-avatar{display:inline-flex;align-items:center;justify-content:center;width:40px;height:40px;border-radius:50%;background:var(--md-sys-color-surface-container-high, #ece6f0);overflow:hidden}.prx-rich-avatar-image{width:100%;height:100%;object-fit:cover}.prx-rich-avatar-fallback{font-weight:600}.prx-rich-card{display:flex;flex-direction:column;gap:8px;padding:16px;border:1px solid var(--md-sys-color-outline-variant, #cac4d0);border-radius:16px;background:var(--md-sys-color-surface, #fff)}.prx-rich-card-title{font-weight:600}.prx-rich-card-subtitle,.prx-rich-metric-caption{color:var(--md-sys-color-on-surface-variant, #49454f);font-size:12px}.prx-rich-metric{display:flex;flex-direction:column;gap:4px}.prx-rich-metric-value{font-size:20px;font-weight:700}.prx-rich-progress{display:flex;flex-direction:column;gap:4px}.prx-rich-progress-bar{width:100%}.prx-rich-media-block{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:12px;align-items:center;min-width:0}.prx-rich-media-block__avatar,.prx-rich-media-block__body,.prx-rich-media-block__trailing{min-width:0}.prx-rich-media-block__body{display:flex;flex-direction:column;gap:6px}.prx-rich-media-block__title{font-weight:700}.prx-rich-media-block__subtitle{color:var(--md-sys-color-on-surface-variant, #49454f);font-size:13px}.prx-rich-media-block__meta{color:var(--md-sys-color-on-surface-variant, #49454f);font-size:12px}.prx-rich-media-block__trailing{display:flex;align-items:center;justify-content:flex-end}.prx-rich-timeline{display:flex;flex-direction:column;gap:12px;min-width:0}.prx-rich-timeline__title{font-weight:700}.prx-rich-timeline__items{display:flex;flex-direction:column;gap:12px}.prx-rich-timeline__item{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:12px;align-items:flex-start;min-width:0}.prx-rich-timeline__item-marker{display:inline-flex;align-items:center;justify-content:center;width:24px;min-height:24px}.prx-rich-timeline__item-icon{font-size:18px;line-height:18px;color:var(--md-sys-color-primary, #6750a4)}.prx-rich-timeline__item-dot{width:10px;height:10px;border-radius:50%;background:var(--md-sys-color-primary, #6750a4);display:inline-block;margin-top:4px}.prx-rich-timeline__item-body{display:flex;flex-direction:column;gap:4px;min-width:0}.prx-rich-timeline__item-title{font-weight:600}.prx-rich-timeline__item-subtitle,.prx-rich-timeline__item-meta,.prx-rich-timeline__empty{color:var(--md-sys-color-on-surface-variant, #49454f);font-size:12px}.prx-rich-timeline__item-badge{display:inline-flex;align-items:flex-start;justify-content:flex-end}\n"] }]
|
|
695
|
+
`, styles: [".prx-rich-content-root{display:block}.prx-rich-content-root--inline{display:inline-flex;align-items:var(--prx-rich-content-inline-align-items, center);flex-wrap:var(--prx-rich-content-inline-flex-wrap, nowrap);gap:var(--prx-rich-content-inline-gap, 6px);min-width:0;max-width:100%;vertical-align:middle}.prx-rich-node{min-width:0}.prx-rich-node--inline{display:inline-flex;align-items:center;min-width:0}.prx-rich-compose{display:flex;align-items:center;gap:8px}.prx-rich-compose.direction-column{flex-direction:column;align-items:stretch}.prx-rich-compose.wrap{flex-wrap:wrap}.prx-rich-badge{display:inline-flex;align-items:center;padding:2px 10px;border-radius:999px;background:var(--md-sys-color-primary-container, #e8def8);color:var(--md-sys-color-on-primary-container, #21005d);font-size:12px;line-height:20px}.prx-rich-link{color:var(--md-sys-color-primary, #6750a4);text-decoration:underline;text-underline-offset:2px}.prx-rich-avatar{display:inline-flex;align-items:center;justify-content:center;width:40px;height:40px;border-radius:50%;background:var(--md-sys-color-surface-container-high, #ece6f0);overflow:hidden}.prx-rich-avatar-image{width:100%;height:100%;object-fit:cover}.prx-rich-avatar-fallback{font-weight:600}.prx-rich-card{display:flex;flex-direction:column;gap:8px;padding:16px;border:1px solid var(--md-sys-color-outline-variant, #cac4d0);border-radius:16px;background:var(--md-sys-color-surface, #fff)}.prx-rich-card-title{font-weight:600}.prx-rich-card-subtitle,.prx-rich-metric-caption{color:var(--md-sys-color-on-surface-variant, #49454f);font-size:12px}.prx-rich-metric{display:flex;flex-direction:column;gap:4px}.prx-rich-metric-value{font-size:20px;font-weight:700}.prx-rich-progress{display:flex;flex-direction:column;gap:4px}.prx-rich-progress-bar{width:100%}.prx-rich-media-block{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:12px;align-items:center;min-width:0}.prx-rich-media-block__avatar,.prx-rich-media-block__body,.prx-rich-media-block__trailing{min-width:0}.prx-rich-media-block__body{display:flex;flex-direction:column;gap:6px}.prx-rich-media-block__title{font-weight:700}.prx-rich-media-block__subtitle{color:var(--md-sys-color-on-surface-variant, #49454f);font-size:13px}.prx-rich-media-block__meta{color:var(--md-sys-color-on-surface-variant, #49454f);font-size:12px}.prx-rich-media-block__trailing{display:flex;align-items:center;justify-content:flex-end}.prx-rich-timeline{display:flex;flex-direction:column;gap:12px;min-width:0}.prx-rich-timeline__title{font-weight:700}.prx-rich-timeline__items{display:flex;flex-direction:column;gap:12px}.prx-rich-timeline__item{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:12px;align-items:flex-start;min-width:0}.prx-rich-timeline__item-marker{display:inline-flex;align-items:center;justify-content:center;width:24px;min-height:24px}.prx-rich-timeline__item-icon{font-size:18px;line-height:18px;color:var(--md-sys-color-primary, #6750a4)}.prx-rich-timeline__item-dot{width:10px;height:10px;border-radius:50%;background:var(--md-sys-color-primary, #6750a4);display:inline-block;margin-top:4px}.prx-rich-timeline__item-body{display:flex;flex-direction:column;gap:4px;min-width:0}.prx-rich-timeline__item-title{font-weight:600}.prx-rich-timeline__item-subtitle,.prx-rich-timeline__item-meta,.prx-rich-timeline__empty{color:var(--md-sys-color-on-surface-variant, #49454f);font-size:12px}.prx-rich-timeline__item-badge{display:inline-flex;align-items:flex-start;justify-content:flex-end}\n"] }]
|
|
625
696
|
}], propDecorators: { document: [{ type: i0.Input, args: [{ isSignal: true, alias: "document", required: false }] }], nodes: [{ type: i0.Input, args: [{ isSignal: true, alias: "nodes", required: false }] }], context: [{ type: i0.Input, args: [{ isSignal: true, alias: "context", required: false }] }], hostCapabilities: [{ type: i0.Input, args: [{ isSignal: true, alias: "hostCapabilities", required: false }] }], layout: [{ type: i0.Input, args: [{ isSignal: true, alias: "layout", required: false }] }], rootClassName: [{
|
|
626
697
|
type: Input
|
|
627
698
|
}] } });
|
|
@@ -637,31 +708,289 @@ const PRAXIS_RICH_CONTENT_EN_US = {
|
|
|
637
708
|
'praxis.richContent.editor.layout.inline': 'Inline',
|
|
638
709
|
'praxis.richContent.editor.rootClass': 'Root class',
|
|
639
710
|
'praxis.richContent.editor.rootClassPlaceholder': 'Example: employee-expansion-rich',
|
|
640
|
-
'praxis.richContent.editor.document': '
|
|
641
|
-
'praxis.richContent.editor.documentHelp': '
|
|
711
|
+
'praxis.richContent.editor.document': 'Advanced JSON',
|
|
712
|
+
'praxis.richContent.editor.documentHelp': 'Edit the canonical RichContentDocument. The inspector validates the shape and renders a preview before apply or save.',
|
|
713
|
+
'praxis.richContent.editor.blocks': 'Blocks',
|
|
714
|
+
'praxis.richContent.editor.blocksHelp': 'Create and edit common top-level rich-content blocks. Use JSON for advanced nested structures.',
|
|
715
|
+
'praxis.richContent.editor.block': 'Block',
|
|
716
|
+
'praxis.richContent.editor.blockType': 'Block type',
|
|
717
|
+
'praxis.richContent.editor.addBlock': 'Add block',
|
|
718
|
+
'praxis.richContent.editor.removeBlock': 'Remove',
|
|
719
|
+
'praxis.richContent.editor.confirmRemove': 'Confirm remove',
|
|
720
|
+
'praxis.richContent.editor.cancelRemove': 'Cancel',
|
|
721
|
+
'praxis.richContent.editor.moveUp': 'Move up',
|
|
722
|
+
'praxis.richContent.editor.moveDown': 'Move down',
|
|
723
|
+
'praxis.richContent.editor.addItem': 'Add item',
|
|
724
|
+
'praxis.richContent.editor.item': 'Item',
|
|
725
|
+
'praxis.richContent.editor.cardContent': 'Card content',
|
|
726
|
+
'praxis.richContent.editor.composeItems': 'Compose items',
|
|
727
|
+
'praxis.richContent.editor.timelineItems': 'Timeline items',
|
|
728
|
+
'praxis.richContent.editor.noBlocks': 'No blocks yet. Add a block to start authoring this document.',
|
|
729
|
+
'praxis.richContent.editor.noPresets': 'No presets registered',
|
|
730
|
+
'praxis.richContent.editor.noPresetsHelp': 'Register rich-block presets through PRAXIS_RICH_BLOCK_PRESETS to author preset references visually.',
|
|
731
|
+
'praxis.richContent.editor.advancedOnly': 'This node type is preserved and can be edited in advanced JSON.',
|
|
732
|
+
'praxis.richContent.editor.mediaBlockAdvancedHelp': 'Meta and trailing content are preserved in advanced JSON.',
|
|
733
|
+
'praxis.richContent.editor.cardContentHelp': 'Card content is preserved in JSON. Use advanced JSON for nested card nodes.',
|
|
734
|
+
'praxis.richContent.editor.nodeId': 'Stable id',
|
|
735
|
+
'praxis.richContent.editor.nodeIdPlaceholder': 'Example: hero-title',
|
|
736
|
+
'praxis.richContent.editor.visibilityRule': 'Visibility rule',
|
|
737
|
+
'praxis.richContent.editor.safeStyle': 'Safe style',
|
|
738
|
+
'praxis.richContent.editor.nodeType.text': 'Text',
|
|
739
|
+
'praxis.richContent.editor.nodeType.badge': 'Badge',
|
|
740
|
+
'praxis.richContent.editor.nodeType.icon': 'Icon',
|
|
741
|
+
'praxis.richContent.editor.nodeType.image': 'Image',
|
|
742
|
+
'praxis.richContent.editor.nodeType.link': 'Link',
|
|
743
|
+
'praxis.richContent.editor.nodeType.metric': 'Metric',
|
|
744
|
+
'praxis.richContent.editor.nodeType.progress': 'Progress',
|
|
745
|
+
'praxis.richContent.editor.nodeType.compose': 'Compose',
|
|
746
|
+
'praxis.richContent.editor.nodeType.card': 'Card',
|
|
747
|
+
'praxis.richContent.editor.nodeType.mediaBlock': 'Media block',
|
|
748
|
+
'praxis.richContent.editor.nodeType.timeline': 'Timeline',
|
|
749
|
+
'praxis.richContent.editor.nodeType.preset': 'Preset',
|
|
750
|
+
'praxis.richContent.editor.field.text': 'Text',
|
|
751
|
+
'praxis.richContent.editor.field.textExpr': 'Text binding',
|
|
752
|
+
'praxis.richContent.editor.field.label': 'Label',
|
|
753
|
+
'praxis.richContent.editor.field.labelExpr': 'Label binding',
|
|
754
|
+
'praxis.richContent.editor.field.icon': 'Icon',
|
|
755
|
+
'praxis.richContent.editor.field.ariaLabel': 'Accessible label',
|
|
756
|
+
'praxis.richContent.editor.field.src': 'Image URL',
|
|
757
|
+
'praxis.richContent.editor.field.href': 'Link URL',
|
|
758
|
+
'praxis.richContent.editor.field.target': 'Target',
|
|
759
|
+
'praxis.richContent.editor.field.alt': 'Alternative text',
|
|
760
|
+
'praxis.richContent.editor.field.testId': 'Test id',
|
|
761
|
+
'praxis.richContent.editor.field.className': 'CSS class',
|
|
762
|
+
'praxis.richContent.editor.field.visibleWhenPath': 'Context path',
|
|
763
|
+
'praxis.richContent.editor.field.visibleWhenValue': 'Expected value',
|
|
764
|
+
'praxis.richContent.editor.field.styleName': 'CSS property',
|
|
765
|
+
'praxis.richContent.editor.field.styleValue': 'CSS value',
|
|
766
|
+
'praxis.richContent.editor.field.valueExpr': 'Value binding',
|
|
767
|
+
'praxis.richContent.editor.field.captionExpr': 'Caption binding',
|
|
768
|
+
'praxis.richContent.editor.field.max': 'Maximum',
|
|
769
|
+
'praxis.richContent.editor.field.direction': 'Direction',
|
|
770
|
+
'praxis.richContent.editor.field.gap': 'Gap',
|
|
771
|
+
'praxis.richContent.editor.field.avatarName': 'Avatar name',
|
|
772
|
+
'praxis.richContent.editor.field.avatarImage': 'Avatar image',
|
|
773
|
+
'praxis.richContent.editor.field.emptyText': 'Empty text',
|
|
774
|
+
'praxis.richContent.editor.field.meta': 'Meta',
|
|
775
|
+
'praxis.richContent.editor.field.badge': 'Badge',
|
|
776
|
+
'praxis.richContent.editor.field.preset': 'Preset',
|
|
777
|
+
'praxis.richContent.editor.field.title': 'Title',
|
|
778
|
+
'praxis.richContent.editor.field.subtitle': 'Subtitle',
|
|
779
|
+
'praxis.richContent.editor.direction.row': 'Row',
|
|
780
|
+
'praxis.richContent.editor.direction.column': 'Column',
|
|
781
|
+
'praxis.richContent.editor.placeholder.textExpr': 'row.name',
|
|
782
|
+
'praxis.richContent.editor.placeholder.labelExpr': 'row.status',
|
|
783
|
+
'praxis.richContent.editor.placeholder.icon': 'check_circle',
|
|
784
|
+
'praxis.richContent.editor.placeholder.testId': 'rich-hero-title',
|
|
785
|
+
'praxis.richContent.editor.placeholder.className': 'hero-title',
|
|
786
|
+
'praxis.richContent.editor.placeholder.visibleWhenPath': 'row.active',
|
|
787
|
+
'praxis.richContent.editor.placeholder.visibleWhenValue': 'true',
|
|
788
|
+
'praxis.richContent.editor.placeholder.styleName': 'color',
|
|
789
|
+
'praxis.richContent.editor.placeholder.styleValue': 'var(--md-sys-color-primary)',
|
|
790
|
+
'praxis.richContent.editor.placeholder.valueExpr': 'row.total',
|
|
791
|
+
'praxis.richContent.editor.placeholder.captionExpr': 'row.caption',
|
|
792
|
+
'praxis.richContent.editor.placeholder.progressExpr': 'row.progress',
|
|
793
|
+
'praxis.richContent.editor.defaultText': 'Text',
|
|
794
|
+
'praxis.richContent.editor.defaultBadge': 'Badge',
|
|
795
|
+
'praxis.richContent.editor.defaultLink': 'Link',
|
|
796
|
+
'praxis.richContent.editor.defaultMetric': 'Metric',
|
|
797
|
+
'praxis.richContent.editor.defaultProgress': 'Progress',
|
|
798
|
+
'praxis.richContent.editor.defaultCard': 'Card',
|
|
799
|
+
'praxis.richContent.editor.defaultCardText': 'Card content',
|
|
800
|
+
'praxis.richContent.editor.defaultAvatarName': 'Person',
|
|
801
|
+
'praxis.richContent.editor.defaultMediaTitle': 'Media title',
|
|
802
|
+
'praxis.richContent.editor.defaultMediaSubtitle': 'Media subtitle',
|
|
803
|
+
'praxis.richContent.editor.defaultTimeline': 'Timeline',
|
|
804
|
+
'praxis.richContent.editor.defaultTimelineEmpty': 'No events.',
|
|
805
|
+
'praxis.richContent.editor.defaultTimelineItem': 'Timeline item',
|
|
806
|
+
'praxis.richContent.editor.defaultTimelineItemSubtitle': 'Item details',
|
|
807
|
+
'praxis.richContent.editor.inspector': 'Document inspector',
|
|
808
|
+
'praxis.richContent.editor.overview': 'Overview',
|
|
809
|
+
'praxis.richContent.editor.nodeCount': 'Nodes',
|
|
810
|
+
'praxis.richContent.editor.nodeTypes': 'Types',
|
|
811
|
+
'praxis.richContent.editor.none': 'None',
|
|
812
|
+
'praxis.richContent.editor.preview': 'Preview',
|
|
642
813
|
'praxis.richContent.editor.restore': 'Restore',
|
|
643
814
|
'praxis.richContent.editor.emptyDocument': 'Empty document',
|
|
644
|
-
'praxis.richContent.editor.validation.documentShape': 'The document must
|
|
815
|
+
'praxis.richContent.editor.validation.documentShape': 'The document must be a valid RichContentDocument.',
|
|
645
816
|
'praxis.richContent.editor.validation.invalidJson': 'Invalid JSON document.',
|
|
817
|
+
'praxis.richContent.editor.validation.invalidDocument': 'Invalid rich content document.',
|
|
818
|
+
'praxis.richContent.editor.validation.documentObject': 'The document must be a JSON object.',
|
|
819
|
+
'praxis.richContent.editor.validation.documentKind': 'The document kind must be "praxis.rich-content".',
|
|
820
|
+
'praxis.richContent.editor.validation.documentVersion': 'The document version must be "1.0.0".',
|
|
821
|
+
'praxis.richContent.editor.validation.documentNodes': 'The document must include a nodes array.',
|
|
822
|
+
'praxis.richContent.editor.validation.maxDepth': 'Rich content nesting is too deep.',
|
|
823
|
+
'praxis.richContent.editor.validation.maxNodes': 'Rich content documents can include at most 500 nodes.',
|
|
824
|
+
'praxis.richContent.editor.validation.nodeObject': 'Each rich content node must be an object.',
|
|
825
|
+
'praxis.richContent.editor.validation.nodeType': 'Each node must use a supported rich-content type.',
|
|
826
|
+
'praxis.richContent.editor.validation.nodeArray': 'This field must be a rich-content node array.',
|
|
827
|
+
'praxis.richContent.editor.validation.timelineItems': 'Timeline nodes must include an items array.',
|
|
828
|
+
'praxis.richContent.editor.validation.timelineItem': 'Each timeline item must be an object.',
|
|
829
|
+
'praxis.richContent.editor.validation.presetRef': 'Preset nodes must include a rich-block ref object.',
|
|
830
|
+
'praxis.richContent.editor.validation.presetKind': 'Preset ref kind must be "rich-block".',
|
|
831
|
+
'praxis.richContent.editor.validation.objectField': 'This field must be an object.',
|
|
832
|
+
'praxis.richContent.editor.validation.ruleArray': 'Conditional rules must be an array.',
|
|
833
|
+
'praxis.richContent.editor.validation.ruleObject': 'Conditional rules must be objects.',
|
|
834
|
+
'praxis.richContent.editor.validation.ruleExpression': 'This rule must include a Json Logic expression.',
|
|
835
|
+
'praxis.richContent.editor.validation.styleObject': 'Styles must be an object with safe property names and values.',
|
|
836
|
+
'praxis.richContent.editor.validation.styleName': 'Style names must be simple CSS property names or CSS custom properties.',
|
|
837
|
+
'praxis.richContent.editor.validation.styleValue': 'Style values must be strings or numbers.',
|
|
838
|
+
'praxis.richContent.editor.validation.unsafeStyleValue': 'Style values cannot contain script, import, binding or data URL expressions.',
|
|
839
|
+
'praxis.richContent.editor.validation.unsafeUrl': 'URLs must be relative, http(s), or safe image data URLs.',
|
|
840
|
+
'praxis.richContent.editor.validation.stringField': 'This field must be a string.',
|
|
841
|
+
'praxis.richContent.editor.validation.requiredStringField': 'This field must be a non-empty string.',
|
|
842
|
+
'praxis.richContent.editor.validation.className': 'Class names must be simple CSS class tokens.',
|
|
843
|
+
'praxis.richContent.editor.validation.numberField': 'This field must be a number.',
|
|
844
|
+
'praxis.richContent.editor.validation.booleanField': 'This field must be a boolean.',
|
|
845
|
+
'praxis.richContent.editor.validation.enumField': 'This field must use one of the supported values.',
|
|
646
846
|
};
|
|
647
847
|
|
|
648
848
|
const PRAXIS_RICH_CONTENT_PT_BR = {
|
|
649
849
|
'praxis.richContent.editor.eyebrow': 'RichContentDocument',
|
|
650
|
-
'praxis.richContent.editor.title': 'Configurar
|
|
651
|
-
'praxis.richContent.editor.subtitle': 'Edite o documento
|
|
652
|
-
'praxis.richContent.editor.valid': 'Documento
|
|
653
|
-
'praxis.richContent.editor.invalid': 'JSON
|
|
850
|
+
'praxis.richContent.editor.title': 'Configurar conteúdo rico',
|
|
851
|
+
'praxis.richContent.editor.subtitle': 'Edite o documento canônico consumido pelo renderer. O payload salvo preserva os demais inputs do widget.',
|
|
852
|
+
'praxis.richContent.editor.valid': 'Documento válido',
|
|
853
|
+
'praxis.richContent.editor.invalid': 'JSON inválido',
|
|
654
854
|
'praxis.richContent.editor.layout': 'Layout',
|
|
655
855
|
'praxis.richContent.editor.layout.block': 'Bloco',
|
|
656
856
|
'praxis.richContent.editor.layout.inline': 'Inline',
|
|
657
857
|
'praxis.richContent.editor.rootClass': 'Classe raiz',
|
|
658
858
|
'praxis.richContent.editor.rootClassPlaceholder': 'Exemplo: employee-expansion-rich',
|
|
659
|
-
'praxis.richContent.editor.document': 'JSON
|
|
660
|
-
'praxis.richContent.editor.documentHelp': '
|
|
859
|
+
'praxis.richContent.editor.document': 'JSON avançado',
|
|
860
|
+
'praxis.richContent.editor.documentHelp': 'Edite o RichContentDocument canônico. O inspetor valida o formato e renderiza uma prévia antes de aplicar ou salvar.',
|
|
861
|
+
'praxis.richContent.editor.blocks': 'Blocos',
|
|
862
|
+
'praxis.richContent.editor.blocksHelp': 'Crie e edite blocos comuns de nível superior de rich-content. Use JSON para estruturas aninhadas avançadas.',
|
|
863
|
+
'praxis.richContent.editor.block': 'Bloco',
|
|
864
|
+
'praxis.richContent.editor.blockType': 'Tipo de bloco',
|
|
865
|
+
'praxis.richContent.editor.addBlock': 'Adicionar bloco',
|
|
866
|
+
'praxis.richContent.editor.removeBlock': 'Remover',
|
|
867
|
+
'praxis.richContent.editor.confirmRemove': 'Confirmar remoção',
|
|
868
|
+
'praxis.richContent.editor.cancelRemove': 'Cancelar',
|
|
869
|
+
'praxis.richContent.editor.moveUp': 'Mover para cima',
|
|
870
|
+
'praxis.richContent.editor.moveDown': 'Mover para baixo',
|
|
871
|
+
'praxis.richContent.editor.addItem': 'Adicionar item',
|
|
872
|
+
'praxis.richContent.editor.item': 'Item',
|
|
873
|
+
'praxis.richContent.editor.cardContent': 'Conteúdo do card',
|
|
874
|
+
'praxis.richContent.editor.composeItems': 'Itens do compose',
|
|
875
|
+
'praxis.richContent.editor.timelineItems': 'Itens da timeline',
|
|
876
|
+
'praxis.richContent.editor.noBlocks': 'Ainda não há blocos. Adicione um bloco para iniciar a autoria deste documento.',
|
|
877
|
+
'praxis.richContent.editor.noPresets': 'Nenhum preset registrado',
|
|
878
|
+
'praxis.richContent.editor.noPresetsHelp': 'Registre presets rich-block por PRAXIS_RICH_BLOCK_PRESETS para editar referências de preset visualmente.',
|
|
879
|
+
'praxis.richContent.editor.advancedOnly': 'Este tipo de node é preservado e pode ser editado no JSON avançado.',
|
|
880
|
+
'praxis.richContent.editor.mediaBlockAdvancedHelp': 'Conteúdo meta e trailing é preservado no JSON avançado.',
|
|
881
|
+
'praxis.richContent.editor.cardContentHelp': 'O conteúdo do card é preservado no JSON. Use o JSON avançado para nodes aninhados do card.',
|
|
882
|
+
'praxis.richContent.editor.nodeId': 'Id estável',
|
|
883
|
+
'praxis.richContent.editor.nodeIdPlaceholder': 'Exemplo: hero-title',
|
|
884
|
+
'praxis.richContent.editor.visibilityRule': 'Regra de visibilidade',
|
|
885
|
+
'praxis.richContent.editor.safeStyle': 'Estilo seguro',
|
|
886
|
+
'praxis.richContent.editor.nodeType.text': 'Texto',
|
|
887
|
+
'praxis.richContent.editor.nodeType.badge': 'Badge',
|
|
888
|
+
'praxis.richContent.editor.nodeType.icon': 'Ícone',
|
|
889
|
+
'praxis.richContent.editor.nodeType.image': 'Imagem',
|
|
890
|
+
'praxis.richContent.editor.nodeType.link': 'Link',
|
|
891
|
+
'praxis.richContent.editor.nodeType.metric': 'Métrica',
|
|
892
|
+
'praxis.richContent.editor.nodeType.progress': 'Progresso',
|
|
893
|
+
'praxis.richContent.editor.nodeType.compose': 'Compose',
|
|
894
|
+
'praxis.richContent.editor.nodeType.card': 'Card',
|
|
895
|
+
'praxis.richContent.editor.nodeType.mediaBlock': 'Bloco de mídia',
|
|
896
|
+
'praxis.richContent.editor.nodeType.timeline': 'Timeline',
|
|
897
|
+
'praxis.richContent.editor.nodeType.preset': 'Preset',
|
|
898
|
+
'praxis.richContent.editor.field.text': 'Texto',
|
|
899
|
+
'praxis.richContent.editor.field.textExpr': 'Binding do texto',
|
|
900
|
+
'praxis.richContent.editor.field.label': 'Label',
|
|
901
|
+
'praxis.richContent.editor.field.labelExpr': 'Binding do label',
|
|
902
|
+
'praxis.richContent.editor.field.icon': 'Ícone',
|
|
903
|
+
'praxis.richContent.editor.field.ariaLabel': 'Label acessível',
|
|
904
|
+
'praxis.richContent.editor.field.src': 'URL da imagem',
|
|
905
|
+
'praxis.richContent.editor.field.href': 'URL do link',
|
|
906
|
+
'praxis.richContent.editor.field.target': 'Destino',
|
|
907
|
+
'praxis.richContent.editor.field.alt': 'Texto alternativo',
|
|
908
|
+
'praxis.richContent.editor.field.testId': 'Id de teste',
|
|
909
|
+
'praxis.richContent.editor.field.className': 'Classe CSS',
|
|
910
|
+
'praxis.richContent.editor.field.visibleWhenPath': 'Path de contexto',
|
|
911
|
+
'praxis.richContent.editor.field.visibleWhenValue': 'Valor esperado',
|
|
912
|
+
'praxis.richContent.editor.field.styleName': 'Propriedade CSS',
|
|
913
|
+
'praxis.richContent.editor.field.styleValue': 'Valor CSS',
|
|
914
|
+
'praxis.richContent.editor.field.valueExpr': 'Binding do valor',
|
|
915
|
+
'praxis.richContent.editor.field.captionExpr': 'Binding da legenda',
|
|
916
|
+
'praxis.richContent.editor.field.max': 'Máximo',
|
|
917
|
+
'praxis.richContent.editor.field.direction': 'Direção',
|
|
918
|
+
'praxis.richContent.editor.field.gap': 'Espaçamento',
|
|
919
|
+
'praxis.richContent.editor.field.avatarName': 'Nome do avatar',
|
|
920
|
+
'praxis.richContent.editor.field.avatarImage': 'Imagem do avatar',
|
|
921
|
+
'praxis.richContent.editor.field.emptyText': 'Texto vazio',
|
|
922
|
+
'praxis.richContent.editor.field.meta': 'Meta',
|
|
923
|
+
'praxis.richContent.editor.field.badge': 'Badge',
|
|
924
|
+
'praxis.richContent.editor.field.preset': 'Preset',
|
|
925
|
+
'praxis.richContent.editor.field.title': 'Título',
|
|
926
|
+
'praxis.richContent.editor.field.subtitle': 'Subtítulo',
|
|
927
|
+
'praxis.richContent.editor.direction.row': 'Linha',
|
|
928
|
+
'praxis.richContent.editor.direction.column': 'Coluna',
|
|
929
|
+
'praxis.richContent.editor.placeholder.textExpr': 'row.name',
|
|
930
|
+
'praxis.richContent.editor.placeholder.labelExpr': 'row.status',
|
|
931
|
+
'praxis.richContent.editor.placeholder.icon': 'check_circle',
|
|
932
|
+
'praxis.richContent.editor.placeholder.testId': 'rich-hero-title',
|
|
933
|
+
'praxis.richContent.editor.placeholder.className': 'hero-title',
|
|
934
|
+
'praxis.richContent.editor.placeholder.visibleWhenPath': 'row.active',
|
|
935
|
+
'praxis.richContent.editor.placeholder.visibleWhenValue': 'true',
|
|
936
|
+
'praxis.richContent.editor.placeholder.styleName': 'color',
|
|
937
|
+
'praxis.richContent.editor.placeholder.styleValue': 'var(--md-sys-color-primary)',
|
|
938
|
+
'praxis.richContent.editor.placeholder.valueExpr': 'row.total',
|
|
939
|
+
'praxis.richContent.editor.placeholder.captionExpr': 'row.caption',
|
|
940
|
+
'praxis.richContent.editor.placeholder.progressExpr': 'row.progress',
|
|
941
|
+
'praxis.richContent.editor.defaultText': 'Texto',
|
|
942
|
+
'praxis.richContent.editor.defaultBadge': 'Badge',
|
|
943
|
+
'praxis.richContent.editor.defaultLink': 'Link',
|
|
944
|
+
'praxis.richContent.editor.defaultMetric': 'Métrica',
|
|
945
|
+
'praxis.richContent.editor.defaultProgress': 'Progresso',
|
|
946
|
+
'praxis.richContent.editor.defaultCard': 'Card',
|
|
947
|
+
'praxis.richContent.editor.defaultCardText': 'Conteúdo do card',
|
|
948
|
+
'praxis.richContent.editor.defaultAvatarName': 'Pessoa',
|
|
949
|
+
'praxis.richContent.editor.defaultMediaTitle': 'Título da mídia',
|
|
950
|
+
'praxis.richContent.editor.defaultMediaSubtitle': 'Subtítulo da mídia',
|
|
951
|
+
'praxis.richContent.editor.defaultTimeline': 'Timeline',
|
|
952
|
+
'praxis.richContent.editor.defaultTimelineEmpty': 'Sem eventos.',
|
|
953
|
+
'praxis.richContent.editor.defaultTimelineItem': 'Item da timeline',
|
|
954
|
+
'praxis.richContent.editor.defaultTimelineItemSubtitle': 'Detalhes do item',
|
|
955
|
+
'praxis.richContent.editor.inspector': 'Inspetor do documento',
|
|
956
|
+
'praxis.richContent.editor.overview': 'Visão geral',
|
|
957
|
+
'praxis.richContent.editor.nodeCount': 'Nodes',
|
|
958
|
+
'praxis.richContent.editor.nodeTypes': 'Tipos',
|
|
959
|
+
'praxis.richContent.editor.none': 'Nenhum',
|
|
960
|
+
'praxis.richContent.editor.preview': 'Prévia',
|
|
661
961
|
'praxis.richContent.editor.restore': 'Restaurar',
|
|
662
962
|
'praxis.richContent.editor.emptyDocument': 'Documento vazio',
|
|
663
|
-
'praxis.richContent.editor.validation.documentShape': 'O documento deve
|
|
664
|
-
'praxis.richContent.editor.validation.invalidJson': 'Documento JSON
|
|
963
|
+
'praxis.richContent.editor.validation.documentShape': 'O documento deve ser um RichContentDocument válido.',
|
|
964
|
+
'praxis.richContent.editor.validation.invalidJson': 'Documento JSON inválido.',
|
|
965
|
+
'praxis.richContent.editor.validation.invalidDocument': 'Documento de conteúdo rico inválido.',
|
|
966
|
+
'praxis.richContent.editor.validation.documentObject': 'O documento deve ser um objeto JSON.',
|
|
967
|
+
'praxis.richContent.editor.validation.documentKind': 'O kind do documento deve ser "praxis.rich-content".',
|
|
968
|
+
'praxis.richContent.editor.validation.documentVersion': 'A versão do documento deve ser "1.0.0".',
|
|
969
|
+
'praxis.richContent.editor.validation.documentNodes': 'O documento deve incluir um array nodes.',
|
|
970
|
+
'praxis.richContent.editor.validation.maxDepth': 'O aninhamento do conteúdo rico é muito profundo.',
|
|
971
|
+
'praxis.richContent.editor.validation.maxNodes': 'Documentos de conteúdo rico podem incluir no máximo 500 nodes.',
|
|
972
|
+
'praxis.richContent.editor.validation.nodeObject': 'Cada node de conteúdo rico deve ser um objeto.',
|
|
973
|
+
'praxis.richContent.editor.validation.nodeType': 'Cada node deve usar um tipo de rich-content suportado.',
|
|
974
|
+
'praxis.richContent.editor.validation.nodeArray': 'Este campo deve ser um array de nodes de conteúdo rico.',
|
|
975
|
+
'praxis.richContent.editor.validation.timelineItems': 'Nodes de timeline devem incluir um array items.',
|
|
976
|
+
'praxis.richContent.editor.validation.timelineItem': 'Cada item da timeline deve ser um objeto.',
|
|
977
|
+
'praxis.richContent.editor.validation.presetRef': 'Nodes de preset devem incluir um objeto ref rich-block.',
|
|
978
|
+
'praxis.richContent.editor.validation.presetKind': 'O kind do ref de preset deve ser "rich-block".',
|
|
979
|
+
'praxis.richContent.editor.validation.objectField': 'Este campo deve ser um objeto.',
|
|
980
|
+
'praxis.richContent.editor.validation.ruleArray': 'Regras condicionais devem ser um array.',
|
|
981
|
+
'praxis.richContent.editor.validation.ruleObject': 'Regras condicionais devem ser objetos.',
|
|
982
|
+
'praxis.richContent.editor.validation.ruleExpression': 'Esta regra deve incluir uma expressão Json Logic.',
|
|
983
|
+
'praxis.richContent.editor.validation.styleObject': 'Estilos devem ser um objeto com nomes e valores seguros.',
|
|
984
|
+
'praxis.richContent.editor.validation.styleName': 'Nomes de estilo devem ser propriedades CSS simples ou propriedades customizadas CSS.',
|
|
985
|
+
'praxis.richContent.editor.validation.styleValue': 'Valores de estilo devem ser strings ou números.',
|
|
986
|
+
'praxis.richContent.editor.validation.unsafeStyleValue': 'Valores de estilo não podem conter script, import, binding ou expressões de URL data.',
|
|
987
|
+
'praxis.richContent.editor.validation.unsafeUrl': 'URLs devem ser relativas, http(s) ou URLs data de imagem seguras.',
|
|
988
|
+
'praxis.richContent.editor.validation.stringField': 'Este campo deve ser uma string.',
|
|
989
|
+
'praxis.richContent.editor.validation.requiredStringField': 'Este campo deve ser uma string não vazia.',
|
|
990
|
+
'praxis.richContent.editor.validation.className': 'Classes devem ser tokens CSS simples.',
|
|
991
|
+
'praxis.richContent.editor.validation.numberField': 'Este campo deve ser um número.',
|
|
992
|
+
'praxis.richContent.editor.validation.booleanField': 'Este campo deve ser booleano.',
|
|
993
|
+
'praxis.richContent.editor.validation.enumField': 'Este campo deve usar um dos valores suportados.',
|
|
665
994
|
};
|
|
666
995
|
|
|
667
996
|
function createPraxisRichContentI18nConfig(options = {}) {
|
|
@@ -696,6 +1025,634 @@ function resolvePraxisRichContentText(i18n, key, fallback) {
|
|
|
696
1025
|
return i18n.t(namespacedKey, undefined, fallback) || fallback;
|
|
697
1026
|
}
|
|
698
1027
|
|
|
1028
|
+
const SUPPORTED_DOCUMENT_KIND = 'praxis.rich-content';
|
|
1029
|
+
const SUPPORTED_DOCUMENT_VERSION = '1.0.0';
|
|
1030
|
+
const MAX_NODE_DEPTH = 12;
|
|
1031
|
+
const MAX_NODE_COUNT = 500;
|
|
1032
|
+
const SUPPORTED_NODE_TYPES = new Set([
|
|
1033
|
+
'text',
|
|
1034
|
+
'icon',
|
|
1035
|
+
'image',
|
|
1036
|
+
'link',
|
|
1037
|
+
'badge',
|
|
1038
|
+
'avatar',
|
|
1039
|
+
'metric',
|
|
1040
|
+
'progress',
|
|
1041
|
+
'compose',
|
|
1042
|
+
'card',
|
|
1043
|
+
'mediaBlock',
|
|
1044
|
+
'timeline',
|
|
1045
|
+
'preset',
|
|
1046
|
+
]);
|
|
1047
|
+
const STYLE_NAME_PATTERN = /^--[a-zA-Z0-9-_]+$|^[a-z][a-zA-Z0-9-]*$/;
|
|
1048
|
+
const CLASS_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_-]*(\s+[a-zA-Z_][a-zA-Z0-9_-]*)*$/;
|
|
1049
|
+
const UNSAFE_STYLE_VALUE_PATTERN = /(?:javascript\s*:|vbscript\s*:|data\s*:|expression\s*\(|@import|-moz-binding|behavior\s*:)/i;
|
|
1050
|
+
function validateRichContentDocument(value) {
|
|
1051
|
+
const issues = [];
|
|
1052
|
+
const state = { nodeCount: 0 };
|
|
1053
|
+
if (!isRecord(value)) {
|
|
1054
|
+
addIssue(issues, '$', 'editor.validation.documentObject', 'The document must be a JSON object.');
|
|
1055
|
+
return { valid: false, issues };
|
|
1056
|
+
}
|
|
1057
|
+
if (value['kind'] !== SUPPORTED_DOCUMENT_KIND) {
|
|
1058
|
+
addIssue(issues, '$.kind', 'editor.validation.documentKind', 'The document kind must be "praxis.rich-content".');
|
|
1059
|
+
}
|
|
1060
|
+
if (value['version'] !== SUPPORTED_DOCUMENT_VERSION) {
|
|
1061
|
+
addIssue(issues, '$.version', 'editor.validation.documentVersion', 'The document version must be "1.0.0".');
|
|
1062
|
+
}
|
|
1063
|
+
if (!Array.isArray(value['nodes'])) {
|
|
1064
|
+
addIssue(issues, '$.nodes', 'editor.validation.documentNodes', 'The document must include a nodes array.');
|
|
1065
|
+
}
|
|
1066
|
+
else {
|
|
1067
|
+
validateNodes(value['nodes'], '$.nodes', issues, state, 0);
|
|
1068
|
+
}
|
|
1069
|
+
return { valid: issues.length === 0, issues };
|
|
1070
|
+
}
|
|
1071
|
+
function isValidRichContentDocument(value) {
|
|
1072
|
+
return validateRichContentDocument(value).valid;
|
|
1073
|
+
}
|
|
1074
|
+
function validateNodes(nodes, path, issues, state, depth) {
|
|
1075
|
+
if (depth > MAX_NODE_DEPTH) {
|
|
1076
|
+
addIssue(issues, path, 'editor.validation.maxDepth', 'Rich content nesting is too deep.');
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
for (const [index, node] of nodes.entries()) {
|
|
1080
|
+
state.nodeCount += 1;
|
|
1081
|
+
if (state.nodeCount > MAX_NODE_COUNT) {
|
|
1082
|
+
addIssue(issues, path, 'editor.validation.maxNodes', 'Rich content documents can include at most 500 nodes.');
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
validateNode(node, `${path}[${index}]`, issues, state, depth);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
function validateNode(value, path, issues, state, depth) {
|
|
1089
|
+
if (!isRecord(value)) {
|
|
1090
|
+
addIssue(issues, path, 'editor.validation.nodeObject', 'Each rich content node must be an object.');
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
const type = value['type'];
|
|
1094
|
+
if (typeof type !== 'string' || !SUPPORTED_NODE_TYPES.has(type)) {
|
|
1095
|
+
addIssue(issues, `${path}.type`, 'editor.validation.nodeType', 'Each node must use a supported rich-content type.');
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
validateBaseNode(value, path, issues);
|
|
1099
|
+
switch (type) {
|
|
1100
|
+
case 'text':
|
|
1101
|
+
validateOptionalString(value, 'text', path, issues);
|
|
1102
|
+
validateOptionalExpressionPath(value, 'textExpr', path, issues);
|
|
1103
|
+
break;
|
|
1104
|
+
case 'icon':
|
|
1105
|
+
validateRequiredString(value, 'icon', path, issues);
|
|
1106
|
+
validateOptionalString(value, 'ariaLabel', path, issues);
|
|
1107
|
+
break;
|
|
1108
|
+
case 'image':
|
|
1109
|
+
validateOptionalUrl(value, 'src', path, issues, 'image');
|
|
1110
|
+
validateOptionalExpressionPath(value, 'srcExpr', path, issues);
|
|
1111
|
+
validateOptionalString(value, 'alt', path, issues);
|
|
1112
|
+
validateOptionalExpressionPath(value, 'altExpr', path, issues);
|
|
1113
|
+
break;
|
|
1114
|
+
case 'link':
|
|
1115
|
+
validateOptionalString(value, 'label', path, issues);
|
|
1116
|
+
validateOptionalExpressionPath(value, 'labelExpr', path, issues);
|
|
1117
|
+
validateRequiredUrl(value, 'href', path, issues, 'link');
|
|
1118
|
+
validateEnum(value, 'target', ['_blank', '_self'], path, issues, false);
|
|
1119
|
+
validateOptionalString(value, 'rel', path, issues);
|
|
1120
|
+
break;
|
|
1121
|
+
case 'badge':
|
|
1122
|
+
validateOptionalString(value, 'label', path, issues);
|
|
1123
|
+
validateOptionalExpressionPath(value, 'labelExpr', path, issues);
|
|
1124
|
+
validateOptionalString(value, 'icon', path, issues);
|
|
1125
|
+
break;
|
|
1126
|
+
case 'avatar':
|
|
1127
|
+
validateOptionalString(value, 'name', path, issues);
|
|
1128
|
+
validateOptionalExpressionPath(value, 'nameExpr', path, issues);
|
|
1129
|
+
validateOptionalUrl(value, 'imageSrc', path, issues, 'image');
|
|
1130
|
+
validateOptionalExpressionPath(value, 'imageSrcExpr', path, issues);
|
|
1131
|
+
validateOptionalString(value, 'initials', path, issues);
|
|
1132
|
+
validateOptionalExpressionPath(value, 'initialsExpr', path, issues);
|
|
1133
|
+
break;
|
|
1134
|
+
case 'metric':
|
|
1135
|
+
validateRequiredString(value, 'label', path, issues);
|
|
1136
|
+
validateRequiredExpressionPath(value, 'valueExpr', path, issues);
|
|
1137
|
+
validateOptionalExpressionPath(value, 'captionExpr', path, issues);
|
|
1138
|
+
validateOptionalString(value, 'icon', path, issues);
|
|
1139
|
+
break;
|
|
1140
|
+
case 'progress':
|
|
1141
|
+
validateRequiredExpressionPath(value, 'valueExpr', path, issues);
|
|
1142
|
+
validateOptionalNumber(value, 'max', path, issues);
|
|
1143
|
+
validateOptionalString(value, 'label', path, issues);
|
|
1144
|
+
validateOptionalExpressionPath(value, 'labelExpr', path, issues);
|
|
1145
|
+
validateOptionalBoolean(value, 'showPercent', path, issues);
|
|
1146
|
+
break;
|
|
1147
|
+
case 'compose':
|
|
1148
|
+
validateEnum(value, 'direction', ['row', 'column'], path, issues, false);
|
|
1149
|
+
validateEnum(value, 'gap', ['xs', 'sm', 'md', 'lg', 'xl'], path, issues, false);
|
|
1150
|
+
validateOptionalBoolean(value, 'wrap', path, issues);
|
|
1151
|
+
validateRequiredNodeArray(value, 'items', path, issues, state, depth + 1);
|
|
1152
|
+
break;
|
|
1153
|
+
case 'card':
|
|
1154
|
+
validateOptionalString(value, 'title', path, issues);
|
|
1155
|
+
validateOptionalExpressionPath(value, 'titleExpr', path, issues);
|
|
1156
|
+
validateOptionalString(value, 'subtitle', path, issues);
|
|
1157
|
+
validateOptionalExpressionPath(value, 'subtitleExpr', path, issues);
|
|
1158
|
+
validateRequiredNodeArray(value, 'content', path, issues, state, depth + 1);
|
|
1159
|
+
break;
|
|
1160
|
+
case 'mediaBlock':
|
|
1161
|
+
validateOptionalChildNode(value, 'avatar', path, issues, state, depth + 1);
|
|
1162
|
+
validateOptionalChildNode(value, 'title', path, issues, state, depth + 1);
|
|
1163
|
+
validateOptionalChildNode(value, 'subtitle', path, issues, state, depth + 1);
|
|
1164
|
+
validateOptionalChildNode(value, 'meta', path, issues, state, depth + 1);
|
|
1165
|
+
validateOptionalNodeArray(value, 'trailing', path, issues, state, depth + 1);
|
|
1166
|
+
break;
|
|
1167
|
+
case 'timeline':
|
|
1168
|
+
validateOptionalString(value, 'title', path, issues);
|
|
1169
|
+
validateOptionalExpressionPath(value, 'titleExpr', path, issues);
|
|
1170
|
+
validateOptionalString(value, 'emptyText', path, issues);
|
|
1171
|
+
validateTimelineItems(value['items'], `${path}.items`, issues);
|
|
1172
|
+
break;
|
|
1173
|
+
case 'preset':
|
|
1174
|
+
validatePresetRef(value['ref'], `${path}.ref`, issues);
|
|
1175
|
+
if (value['inputs'] !== undefined && !isRecord(value['inputs'])) {
|
|
1176
|
+
addIssue(issues, `${path}.inputs`, 'editor.validation.objectField', 'Preset inputs must be an object.');
|
|
1177
|
+
}
|
|
1178
|
+
break;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
function validateBaseNode(node, path, issues) {
|
|
1182
|
+
validateOptionalString(node, 'id', path, issues);
|
|
1183
|
+
validateOptionalString(node, 'testId', path, issues);
|
|
1184
|
+
validateOptionalClassName(node, 'className', path, issues);
|
|
1185
|
+
validateStyleObject(node['style'], `${path}.style`, issues);
|
|
1186
|
+
validateRuleExpression(node['visibleWhen'], `${path}.visibleWhen`, issues);
|
|
1187
|
+
validateRuleExpression(node['disabledWhen'], `${path}.disabledWhen`, issues);
|
|
1188
|
+
validateRuleExpression(node['loadWhen'], `${path}.loadWhen`, issues);
|
|
1189
|
+
validateClassWhen(node['classWhen'], `${path}.classWhen`, issues);
|
|
1190
|
+
validateStyleWhen(node['styleWhen'], `${path}.styleWhen`, issues);
|
|
1191
|
+
}
|
|
1192
|
+
function validateRequiredNodeArray(node, field, path, issues, state, depth) {
|
|
1193
|
+
const value = node[field];
|
|
1194
|
+
if (!Array.isArray(value)) {
|
|
1195
|
+
addIssue(issues, `${path}.${field}`, 'editor.validation.nodeArray', 'This field must be a rich-content node array.');
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
validateNodes(value, `${path}.${field}`, issues, state, depth);
|
|
1199
|
+
}
|
|
1200
|
+
function validateOptionalNodeArray(node, field, path, issues, state, depth) {
|
|
1201
|
+
if (node[field] === undefined)
|
|
1202
|
+
return;
|
|
1203
|
+
validateRequiredNodeArray(node, field, path, issues, state, depth);
|
|
1204
|
+
}
|
|
1205
|
+
function validateOptionalChildNode(node, field, path, issues, state, depth) {
|
|
1206
|
+
if (node[field] === undefined)
|
|
1207
|
+
return;
|
|
1208
|
+
validateNode(node[field], `${path}.${field}`, issues, state, depth);
|
|
1209
|
+
}
|
|
1210
|
+
function validateTimelineItems(value, path, issues) {
|
|
1211
|
+
if (!Array.isArray(value)) {
|
|
1212
|
+
addIssue(issues, path, 'editor.validation.timelineItems', 'Timeline nodes must include an items array.');
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
for (const [index, item] of value.entries()) {
|
|
1216
|
+
const itemPath = `${path}[${index}]`;
|
|
1217
|
+
if (!isRecord(item)) {
|
|
1218
|
+
addIssue(issues, itemPath, 'editor.validation.timelineItem', 'Each timeline item must be an object.');
|
|
1219
|
+
continue;
|
|
1220
|
+
}
|
|
1221
|
+
const record = item;
|
|
1222
|
+
for (const field of ['id', 'title', 'subtitle', 'meta', 'icon', 'badge']) {
|
|
1223
|
+
validateOptionalString(record, field, itemPath, issues);
|
|
1224
|
+
}
|
|
1225
|
+
for (const field of ['titleExpr', 'subtitleExpr', 'metaExpr', 'iconExpr', 'badgeExpr']) {
|
|
1226
|
+
validateOptionalExpressionPath(record, field, itemPath, issues);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
function validatePresetRef(value, path, issues) {
|
|
1231
|
+
if (!isRecord(value)) {
|
|
1232
|
+
addIssue(issues, path, 'editor.validation.presetRef', 'Preset nodes must include a rich-block ref object.');
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
const ref = value;
|
|
1236
|
+
if (ref.kind !== 'rich-block') {
|
|
1237
|
+
addIssue(issues, `${path}.kind`, 'editor.validation.presetKind', 'Preset ref kind must be "rich-block".');
|
|
1238
|
+
}
|
|
1239
|
+
validateRequiredString(ref, 'namespace', path, issues);
|
|
1240
|
+
validateRequiredString(ref, 'presetId', path, issues);
|
|
1241
|
+
validateOptionalString(ref, 'version', path, issues);
|
|
1242
|
+
}
|
|
1243
|
+
function validateClassWhen(value, path, issues) {
|
|
1244
|
+
if (value === undefined)
|
|
1245
|
+
return;
|
|
1246
|
+
if (!Array.isArray(value)) {
|
|
1247
|
+
addIssue(issues, path, 'editor.validation.ruleArray', 'Conditional class rules must be an array.');
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
for (const [index, rule] of value.entries()) {
|
|
1251
|
+
const rulePath = `${path}[${index}]`;
|
|
1252
|
+
if (!isRecord(rule)) {
|
|
1253
|
+
addIssue(issues, rulePath, 'editor.validation.ruleObject', 'Conditional class rules must be objects.');
|
|
1254
|
+
continue;
|
|
1255
|
+
}
|
|
1256
|
+
validateOptionalClassName(rule, 'className', rulePath, issues, true);
|
|
1257
|
+
validateRuleExpression(rule['expr'], `${rulePath}.expr`, issues, true);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
function validateStyleWhen(value, path, issues) {
|
|
1261
|
+
if (value === undefined)
|
|
1262
|
+
return;
|
|
1263
|
+
if (!Array.isArray(value)) {
|
|
1264
|
+
addIssue(issues, path, 'editor.validation.ruleArray', 'Conditional style rules must be an array.');
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
for (const [index, rule] of value.entries()) {
|
|
1268
|
+
const rulePath = `${path}[${index}]`;
|
|
1269
|
+
if (!isRecord(rule)) {
|
|
1270
|
+
addIssue(issues, rulePath, 'editor.validation.ruleObject', 'Conditional style rules must be objects.');
|
|
1271
|
+
continue;
|
|
1272
|
+
}
|
|
1273
|
+
validateStyleObject(rule['style'], `${rulePath}.style`, issues, true);
|
|
1274
|
+
validateRuleExpression(rule['expr'], `${rulePath}.expr`, issues, true);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
function validateRuleExpression(value, path, issues, required = false) {
|
|
1278
|
+
if (value === undefined || value === null) {
|
|
1279
|
+
if (required) {
|
|
1280
|
+
addIssue(issues, path, 'editor.validation.ruleExpression', 'This rule must include a Json Logic expression.');
|
|
1281
|
+
}
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
if (!isRecord(value) && !Array.isArray(value)) {
|
|
1285
|
+
addIssue(issues, path, 'editor.validation.ruleExpression', 'Rules must use Json Logic objects or arrays.');
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
function validateStyleObject(value, path, issues, required = false) {
|
|
1289
|
+
if (value === undefined || value === null) {
|
|
1290
|
+
if (required) {
|
|
1291
|
+
addIssue(issues, path, 'editor.validation.styleObject', 'Styles must be an object with safe property names and values.');
|
|
1292
|
+
}
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
if (!isRecord(value)) {
|
|
1296
|
+
addIssue(issues, path, 'editor.validation.styleObject', 'Styles must be an object with safe property names and values.');
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
for (const [name, styleValue] of Object.entries(value)) {
|
|
1300
|
+
if (!STYLE_NAME_PATTERN.test(name)) {
|
|
1301
|
+
addIssue(issues, `${path}.${name}`, 'editor.validation.styleName', 'Style names must be simple CSS property names or CSS custom properties.');
|
|
1302
|
+
}
|
|
1303
|
+
if (typeof styleValue !== 'string' &&
|
|
1304
|
+
typeof styleValue !== 'number') {
|
|
1305
|
+
addIssue(issues, `${path}.${name}`, 'editor.validation.styleValue', 'Style values must be strings or numbers.');
|
|
1306
|
+
continue;
|
|
1307
|
+
}
|
|
1308
|
+
if (typeof styleValue === 'string' &&
|
|
1309
|
+
UNSAFE_STYLE_VALUE_PATTERN.test(styleValue)) {
|
|
1310
|
+
addIssue(issues, `${path}.${name}`, 'editor.validation.unsafeStyleValue', 'Style values cannot contain script, import, binding or data URL expressions.');
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
function validateOptionalUrl(node, field, path, issues, kind) {
|
|
1315
|
+
const value = node[field];
|
|
1316
|
+
if (value === undefined || value === null || value === '')
|
|
1317
|
+
return;
|
|
1318
|
+
if (typeof value !== 'string') {
|
|
1319
|
+
addIssue(issues, `${path}.${field}`, 'editor.validation.stringField', 'This field must be a string.');
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
if (!isSafeUrl(value, kind)) {
|
|
1323
|
+
addIssue(issues, `${path}.${field}`, 'editor.validation.unsafeUrl', 'URLs must be relative, http(s), or safe image data URLs.');
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
function validateRequiredUrl(node, field, path, issues, kind) {
|
|
1327
|
+
const value = node[field];
|
|
1328
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
1329
|
+
addIssue(issues, `${path}.${field}`, 'editor.validation.requiredStringField', 'This field must be a non-empty string.');
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
if (!isSafeUrl(value, kind)) {
|
|
1333
|
+
addIssue(issues, `${path}.${field}`, 'editor.validation.unsafeUrl', 'URLs must be relative, http(s), mailto or tel links, or safe image data URLs.');
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
function isSafeUrl(value, kind) {
|
|
1337
|
+
const trimmed = value.trim();
|
|
1338
|
+
if (!trimmed)
|
|
1339
|
+
return true;
|
|
1340
|
+
const lower = trimmed.toLowerCase();
|
|
1341
|
+
if (lower.startsWith('javascript:') ||
|
|
1342
|
+
lower.startsWith('vbscript:') ||
|
|
1343
|
+
lower.startsWith('file:') ||
|
|
1344
|
+
lower.includes('\u0000')) {
|
|
1345
|
+
return false;
|
|
1346
|
+
}
|
|
1347
|
+
if (lower.startsWith('data:')) {
|
|
1348
|
+
return kind === 'image' && /^data:image\/(png|jpeg|jpg|gif|webp);base64,[a-z0-9+/=\s]+$/i.test(trimmed);
|
|
1349
|
+
}
|
|
1350
|
+
if (kind === 'link' && (lower.startsWith('mailto:') || lower.startsWith('tel:') || lower.startsWith('#'))) {
|
|
1351
|
+
return true;
|
|
1352
|
+
}
|
|
1353
|
+
try {
|
|
1354
|
+
const url = new URL(trimmed, 'https://praxis.local');
|
|
1355
|
+
return ['http:', 'https:'].includes(url.protocol);
|
|
1356
|
+
}
|
|
1357
|
+
catch {
|
|
1358
|
+
return false;
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
function validateRequiredString(node, field, path, issues) {
|
|
1362
|
+
if (typeof node[field] !== 'string' || !node[field].trim()) {
|
|
1363
|
+
addIssue(issues, `${path}.${field}`, 'editor.validation.requiredStringField', 'This field must be a non-empty string.');
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
function validateOptionalString(node, field, path, issues) {
|
|
1367
|
+
if (node[field] !== undefined && typeof node[field] !== 'string') {
|
|
1368
|
+
addIssue(issues, `${path}.${field}`, 'editor.validation.stringField', 'This field must be a string.');
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
function validateRequiredExpressionPath(node, field, path, issues) {
|
|
1372
|
+
validateRequiredString(node, field, path, issues);
|
|
1373
|
+
}
|
|
1374
|
+
function validateOptionalExpressionPath(node, field, path, issues) {
|
|
1375
|
+
validateOptionalString(node, field, path, issues);
|
|
1376
|
+
}
|
|
1377
|
+
function validateOptionalClassName(node, field, path, issues, required = false) {
|
|
1378
|
+
const value = node[field];
|
|
1379
|
+
if (value === undefined || value === null || value === '') {
|
|
1380
|
+
if (required) {
|
|
1381
|
+
addIssue(issues, `${path}.${field}`, 'editor.validation.className', 'Class names must be simple CSS class tokens.');
|
|
1382
|
+
}
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
if (typeof value !== 'string' || !CLASS_NAME_PATTERN.test(value.trim())) {
|
|
1386
|
+
addIssue(issues, `${path}.${field}`, 'editor.validation.className', 'Class names must be simple CSS class tokens.');
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
function validateOptionalNumber(node, field, path, issues) {
|
|
1390
|
+
if (node[field] !== undefined && typeof node[field] !== 'number') {
|
|
1391
|
+
addIssue(issues, `${path}.${field}`, 'editor.validation.numberField', 'This field must be a number.');
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
function validateOptionalBoolean(node, field, path, issues) {
|
|
1395
|
+
if (node[field] !== undefined && typeof node[field] !== 'boolean') {
|
|
1396
|
+
addIssue(issues, `${path}.${field}`, 'editor.validation.booleanField', 'This field must be a boolean.');
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
function validateEnum(node, field, allowedValues, path, issues, required) {
|
|
1400
|
+
const value = node[field];
|
|
1401
|
+
if (value === undefined || value === null) {
|
|
1402
|
+
if (required) {
|
|
1403
|
+
addIssue(issues, `${path}.${field}`, 'editor.validation.enumField', 'This field must use one of the supported values.');
|
|
1404
|
+
}
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
if (typeof value !== 'string' || !allowedValues.includes(value)) {
|
|
1408
|
+
addIssue(issues, `${path}.${field}`, 'editor.validation.enumField', 'This field must use one of the supported values.');
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
function addIssue(issues, path, messageKey, fallback) {
|
|
1412
|
+
issues.push({ path, messageKey, fallback });
|
|
1413
|
+
}
|
|
1414
|
+
function isRecord(value) {
|
|
1415
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
const EDITABLE_TOP_LEVEL_NODE_TYPES = [
|
|
1419
|
+
'text',
|
|
1420
|
+
'badge',
|
|
1421
|
+
'icon',
|
|
1422
|
+
'image',
|
|
1423
|
+
'link',
|
|
1424
|
+
'metric',
|
|
1425
|
+
'progress',
|
|
1426
|
+
'compose',
|
|
1427
|
+
'card',
|
|
1428
|
+
'mediaBlock',
|
|
1429
|
+
'timeline',
|
|
1430
|
+
'preset',
|
|
1431
|
+
];
|
|
1432
|
+
const EDITABLE_PRESENTER_NODE_TYPES = [
|
|
1433
|
+
'text',
|
|
1434
|
+
'badge',
|
|
1435
|
+
'icon',
|
|
1436
|
+
'image',
|
|
1437
|
+
'link',
|
|
1438
|
+
'metric',
|
|
1439
|
+
'progress',
|
|
1440
|
+
];
|
|
1441
|
+
const EDITABLE_CARD_CONTENT_NODE_TYPES = [
|
|
1442
|
+
...EDITABLE_PRESENTER_NODE_TYPES,
|
|
1443
|
+
'compose',
|
|
1444
|
+
];
|
|
1445
|
+
function createDefaultRichContentNode(type, tx, presetRef) {
|
|
1446
|
+
switch (type) {
|
|
1447
|
+
case 'badge':
|
|
1448
|
+
return { type: 'badge', label: tx('editor.defaultBadge', 'Badge') };
|
|
1449
|
+
case 'icon':
|
|
1450
|
+
return { type: 'icon', icon: 'check_circle' };
|
|
1451
|
+
case 'image':
|
|
1452
|
+
return { type: 'image', src: '/assets/placeholder.png', alt: '' };
|
|
1453
|
+
case 'link':
|
|
1454
|
+
return { type: 'link', label: tx('editor.defaultLink', 'Link'), href: '#' };
|
|
1455
|
+
case 'metric':
|
|
1456
|
+
return {
|
|
1457
|
+
type: 'metric',
|
|
1458
|
+
label: tx('editor.defaultMetric', 'Metric'),
|
|
1459
|
+
valueExpr: 'row.value',
|
|
1460
|
+
};
|
|
1461
|
+
case 'progress':
|
|
1462
|
+
return {
|
|
1463
|
+
type: 'progress',
|
|
1464
|
+
label: tx('editor.defaultProgress', 'Progress'),
|
|
1465
|
+
valueExpr: 'row.progress',
|
|
1466
|
+
max: 100,
|
|
1467
|
+
};
|
|
1468
|
+
case 'compose':
|
|
1469
|
+
return {
|
|
1470
|
+
type: 'compose',
|
|
1471
|
+
direction: 'row',
|
|
1472
|
+
gap: 'md',
|
|
1473
|
+
items: [createDefaultPresenterNode('text', tx)],
|
|
1474
|
+
};
|
|
1475
|
+
case 'card':
|
|
1476
|
+
return {
|
|
1477
|
+
type: 'card',
|
|
1478
|
+
title: tx('editor.defaultCard', 'Card'),
|
|
1479
|
+
content: [
|
|
1480
|
+
{
|
|
1481
|
+
type: 'text',
|
|
1482
|
+
text: tx('editor.defaultCardText', 'Card content'),
|
|
1483
|
+
},
|
|
1484
|
+
],
|
|
1485
|
+
};
|
|
1486
|
+
case 'mediaBlock':
|
|
1487
|
+
return {
|
|
1488
|
+
type: 'mediaBlock',
|
|
1489
|
+
avatar: {
|
|
1490
|
+
type: 'avatar',
|
|
1491
|
+
name: tx('editor.defaultAvatarName', 'Person'),
|
|
1492
|
+
},
|
|
1493
|
+
title: {
|
|
1494
|
+
type: 'text',
|
|
1495
|
+
text: tx('editor.defaultMediaTitle', 'Media title'),
|
|
1496
|
+
},
|
|
1497
|
+
subtitle: {
|
|
1498
|
+
type: 'text',
|
|
1499
|
+
text: tx('editor.defaultMediaSubtitle', 'Media subtitle'),
|
|
1500
|
+
},
|
|
1501
|
+
};
|
|
1502
|
+
case 'timeline':
|
|
1503
|
+
return {
|
|
1504
|
+
type: 'timeline',
|
|
1505
|
+
title: tx('editor.defaultTimeline', 'Timeline'),
|
|
1506
|
+
emptyText: tx('editor.defaultTimelineEmpty', 'No events.'),
|
|
1507
|
+
items: [createDefaultTimelineItem(tx)],
|
|
1508
|
+
};
|
|
1509
|
+
case 'preset':
|
|
1510
|
+
return {
|
|
1511
|
+
type: 'preset',
|
|
1512
|
+
ref: { ...presetRef },
|
|
1513
|
+
};
|
|
1514
|
+
case 'text':
|
|
1515
|
+
default:
|
|
1516
|
+
return { type: 'text', text: tx('editor.defaultText', 'Text') };
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
function createDefaultPresenterNode(type, tx) {
|
|
1520
|
+
switch (type) {
|
|
1521
|
+
case 'badge':
|
|
1522
|
+
return { type: 'badge', label: tx('editor.defaultBadge', 'Badge') };
|
|
1523
|
+
case 'icon':
|
|
1524
|
+
return { type: 'icon', icon: 'check_circle' };
|
|
1525
|
+
case 'image':
|
|
1526
|
+
return { type: 'image', src: '/assets/placeholder.png', alt: '' };
|
|
1527
|
+
case 'link':
|
|
1528
|
+
return { type: 'link', label: tx('editor.defaultLink', 'Link'), href: '#' };
|
|
1529
|
+
case 'metric':
|
|
1530
|
+
return {
|
|
1531
|
+
type: 'metric',
|
|
1532
|
+
label: tx('editor.defaultMetric', 'Metric'),
|
|
1533
|
+
valueExpr: 'row.value',
|
|
1534
|
+
};
|
|
1535
|
+
case 'progress':
|
|
1536
|
+
return {
|
|
1537
|
+
type: 'progress',
|
|
1538
|
+
label: tx('editor.defaultProgress', 'Progress'),
|
|
1539
|
+
valueExpr: 'row.progress',
|
|
1540
|
+
max: 100,
|
|
1541
|
+
};
|
|
1542
|
+
case 'text':
|
|
1543
|
+
default:
|
|
1544
|
+
return { type: 'text', text: tx('editor.defaultText', 'Text') };
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
function createDefaultTimelineItem(tx) {
|
|
1548
|
+
return {
|
|
1549
|
+
title: tx('editor.defaultTimelineItem', 'Timeline item'),
|
|
1550
|
+
subtitle: tx('editor.defaultTimelineItemSubtitle', 'Item details'),
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
function getPresetOptionValue(ref) {
|
|
1554
|
+
return `${ref.namespace}::${ref.presetId}::${ref.version ?? ''}`;
|
|
1555
|
+
}
|
|
1556
|
+
function getStringField(node, field) {
|
|
1557
|
+
const value = node[field];
|
|
1558
|
+
return typeof value === 'string' ? value : '';
|
|
1559
|
+
}
|
|
1560
|
+
function setStringOnNode(node, field, value) {
|
|
1561
|
+
const record = node;
|
|
1562
|
+
const trimmedValue = value.trim();
|
|
1563
|
+
if (trimmedValue) {
|
|
1564
|
+
record[field] = value;
|
|
1565
|
+
}
|
|
1566
|
+
else {
|
|
1567
|
+
delete record[field];
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
function getNumberField(node, field) {
|
|
1571
|
+
const value = node[field];
|
|
1572
|
+
return typeof value === 'number' ? value : null;
|
|
1573
|
+
}
|
|
1574
|
+
function setNumberOnNode(node, field, value) {
|
|
1575
|
+
const record = node;
|
|
1576
|
+
const normalized = typeof value === 'number'
|
|
1577
|
+
? value
|
|
1578
|
+
: value === '' || value == null
|
|
1579
|
+
? null
|
|
1580
|
+
: Number(value);
|
|
1581
|
+
if (normalized == null || Number.isNaN(normalized)) {
|
|
1582
|
+
delete record[field];
|
|
1583
|
+
}
|
|
1584
|
+
else {
|
|
1585
|
+
record[field] = normalized;
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
function parseSimpleVisibleWhen(expression) {
|
|
1589
|
+
if (!expression || typeof expression !== 'object' || Array.isArray(expression)) {
|
|
1590
|
+
return null;
|
|
1591
|
+
}
|
|
1592
|
+
const equality = expression['==='];
|
|
1593
|
+
if (!Array.isArray(equality) || equality.length !== 2) {
|
|
1594
|
+
return null;
|
|
1595
|
+
}
|
|
1596
|
+
const [left, right] = equality;
|
|
1597
|
+
if (!left || typeof left !== 'object' || Array.isArray(left)) {
|
|
1598
|
+
return null;
|
|
1599
|
+
}
|
|
1600
|
+
const path = left['var'];
|
|
1601
|
+
return typeof path === 'string'
|
|
1602
|
+
? { path, value: stringifyScalar(right) }
|
|
1603
|
+
: null;
|
|
1604
|
+
}
|
|
1605
|
+
function setSimpleVisibleWhen(node, path, value) {
|
|
1606
|
+
const record = node;
|
|
1607
|
+
const trimmedPath = path.trim();
|
|
1608
|
+
if (!trimmedPath) {
|
|
1609
|
+
delete record['visibleWhen'];
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
record['visibleWhen'] = {
|
|
1613
|
+
'===': [{ var: trimmedPath }, parseScalar(value)],
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
function getFirstStyleName(node) {
|
|
1617
|
+
return Object.keys(node.style ?? {})[0] ?? '';
|
|
1618
|
+
}
|
|
1619
|
+
function getFirstStyleValue(node) {
|
|
1620
|
+
const styleName = getFirstStyleName(node);
|
|
1621
|
+
if (!styleName)
|
|
1622
|
+
return '';
|
|
1623
|
+
const value = node.style?.[styleName];
|
|
1624
|
+
return value == null ? '' : String(value);
|
|
1625
|
+
}
|
|
1626
|
+
function setStyleEntry(node, propertyName, propertyValue) {
|
|
1627
|
+
const record = node;
|
|
1628
|
+
const trimmedName = propertyName.trim();
|
|
1629
|
+
const trimmedValue = propertyValue.trim();
|
|
1630
|
+
if (!trimmedName) {
|
|
1631
|
+
delete record['style'];
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
record['style'] = { [trimmedName]: trimmedValue ? propertyValue : '' };
|
|
1635
|
+
}
|
|
1636
|
+
function parseScalar(value) {
|
|
1637
|
+
const trimmed = value.trim();
|
|
1638
|
+
if (trimmed === 'true')
|
|
1639
|
+
return true;
|
|
1640
|
+
if (trimmed === 'false')
|
|
1641
|
+
return false;
|
|
1642
|
+
if (trimmed !== '' && !Number.isNaN(Number(trimmed))) {
|
|
1643
|
+
return Number(trimmed);
|
|
1644
|
+
}
|
|
1645
|
+
return value;
|
|
1646
|
+
}
|
|
1647
|
+
function stringifyScalar(value) {
|
|
1648
|
+
if (typeof value === 'string')
|
|
1649
|
+
return value;
|
|
1650
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
1651
|
+
return String(value);
|
|
1652
|
+
}
|
|
1653
|
+
return '';
|
|
1654
|
+
}
|
|
1655
|
+
|
|
699
1656
|
const EMPTY_DOCUMENT = {
|
|
700
1657
|
kind: 'praxis.rich-content',
|
|
701
1658
|
version: '1.0.0',
|
|
@@ -704,6 +1661,7 @@ const EMPTY_DOCUMENT = {
|
|
|
704
1661
|
class PraxisRichContentConfigEditor {
|
|
705
1662
|
cdr = inject(ChangeDetectorRef);
|
|
706
1663
|
i18n = inject(PraxisI18nService);
|
|
1664
|
+
presetRegistry = inject(RichContentPresetRegistryService);
|
|
707
1665
|
injectedData = inject(SETTINGS_PANEL_DATA, {
|
|
708
1666
|
optional: true,
|
|
709
1667
|
});
|
|
@@ -711,12 +1669,22 @@ class PraxisRichContentConfigEditor {
|
|
|
711
1669
|
isDirty$ = new BehaviorSubject(false);
|
|
712
1670
|
isValid$ = new BehaviorSubject(true);
|
|
713
1671
|
isBusy$ = new BehaviorSubject(false);
|
|
1672
|
+
editableNodeTypes = EDITABLE_TOP_LEVEL_NODE_TYPES;
|
|
1673
|
+
presenterNodeTypes = EDITABLE_PRESENTER_NODE_TYPES;
|
|
1674
|
+
cardContentNodeTypes = EDITABLE_CARD_CONTENT_NODE_TYPES;
|
|
1675
|
+
presetOptions = this.presetRegistry.list();
|
|
714
1676
|
documentJson = '';
|
|
715
1677
|
layout = 'block';
|
|
716
1678
|
rootClassName = '';
|
|
1679
|
+
newNodeType = 'text';
|
|
717
1680
|
errorMessage = '';
|
|
718
1681
|
valid = true;
|
|
1682
|
+
parsedDocument = null;
|
|
1683
|
+
validationIssues = [];
|
|
1684
|
+
nodeCount = 0;
|
|
1685
|
+
nodeTypeSummary = '';
|
|
719
1686
|
initialInputs = this.normalizeInputs(this.inputs);
|
|
1687
|
+
pendingRemoval = null;
|
|
720
1688
|
ngOnChanges(changes) {
|
|
721
1689
|
if (changes['inputs']) {
|
|
722
1690
|
this.load(this.normalizeInputs(this.inputs), false);
|
|
@@ -767,6 +1735,236 @@ class PraxisRichContentConfigEditor {
|
|
|
767
1735
|
this.parseDocument();
|
|
768
1736
|
this.markDirty();
|
|
769
1737
|
}
|
|
1738
|
+
addTopLevelNode() {
|
|
1739
|
+
const document = this.ensureEditableDocument();
|
|
1740
|
+
document.nodes.push(this.createDefaultNode(this.newNodeType));
|
|
1741
|
+
this.syncStructuredDocumentChange();
|
|
1742
|
+
}
|
|
1743
|
+
removeTopLevelNode(index) {
|
|
1744
|
+
const document = this.ensureEditableDocument();
|
|
1745
|
+
document.nodes.splice(index, 1);
|
|
1746
|
+
this.syncStructuredDocumentChange();
|
|
1747
|
+
}
|
|
1748
|
+
moveTopLevelNode(index, delta) {
|
|
1749
|
+
const document = this.ensureEditableDocument();
|
|
1750
|
+
const nextIndex = index + delta;
|
|
1751
|
+
if (nextIndex < 0 || nextIndex >= document.nodes.length) {
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
const [node] = document.nodes.splice(index, 1);
|
|
1755
|
+
document.nodes.splice(nextIndex, 0, node);
|
|
1756
|
+
this.syncStructuredDocumentChange();
|
|
1757
|
+
}
|
|
1758
|
+
changeTopLevelNodeType(index, nextType) {
|
|
1759
|
+
const document = this.ensureEditableDocument();
|
|
1760
|
+
if (!this.isEditableNodeType(nextType)) {
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
document.nodes[index] = this.createDefaultNode(nextType);
|
|
1764
|
+
this.syncStructuredDocumentChange();
|
|
1765
|
+
}
|
|
1766
|
+
getStringField(node, field) {
|
|
1767
|
+
return getStringField(node, field);
|
|
1768
|
+
}
|
|
1769
|
+
setStringField(index, field, value) {
|
|
1770
|
+
const node = this.ensureEditableDocument().nodes[index];
|
|
1771
|
+
setStringOnNode(node, field, value);
|
|
1772
|
+
this.syncStructuredDocumentChange();
|
|
1773
|
+
}
|
|
1774
|
+
getNumberField(node, field) {
|
|
1775
|
+
return getNumberField(node, field);
|
|
1776
|
+
}
|
|
1777
|
+
setNumberField(index, field, value) {
|
|
1778
|
+
const node = this.ensureEditableDocument().nodes[index];
|
|
1779
|
+
setNumberOnNode(node, field, value);
|
|
1780
|
+
this.syncStructuredDocumentChange();
|
|
1781
|
+
}
|
|
1782
|
+
getVisibleWhenPath(node) {
|
|
1783
|
+
return parseSimpleVisibleWhen(node.visibleWhen)?.path ?? '';
|
|
1784
|
+
}
|
|
1785
|
+
getVisibleWhenValue(node) {
|
|
1786
|
+
return parseSimpleVisibleWhen(node.visibleWhen)?.value ?? '';
|
|
1787
|
+
}
|
|
1788
|
+
setVisibleWhenPath(index, path) {
|
|
1789
|
+
const node = this.ensureEditableDocument().nodes[index];
|
|
1790
|
+
setSimpleVisibleWhen(node, path, this.getVisibleWhenValue(node));
|
|
1791
|
+
this.syncStructuredDocumentChange();
|
|
1792
|
+
}
|
|
1793
|
+
setVisibleWhenValue(index, value) {
|
|
1794
|
+
const node = this.ensureEditableDocument().nodes[index];
|
|
1795
|
+
setSimpleVisibleWhen(node, this.getVisibleWhenPath(node), value);
|
|
1796
|
+
this.syncStructuredDocumentChange();
|
|
1797
|
+
}
|
|
1798
|
+
getFirstStyleName(node) {
|
|
1799
|
+
return getFirstStyleName(node);
|
|
1800
|
+
}
|
|
1801
|
+
getFirstStyleValue(node) {
|
|
1802
|
+
return getFirstStyleValue(node);
|
|
1803
|
+
}
|
|
1804
|
+
setFirstStyleName(index, propertyName) {
|
|
1805
|
+
const node = this.ensureEditableDocument().nodes[index];
|
|
1806
|
+
setStyleEntry(node, propertyName, this.getFirstStyleValue(node));
|
|
1807
|
+
this.syncStructuredDocumentChange();
|
|
1808
|
+
}
|
|
1809
|
+
setFirstStyleValue(index, propertyValue) {
|
|
1810
|
+
const node = this.ensureEditableDocument().nodes[index];
|
|
1811
|
+
setStyleEntry(node, this.getFirstStyleName(node), propertyValue);
|
|
1812
|
+
this.syncStructuredDocumentChange();
|
|
1813
|
+
}
|
|
1814
|
+
requestTopLevelNodeRemoval(index) {
|
|
1815
|
+
this.pendingRemoval = { scope: 'nodes', index };
|
|
1816
|
+
}
|
|
1817
|
+
confirmTopLevelNodeRemoval(index) {
|
|
1818
|
+
this.removeTopLevelNode(index);
|
|
1819
|
+
this.cancelRemoval();
|
|
1820
|
+
}
|
|
1821
|
+
requestNestedRemoval(scope, index) {
|
|
1822
|
+
this.pendingRemoval = { scope, index };
|
|
1823
|
+
}
|
|
1824
|
+
cancelRemoval() {
|
|
1825
|
+
this.pendingRemoval = null;
|
|
1826
|
+
}
|
|
1827
|
+
isRemovalPending(scope, index) {
|
|
1828
|
+
return this.pendingRemoval?.scope === scope && this.pendingRemoval.index === index;
|
|
1829
|
+
}
|
|
1830
|
+
addCardContentNode(cardIndex) {
|
|
1831
|
+
const card = this.ensureCardNode(cardIndex);
|
|
1832
|
+
card.content.push(createDefaultPresenterNode('text', this.tx.bind(this)));
|
|
1833
|
+
this.syncStructuredDocumentChange();
|
|
1834
|
+
}
|
|
1835
|
+
setCardContentStringField(cardIndex, contentIndex, field, value) {
|
|
1836
|
+
const node = this.ensureCardNode(cardIndex).content[contentIndex];
|
|
1837
|
+
setStringOnNode(node, field, value);
|
|
1838
|
+
this.syncStructuredDocumentChange();
|
|
1839
|
+
}
|
|
1840
|
+
setCardContentNumberField(cardIndex, contentIndex, field, value) {
|
|
1841
|
+
const node = this.ensureCardNode(cardIndex).content[contentIndex];
|
|
1842
|
+
setNumberOnNode(node, field, value);
|
|
1843
|
+
this.syncStructuredDocumentChange();
|
|
1844
|
+
}
|
|
1845
|
+
changeCardContentType(cardIndex, contentIndex, type) {
|
|
1846
|
+
if (!this.isPresenterNodeType(type))
|
|
1847
|
+
return;
|
|
1848
|
+
this.ensureCardNode(cardIndex).content[contentIndex] =
|
|
1849
|
+
createDefaultPresenterNode(type, this.tx.bind(this));
|
|
1850
|
+
this.syncStructuredDocumentChange();
|
|
1851
|
+
}
|
|
1852
|
+
confirmCardContentRemoval(cardIndex, contentIndex) {
|
|
1853
|
+
this.ensureCardNode(cardIndex).content.splice(contentIndex, 1);
|
|
1854
|
+
this.cancelRemoval();
|
|
1855
|
+
this.syncStructuredDocumentChange();
|
|
1856
|
+
}
|
|
1857
|
+
addComposeItem(composeIndex) {
|
|
1858
|
+
const compose = this.ensureComposeNode(composeIndex);
|
|
1859
|
+
compose.items.push(createDefaultPresenterNode('text', this.tx.bind(this)));
|
|
1860
|
+
this.syncStructuredDocumentChange();
|
|
1861
|
+
}
|
|
1862
|
+
setComposeItemStringField(composeIndex, itemIndex, field, value) {
|
|
1863
|
+
const node = this.ensureComposeNode(composeIndex).items[itemIndex];
|
|
1864
|
+
setStringOnNode(node, field, value);
|
|
1865
|
+
this.syncStructuredDocumentChange();
|
|
1866
|
+
}
|
|
1867
|
+
setComposeItemNumberField(composeIndex, itemIndex, field, value) {
|
|
1868
|
+
const node = this.ensureComposeNode(composeIndex).items[itemIndex];
|
|
1869
|
+
setNumberOnNode(node, field, value);
|
|
1870
|
+
this.syncStructuredDocumentChange();
|
|
1871
|
+
}
|
|
1872
|
+
changeComposeItemType(composeIndex, itemIndex, type) {
|
|
1873
|
+
if (!this.isPresenterNodeType(type))
|
|
1874
|
+
return;
|
|
1875
|
+
this.ensureComposeNode(composeIndex).items[itemIndex] =
|
|
1876
|
+
createDefaultPresenterNode(type, this.tx.bind(this));
|
|
1877
|
+
this.syncStructuredDocumentChange();
|
|
1878
|
+
}
|
|
1879
|
+
confirmComposeItemRemoval(composeIndex, itemIndex) {
|
|
1880
|
+
this.ensureComposeNode(composeIndex).items.splice(itemIndex, 1);
|
|
1881
|
+
this.cancelRemoval();
|
|
1882
|
+
this.syncStructuredDocumentChange();
|
|
1883
|
+
}
|
|
1884
|
+
isPresenterNode(node) {
|
|
1885
|
+
return this.isPresenterNodeType(node.type);
|
|
1886
|
+
}
|
|
1887
|
+
isCardContentEditable(type) {
|
|
1888
|
+
return this.isPresenterNodeType(type);
|
|
1889
|
+
}
|
|
1890
|
+
setMediaBlockAvatarField(index, field, value) {
|
|
1891
|
+
const avatar = this.ensureMediaBlockAvatar(this.ensureMediaBlockNode(index));
|
|
1892
|
+
setStringOnNode(avatar, field, value);
|
|
1893
|
+
this.syncStructuredDocumentChange();
|
|
1894
|
+
}
|
|
1895
|
+
getMediaBlockAvatarField(index, field) {
|
|
1896
|
+
const node = this.ensureEditableDocument().nodes[index];
|
|
1897
|
+
if (node.type !== 'mediaBlock' || !node.avatar)
|
|
1898
|
+
return '';
|
|
1899
|
+
return getStringField(node.avatar, field);
|
|
1900
|
+
}
|
|
1901
|
+
setMediaBlockTextField(index, field, value) {
|
|
1902
|
+
const mediaBlock = this.ensureMediaBlockNode(index);
|
|
1903
|
+
const record = mediaBlock;
|
|
1904
|
+
if (value.trim()) {
|
|
1905
|
+
record[field] = { type: 'text', text: value };
|
|
1906
|
+
}
|
|
1907
|
+
else {
|
|
1908
|
+
delete record[field];
|
|
1909
|
+
}
|
|
1910
|
+
this.syncStructuredDocumentChange();
|
|
1911
|
+
}
|
|
1912
|
+
getMediaBlockTextField(index, field) {
|
|
1913
|
+
const node = this.ensureEditableDocument().nodes[index];
|
|
1914
|
+
if (node.type !== 'mediaBlock')
|
|
1915
|
+
return '';
|
|
1916
|
+
const value = node[field];
|
|
1917
|
+
return value?.type === 'text' ? getStringField(value, 'text') : '';
|
|
1918
|
+
}
|
|
1919
|
+
addTimelineItem(index) {
|
|
1920
|
+
this.ensureTimelineNode(index).items.push(createDefaultTimelineItem(this.tx.bind(this)));
|
|
1921
|
+
this.syncStructuredDocumentChange();
|
|
1922
|
+
}
|
|
1923
|
+
setTimelineItemField(index, itemIndex, field, value) {
|
|
1924
|
+
const item = this.ensureTimelineNode(index).items[itemIndex];
|
|
1925
|
+
if (value.trim()) {
|
|
1926
|
+
item[field] = value;
|
|
1927
|
+
}
|
|
1928
|
+
else {
|
|
1929
|
+
delete item[field];
|
|
1930
|
+
}
|
|
1931
|
+
this.syncStructuredDocumentChange();
|
|
1932
|
+
}
|
|
1933
|
+
getTimelineItemField(index, itemIndex, field) {
|
|
1934
|
+
const item = this.ensureTimelineNode(index).items[itemIndex];
|
|
1935
|
+
const value = item?.[field];
|
|
1936
|
+
return typeof value === 'string' ? value : '';
|
|
1937
|
+
}
|
|
1938
|
+
confirmTimelineItemRemoval(index, itemIndex) {
|
|
1939
|
+
this.ensureTimelineNode(index).items.splice(itemIndex, 1);
|
|
1940
|
+
this.cancelRemoval();
|
|
1941
|
+
this.syncStructuredDocumentChange();
|
|
1942
|
+
}
|
|
1943
|
+
setPresetSelection(index, optionValue) {
|
|
1944
|
+
const preset = this.presetOptions.find((candidate) => getPresetOptionValue(candidate.ref) === optionValue);
|
|
1945
|
+
const node = this.ensureEditableDocument().nodes[index];
|
|
1946
|
+
if (!preset || node.type !== 'preset')
|
|
1947
|
+
return;
|
|
1948
|
+
node.ref = { ...preset.ref };
|
|
1949
|
+
this.syncStructuredDocumentChange();
|
|
1950
|
+
}
|
|
1951
|
+
getPresetSelection(node) {
|
|
1952
|
+
return node.type === 'preset' ? getPresetOptionValue(node.ref) : '';
|
|
1953
|
+
}
|
|
1954
|
+
getPresetOptionValue(ref) {
|
|
1955
|
+
return getPresetOptionValue(ref);
|
|
1956
|
+
}
|
|
1957
|
+
getIssueMessages(path) {
|
|
1958
|
+
return this.validationIssues
|
|
1959
|
+
.filter((issue) => issue.path === path || issue.path.startsWith(`${path}.`))
|
|
1960
|
+
.map((issue) => this.tx(issue.messageKey, issue.fallback));
|
|
1961
|
+
}
|
|
1962
|
+
isEditableNodeType(type) {
|
|
1963
|
+
return EDITABLE_TOP_LEVEL_NODE_TYPES.includes(type);
|
|
1964
|
+
}
|
|
1965
|
+
isPresenterNodeType(type) {
|
|
1966
|
+
return EDITABLE_PRESENTER_NODE_TYPES.includes(type);
|
|
1967
|
+
}
|
|
770
1968
|
load(inputs, dirty) {
|
|
771
1969
|
const document = inputs.document ?? EMPTY_DOCUMENT;
|
|
772
1970
|
const layout = inputs.layout === 'inline' ? 'inline' : 'block';
|
|
@@ -786,30 +1984,129 @@ class PraxisRichContentConfigEditor {
|
|
|
786
1984
|
normalizeInputs(inputs) {
|
|
787
1985
|
return this.cloneInputs(inputs ?? {});
|
|
788
1986
|
}
|
|
1987
|
+
ensureEditableDocument() {
|
|
1988
|
+
if (this.parsedDocument) {
|
|
1989
|
+
return this.parsedDocument;
|
|
1990
|
+
}
|
|
1991
|
+
this.parsedDocument = this.cloneValue(EMPTY_DOCUMENT);
|
|
1992
|
+
return this.parsedDocument;
|
|
1993
|
+
}
|
|
1994
|
+
syncStructuredDocumentChange() {
|
|
1995
|
+
if (!this.parsedDocument) {
|
|
1996
|
+
return;
|
|
1997
|
+
}
|
|
1998
|
+
this.documentJson = this.stringify(this.parsedDocument);
|
|
1999
|
+
this.parseDocument();
|
|
2000
|
+
this.markDirty();
|
|
2001
|
+
}
|
|
2002
|
+
createDefaultNode(type) {
|
|
2003
|
+
return createDefaultRichContentNode(type, this.tx.bind(this), this.getDefaultPresetRef());
|
|
2004
|
+
}
|
|
2005
|
+
getDefaultPresetRef() {
|
|
2006
|
+
return (this.presetOptions[0]?.ref ?? {
|
|
2007
|
+
kind: 'rich-block',
|
|
2008
|
+
namespace: 'praxis.rich-content',
|
|
2009
|
+
presetId: 'default',
|
|
2010
|
+
});
|
|
2011
|
+
}
|
|
2012
|
+
ensureCardNode(index) {
|
|
2013
|
+
const node = this.ensureEditableDocument().nodes[index];
|
|
2014
|
+
if (node.type !== 'card') {
|
|
2015
|
+
throw new Error('Expected card node.');
|
|
2016
|
+
}
|
|
2017
|
+
return node;
|
|
2018
|
+
}
|
|
2019
|
+
ensureComposeNode(index) {
|
|
2020
|
+
const node = this.ensureEditableDocument().nodes[index];
|
|
2021
|
+
if (node.type !== 'compose') {
|
|
2022
|
+
throw new Error('Expected compose node.');
|
|
2023
|
+
}
|
|
2024
|
+
return node;
|
|
2025
|
+
}
|
|
2026
|
+
ensureMediaBlockNode(index) {
|
|
2027
|
+
const node = this.ensureEditableDocument().nodes[index];
|
|
2028
|
+
if (node.type !== 'mediaBlock') {
|
|
2029
|
+
throw new Error('Expected mediaBlock node.');
|
|
2030
|
+
}
|
|
2031
|
+
return node;
|
|
2032
|
+
}
|
|
2033
|
+
ensureMediaBlockAvatar(node) {
|
|
2034
|
+
if (!node.avatar || node.avatar.type !== 'avatar') {
|
|
2035
|
+
node.avatar = { type: 'avatar' };
|
|
2036
|
+
}
|
|
2037
|
+
return node.avatar;
|
|
2038
|
+
}
|
|
2039
|
+
ensureTimelineNode(index) {
|
|
2040
|
+
const node = this.ensureEditableDocument().nodes[index];
|
|
2041
|
+
if (node.type !== 'timeline') {
|
|
2042
|
+
throw new Error('Expected timeline node.');
|
|
2043
|
+
}
|
|
2044
|
+
return node;
|
|
2045
|
+
}
|
|
789
2046
|
parseDocument() {
|
|
790
2047
|
try {
|
|
791
2048
|
const parsed = JSON.parse(this.documentJson);
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
2049
|
+
const validation = validateRichContentDocument(parsed);
|
|
2050
|
+
this.validationIssues = validation.issues;
|
|
2051
|
+
if (!validation.valid) {
|
|
2052
|
+
this.setInvalidState(this.tx('editor.validation.documentShape', 'The document must be a valid RichContentDocument.'));
|
|
2053
|
+
return null;
|
|
797
2054
|
}
|
|
2055
|
+
const document = parsed;
|
|
798
2056
|
this.valid = true;
|
|
799
2057
|
this.errorMessage = '';
|
|
800
2058
|
this.isValid$.next(true);
|
|
801
|
-
|
|
2059
|
+
this.parsedDocument = document;
|
|
2060
|
+
this.updateDocumentOverview(document);
|
|
2061
|
+
return document;
|
|
802
2062
|
}
|
|
803
2063
|
catch (error) {
|
|
804
|
-
this.
|
|
805
|
-
this.
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
: this.tx('editor.validation.invalidJson', 'Invalid JSON document.');
|
|
809
|
-
this.isValid$.next(false);
|
|
2064
|
+
this.validationIssues = [];
|
|
2065
|
+
this.setInvalidState(error instanceof SyntaxError
|
|
2066
|
+
? this.tx('editor.validation.invalidJson', 'Invalid JSON document.')
|
|
2067
|
+
: this.tx('editor.validation.invalidDocument', 'Invalid rich content document.'));
|
|
810
2068
|
return null;
|
|
811
2069
|
}
|
|
812
2070
|
}
|
|
2071
|
+
setInvalidState(message) {
|
|
2072
|
+
this.valid = false;
|
|
2073
|
+
this.errorMessage = message;
|
|
2074
|
+
this.isValid$.next(false);
|
|
2075
|
+
this.parsedDocument = null;
|
|
2076
|
+
this.nodeCount = 0;
|
|
2077
|
+
this.nodeTypeSummary = '';
|
|
2078
|
+
}
|
|
2079
|
+
updateDocumentOverview(document) {
|
|
2080
|
+
const typeCounts = new Map();
|
|
2081
|
+
let count = 0;
|
|
2082
|
+
const visit = (nodes) => {
|
|
2083
|
+
for (const node of nodes) {
|
|
2084
|
+
count += 1;
|
|
2085
|
+
typeCounts.set(node.type, (typeCounts.get(node.type) ?? 0) + 1);
|
|
2086
|
+
if (node.type === 'compose') {
|
|
2087
|
+
visit(node.items);
|
|
2088
|
+
}
|
|
2089
|
+
else if (node.type === 'card') {
|
|
2090
|
+
visit(node.content);
|
|
2091
|
+
}
|
|
2092
|
+
else if (node.type === 'mediaBlock') {
|
|
2093
|
+
visit([
|
|
2094
|
+
node.avatar,
|
|
2095
|
+
node.title,
|
|
2096
|
+
node.subtitle,
|
|
2097
|
+
node.meta,
|
|
2098
|
+
...(node.trailing ?? []),
|
|
2099
|
+
].filter(Boolean));
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
};
|
|
2103
|
+
visit(document.nodes);
|
|
2104
|
+
this.nodeCount = count;
|
|
2105
|
+
this.nodeTypeSummary = Array.from(typeCounts.entries())
|
|
2106
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
2107
|
+
.map(([type, total]) => `${type} (${total})`)
|
|
2108
|
+
.join(', ');
|
|
2109
|
+
}
|
|
813
2110
|
matchesInitialState() {
|
|
814
2111
|
const current = this.getSettingsValue();
|
|
815
2112
|
if (!current)
|
|
@@ -888,37 +2185,781 @@ class PraxisRichContentConfigEditor {
|
|
|
888
2185
|
</label>
|
|
889
2186
|
</div>
|
|
890
2187
|
|
|
2188
|
+
<ng-template
|
|
2189
|
+
#presenterFields
|
|
2190
|
+
let-node="node"
|
|
2191
|
+
let-path="path"
|
|
2192
|
+
let-setString="setString"
|
|
2193
|
+
let-setNumber="setNumber"
|
|
2194
|
+
>
|
|
2195
|
+
@switch (node.type) {
|
|
2196
|
+
@case ('text') {
|
|
2197
|
+
<label class="prx-rich-editor__wide-field">
|
|
2198
|
+
<span>{{ tx('editor.field.text', 'Text') }}</span>
|
|
2199
|
+
<textarea
|
|
2200
|
+
[ngModel]="getStringField(node, 'text')"
|
|
2201
|
+
(ngModelChange)="setString('text', $event)"
|
|
2202
|
+
></textarea>
|
|
2203
|
+
</label>
|
|
2204
|
+
}
|
|
2205
|
+
@case ('badge') {
|
|
2206
|
+
<label>
|
|
2207
|
+
<span>{{ tx('editor.field.label', 'Label') }}</span>
|
|
2208
|
+
<input
|
|
2209
|
+
[ngModel]="getStringField(node, 'label')"
|
|
2210
|
+
(ngModelChange)="setString('label', $event)"
|
|
2211
|
+
/>
|
|
2212
|
+
</label>
|
|
2213
|
+
}
|
|
2214
|
+
@case ('icon') {
|
|
2215
|
+
<label>
|
|
2216
|
+
<span>{{ tx('editor.field.icon', 'Icon') }}</span>
|
|
2217
|
+
<input
|
|
2218
|
+
[ngModel]="getStringField(node, 'icon')"
|
|
2219
|
+
(ngModelChange)="setString('icon', $event)"
|
|
2220
|
+
/>
|
|
2221
|
+
</label>
|
|
2222
|
+
}
|
|
2223
|
+
@case ('image') {
|
|
2224
|
+
<label>
|
|
2225
|
+
<span>{{ tx('editor.field.src', 'Image URL') }}</span>
|
|
2226
|
+
<input
|
|
2227
|
+
[ngModel]="getStringField(node, 'src')"
|
|
2228
|
+
(ngModelChange)="setString('src', $event)"
|
|
2229
|
+
/>
|
|
2230
|
+
</label>
|
|
2231
|
+
<label>
|
|
2232
|
+
<span>{{ tx('editor.field.alt', 'Alternative text') }}</span>
|
|
2233
|
+
<input
|
|
2234
|
+
[ngModel]="getStringField(node, 'alt')"
|
|
2235
|
+
(ngModelChange)="setString('alt', $event)"
|
|
2236
|
+
/>
|
|
2237
|
+
</label>
|
|
2238
|
+
}
|
|
2239
|
+
@case ('link') {
|
|
2240
|
+
<label>
|
|
2241
|
+
<span>{{ tx('editor.field.label', 'Label') }}</span>
|
|
2242
|
+
<input
|
|
2243
|
+
[ngModel]="getStringField(node, 'label')"
|
|
2244
|
+
(ngModelChange)="setString('label', $event)"
|
|
2245
|
+
/>
|
|
2246
|
+
</label>
|
|
2247
|
+
<label>
|
|
2248
|
+
<span>{{ tx('editor.field.href', 'Link URL') }}</span>
|
|
2249
|
+
<input
|
|
2250
|
+
[ngModel]="getStringField(node, 'href')"
|
|
2251
|
+
(ngModelChange)="setString('href', $event)"
|
|
2252
|
+
/>
|
|
2253
|
+
</label>
|
|
2254
|
+
}
|
|
2255
|
+
@case ('metric') {
|
|
2256
|
+
<label>
|
|
2257
|
+
<span>{{ tx('editor.field.label', 'Label') }}</span>
|
|
2258
|
+
<input
|
|
2259
|
+
[ngModel]="getStringField(node, 'label')"
|
|
2260
|
+
(ngModelChange)="setString('label', $event)"
|
|
2261
|
+
/>
|
|
2262
|
+
</label>
|
|
2263
|
+
<label>
|
|
2264
|
+
<span>{{ tx('editor.field.valueExpr', 'Value binding') }}</span>
|
|
2265
|
+
<input
|
|
2266
|
+
[ngModel]="getStringField(node, 'valueExpr')"
|
|
2267
|
+
(ngModelChange)="setString('valueExpr', $event)"
|
|
2268
|
+
/>
|
|
2269
|
+
</label>
|
|
2270
|
+
}
|
|
2271
|
+
@case ('progress') {
|
|
2272
|
+
<label>
|
|
2273
|
+
<span>{{ tx('editor.field.label', 'Label') }}</span>
|
|
2274
|
+
<input
|
|
2275
|
+
[ngModel]="getStringField(node, 'label')"
|
|
2276
|
+
(ngModelChange)="setString('label', $event)"
|
|
2277
|
+
/>
|
|
2278
|
+
</label>
|
|
2279
|
+
<label>
|
|
2280
|
+
<span>{{ tx('editor.field.max', 'Maximum') }}</span>
|
|
2281
|
+
<input
|
|
2282
|
+
type="number"
|
|
2283
|
+
[ngModel]="getNumberField(node, 'max')"
|
|
2284
|
+
(ngModelChange)="setNumber('max', $event)"
|
|
2285
|
+
/>
|
|
2286
|
+
</label>
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
@if (getIssueMessages(path).length) {
|
|
2290
|
+
<ul class="prx-rich-editor__field-errors">
|
|
2291
|
+
@for (message of getIssueMessages(path); track message) {
|
|
2292
|
+
<li>{{ message }}</li>
|
|
2293
|
+
}
|
|
2294
|
+
</ul>
|
|
2295
|
+
}
|
|
2296
|
+
</ng-template>
|
|
2297
|
+
|
|
2298
|
+
<section class="prx-rich-editor__blocks">
|
|
2299
|
+
<header class="prx-rich-editor__section-header">
|
|
2300
|
+
<div>
|
|
2301
|
+
<h3>{{ tx('editor.blocks', 'Blocks') }}</h3>
|
|
2302
|
+
<p>
|
|
2303
|
+
{{
|
|
2304
|
+
tx(
|
|
2305
|
+
'editor.blocksHelp',
|
|
2306
|
+
'Create and edit common top-level rich-content blocks. Use JSON for advanced nested structures.'
|
|
2307
|
+
)
|
|
2308
|
+
}}
|
|
2309
|
+
</p>
|
|
2310
|
+
</div>
|
|
2311
|
+
<div class="prx-rich-editor__add-block">
|
|
2312
|
+
<label>
|
|
2313
|
+
<span>{{ tx('editor.blockType', 'Block type') }}</span>
|
|
2314
|
+
<select
|
|
2315
|
+
name="rich-content-new-node-type"
|
|
2316
|
+
[(ngModel)]="newNodeType"
|
|
2317
|
+
data-testid="rich-content-new-node-type"
|
|
2318
|
+
>
|
|
2319
|
+
@for (type of editableNodeTypes; track type) {
|
|
2320
|
+
<option [value]="type">{{ tx('editor.nodeType.' + type, type) }}</option>
|
|
2321
|
+
}
|
|
2322
|
+
</select>
|
|
2323
|
+
</label>
|
|
2324
|
+
<button
|
|
2325
|
+
type="button"
|
|
2326
|
+
(click)="addTopLevelNode()"
|
|
2327
|
+
data-testid="rich-content-add-node"
|
|
2328
|
+
>
|
|
2329
|
+
{{ tx('editor.addBlock', 'Add block') }}
|
|
2330
|
+
</button>
|
|
2331
|
+
</div>
|
|
2332
|
+
</header>
|
|
2333
|
+
|
|
2334
|
+
@if (parsedDocument?.nodes?.length) {
|
|
2335
|
+
<div class="prx-rich-editor__node-list">
|
|
2336
|
+
@for (node of parsedDocument?.nodes ?? []; track node.id ?? $index) {
|
|
2337
|
+
<article
|
|
2338
|
+
class="prx-rich-editor__node-card"
|
|
2339
|
+
[attr.data-rich-editor-node-type]="node.type"
|
|
2340
|
+
>
|
|
2341
|
+
<header class="prx-rich-editor__node-header">
|
|
2342
|
+
<div>
|
|
2343
|
+
<p class="prx-rich-editor__node-eyebrow">
|
|
2344
|
+
{{ tx('editor.block', 'Block') }} {{ $index + 1 }}
|
|
2345
|
+
</p>
|
|
2346
|
+
<h4>{{ tx('editor.nodeType.' + node.type, node.type) }}</h4>
|
|
2347
|
+
</div>
|
|
2348
|
+
<div class="prx-rich-editor__node-actions">
|
|
2349
|
+
<button type="button" (click)="moveTopLevelNode($index, -1)" [disabled]="$index === 0">
|
|
2350
|
+
{{ tx('editor.moveUp', 'Move up') }}
|
|
2351
|
+
</button>
|
|
2352
|
+
<button
|
|
2353
|
+
type="button"
|
|
2354
|
+
(click)="moveTopLevelNode($index, 1)"
|
|
2355
|
+
[disabled]="$index === (parsedDocument?.nodes?.length ?? 0) - 1"
|
|
2356
|
+
>
|
|
2357
|
+
{{ tx('editor.moveDown', 'Move down') }}
|
|
2358
|
+
</button>
|
|
2359
|
+
@if (isRemovalPending('nodes', $index)) {
|
|
2360
|
+
<button type="button" (click)="confirmTopLevelNodeRemoval($index)">
|
|
2361
|
+
{{ tx('editor.confirmRemove', 'Confirm remove') }}
|
|
2362
|
+
</button>
|
|
2363
|
+
<button type="button" (click)="cancelRemoval()">
|
|
2364
|
+
{{ tx('editor.cancelRemove', 'Cancel') }}
|
|
2365
|
+
</button>
|
|
2366
|
+
} @else {
|
|
2367
|
+
<button type="button" (click)="requestTopLevelNodeRemoval($index)">
|
|
2368
|
+
{{ tx('editor.removeBlock', 'Remove') }}
|
|
2369
|
+
</button>
|
|
2370
|
+
}
|
|
2371
|
+
</div>
|
|
2372
|
+
</header>
|
|
2373
|
+
|
|
2374
|
+
@if (getIssueMessages('$.nodes[' + $index + ']').length) {
|
|
2375
|
+
<ul class="prx-rich-editor__field-errors">
|
|
2376
|
+
@for (message of getIssueMessages('$.nodes[' + $index + ']'); track message) {
|
|
2377
|
+
<li>{{ message }}</li>
|
|
2378
|
+
}
|
|
2379
|
+
</ul>
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
<div class="prx-rich-editor__node-grid">
|
|
2383
|
+
<label>
|
|
2384
|
+
<span>{{ tx('editor.blockType', 'Block type') }}</span>
|
|
2385
|
+
<select
|
|
2386
|
+
[ngModel]="isEditableNodeType(node.type) ? node.type : 'text'"
|
|
2387
|
+
(ngModelChange)="changeTopLevelNodeType($index, $event)"
|
|
2388
|
+
[disabled]="!isEditableNodeType(node.type)"
|
|
2389
|
+
>
|
|
2390
|
+
@for (type of editableNodeTypes; track type) {
|
|
2391
|
+
<option [value]="type">{{ tx('editor.nodeType.' + type, type) }}</option>
|
|
2392
|
+
}
|
|
2393
|
+
</select>
|
|
2394
|
+
</label>
|
|
2395
|
+
|
|
2396
|
+
<label>
|
|
2397
|
+
<span>{{ tx('editor.nodeId', 'Stable id') }}</span>
|
|
2398
|
+
<input
|
|
2399
|
+
[ngModel]="getStringField(node, 'id')"
|
|
2400
|
+
(ngModelChange)="setStringField($index, 'id', $event)"
|
|
2401
|
+
[placeholder]="tx('editor.nodeIdPlaceholder', 'Example: hero-title')"
|
|
2402
|
+
/>
|
|
2403
|
+
</label>
|
|
2404
|
+
|
|
2405
|
+
@switch (node.type) {
|
|
2406
|
+
@case ('text') {
|
|
2407
|
+
<label class="prx-rich-editor__wide-field">
|
|
2408
|
+
<span>{{ tx('editor.field.text', 'Text') }}</span>
|
|
2409
|
+
<textarea
|
|
2410
|
+
[ngModel]="getStringField(node, 'text')"
|
|
2411
|
+
(ngModelChange)="setStringField($index, 'text', $event)"
|
|
2412
|
+
></textarea>
|
|
2413
|
+
@if (getIssueMessages('$.nodes[' + $index + '].text').length) {
|
|
2414
|
+
<small class="prx-rich-editor__field-error">
|
|
2415
|
+
{{ getIssueMessages('$.nodes[' + $index + '].text').join(' ') }}
|
|
2416
|
+
</small>
|
|
2417
|
+
}
|
|
2418
|
+
</label>
|
|
2419
|
+
<label>
|
|
2420
|
+
<span>{{ tx('editor.field.textExpr', 'Text binding') }}</span>
|
|
2421
|
+
<input
|
|
2422
|
+
[ngModel]="getStringField(node, 'textExpr')"
|
|
2423
|
+
(ngModelChange)="setStringField($index, 'textExpr', $event)"
|
|
2424
|
+
[placeholder]="tx('editor.placeholder.textExpr', 'row.name')"
|
|
2425
|
+
/>
|
|
2426
|
+
@if (getIssueMessages('$.nodes[' + $index + '].textExpr').length) {
|
|
2427
|
+
<small class="prx-rich-editor__field-error">
|
|
2428
|
+
{{ getIssueMessages('$.nodes[' + $index + '].textExpr').join(' ') }}
|
|
2429
|
+
</small>
|
|
2430
|
+
}
|
|
2431
|
+
</label>
|
|
2432
|
+
}
|
|
2433
|
+
@case ('badge') {
|
|
2434
|
+
<label>
|
|
2435
|
+
<span>{{ tx('editor.field.label', 'Label') }}</span>
|
|
2436
|
+
<input
|
|
2437
|
+
[ngModel]="getStringField(node, 'label')"
|
|
2438
|
+
(ngModelChange)="setStringField($index, 'label', $event)"
|
|
2439
|
+
/>
|
|
2440
|
+
</label>
|
|
2441
|
+
<label>
|
|
2442
|
+
<span>{{ tx('editor.field.labelExpr', 'Label binding') }}</span>
|
|
2443
|
+
<input
|
|
2444
|
+
[ngModel]="getStringField(node, 'labelExpr')"
|
|
2445
|
+
(ngModelChange)="setStringField($index, 'labelExpr', $event)"
|
|
2446
|
+
[placeholder]="tx('editor.placeholder.labelExpr', 'row.status')"
|
|
2447
|
+
/>
|
|
2448
|
+
</label>
|
|
2449
|
+
<label>
|
|
2450
|
+
<span>{{ tx('editor.field.icon', 'Icon') }}</span>
|
|
2451
|
+
<input
|
|
2452
|
+
[ngModel]="getStringField(node, 'icon')"
|
|
2453
|
+
(ngModelChange)="setStringField($index, 'icon', $event)"
|
|
2454
|
+
[placeholder]="tx('editor.placeholder.icon', 'check_circle')"
|
|
2455
|
+
/>
|
|
2456
|
+
</label>
|
|
2457
|
+
}
|
|
2458
|
+
@case ('icon') {
|
|
2459
|
+
<label>
|
|
2460
|
+
<span>{{ tx('editor.field.icon', 'Icon') }}</span>
|
|
2461
|
+
<input
|
|
2462
|
+
[ngModel]="getStringField(node, 'icon')"
|
|
2463
|
+
(ngModelChange)="setStringField($index, 'icon', $event)"
|
|
2464
|
+
[placeholder]="tx('editor.placeholder.icon', 'check_circle')"
|
|
2465
|
+
/>
|
|
2466
|
+
</label>
|
|
2467
|
+
<label>
|
|
2468
|
+
<span>{{ tx('editor.field.ariaLabel', 'Accessible label') }}</span>
|
|
2469
|
+
<input
|
|
2470
|
+
[ngModel]="getStringField(node, 'ariaLabel')"
|
|
2471
|
+
(ngModelChange)="setStringField($index, 'ariaLabel', $event)"
|
|
2472
|
+
/>
|
|
2473
|
+
</label>
|
|
2474
|
+
}
|
|
2475
|
+
@case ('image') {
|
|
2476
|
+
<label>
|
|
2477
|
+
<span>{{ tx('editor.field.src', 'Image URL') }}</span>
|
|
2478
|
+
<input
|
|
2479
|
+
[ngModel]="getStringField(node, 'src')"
|
|
2480
|
+
(ngModelChange)="setStringField($index, 'src', $event)"
|
|
2481
|
+
/>
|
|
2482
|
+
</label>
|
|
2483
|
+
<label>
|
|
2484
|
+
<span>{{ tx('editor.field.alt', 'Alternative text') }}</span>
|
|
2485
|
+
<input
|
|
2486
|
+
[ngModel]="getStringField(node, 'alt')"
|
|
2487
|
+
(ngModelChange)="setStringField($index, 'alt', $event)"
|
|
2488
|
+
/>
|
|
2489
|
+
</label>
|
|
2490
|
+
}
|
|
2491
|
+
@case ('metric') {
|
|
2492
|
+
<label>
|
|
2493
|
+
<span>{{ tx('editor.field.label', 'Label') }}</span>
|
|
2494
|
+
<input
|
|
2495
|
+
[ngModel]="getStringField(node, 'label')"
|
|
2496
|
+
(ngModelChange)="setStringField($index, 'label', $event)"
|
|
2497
|
+
/>
|
|
2498
|
+
</label>
|
|
2499
|
+
<label>
|
|
2500
|
+
<span>{{ tx('editor.field.valueExpr', 'Value binding') }}</span>
|
|
2501
|
+
<input
|
|
2502
|
+
[ngModel]="getStringField(node, 'valueExpr')"
|
|
2503
|
+
(ngModelChange)="setStringField($index, 'valueExpr', $event)"
|
|
2504
|
+
[placeholder]="tx('editor.placeholder.valueExpr', 'row.total')"
|
|
2505
|
+
/>
|
|
2506
|
+
</label>
|
|
2507
|
+
<label>
|
|
2508
|
+
<span>{{ tx('editor.field.captionExpr', 'Caption binding') }}</span>
|
|
2509
|
+
<input
|
|
2510
|
+
[ngModel]="getStringField(node, 'captionExpr')"
|
|
2511
|
+
(ngModelChange)="setStringField($index, 'captionExpr', $event)"
|
|
2512
|
+
[placeholder]="tx('editor.placeholder.captionExpr', 'row.caption')"
|
|
2513
|
+
/>
|
|
2514
|
+
</label>
|
|
2515
|
+
}
|
|
2516
|
+
@case ('progress') {
|
|
2517
|
+
<label>
|
|
2518
|
+
<span>{{ tx('editor.field.label', 'Label') }}</span>
|
|
2519
|
+
<input
|
|
2520
|
+
[ngModel]="getStringField(node, 'label')"
|
|
2521
|
+
(ngModelChange)="setStringField($index, 'label', $event)"
|
|
2522
|
+
/>
|
|
2523
|
+
</label>
|
|
2524
|
+
<label>
|
|
2525
|
+
<span>{{ tx('editor.field.valueExpr', 'Value binding') }}</span>
|
|
2526
|
+
<input
|
|
2527
|
+
[ngModel]="getStringField(node, 'valueExpr')"
|
|
2528
|
+
(ngModelChange)="setStringField($index, 'valueExpr', $event)"
|
|
2529
|
+
[placeholder]="tx('editor.placeholder.progressExpr', 'row.progress')"
|
|
2530
|
+
/>
|
|
2531
|
+
</label>
|
|
2532
|
+
<label>
|
|
2533
|
+
<span>{{ tx('editor.field.max', 'Maximum') }}</span>
|
|
2534
|
+
<input
|
|
2535
|
+
type="number"
|
|
2536
|
+
[ngModel]="getNumberField(node, 'max')"
|
|
2537
|
+
(ngModelChange)="setNumberField($index, 'max', $event)"
|
|
2538
|
+
/>
|
|
2539
|
+
</label>
|
|
2540
|
+
}
|
|
2541
|
+
@case ('compose') {
|
|
2542
|
+
<label>
|
|
2543
|
+
<span>{{ tx('editor.field.direction', 'Direction') }}</span>
|
|
2544
|
+
<select
|
|
2545
|
+
[ngModel]="getStringField(node, 'direction') || 'row'"
|
|
2546
|
+
(ngModelChange)="setStringField($index, 'direction', $event)"
|
|
2547
|
+
>
|
|
2548
|
+
<option value="row">{{ tx('editor.direction.row', 'Row') }}</option>
|
|
2549
|
+
<option value="column">{{ tx('editor.direction.column', 'Column') }}</option>
|
|
2550
|
+
</select>
|
|
2551
|
+
</label>
|
|
2552
|
+
<label>
|
|
2553
|
+
<span>{{ tx('editor.field.gap', 'Gap') }}</span>
|
|
2554
|
+
<select
|
|
2555
|
+
[ngModel]="getStringField(node, 'gap') || 'md'"
|
|
2556
|
+
(ngModelChange)="setStringField($index, 'gap', $event)"
|
|
2557
|
+
>
|
|
2558
|
+
<option value="xs">xs</option>
|
|
2559
|
+
<option value="sm">sm</option>
|
|
2560
|
+
<option value="md">md</option>
|
|
2561
|
+
<option value="lg">lg</option>
|
|
2562
|
+
<option value="xl">xl</option>
|
|
2563
|
+
</select>
|
|
2564
|
+
</label>
|
|
2565
|
+
<div class="prx-rich-editor__nested-editor">
|
|
2566
|
+
<header class="prx-rich-editor__nested-header">
|
|
2567
|
+
<h5>{{ tx('editor.composeItems', 'Compose items') }}</h5>
|
|
2568
|
+
<button type="button" (click)="addComposeItem($index)">
|
|
2569
|
+
{{ tx('editor.addItem', 'Add item') }}
|
|
2570
|
+
</button>
|
|
2571
|
+
</header>
|
|
2572
|
+
@for (item of node.items; track item.id ?? $index; let itemIndex = $index) {
|
|
2573
|
+
<div class="prx-rich-editor__nested-node">
|
|
2574
|
+
<div class="prx-rich-editor__nested-actions">
|
|
2575
|
+
<strong>{{ tx('editor.item', 'Item') }} {{ itemIndex + 1 }}</strong>
|
|
2576
|
+
@if (isRemovalPending('nodes.' + $index + '.items', itemIndex)) {
|
|
2577
|
+
<button type="button" (click)="confirmComposeItemRemoval($index, itemIndex)">
|
|
2578
|
+
{{ tx('editor.confirmRemove', 'Confirm remove') }}
|
|
2579
|
+
</button>
|
|
2580
|
+
<button type="button" (click)="cancelRemoval()">
|
|
2581
|
+
{{ tx('editor.cancelRemove', 'Cancel') }}
|
|
2582
|
+
</button>
|
|
2583
|
+
} @else {
|
|
2584
|
+
<button type="button" (click)="requestNestedRemoval('nodes.' + $index + '.items', itemIndex)">
|
|
2585
|
+
{{ tx('editor.removeBlock', 'Remove') }}
|
|
2586
|
+
</button>
|
|
2587
|
+
}
|
|
2588
|
+
</div>
|
|
2589
|
+
<div class="prx-rich-editor__node-grid">
|
|
2590
|
+
<label>
|
|
2591
|
+
<span>{{ tx('editor.blockType', 'Block type') }}</span>
|
|
2592
|
+
<select
|
|
2593
|
+
[ngModel]="item.type"
|
|
2594
|
+
(ngModelChange)="changeComposeItemType($index, itemIndex, $event)"
|
|
2595
|
+
>
|
|
2596
|
+
@for (type of presenterNodeTypes; track type) {
|
|
2597
|
+
<option [value]="type">{{ tx('editor.nodeType.' + type, type) }}</option>
|
|
2598
|
+
}
|
|
2599
|
+
</select>
|
|
2600
|
+
</label>
|
|
2601
|
+
<ng-container
|
|
2602
|
+
*ngTemplateOutlet="presenterFields; context: {
|
|
2603
|
+
node: item,
|
|
2604
|
+
path: '$.nodes[' + $index + '].items[' + itemIndex + ']',
|
|
2605
|
+
setString: setComposeItemStringField.bind(this, $index, itemIndex),
|
|
2606
|
+
setNumber: setComposeItemNumberField.bind(this, $index, itemIndex)
|
|
2607
|
+
}"
|
|
2608
|
+
></ng-container>
|
|
2609
|
+
</div>
|
|
2610
|
+
</div>
|
|
2611
|
+
}
|
|
2612
|
+
</div>
|
|
2613
|
+
}
|
|
2614
|
+
@case ('card') {
|
|
2615
|
+
<label>
|
|
2616
|
+
<span>{{ tx('editor.field.title', 'Title') }}</span>
|
|
2617
|
+
<input
|
|
2618
|
+
[ngModel]="getStringField(node, 'title')"
|
|
2619
|
+
(ngModelChange)="setStringField($index, 'title', $event)"
|
|
2620
|
+
/>
|
|
2621
|
+
</label>
|
|
2622
|
+
<label>
|
|
2623
|
+
<span>{{ tx('editor.field.subtitle', 'Subtitle') }}</span>
|
|
2624
|
+
<input
|
|
2625
|
+
[ngModel]="getStringField(node, 'subtitle')"
|
|
2626
|
+
(ngModelChange)="setStringField($index, 'subtitle', $event)"
|
|
2627
|
+
/>
|
|
2628
|
+
</label>
|
|
2629
|
+
<div class="prx-rich-editor__nested-editor">
|
|
2630
|
+
<header class="prx-rich-editor__nested-header">
|
|
2631
|
+
<h5>{{ tx('editor.cardContent', 'Card content') }}</h5>
|
|
2632
|
+
<button type="button" (click)="addCardContentNode($index)">
|
|
2633
|
+
{{ tx('editor.addItem', 'Add item') }}
|
|
2634
|
+
</button>
|
|
2635
|
+
</header>
|
|
2636
|
+
@for (item of node.content; track item.id ?? $index; let itemIndex = $index) {
|
|
2637
|
+
<div class="prx-rich-editor__nested-node">
|
|
2638
|
+
<div class="prx-rich-editor__nested-actions">
|
|
2639
|
+
<strong>{{ tx('editor.item', 'Item') }} {{ itemIndex + 1 }}</strong>
|
|
2640
|
+
@if (isRemovalPending('nodes.' + $index + '.content', itemIndex)) {
|
|
2641
|
+
<button type="button" (click)="confirmCardContentRemoval($index, itemIndex)">
|
|
2642
|
+
{{ tx('editor.confirmRemove', 'Confirm remove') }}
|
|
2643
|
+
</button>
|
|
2644
|
+
<button type="button" (click)="cancelRemoval()">
|
|
2645
|
+
{{ tx('editor.cancelRemove', 'Cancel') }}
|
|
2646
|
+
</button>
|
|
2647
|
+
} @else {
|
|
2648
|
+
<button type="button" (click)="requestNestedRemoval('nodes.' + $index + '.content', itemIndex)">
|
|
2649
|
+
{{ tx('editor.removeBlock', 'Remove') }}
|
|
2650
|
+
</button>
|
|
2651
|
+
}
|
|
2652
|
+
</div>
|
|
2653
|
+
<div class="prx-rich-editor__node-grid">
|
|
2654
|
+
<label>
|
|
2655
|
+
<span>{{ tx('editor.blockType', 'Block type') }}</span>
|
|
2656
|
+
<select
|
|
2657
|
+
[ngModel]="isCardContentEditable(item.type) ? item.type : 'text'"
|
|
2658
|
+
(ngModelChange)="changeCardContentType($index, itemIndex, $event)"
|
|
2659
|
+
[disabled]="!isCardContentEditable(item.type)"
|
|
2660
|
+
>
|
|
2661
|
+
@for (type of cardContentNodeTypes; track type) {
|
|
2662
|
+
<option [value]="type">{{ tx('editor.nodeType.' + type, type) }}</option>
|
|
2663
|
+
}
|
|
2664
|
+
</select>
|
|
2665
|
+
</label>
|
|
2666
|
+
@if (isPresenterNode(item)) {
|
|
2667
|
+
<ng-container
|
|
2668
|
+
*ngTemplateOutlet="presenterFields; context: {
|
|
2669
|
+
node: item,
|
|
2670
|
+
path: '$.nodes[' + $index + '].content[' + itemIndex + ']',
|
|
2671
|
+
setString: setCardContentStringField.bind(this, $index, itemIndex),
|
|
2672
|
+
setNumber: setCardContentNumberField.bind(this, $index, itemIndex)
|
|
2673
|
+
}"
|
|
2674
|
+
></ng-container>
|
|
2675
|
+
} @else {
|
|
2676
|
+
<p class="prx-rich-editor__node-note">
|
|
2677
|
+
{{ tx('editor.advancedOnly', 'This node type is preserved and can be edited in advanced JSON.') }}
|
|
2678
|
+
</p>
|
|
2679
|
+
}
|
|
2680
|
+
</div>
|
|
2681
|
+
</div>
|
|
2682
|
+
}
|
|
2683
|
+
</div>
|
|
2684
|
+
}
|
|
2685
|
+
@case ('mediaBlock') {
|
|
2686
|
+
<label>
|
|
2687
|
+
<span>{{ tx('editor.field.avatarName', 'Avatar name') }}</span>
|
|
2688
|
+
<input
|
|
2689
|
+
[ngModel]="getMediaBlockAvatarField($index, 'name')"
|
|
2690
|
+
(ngModelChange)="setMediaBlockAvatarField($index, 'name', $event)"
|
|
2691
|
+
/>
|
|
2692
|
+
</label>
|
|
2693
|
+
<label>
|
|
2694
|
+
<span>{{ tx('editor.field.title', 'Title') }}</span>
|
|
2695
|
+
<input
|
|
2696
|
+
[ngModel]="getMediaBlockTextField($index, 'title')"
|
|
2697
|
+
(ngModelChange)="setMediaBlockTextField($index, 'title', $event)"
|
|
2698
|
+
/>
|
|
2699
|
+
</label>
|
|
2700
|
+
<label>
|
|
2701
|
+
<span>{{ tx('editor.field.subtitle', 'Subtitle') }}</span>
|
|
2702
|
+
<input
|
|
2703
|
+
[ngModel]="getMediaBlockTextField($index, 'subtitle')"
|
|
2704
|
+
(ngModelChange)="setMediaBlockTextField($index, 'subtitle', $event)"
|
|
2705
|
+
/>
|
|
2706
|
+
</label>
|
|
2707
|
+
<p class="prx-rich-editor__node-note">
|
|
2708
|
+
{{ tx('editor.mediaBlockAdvancedHelp', 'Meta and trailing content are preserved in advanced JSON.') }}
|
|
2709
|
+
</p>
|
|
2710
|
+
}
|
|
2711
|
+
@case ('timeline') {
|
|
2712
|
+
<label>
|
|
2713
|
+
<span>{{ tx('editor.field.title', 'Title') }}</span>
|
|
2714
|
+
<input
|
|
2715
|
+
[ngModel]="getStringField(node, 'title')"
|
|
2716
|
+
(ngModelChange)="setStringField($index, 'title', $event)"
|
|
2717
|
+
/>
|
|
2718
|
+
</label>
|
|
2719
|
+
<label>
|
|
2720
|
+
<span>{{ tx('editor.field.emptyText', 'Empty text') }}</span>
|
|
2721
|
+
<input
|
|
2722
|
+
[ngModel]="getStringField(node, 'emptyText')"
|
|
2723
|
+
(ngModelChange)="setStringField($index, 'emptyText', $event)"
|
|
2724
|
+
/>
|
|
2725
|
+
</label>
|
|
2726
|
+
<div class="prx-rich-editor__nested-editor">
|
|
2727
|
+
<header class="prx-rich-editor__nested-header">
|
|
2728
|
+
<h5>{{ tx('editor.timelineItems', 'Timeline items') }}</h5>
|
|
2729
|
+
<button type="button" (click)="addTimelineItem($index)">
|
|
2730
|
+
{{ tx('editor.addItem', 'Add item') }}
|
|
2731
|
+
</button>
|
|
2732
|
+
</header>
|
|
2733
|
+
@for (item of node.items; track item.id ?? $index; let itemIndex = $index) {
|
|
2734
|
+
<div class="prx-rich-editor__nested-node">
|
|
2735
|
+
<div class="prx-rich-editor__nested-actions">
|
|
2736
|
+
<strong>{{ tx('editor.item', 'Item') }} {{ itemIndex + 1 }}</strong>
|
|
2737
|
+
@if (isRemovalPending('nodes.' + $index + '.timelineItems', itemIndex)) {
|
|
2738
|
+
<button type="button" (click)="confirmTimelineItemRemoval($index, itemIndex)">
|
|
2739
|
+
{{ tx('editor.confirmRemove', 'Confirm remove') }}
|
|
2740
|
+
</button>
|
|
2741
|
+
<button type="button" (click)="cancelRemoval()">
|
|
2742
|
+
{{ tx('editor.cancelRemove', 'Cancel') }}
|
|
2743
|
+
</button>
|
|
2744
|
+
} @else {
|
|
2745
|
+
<button type="button" (click)="requestNestedRemoval('nodes.' + $index + '.timelineItems', itemIndex)">
|
|
2746
|
+
{{ tx('editor.removeBlock', 'Remove') }}
|
|
2747
|
+
</button>
|
|
2748
|
+
}
|
|
2749
|
+
</div>
|
|
2750
|
+
<div class="prx-rich-editor__node-grid">
|
|
2751
|
+
<label>
|
|
2752
|
+
<span>{{ tx('editor.field.title', 'Title') }}</span>
|
|
2753
|
+
<input
|
|
2754
|
+
[ngModel]="getTimelineItemField($index, itemIndex, 'title')"
|
|
2755
|
+
(ngModelChange)="setTimelineItemField($index, itemIndex, 'title', $event)"
|
|
2756
|
+
/>
|
|
2757
|
+
</label>
|
|
2758
|
+
<label>
|
|
2759
|
+
<span>{{ tx('editor.field.subtitle', 'Subtitle') }}</span>
|
|
2760
|
+
<input
|
|
2761
|
+
[ngModel]="getTimelineItemField($index, itemIndex, 'subtitle')"
|
|
2762
|
+
(ngModelChange)="setTimelineItemField($index, itemIndex, 'subtitle', $event)"
|
|
2763
|
+
/>
|
|
2764
|
+
</label>
|
|
2765
|
+
<label>
|
|
2766
|
+
<span>{{ tx('editor.field.badge', 'Badge') }}</span>
|
|
2767
|
+
<input
|
|
2768
|
+
[ngModel]="getTimelineItemField($index, itemIndex, 'badge')"
|
|
2769
|
+
(ngModelChange)="setTimelineItemField($index, itemIndex, 'badge', $event)"
|
|
2770
|
+
/>
|
|
2771
|
+
</label>
|
|
2772
|
+
<label>
|
|
2773
|
+
<span>{{ tx('editor.field.icon', 'Icon') }}</span>
|
|
2774
|
+
<input
|
|
2775
|
+
[ngModel]="getTimelineItemField($index, itemIndex, 'icon')"
|
|
2776
|
+
(ngModelChange)="setTimelineItemField($index, itemIndex, 'icon', $event)"
|
|
2777
|
+
[placeholder]="tx('editor.placeholder.icon', 'check_circle')"
|
|
2778
|
+
/>
|
|
2779
|
+
</label>
|
|
2780
|
+
</div>
|
|
2781
|
+
</div>
|
|
2782
|
+
}
|
|
2783
|
+
</div>
|
|
2784
|
+
}
|
|
2785
|
+
@case ('preset') {
|
|
2786
|
+
@if (presetOptions.length) {
|
|
2787
|
+
<label>
|
|
2788
|
+
<span>{{ tx('editor.field.preset', 'Preset') }}</span>
|
|
2789
|
+
<select
|
|
2790
|
+
[ngModel]="getPresetSelection(node)"
|
|
2791
|
+
(ngModelChange)="setPresetSelection($index, $event)"
|
|
2792
|
+
>
|
|
2793
|
+
@for (preset of presetOptions; track getPresetOptionValue(preset.ref)) {
|
|
2794
|
+
<option [value]="getPresetOptionValue(preset.ref)">
|
|
2795
|
+
{{ preset.label || preset.ref.presetId }}
|
|
2796
|
+
</option>
|
|
2797
|
+
}
|
|
2798
|
+
</select>
|
|
2799
|
+
</label>
|
|
2800
|
+
} @else {
|
|
2801
|
+
<p class="prx-rich-editor__node-note">
|
|
2802
|
+
{{ tx('editor.noPresets', 'No presets registered') }}.
|
|
2803
|
+
{{ tx('editor.noPresetsHelp', 'Register rich-block presets through PRAXIS_RICH_BLOCK_PRESETS to author preset references visually.') }}
|
|
2804
|
+
</p>
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
@default {
|
|
2808
|
+
<p class="prx-rich-editor__node-note">
|
|
2809
|
+
{{
|
|
2810
|
+
tx(
|
|
2811
|
+
'editor.advancedOnly',
|
|
2812
|
+
'This node type is preserved and can be edited in advanced JSON.'
|
|
2813
|
+
)
|
|
2814
|
+
}}
|
|
2815
|
+
</p>
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
<fieldset class="prx-rich-editor__wide-field prx-rich-editor__field-group">
|
|
2819
|
+
<legend>{{ tx('editor.commonMetadata', 'Common metadata') }}</legend>
|
|
2820
|
+
<label>
|
|
2821
|
+
<span>{{ tx('editor.field.testId', 'Test id') }}</span>
|
|
2822
|
+
<input
|
|
2823
|
+
[ngModel]="getStringField(node, 'testId')"
|
|
2824
|
+
(ngModelChange)="setStringField($index, 'testId', $event)"
|
|
2825
|
+
[placeholder]="tx('editor.placeholder.testId', 'rich-hero-title')"
|
|
2826
|
+
/>
|
|
2827
|
+
</label>
|
|
2828
|
+
<label>
|
|
2829
|
+
<span>{{ tx('editor.field.className', 'CSS class') }}</span>
|
|
2830
|
+
<input
|
|
2831
|
+
[ngModel]="getStringField(node, 'className')"
|
|
2832
|
+
(ngModelChange)="setStringField($index, 'className', $event)"
|
|
2833
|
+
[placeholder]="tx('editor.placeholder.className', 'hero-title')"
|
|
2834
|
+
/>
|
|
2835
|
+
</label>
|
|
2836
|
+
</fieldset>
|
|
2837
|
+
|
|
2838
|
+
<fieldset class="prx-rich-editor__wide-field prx-rich-editor__field-group">
|
|
2839
|
+
<legend>{{ tx('editor.visibilityRule', 'Visibility rule') }}</legend>
|
|
2840
|
+
<label>
|
|
2841
|
+
<span>{{ tx('editor.field.visibleWhenPath', 'Context path') }}</span>
|
|
2842
|
+
<input
|
|
2843
|
+
[ngModel]="getVisibleWhenPath(node)"
|
|
2844
|
+
(ngModelChange)="setVisibleWhenPath($index, $event)"
|
|
2845
|
+
[placeholder]="tx('editor.placeholder.visibleWhenPath', 'row.active')"
|
|
2846
|
+
/>
|
|
2847
|
+
</label>
|
|
2848
|
+
<label>
|
|
2849
|
+
<span>{{ tx('editor.field.visibleWhenValue', 'Expected value') }}</span>
|
|
2850
|
+
<input
|
|
2851
|
+
[ngModel]="getVisibleWhenValue(node)"
|
|
2852
|
+
(ngModelChange)="setVisibleWhenValue($index, $event)"
|
|
2853
|
+
[placeholder]="tx('editor.placeholder.visibleWhenValue', 'true')"
|
|
2854
|
+
/>
|
|
2855
|
+
</label>
|
|
2856
|
+
</fieldset>
|
|
2857
|
+
|
|
2858
|
+
<fieldset class="prx-rich-editor__wide-field prx-rich-editor__field-group">
|
|
2859
|
+
<legend>{{ tx('editor.safeStyle', 'Safe style') }}</legend>
|
|
2860
|
+
<label>
|
|
2861
|
+
<span>{{ tx('editor.field.styleName', 'CSS property') }}</span>
|
|
2862
|
+
<input
|
|
2863
|
+
[ngModel]="getFirstStyleName(node)"
|
|
2864
|
+
(ngModelChange)="setFirstStyleName($index, $event)"
|
|
2865
|
+
[placeholder]="tx('editor.placeholder.styleName', 'color')"
|
|
2866
|
+
/>
|
|
2867
|
+
</label>
|
|
2868
|
+
<label>
|
|
2869
|
+
<span>{{ tx('editor.field.styleValue', 'CSS value') }}</span>
|
|
2870
|
+
<input
|
|
2871
|
+
[ngModel]="getFirstStyleValue(node)"
|
|
2872
|
+
(ngModelChange)="setFirstStyleValue($index, $event)"
|
|
2873
|
+
[placeholder]="tx('editor.placeholder.styleValue', 'var(--md-sys-color-primary)')"
|
|
2874
|
+
/>
|
|
2875
|
+
</label>
|
|
2876
|
+
</fieldset>
|
|
2877
|
+
</div>
|
|
2878
|
+
</article>
|
|
2879
|
+
}
|
|
2880
|
+
</div>
|
|
2881
|
+
} @else {
|
|
2882
|
+
<p class="prx-rich-editor__empty">
|
|
2883
|
+
{{ tx('editor.noBlocks', 'No blocks yet. Add a block to start authoring this document.') }}
|
|
2884
|
+
</p>
|
|
2885
|
+
}
|
|
2886
|
+
</section>
|
|
2887
|
+
|
|
891
2888
|
<label class="prx-rich-editor__document">
|
|
892
|
-
<span>{{ tx('editor.document', '
|
|
893
|
-
<
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
2889
|
+
<span>{{ tx('editor.document', 'Advanced JSON') }}</span>
|
|
2890
|
+
<div class="prx-rich-editor__workbench">
|
|
2891
|
+
<textarea
|
|
2892
|
+
name="rich-content-document"
|
|
2893
|
+
[(ngModel)]="documentJson"
|
|
2894
|
+
(ngModelChange)="onDocumentJsonChange()"
|
|
2895
|
+
[attr.aria-describedby]="
|
|
2896
|
+
errorMessage
|
|
2897
|
+
? 'rich-content-config-error'
|
|
2898
|
+
: 'rich-content-document-help'
|
|
2899
|
+
"
|
|
2900
|
+
[attr.aria-invalid]="!valid"
|
|
2901
|
+
spellcheck="false"
|
|
2902
|
+
data-testid="rich-content-document-input"
|
|
2903
|
+
></textarea>
|
|
2904
|
+
|
|
2905
|
+
<aside
|
|
2906
|
+
class="prx-rich-editor__inspector"
|
|
2907
|
+
[attr.aria-label]="tx('editor.inspector', 'Document inspector')"
|
|
2908
|
+
>
|
|
2909
|
+
<section>
|
|
2910
|
+
<h3>{{ tx('editor.overview', 'Overview') }}</h3>
|
|
2911
|
+
<dl>
|
|
2912
|
+
<div>
|
|
2913
|
+
<dt>{{ tx('editor.nodeCount', 'Nodes') }}</dt>
|
|
2914
|
+
<dd>{{ nodeCount }}</dd>
|
|
2915
|
+
</div>
|
|
2916
|
+
<div>
|
|
2917
|
+
<dt>{{ tx('editor.nodeTypes', 'Types') }}</dt>
|
|
2918
|
+
<dd>{{ nodeTypeSummary || tx('editor.none', 'None') }}</dd>
|
|
2919
|
+
</div>
|
|
2920
|
+
</dl>
|
|
2921
|
+
</section>
|
|
2922
|
+
|
|
2923
|
+
@if (parsedDocument) {
|
|
2924
|
+
<section>
|
|
2925
|
+
<h3>{{ tx('editor.preview', 'Preview') }}</h3>
|
|
2926
|
+
<div class="prx-rich-editor__preview">
|
|
2927
|
+
<praxis-rich-content
|
|
2928
|
+
[document]="parsedDocument"
|
|
2929
|
+
[layout]="layout"
|
|
2930
|
+
[rootClassName]="rootClassName"
|
|
2931
|
+
></praxis-rich-content>
|
|
2932
|
+
</div>
|
|
2933
|
+
</section>
|
|
2934
|
+
}
|
|
2935
|
+
</aside>
|
|
2936
|
+
</div>
|
|
906
2937
|
</label>
|
|
907
2938
|
|
|
908
2939
|
@if (errorMessage) {
|
|
909
|
-
<
|
|
2940
|
+
<div
|
|
910
2941
|
id="rich-content-config-error"
|
|
911
2942
|
class="prx-rich-editor__error"
|
|
912
2943
|
data-testid="rich-content-config-error"
|
|
913
2944
|
>
|
|
914
|
-
{{ errorMessage }}
|
|
915
|
-
|
|
2945
|
+
<p>{{ errorMessage }}</p>
|
|
2946
|
+
@if (validationIssues.length) {
|
|
2947
|
+
<ul>
|
|
2948
|
+
@for (issue of validationIssues; track issue.path + issue.messageKey) {
|
|
2949
|
+
<li>
|
|
2950
|
+
<code>{{ issue.path }}</code>
|
|
2951
|
+
{{ tx(issue.messageKey, issue.fallback) }}
|
|
2952
|
+
</li>
|
|
2953
|
+
}
|
|
2954
|
+
</ul>
|
|
2955
|
+
}
|
|
2956
|
+
</div>
|
|
916
2957
|
} @else {
|
|
917
2958
|
<p id="rich-content-document-help" class="prx-rich-editor__help">
|
|
918
2959
|
{{
|
|
919
2960
|
tx(
|
|
920
2961
|
'editor.documentHelp',
|
|
921
|
-
'
|
|
2962
|
+
'Edit the canonical RichContentDocument. The inspector validates the shape and renders a preview before apply or save.'
|
|
922
2963
|
)
|
|
923
2964
|
}}
|
|
924
2965
|
</p>
|
|
@@ -933,11 +2974,11 @@ class PraxisRichContentConfigEditor {
|
|
|
933
2974
|
</button>
|
|
934
2975
|
</div>
|
|
935
2976
|
</section>
|
|
936
|
-
`, isInline: true, styles: [":host{display:block;min-width:0}.prx-rich-editor{display:grid;gap:18px;color:var(--md-sys-color-on-surface, #1f2937)}.prx-rich-editor__header{display:flex;gap:16px;align-items:flex-start;justify-content:space-between}.prx-rich-editor__header h2{margin:0;font-size:1.25rem}.prx-rich-editor__header p{margin:6px 0 0;color:var(--md-sys-color-on-surface-variant, #5f6673)}.prx-rich-editor__eyebrow{margin:0 0 4px;text-transform:uppercase;letter-spacing:.08em;font-size:.72rem;color:var(--md-sys-color-primary, #3154e7)}.prx-rich-editor__status{flex:0 0 auto;padding:6px 10px;border-radius:999px;background:color-mix(in srgb,#16a34a 12%,transparent);color:#166534;font-size:.78rem;font-weight:700}.prx-rich-editor__status.invalid{background:color-mix(in srgb,#dc2626 12%,transparent);color:#991b1b}.prx-rich-editor__grid{display:grid;gap:14px;grid-template-columns:minmax(0,180px) minmax(0,1fr)}label,.prx-rich-editor__document{display:grid;gap:7px;font-weight:700}input,select,textarea{box-sizing:border-box;width:100%;border:1px solid var(--md-sys-color-outline-variant, #d7dce5);border-radius:12px;padding:10px 12px;font:inherit;color:inherit;background:var(--md-sys-color-surface, #fff)}textarea{min-height:360px;resize:vertical;font-family:Cascadia Code,Fira Code,Consolas,monospace;font-size:.86rem;line-height:1.45}.prx-rich-editor__help{margin:-8px 0 0;color:var(--md-sys-color-on-surface-variant, #5f6673);font-size:.85rem}.prx-rich-editor__error{margin:0;padding:10px 12px;border-radius:
|
|
2977
|
+
`, isInline: true, styles: [":host{display:block;min-width:0}.prx-rich-editor{display:grid;gap:18px;color:var(--md-sys-color-on-surface, #1f2937)}.prx-rich-editor__header{display:flex;gap:16px;align-items:flex-start;justify-content:space-between}.prx-rich-editor__header h2{margin:0;font-size:1.25rem}.prx-rich-editor__header p{margin:6px 0 0;color:var(--md-sys-color-on-surface-variant, #5f6673)}.prx-rich-editor__eyebrow{margin:0 0 4px;text-transform:uppercase;letter-spacing:.08em;font-size:.72rem;color:var(--md-sys-color-primary, #3154e7)}.prx-rich-editor__status{flex:0 0 auto;padding:6px 10px;border-radius:999px;background:color-mix(in srgb,#16a34a 12%,transparent);color:#166534;font-size:.78rem;font-weight:700}.prx-rich-editor__status.invalid{background:color-mix(in srgb,#dc2626 12%,transparent);color:#991b1b}.prx-rich-editor__grid{display:grid;gap:14px;grid-template-columns:minmax(0,180px) minmax(0,1fr)}.prx-rich-editor__blocks{display:grid;gap:14px}.prx-rich-editor__section-header,.prx-rich-editor__node-header{display:flex;gap:16px;align-items:flex-start;justify-content:space-between}.prx-rich-editor__section-header h3,.prx-rich-editor__node-header h4{margin:0}.prx-rich-editor__section-header p,.prx-rich-editor__node-note,.prx-rich-editor__empty{margin:6px 0 0;color:var(--md-sys-color-on-surface-variant, #5f6673);font-size:.88rem}.prx-rich-editor__add-block,.prx-rich-editor__node-actions{display:flex;flex-wrap:wrap;gap:10px;align-items:flex-end}.prx-rich-editor__add-block label{min-width:180px}.prx-rich-editor__node-list{display:grid;gap:12px}.prx-rich-editor__node-card{display:grid;gap:14px;border:1px solid var(--md-sys-color-outline-variant, #d7dce5);border-radius:8px;padding:14px;background:var(--md-sys-color-surface-container-lowest, #fff)}.prx-rich-editor__node-eyebrow{margin:0 0 4px;color:var(--md-sys-color-on-surface-variant, #5f6673);font-size:.75rem;font-weight:700;text-transform:uppercase}.prx-rich-editor__node-grid{display:grid;gap:12px;grid-template-columns:repeat(2,minmax(0,1fr))}.prx-rich-editor__wide-field,.prx-rich-editor__node-note{grid-column:1 / -1}.prx-rich-editor__field-group,.prx-rich-editor__nested-editor,.prx-rich-editor__nested-node{grid-column:1 / -1;display:grid;gap:12px}.prx-rich-editor__field-group,.prx-rich-editor__nested-node{margin:0;border:1px solid var(--md-sys-color-outline-variant, #d7dce5);border-radius:8px;padding:12px}.prx-rich-editor__field-group{grid-template-columns:repeat(2,minmax(0,1fr))}.prx-rich-editor__field-group legend{padding:0 4px;font-weight:700}.prx-rich-editor__nested-header,.prx-rich-editor__nested-actions{display:flex;flex-wrap:wrap;gap:10px;align-items:center;justify-content:space-between}.prx-rich-editor__node-grid textarea{min-height:96px;font-family:inherit}label,.prx-rich-editor__document{display:grid;gap:7px;font-weight:700}input,select,textarea{box-sizing:border-box;width:100%;border:1px solid var(--md-sys-color-outline-variant, #d7dce5);border-radius:12px;padding:10px 12px;font:inherit;color:inherit;background:var(--md-sys-color-surface, #fff)}textarea{min-height:360px;resize:vertical;font-family:Cascadia Code,Fira Code,Consolas,monospace;font-size:.86rem;line-height:1.45}.prx-rich-editor__workbench{display:grid;gap:14px;grid-template-columns:minmax(0,1.2fr) minmax(260px,.8fr)}.prx-rich-editor__inspector{display:grid;align-content:start;gap:14px;min-width:0;border:1px solid var(--md-sys-color-outline-variant, #d7dce5);border-radius:8px;padding:12px;background:var(--md-sys-color-surface-container-lowest, #fff)}.prx-rich-editor__inspector h3{margin:0 0 8px;font-size:.95rem}.prx-rich-editor__inspector dl{display:grid;gap:8px;margin:0}.prx-rich-editor__inspector dl div{display:grid;gap:2px}.prx-rich-editor__inspector dt{color:var(--md-sys-color-on-surface-variant, #5f6673);font-size:.78rem;font-weight:700}.prx-rich-editor__inspector dd{margin:0;overflow-wrap:anywhere;font-weight:600}.prx-rich-editor__preview{max-height:260px;overflow:auto;border:1px solid var(--md-sys-color-outline-variant, #d7dce5);border-radius:8px;padding:12px;background:var(--md-sys-color-surface, #fff)}.prx-rich-editor__help{margin:-8px 0 0;color:var(--md-sys-color-on-surface-variant, #5f6673);font-size:.85rem}.prx-rich-editor__error{margin:0;padding:10px 12px;border-radius:8px;color:#991b1b;background:color-mix(in srgb,#dc2626 9%,transparent)}.prx-rich-editor__error p{margin:0}.prx-rich-editor__error ul{display:grid;gap:6px;margin:8px 0 0;padding-inline-start:18px}.prx-rich-editor__error code{margin-right:4px;font-family:Cascadia Code,Fira Code,Consolas,monospace}.prx-rich-editor__actions{display:flex;flex-wrap:wrap;gap:10px}button{border:1px solid var(--md-sys-color-outline-variant, #d7dce5);border-radius:8px;padding:8px 14px;background:var(--md-sys-color-surface, #fff);color:var(--md-sys-color-primary, #3154e7);font-weight:700;cursor:pointer}button:disabled{cursor:not-allowed;opacity:.55}@media(max-width:760px){.prx-rich-editor__header,.prx-rich-editor__section-header,.prx-rich-editor__node-header,.prx-rich-editor__grid,.prx-rich-editor__field-group,.prx-rich-editor__node-grid,.prx-rich-editor__workbench{grid-template-columns:1fr;display:grid}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: PraxisRichContent, selector: "praxis-rich-content", inputs: ["document", "nodes", "context", "hostCapabilities", "layout", "rootClassName"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
937
2978
|
}
|
|
938
2979
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisRichContentConfigEditor, decorators: [{
|
|
939
2980
|
type: Component,
|
|
940
|
-
args: [{ selector: 'praxis-rich-content-config-editor', standalone: true, imports: [CommonModule, FormsModule], providers: [providePraxisRichContentI18n()], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
2981
|
+
args: [{ selector: 'praxis-rich-content-config-editor', standalone: true, imports: [CommonModule, FormsModule, PraxisRichContent], providers: [providePraxisRichContentI18n()], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
941
2982
|
<section class="prx-rich-editor" data-testid="rich-content-config-editor">
|
|
942
2983
|
<header class="prx-rich-editor__header">
|
|
943
2984
|
<div>
|
|
@@ -999,37 +3040,781 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
999
3040
|
</label>
|
|
1000
3041
|
</div>
|
|
1001
3042
|
|
|
3043
|
+
<ng-template
|
|
3044
|
+
#presenterFields
|
|
3045
|
+
let-node="node"
|
|
3046
|
+
let-path="path"
|
|
3047
|
+
let-setString="setString"
|
|
3048
|
+
let-setNumber="setNumber"
|
|
3049
|
+
>
|
|
3050
|
+
@switch (node.type) {
|
|
3051
|
+
@case ('text') {
|
|
3052
|
+
<label class="prx-rich-editor__wide-field">
|
|
3053
|
+
<span>{{ tx('editor.field.text', 'Text') }}</span>
|
|
3054
|
+
<textarea
|
|
3055
|
+
[ngModel]="getStringField(node, 'text')"
|
|
3056
|
+
(ngModelChange)="setString('text', $event)"
|
|
3057
|
+
></textarea>
|
|
3058
|
+
</label>
|
|
3059
|
+
}
|
|
3060
|
+
@case ('badge') {
|
|
3061
|
+
<label>
|
|
3062
|
+
<span>{{ tx('editor.field.label', 'Label') }}</span>
|
|
3063
|
+
<input
|
|
3064
|
+
[ngModel]="getStringField(node, 'label')"
|
|
3065
|
+
(ngModelChange)="setString('label', $event)"
|
|
3066
|
+
/>
|
|
3067
|
+
</label>
|
|
3068
|
+
}
|
|
3069
|
+
@case ('icon') {
|
|
3070
|
+
<label>
|
|
3071
|
+
<span>{{ tx('editor.field.icon', 'Icon') }}</span>
|
|
3072
|
+
<input
|
|
3073
|
+
[ngModel]="getStringField(node, 'icon')"
|
|
3074
|
+
(ngModelChange)="setString('icon', $event)"
|
|
3075
|
+
/>
|
|
3076
|
+
</label>
|
|
3077
|
+
}
|
|
3078
|
+
@case ('image') {
|
|
3079
|
+
<label>
|
|
3080
|
+
<span>{{ tx('editor.field.src', 'Image URL') }}</span>
|
|
3081
|
+
<input
|
|
3082
|
+
[ngModel]="getStringField(node, 'src')"
|
|
3083
|
+
(ngModelChange)="setString('src', $event)"
|
|
3084
|
+
/>
|
|
3085
|
+
</label>
|
|
3086
|
+
<label>
|
|
3087
|
+
<span>{{ tx('editor.field.alt', 'Alternative text') }}</span>
|
|
3088
|
+
<input
|
|
3089
|
+
[ngModel]="getStringField(node, 'alt')"
|
|
3090
|
+
(ngModelChange)="setString('alt', $event)"
|
|
3091
|
+
/>
|
|
3092
|
+
</label>
|
|
3093
|
+
}
|
|
3094
|
+
@case ('link') {
|
|
3095
|
+
<label>
|
|
3096
|
+
<span>{{ tx('editor.field.label', 'Label') }}</span>
|
|
3097
|
+
<input
|
|
3098
|
+
[ngModel]="getStringField(node, 'label')"
|
|
3099
|
+
(ngModelChange)="setString('label', $event)"
|
|
3100
|
+
/>
|
|
3101
|
+
</label>
|
|
3102
|
+
<label>
|
|
3103
|
+
<span>{{ tx('editor.field.href', 'Link URL') }}</span>
|
|
3104
|
+
<input
|
|
3105
|
+
[ngModel]="getStringField(node, 'href')"
|
|
3106
|
+
(ngModelChange)="setString('href', $event)"
|
|
3107
|
+
/>
|
|
3108
|
+
</label>
|
|
3109
|
+
}
|
|
3110
|
+
@case ('metric') {
|
|
3111
|
+
<label>
|
|
3112
|
+
<span>{{ tx('editor.field.label', 'Label') }}</span>
|
|
3113
|
+
<input
|
|
3114
|
+
[ngModel]="getStringField(node, 'label')"
|
|
3115
|
+
(ngModelChange)="setString('label', $event)"
|
|
3116
|
+
/>
|
|
3117
|
+
</label>
|
|
3118
|
+
<label>
|
|
3119
|
+
<span>{{ tx('editor.field.valueExpr', 'Value binding') }}</span>
|
|
3120
|
+
<input
|
|
3121
|
+
[ngModel]="getStringField(node, 'valueExpr')"
|
|
3122
|
+
(ngModelChange)="setString('valueExpr', $event)"
|
|
3123
|
+
/>
|
|
3124
|
+
</label>
|
|
3125
|
+
}
|
|
3126
|
+
@case ('progress') {
|
|
3127
|
+
<label>
|
|
3128
|
+
<span>{{ tx('editor.field.label', 'Label') }}</span>
|
|
3129
|
+
<input
|
|
3130
|
+
[ngModel]="getStringField(node, 'label')"
|
|
3131
|
+
(ngModelChange)="setString('label', $event)"
|
|
3132
|
+
/>
|
|
3133
|
+
</label>
|
|
3134
|
+
<label>
|
|
3135
|
+
<span>{{ tx('editor.field.max', 'Maximum') }}</span>
|
|
3136
|
+
<input
|
|
3137
|
+
type="number"
|
|
3138
|
+
[ngModel]="getNumberField(node, 'max')"
|
|
3139
|
+
(ngModelChange)="setNumber('max', $event)"
|
|
3140
|
+
/>
|
|
3141
|
+
</label>
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
@if (getIssueMessages(path).length) {
|
|
3145
|
+
<ul class="prx-rich-editor__field-errors">
|
|
3146
|
+
@for (message of getIssueMessages(path); track message) {
|
|
3147
|
+
<li>{{ message }}</li>
|
|
3148
|
+
}
|
|
3149
|
+
</ul>
|
|
3150
|
+
}
|
|
3151
|
+
</ng-template>
|
|
3152
|
+
|
|
3153
|
+
<section class="prx-rich-editor__blocks">
|
|
3154
|
+
<header class="prx-rich-editor__section-header">
|
|
3155
|
+
<div>
|
|
3156
|
+
<h3>{{ tx('editor.blocks', 'Blocks') }}</h3>
|
|
3157
|
+
<p>
|
|
3158
|
+
{{
|
|
3159
|
+
tx(
|
|
3160
|
+
'editor.blocksHelp',
|
|
3161
|
+
'Create and edit common top-level rich-content blocks. Use JSON for advanced nested structures.'
|
|
3162
|
+
)
|
|
3163
|
+
}}
|
|
3164
|
+
</p>
|
|
3165
|
+
</div>
|
|
3166
|
+
<div class="prx-rich-editor__add-block">
|
|
3167
|
+
<label>
|
|
3168
|
+
<span>{{ tx('editor.blockType', 'Block type') }}</span>
|
|
3169
|
+
<select
|
|
3170
|
+
name="rich-content-new-node-type"
|
|
3171
|
+
[(ngModel)]="newNodeType"
|
|
3172
|
+
data-testid="rich-content-new-node-type"
|
|
3173
|
+
>
|
|
3174
|
+
@for (type of editableNodeTypes; track type) {
|
|
3175
|
+
<option [value]="type">{{ tx('editor.nodeType.' + type, type) }}</option>
|
|
3176
|
+
}
|
|
3177
|
+
</select>
|
|
3178
|
+
</label>
|
|
3179
|
+
<button
|
|
3180
|
+
type="button"
|
|
3181
|
+
(click)="addTopLevelNode()"
|
|
3182
|
+
data-testid="rich-content-add-node"
|
|
3183
|
+
>
|
|
3184
|
+
{{ tx('editor.addBlock', 'Add block') }}
|
|
3185
|
+
</button>
|
|
3186
|
+
</div>
|
|
3187
|
+
</header>
|
|
3188
|
+
|
|
3189
|
+
@if (parsedDocument?.nodes?.length) {
|
|
3190
|
+
<div class="prx-rich-editor__node-list">
|
|
3191
|
+
@for (node of parsedDocument?.nodes ?? []; track node.id ?? $index) {
|
|
3192
|
+
<article
|
|
3193
|
+
class="prx-rich-editor__node-card"
|
|
3194
|
+
[attr.data-rich-editor-node-type]="node.type"
|
|
3195
|
+
>
|
|
3196
|
+
<header class="prx-rich-editor__node-header">
|
|
3197
|
+
<div>
|
|
3198
|
+
<p class="prx-rich-editor__node-eyebrow">
|
|
3199
|
+
{{ tx('editor.block', 'Block') }} {{ $index + 1 }}
|
|
3200
|
+
</p>
|
|
3201
|
+
<h4>{{ tx('editor.nodeType.' + node.type, node.type) }}</h4>
|
|
3202
|
+
</div>
|
|
3203
|
+
<div class="prx-rich-editor__node-actions">
|
|
3204
|
+
<button type="button" (click)="moveTopLevelNode($index, -1)" [disabled]="$index === 0">
|
|
3205
|
+
{{ tx('editor.moveUp', 'Move up') }}
|
|
3206
|
+
</button>
|
|
3207
|
+
<button
|
|
3208
|
+
type="button"
|
|
3209
|
+
(click)="moveTopLevelNode($index, 1)"
|
|
3210
|
+
[disabled]="$index === (parsedDocument?.nodes?.length ?? 0) - 1"
|
|
3211
|
+
>
|
|
3212
|
+
{{ tx('editor.moveDown', 'Move down') }}
|
|
3213
|
+
</button>
|
|
3214
|
+
@if (isRemovalPending('nodes', $index)) {
|
|
3215
|
+
<button type="button" (click)="confirmTopLevelNodeRemoval($index)">
|
|
3216
|
+
{{ tx('editor.confirmRemove', 'Confirm remove') }}
|
|
3217
|
+
</button>
|
|
3218
|
+
<button type="button" (click)="cancelRemoval()">
|
|
3219
|
+
{{ tx('editor.cancelRemove', 'Cancel') }}
|
|
3220
|
+
</button>
|
|
3221
|
+
} @else {
|
|
3222
|
+
<button type="button" (click)="requestTopLevelNodeRemoval($index)">
|
|
3223
|
+
{{ tx('editor.removeBlock', 'Remove') }}
|
|
3224
|
+
</button>
|
|
3225
|
+
}
|
|
3226
|
+
</div>
|
|
3227
|
+
</header>
|
|
3228
|
+
|
|
3229
|
+
@if (getIssueMessages('$.nodes[' + $index + ']').length) {
|
|
3230
|
+
<ul class="prx-rich-editor__field-errors">
|
|
3231
|
+
@for (message of getIssueMessages('$.nodes[' + $index + ']'); track message) {
|
|
3232
|
+
<li>{{ message }}</li>
|
|
3233
|
+
}
|
|
3234
|
+
</ul>
|
|
3235
|
+
}
|
|
3236
|
+
|
|
3237
|
+
<div class="prx-rich-editor__node-grid">
|
|
3238
|
+
<label>
|
|
3239
|
+
<span>{{ tx('editor.blockType', 'Block type') }}</span>
|
|
3240
|
+
<select
|
|
3241
|
+
[ngModel]="isEditableNodeType(node.type) ? node.type : 'text'"
|
|
3242
|
+
(ngModelChange)="changeTopLevelNodeType($index, $event)"
|
|
3243
|
+
[disabled]="!isEditableNodeType(node.type)"
|
|
3244
|
+
>
|
|
3245
|
+
@for (type of editableNodeTypes; track type) {
|
|
3246
|
+
<option [value]="type">{{ tx('editor.nodeType.' + type, type) }}</option>
|
|
3247
|
+
}
|
|
3248
|
+
</select>
|
|
3249
|
+
</label>
|
|
3250
|
+
|
|
3251
|
+
<label>
|
|
3252
|
+
<span>{{ tx('editor.nodeId', 'Stable id') }}</span>
|
|
3253
|
+
<input
|
|
3254
|
+
[ngModel]="getStringField(node, 'id')"
|
|
3255
|
+
(ngModelChange)="setStringField($index, 'id', $event)"
|
|
3256
|
+
[placeholder]="tx('editor.nodeIdPlaceholder', 'Example: hero-title')"
|
|
3257
|
+
/>
|
|
3258
|
+
</label>
|
|
3259
|
+
|
|
3260
|
+
@switch (node.type) {
|
|
3261
|
+
@case ('text') {
|
|
3262
|
+
<label class="prx-rich-editor__wide-field">
|
|
3263
|
+
<span>{{ tx('editor.field.text', 'Text') }}</span>
|
|
3264
|
+
<textarea
|
|
3265
|
+
[ngModel]="getStringField(node, 'text')"
|
|
3266
|
+
(ngModelChange)="setStringField($index, 'text', $event)"
|
|
3267
|
+
></textarea>
|
|
3268
|
+
@if (getIssueMessages('$.nodes[' + $index + '].text').length) {
|
|
3269
|
+
<small class="prx-rich-editor__field-error">
|
|
3270
|
+
{{ getIssueMessages('$.nodes[' + $index + '].text').join(' ') }}
|
|
3271
|
+
</small>
|
|
3272
|
+
}
|
|
3273
|
+
</label>
|
|
3274
|
+
<label>
|
|
3275
|
+
<span>{{ tx('editor.field.textExpr', 'Text binding') }}</span>
|
|
3276
|
+
<input
|
|
3277
|
+
[ngModel]="getStringField(node, 'textExpr')"
|
|
3278
|
+
(ngModelChange)="setStringField($index, 'textExpr', $event)"
|
|
3279
|
+
[placeholder]="tx('editor.placeholder.textExpr', 'row.name')"
|
|
3280
|
+
/>
|
|
3281
|
+
@if (getIssueMessages('$.nodes[' + $index + '].textExpr').length) {
|
|
3282
|
+
<small class="prx-rich-editor__field-error">
|
|
3283
|
+
{{ getIssueMessages('$.nodes[' + $index + '].textExpr').join(' ') }}
|
|
3284
|
+
</small>
|
|
3285
|
+
}
|
|
3286
|
+
</label>
|
|
3287
|
+
}
|
|
3288
|
+
@case ('badge') {
|
|
3289
|
+
<label>
|
|
3290
|
+
<span>{{ tx('editor.field.label', 'Label') }}</span>
|
|
3291
|
+
<input
|
|
3292
|
+
[ngModel]="getStringField(node, 'label')"
|
|
3293
|
+
(ngModelChange)="setStringField($index, 'label', $event)"
|
|
3294
|
+
/>
|
|
3295
|
+
</label>
|
|
3296
|
+
<label>
|
|
3297
|
+
<span>{{ tx('editor.field.labelExpr', 'Label binding') }}</span>
|
|
3298
|
+
<input
|
|
3299
|
+
[ngModel]="getStringField(node, 'labelExpr')"
|
|
3300
|
+
(ngModelChange)="setStringField($index, 'labelExpr', $event)"
|
|
3301
|
+
[placeholder]="tx('editor.placeholder.labelExpr', 'row.status')"
|
|
3302
|
+
/>
|
|
3303
|
+
</label>
|
|
3304
|
+
<label>
|
|
3305
|
+
<span>{{ tx('editor.field.icon', 'Icon') }}</span>
|
|
3306
|
+
<input
|
|
3307
|
+
[ngModel]="getStringField(node, 'icon')"
|
|
3308
|
+
(ngModelChange)="setStringField($index, 'icon', $event)"
|
|
3309
|
+
[placeholder]="tx('editor.placeholder.icon', 'check_circle')"
|
|
3310
|
+
/>
|
|
3311
|
+
</label>
|
|
3312
|
+
}
|
|
3313
|
+
@case ('icon') {
|
|
3314
|
+
<label>
|
|
3315
|
+
<span>{{ tx('editor.field.icon', 'Icon') }}</span>
|
|
3316
|
+
<input
|
|
3317
|
+
[ngModel]="getStringField(node, 'icon')"
|
|
3318
|
+
(ngModelChange)="setStringField($index, 'icon', $event)"
|
|
3319
|
+
[placeholder]="tx('editor.placeholder.icon', 'check_circle')"
|
|
3320
|
+
/>
|
|
3321
|
+
</label>
|
|
3322
|
+
<label>
|
|
3323
|
+
<span>{{ tx('editor.field.ariaLabel', 'Accessible label') }}</span>
|
|
3324
|
+
<input
|
|
3325
|
+
[ngModel]="getStringField(node, 'ariaLabel')"
|
|
3326
|
+
(ngModelChange)="setStringField($index, 'ariaLabel', $event)"
|
|
3327
|
+
/>
|
|
3328
|
+
</label>
|
|
3329
|
+
}
|
|
3330
|
+
@case ('image') {
|
|
3331
|
+
<label>
|
|
3332
|
+
<span>{{ tx('editor.field.src', 'Image URL') }}</span>
|
|
3333
|
+
<input
|
|
3334
|
+
[ngModel]="getStringField(node, 'src')"
|
|
3335
|
+
(ngModelChange)="setStringField($index, 'src', $event)"
|
|
3336
|
+
/>
|
|
3337
|
+
</label>
|
|
3338
|
+
<label>
|
|
3339
|
+
<span>{{ tx('editor.field.alt', 'Alternative text') }}</span>
|
|
3340
|
+
<input
|
|
3341
|
+
[ngModel]="getStringField(node, 'alt')"
|
|
3342
|
+
(ngModelChange)="setStringField($index, 'alt', $event)"
|
|
3343
|
+
/>
|
|
3344
|
+
</label>
|
|
3345
|
+
}
|
|
3346
|
+
@case ('metric') {
|
|
3347
|
+
<label>
|
|
3348
|
+
<span>{{ tx('editor.field.label', 'Label') }}</span>
|
|
3349
|
+
<input
|
|
3350
|
+
[ngModel]="getStringField(node, 'label')"
|
|
3351
|
+
(ngModelChange)="setStringField($index, 'label', $event)"
|
|
3352
|
+
/>
|
|
3353
|
+
</label>
|
|
3354
|
+
<label>
|
|
3355
|
+
<span>{{ tx('editor.field.valueExpr', 'Value binding') }}</span>
|
|
3356
|
+
<input
|
|
3357
|
+
[ngModel]="getStringField(node, 'valueExpr')"
|
|
3358
|
+
(ngModelChange)="setStringField($index, 'valueExpr', $event)"
|
|
3359
|
+
[placeholder]="tx('editor.placeholder.valueExpr', 'row.total')"
|
|
3360
|
+
/>
|
|
3361
|
+
</label>
|
|
3362
|
+
<label>
|
|
3363
|
+
<span>{{ tx('editor.field.captionExpr', 'Caption binding') }}</span>
|
|
3364
|
+
<input
|
|
3365
|
+
[ngModel]="getStringField(node, 'captionExpr')"
|
|
3366
|
+
(ngModelChange)="setStringField($index, 'captionExpr', $event)"
|
|
3367
|
+
[placeholder]="tx('editor.placeholder.captionExpr', 'row.caption')"
|
|
3368
|
+
/>
|
|
3369
|
+
</label>
|
|
3370
|
+
}
|
|
3371
|
+
@case ('progress') {
|
|
3372
|
+
<label>
|
|
3373
|
+
<span>{{ tx('editor.field.label', 'Label') }}</span>
|
|
3374
|
+
<input
|
|
3375
|
+
[ngModel]="getStringField(node, 'label')"
|
|
3376
|
+
(ngModelChange)="setStringField($index, 'label', $event)"
|
|
3377
|
+
/>
|
|
3378
|
+
</label>
|
|
3379
|
+
<label>
|
|
3380
|
+
<span>{{ tx('editor.field.valueExpr', 'Value binding') }}</span>
|
|
3381
|
+
<input
|
|
3382
|
+
[ngModel]="getStringField(node, 'valueExpr')"
|
|
3383
|
+
(ngModelChange)="setStringField($index, 'valueExpr', $event)"
|
|
3384
|
+
[placeholder]="tx('editor.placeholder.progressExpr', 'row.progress')"
|
|
3385
|
+
/>
|
|
3386
|
+
</label>
|
|
3387
|
+
<label>
|
|
3388
|
+
<span>{{ tx('editor.field.max', 'Maximum') }}</span>
|
|
3389
|
+
<input
|
|
3390
|
+
type="number"
|
|
3391
|
+
[ngModel]="getNumberField(node, 'max')"
|
|
3392
|
+
(ngModelChange)="setNumberField($index, 'max', $event)"
|
|
3393
|
+
/>
|
|
3394
|
+
</label>
|
|
3395
|
+
}
|
|
3396
|
+
@case ('compose') {
|
|
3397
|
+
<label>
|
|
3398
|
+
<span>{{ tx('editor.field.direction', 'Direction') }}</span>
|
|
3399
|
+
<select
|
|
3400
|
+
[ngModel]="getStringField(node, 'direction') || 'row'"
|
|
3401
|
+
(ngModelChange)="setStringField($index, 'direction', $event)"
|
|
3402
|
+
>
|
|
3403
|
+
<option value="row">{{ tx('editor.direction.row', 'Row') }}</option>
|
|
3404
|
+
<option value="column">{{ tx('editor.direction.column', 'Column') }}</option>
|
|
3405
|
+
</select>
|
|
3406
|
+
</label>
|
|
3407
|
+
<label>
|
|
3408
|
+
<span>{{ tx('editor.field.gap', 'Gap') }}</span>
|
|
3409
|
+
<select
|
|
3410
|
+
[ngModel]="getStringField(node, 'gap') || 'md'"
|
|
3411
|
+
(ngModelChange)="setStringField($index, 'gap', $event)"
|
|
3412
|
+
>
|
|
3413
|
+
<option value="xs">xs</option>
|
|
3414
|
+
<option value="sm">sm</option>
|
|
3415
|
+
<option value="md">md</option>
|
|
3416
|
+
<option value="lg">lg</option>
|
|
3417
|
+
<option value="xl">xl</option>
|
|
3418
|
+
</select>
|
|
3419
|
+
</label>
|
|
3420
|
+
<div class="prx-rich-editor__nested-editor">
|
|
3421
|
+
<header class="prx-rich-editor__nested-header">
|
|
3422
|
+
<h5>{{ tx('editor.composeItems', 'Compose items') }}</h5>
|
|
3423
|
+
<button type="button" (click)="addComposeItem($index)">
|
|
3424
|
+
{{ tx('editor.addItem', 'Add item') }}
|
|
3425
|
+
</button>
|
|
3426
|
+
</header>
|
|
3427
|
+
@for (item of node.items; track item.id ?? $index; let itemIndex = $index) {
|
|
3428
|
+
<div class="prx-rich-editor__nested-node">
|
|
3429
|
+
<div class="prx-rich-editor__nested-actions">
|
|
3430
|
+
<strong>{{ tx('editor.item', 'Item') }} {{ itemIndex + 1 }}</strong>
|
|
3431
|
+
@if (isRemovalPending('nodes.' + $index + '.items', itemIndex)) {
|
|
3432
|
+
<button type="button" (click)="confirmComposeItemRemoval($index, itemIndex)">
|
|
3433
|
+
{{ tx('editor.confirmRemove', 'Confirm remove') }}
|
|
3434
|
+
</button>
|
|
3435
|
+
<button type="button" (click)="cancelRemoval()">
|
|
3436
|
+
{{ tx('editor.cancelRemove', 'Cancel') }}
|
|
3437
|
+
</button>
|
|
3438
|
+
} @else {
|
|
3439
|
+
<button type="button" (click)="requestNestedRemoval('nodes.' + $index + '.items', itemIndex)">
|
|
3440
|
+
{{ tx('editor.removeBlock', 'Remove') }}
|
|
3441
|
+
</button>
|
|
3442
|
+
}
|
|
3443
|
+
</div>
|
|
3444
|
+
<div class="prx-rich-editor__node-grid">
|
|
3445
|
+
<label>
|
|
3446
|
+
<span>{{ tx('editor.blockType', 'Block type') }}</span>
|
|
3447
|
+
<select
|
|
3448
|
+
[ngModel]="item.type"
|
|
3449
|
+
(ngModelChange)="changeComposeItemType($index, itemIndex, $event)"
|
|
3450
|
+
>
|
|
3451
|
+
@for (type of presenterNodeTypes; track type) {
|
|
3452
|
+
<option [value]="type">{{ tx('editor.nodeType.' + type, type) }}</option>
|
|
3453
|
+
}
|
|
3454
|
+
</select>
|
|
3455
|
+
</label>
|
|
3456
|
+
<ng-container
|
|
3457
|
+
*ngTemplateOutlet="presenterFields; context: {
|
|
3458
|
+
node: item,
|
|
3459
|
+
path: '$.nodes[' + $index + '].items[' + itemIndex + ']',
|
|
3460
|
+
setString: setComposeItemStringField.bind(this, $index, itemIndex),
|
|
3461
|
+
setNumber: setComposeItemNumberField.bind(this, $index, itemIndex)
|
|
3462
|
+
}"
|
|
3463
|
+
></ng-container>
|
|
3464
|
+
</div>
|
|
3465
|
+
</div>
|
|
3466
|
+
}
|
|
3467
|
+
</div>
|
|
3468
|
+
}
|
|
3469
|
+
@case ('card') {
|
|
3470
|
+
<label>
|
|
3471
|
+
<span>{{ tx('editor.field.title', 'Title') }}</span>
|
|
3472
|
+
<input
|
|
3473
|
+
[ngModel]="getStringField(node, 'title')"
|
|
3474
|
+
(ngModelChange)="setStringField($index, 'title', $event)"
|
|
3475
|
+
/>
|
|
3476
|
+
</label>
|
|
3477
|
+
<label>
|
|
3478
|
+
<span>{{ tx('editor.field.subtitle', 'Subtitle') }}</span>
|
|
3479
|
+
<input
|
|
3480
|
+
[ngModel]="getStringField(node, 'subtitle')"
|
|
3481
|
+
(ngModelChange)="setStringField($index, 'subtitle', $event)"
|
|
3482
|
+
/>
|
|
3483
|
+
</label>
|
|
3484
|
+
<div class="prx-rich-editor__nested-editor">
|
|
3485
|
+
<header class="prx-rich-editor__nested-header">
|
|
3486
|
+
<h5>{{ tx('editor.cardContent', 'Card content') }}</h5>
|
|
3487
|
+
<button type="button" (click)="addCardContentNode($index)">
|
|
3488
|
+
{{ tx('editor.addItem', 'Add item') }}
|
|
3489
|
+
</button>
|
|
3490
|
+
</header>
|
|
3491
|
+
@for (item of node.content; track item.id ?? $index; let itemIndex = $index) {
|
|
3492
|
+
<div class="prx-rich-editor__nested-node">
|
|
3493
|
+
<div class="prx-rich-editor__nested-actions">
|
|
3494
|
+
<strong>{{ tx('editor.item', 'Item') }} {{ itemIndex + 1 }}</strong>
|
|
3495
|
+
@if (isRemovalPending('nodes.' + $index + '.content', itemIndex)) {
|
|
3496
|
+
<button type="button" (click)="confirmCardContentRemoval($index, itemIndex)">
|
|
3497
|
+
{{ tx('editor.confirmRemove', 'Confirm remove') }}
|
|
3498
|
+
</button>
|
|
3499
|
+
<button type="button" (click)="cancelRemoval()">
|
|
3500
|
+
{{ tx('editor.cancelRemove', 'Cancel') }}
|
|
3501
|
+
</button>
|
|
3502
|
+
} @else {
|
|
3503
|
+
<button type="button" (click)="requestNestedRemoval('nodes.' + $index + '.content', itemIndex)">
|
|
3504
|
+
{{ tx('editor.removeBlock', 'Remove') }}
|
|
3505
|
+
</button>
|
|
3506
|
+
}
|
|
3507
|
+
</div>
|
|
3508
|
+
<div class="prx-rich-editor__node-grid">
|
|
3509
|
+
<label>
|
|
3510
|
+
<span>{{ tx('editor.blockType', 'Block type') }}</span>
|
|
3511
|
+
<select
|
|
3512
|
+
[ngModel]="isCardContentEditable(item.type) ? item.type : 'text'"
|
|
3513
|
+
(ngModelChange)="changeCardContentType($index, itemIndex, $event)"
|
|
3514
|
+
[disabled]="!isCardContentEditable(item.type)"
|
|
3515
|
+
>
|
|
3516
|
+
@for (type of cardContentNodeTypes; track type) {
|
|
3517
|
+
<option [value]="type">{{ tx('editor.nodeType.' + type, type) }}</option>
|
|
3518
|
+
}
|
|
3519
|
+
</select>
|
|
3520
|
+
</label>
|
|
3521
|
+
@if (isPresenterNode(item)) {
|
|
3522
|
+
<ng-container
|
|
3523
|
+
*ngTemplateOutlet="presenterFields; context: {
|
|
3524
|
+
node: item,
|
|
3525
|
+
path: '$.nodes[' + $index + '].content[' + itemIndex + ']',
|
|
3526
|
+
setString: setCardContentStringField.bind(this, $index, itemIndex),
|
|
3527
|
+
setNumber: setCardContentNumberField.bind(this, $index, itemIndex)
|
|
3528
|
+
}"
|
|
3529
|
+
></ng-container>
|
|
3530
|
+
} @else {
|
|
3531
|
+
<p class="prx-rich-editor__node-note">
|
|
3532
|
+
{{ tx('editor.advancedOnly', 'This node type is preserved and can be edited in advanced JSON.') }}
|
|
3533
|
+
</p>
|
|
3534
|
+
}
|
|
3535
|
+
</div>
|
|
3536
|
+
</div>
|
|
3537
|
+
}
|
|
3538
|
+
</div>
|
|
3539
|
+
}
|
|
3540
|
+
@case ('mediaBlock') {
|
|
3541
|
+
<label>
|
|
3542
|
+
<span>{{ tx('editor.field.avatarName', 'Avatar name') }}</span>
|
|
3543
|
+
<input
|
|
3544
|
+
[ngModel]="getMediaBlockAvatarField($index, 'name')"
|
|
3545
|
+
(ngModelChange)="setMediaBlockAvatarField($index, 'name', $event)"
|
|
3546
|
+
/>
|
|
3547
|
+
</label>
|
|
3548
|
+
<label>
|
|
3549
|
+
<span>{{ tx('editor.field.title', 'Title') }}</span>
|
|
3550
|
+
<input
|
|
3551
|
+
[ngModel]="getMediaBlockTextField($index, 'title')"
|
|
3552
|
+
(ngModelChange)="setMediaBlockTextField($index, 'title', $event)"
|
|
3553
|
+
/>
|
|
3554
|
+
</label>
|
|
3555
|
+
<label>
|
|
3556
|
+
<span>{{ tx('editor.field.subtitle', 'Subtitle') }}</span>
|
|
3557
|
+
<input
|
|
3558
|
+
[ngModel]="getMediaBlockTextField($index, 'subtitle')"
|
|
3559
|
+
(ngModelChange)="setMediaBlockTextField($index, 'subtitle', $event)"
|
|
3560
|
+
/>
|
|
3561
|
+
</label>
|
|
3562
|
+
<p class="prx-rich-editor__node-note">
|
|
3563
|
+
{{ tx('editor.mediaBlockAdvancedHelp', 'Meta and trailing content are preserved in advanced JSON.') }}
|
|
3564
|
+
</p>
|
|
3565
|
+
}
|
|
3566
|
+
@case ('timeline') {
|
|
3567
|
+
<label>
|
|
3568
|
+
<span>{{ tx('editor.field.title', 'Title') }}</span>
|
|
3569
|
+
<input
|
|
3570
|
+
[ngModel]="getStringField(node, 'title')"
|
|
3571
|
+
(ngModelChange)="setStringField($index, 'title', $event)"
|
|
3572
|
+
/>
|
|
3573
|
+
</label>
|
|
3574
|
+
<label>
|
|
3575
|
+
<span>{{ tx('editor.field.emptyText', 'Empty text') }}</span>
|
|
3576
|
+
<input
|
|
3577
|
+
[ngModel]="getStringField(node, 'emptyText')"
|
|
3578
|
+
(ngModelChange)="setStringField($index, 'emptyText', $event)"
|
|
3579
|
+
/>
|
|
3580
|
+
</label>
|
|
3581
|
+
<div class="prx-rich-editor__nested-editor">
|
|
3582
|
+
<header class="prx-rich-editor__nested-header">
|
|
3583
|
+
<h5>{{ tx('editor.timelineItems', 'Timeline items') }}</h5>
|
|
3584
|
+
<button type="button" (click)="addTimelineItem($index)">
|
|
3585
|
+
{{ tx('editor.addItem', 'Add item') }}
|
|
3586
|
+
</button>
|
|
3587
|
+
</header>
|
|
3588
|
+
@for (item of node.items; track item.id ?? $index; let itemIndex = $index) {
|
|
3589
|
+
<div class="prx-rich-editor__nested-node">
|
|
3590
|
+
<div class="prx-rich-editor__nested-actions">
|
|
3591
|
+
<strong>{{ tx('editor.item', 'Item') }} {{ itemIndex + 1 }}</strong>
|
|
3592
|
+
@if (isRemovalPending('nodes.' + $index + '.timelineItems', itemIndex)) {
|
|
3593
|
+
<button type="button" (click)="confirmTimelineItemRemoval($index, itemIndex)">
|
|
3594
|
+
{{ tx('editor.confirmRemove', 'Confirm remove') }}
|
|
3595
|
+
</button>
|
|
3596
|
+
<button type="button" (click)="cancelRemoval()">
|
|
3597
|
+
{{ tx('editor.cancelRemove', 'Cancel') }}
|
|
3598
|
+
</button>
|
|
3599
|
+
} @else {
|
|
3600
|
+
<button type="button" (click)="requestNestedRemoval('nodes.' + $index + '.timelineItems', itemIndex)">
|
|
3601
|
+
{{ tx('editor.removeBlock', 'Remove') }}
|
|
3602
|
+
</button>
|
|
3603
|
+
}
|
|
3604
|
+
</div>
|
|
3605
|
+
<div class="prx-rich-editor__node-grid">
|
|
3606
|
+
<label>
|
|
3607
|
+
<span>{{ tx('editor.field.title', 'Title') }}</span>
|
|
3608
|
+
<input
|
|
3609
|
+
[ngModel]="getTimelineItemField($index, itemIndex, 'title')"
|
|
3610
|
+
(ngModelChange)="setTimelineItemField($index, itemIndex, 'title', $event)"
|
|
3611
|
+
/>
|
|
3612
|
+
</label>
|
|
3613
|
+
<label>
|
|
3614
|
+
<span>{{ tx('editor.field.subtitle', 'Subtitle') }}</span>
|
|
3615
|
+
<input
|
|
3616
|
+
[ngModel]="getTimelineItemField($index, itemIndex, 'subtitle')"
|
|
3617
|
+
(ngModelChange)="setTimelineItemField($index, itemIndex, 'subtitle', $event)"
|
|
3618
|
+
/>
|
|
3619
|
+
</label>
|
|
3620
|
+
<label>
|
|
3621
|
+
<span>{{ tx('editor.field.badge', 'Badge') }}</span>
|
|
3622
|
+
<input
|
|
3623
|
+
[ngModel]="getTimelineItemField($index, itemIndex, 'badge')"
|
|
3624
|
+
(ngModelChange)="setTimelineItemField($index, itemIndex, 'badge', $event)"
|
|
3625
|
+
/>
|
|
3626
|
+
</label>
|
|
3627
|
+
<label>
|
|
3628
|
+
<span>{{ tx('editor.field.icon', 'Icon') }}</span>
|
|
3629
|
+
<input
|
|
3630
|
+
[ngModel]="getTimelineItemField($index, itemIndex, 'icon')"
|
|
3631
|
+
(ngModelChange)="setTimelineItemField($index, itemIndex, 'icon', $event)"
|
|
3632
|
+
[placeholder]="tx('editor.placeholder.icon', 'check_circle')"
|
|
3633
|
+
/>
|
|
3634
|
+
</label>
|
|
3635
|
+
</div>
|
|
3636
|
+
</div>
|
|
3637
|
+
}
|
|
3638
|
+
</div>
|
|
3639
|
+
}
|
|
3640
|
+
@case ('preset') {
|
|
3641
|
+
@if (presetOptions.length) {
|
|
3642
|
+
<label>
|
|
3643
|
+
<span>{{ tx('editor.field.preset', 'Preset') }}</span>
|
|
3644
|
+
<select
|
|
3645
|
+
[ngModel]="getPresetSelection(node)"
|
|
3646
|
+
(ngModelChange)="setPresetSelection($index, $event)"
|
|
3647
|
+
>
|
|
3648
|
+
@for (preset of presetOptions; track getPresetOptionValue(preset.ref)) {
|
|
3649
|
+
<option [value]="getPresetOptionValue(preset.ref)">
|
|
3650
|
+
{{ preset.label || preset.ref.presetId }}
|
|
3651
|
+
</option>
|
|
3652
|
+
}
|
|
3653
|
+
</select>
|
|
3654
|
+
</label>
|
|
3655
|
+
} @else {
|
|
3656
|
+
<p class="prx-rich-editor__node-note">
|
|
3657
|
+
{{ tx('editor.noPresets', 'No presets registered') }}.
|
|
3658
|
+
{{ tx('editor.noPresetsHelp', 'Register rich-block presets through PRAXIS_RICH_BLOCK_PRESETS to author preset references visually.') }}
|
|
3659
|
+
</p>
|
|
3660
|
+
}
|
|
3661
|
+
}
|
|
3662
|
+
@default {
|
|
3663
|
+
<p class="prx-rich-editor__node-note">
|
|
3664
|
+
{{
|
|
3665
|
+
tx(
|
|
3666
|
+
'editor.advancedOnly',
|
|
3667
|
+
'This node type is preserved and can be edited in advanced JSON.'
|
|
3668
|
+
)
|
|
3669
|
+
}}
|
|
3670
|
+
</p>
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
3673
|
+
<fieldset class="prx-rich-editor__wide-field prx-rich-editor__field-group">
|
|
3674
|
+
<legend>{{ tx('editor.commonMetadata', 'Common metadata') }}</legend>
|
|
3675
|
+
<label>
|
|
3676
|
+
<span>{{ tx('editor.field.testId', 'Test id') }}</span>
|
|
3677
|
+
<input
|
|
3678
|
+
[ngModel]="getStringField(node, 'testId')"
|
|
3679
|
+
(ngModelChange)="setStringField($index, 'testId', $event)"
|
|
3680
|
+
[placeholder]="tx('editor.placeholder.testId', 'rich-hero-title')"
|
|
3681
|
+
/>
|
|
3682
|
+
</label>
|
|
3683
|
+
<label>
|
|
3684
|
+
<span>{{ tx('editor.field.className', 'CSS class') }}</span>
|
|
3685
|
+
<input
|
|
3686
|
+
[ngModel]="getStringField(node, 'className')"
|
|
3687
|
+
(ngModelChange)="setStringField($index, 'className', $event)"
|
|
3688
|
+
[placeholder]="tx('editor.placeholder.className', 'hero-title')"
|
|
3689
|
+
/>
|
|
3690
|
+
</label>
|
|
3691
|
+
</fieldset>
|
|
3692
|
+
|
|
3693
|
+
<fieldset class="prx-rich-editor__wide-field prx-rich-editor__field-group">
|
|
3694
|
+
<legend>{{ tx('editor.visibilityRule', 'Visibility rule') }}</legend>
|
|
3695
|
+
<label>
|
|
3696
|
+
<span>{{ tx('editor.field.visibleWhenPath', 'Context path') }}</span>
|
|
3697
|
+
<input
|
|
3698
|
+
[ngModel]="getVisibleWhenPath(node)"
|
|
3699
|
+
(ngModelChange)="setVisibleWhenPath($index, $event)"
|
|
3700
|
+
[placeholder]="tx('editor.placeholder.visibleWhenPath', 'row.active')"
|
|
3701
|
+
/>
|
|
3702
|
+
</label>
|
|
3703
|
+
<label>
|
|
3704
|
+
<span>{{ tx('editor.field.visibleWhenValue', 'Expected value') }}</span>
|
|
3705
|
+
<input
|
|
3706
|
+
[ngModel]="getVisibleWhenValue(node)"
|
|
3707
|
+
(ngModelChange)="setVisibleWhenValue($index, $event)"
|
|
3708
|
+
[placeholder]="tx('editor.placeholder.visibleWhenValue', 'true')"
|
|
3709
|
+
/>
|
|
3710
|
+
</label>
|
|
3711
|
+
</fieldset>
|
|
3712
|
+
|
|
3713
|
+
<fieldset class="prx-rich-editor__wide-field prx-rich-editor__field-group">
|
|
3714
|
+
<legend>{{ tx('editor.safeStyle', 'Safe style') }}</legend>
|
|
3715
|
+
<label>
|
|
3716
|
+
<span>{{ tx('editor.field.styleName', 'CSS property') }}</span>
|
|
3717
|
+
<input
|
|
3718
|
+
[ngModel]="getFirstStyleName(node)"
|
|
3719
|
+
(ngModelChange)="setFirstStyleName($index, $event)"
|
|
3720
|
+
[placeholder]="tx('editor.placeholder.styleName', 'color')"
|
|
3721
|
+
/>
|
|
3722
|
+
</label>
|
|
3723
|
+
<label>
|
|
3724
|
+
<span>{{ tx('editor.field.styleValue', 'CSS value') }}</span>
|
|
3725
|
+
<input
|
|
3726
|
+
[ngModel]="getFirstStyleValue(node)"
|
|
3727
|
+
(ngModelChange)="setFirstStyleValue($index, $event)"
|
|
3728
|
+
[placeholder]="tx('editor.placeholder.styleValue', 'var(--md-sys-color-primary)')"
|
|
3729
|
+
/>
|
|
3730
|
+
</label>
|
|
3731
|
+
</fieldset>
|
|
3732
|
+
</div>
|
|
3733
|
+
</article>
|
|
3734
|
+
}
|
|
3735
|
+
</div>
|
|
3736
|
+
} @else {
|
|
3737
|
+
<p class="prx-rich-editor__empty">
|
|
3738
|
+
{{ tx('editor.noBlocks', 'No blocks yet. Add a block to start authoring this document.') }}
|
|
3739
|
+
</p>
|
|
3740
|
+
}
|
|
3741
|
+
</section>
|
|
3742
|
+
|
|
1002
3743
|
<label class="prx-rich-editor__document">
|
|
1003
|
-
<span>{{ tx('editor.document', '
|
|
1004
|
-
<
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
3744
|
+
<span>{{ tx('editor.document', 'Advanced JSON') }}</span>
|
|
3745
|
+
<div class="prx-rich-editor__workbench">
|
|
3746
|
+
<textarea
|
|
3747
|
+
name="rich-content-document"
|
|
3748
|
+
[(ngModel)]="documentJson"
|
|
3749
|
+
(ngModelChange)="onDocumentJsonChange()"
|
|
3750
|
+
[attr.aria-describedby]="
|
|
3751
|
+
errorMessage
|
|
3752
|
+
? 'rich-content-config-error'
|
|
3753
|
+
: 'rich-content-document-help'
|
|
3754
|
+
"
|
|
3755
|
+
[attr.aria-invalid]="!valid"
|
|
3756
|
+
spellcheck="false"
|
|
3757
|
+
data-testid="rich-content-document-input"
|
|
3758
|
+
></textarea>
|
|
3759
|
+
|
|
3760
|
+
<aside
|
|
3761
|
+
class="prx-rich-editor__inspector"
|
|
3762
|
+
[attr.aria-label]="tx('editor.inspector', 'Document inspector')"
|
|
3763
|
+
>
|
|
3764
|
+
<section>
|
|
3765
|
+
<h3>{{ tx('editor.overview', 'Overview') }}</h3>
|
|
3766
|
+
<dl>
|
|
3767
|
+
<div>
|
|
3768
|
+
<dt>{{ tx('editor.nodeCount', 'Nodes') }}</dt>
|
|
3769
|
+
<dd>{{ nodeCount }}</dd>
|
|
3770
|
+
</div>
|
|
3771
|
+
<div>
|
|
3772
|
+
<dt>{{ tx('editor.nodeTypes', 'Types') }}</dt>
|
|
3773
|
+
<dd>{{ nodeTypeSummary || tx('editor.none', 'None') }}</dd>
|
|
3774
|
+
</div>
|
|
3775
|
+
</dl>
|
|
3776
|
+
</section>
|
|
3777
|
+
|
|
3778
|
+
@if (parsedDocument) {
|
|
3779
|
+
<section>
|
|
3780
|
+
<h3>{{ tx('editor.preview', 'Preview') }}</h3>
|
|
3781
|
+
<div class="prx-rich-editor__preview">
|
|
3782
|
+
<praxis-rich-content
|
|
3783
|
+
[document]="parsedDocument"
|
|
3784
|
+
[layout]="layout"
|
|
3785
|
+
[rootClassName]="rootClassName"
|
|
3786
|
+
></praxis-rich-content>
|
|
3787
|
+
</div>
|
|
3788
|
+
</section>
|
|
3789
|
+
}
|
|
3790
|
+
</aside>
|
|
3791
|
+
</div>
|
|
1017
3792
|
</label>
|
|
1018
3793
|
|
|
1019
3794
|
@if (errorMessage) {
|
|
1020
|
-
<
|
|
3795
|
+
<div
|
|
1021
3796
|
id="rich-content-config-error"
|
|
1022
3797
|
class="prx-rich-editor__error"
|
|
1023
3798
|
data-testid="rich-content-config-error"
|
|
1024
3799
|
>
|
|
1025
|
-
{{ errorMessage }}
|
|
1026
|
-
|
|
3800
|
+
<p>{{ errorMessage }}</p>
|
|
3801
|
+
@if (validationIssues.length) {
|
|
3802
|
+
<ul>
|
|
3803
|
+
@for (issue of validationIssues; track issue.path + issue.messageKey) {
|
|
3804
|
+
<li>
|
|
3805
|
+
<code>{{ issue.path }}</code>
|
|
3806
|
+
{{ tx(issue.messageKey, issue.fallback) }}
|
|
3807
|
+
</li>
|
|
3808
|
+
}
|
|
3809
|
+
</ul>
|
|
3810
|
+
}
|
|
3811
|
+
</div>
|
|
1027
3812
|
} @else {
|
|
1028
3813
|
<p id="rich-content-document-help" class="prx-rich-editor__help">
|
|
1029
3814
|
{{
|
|
1030
3815
|
tx(
|
|
1031
3816
|
'editor.documentHelp',
|
|
1032
|
-
'
|
|
3817
|
+
'Edit the canonical RichContentDocument. The inspector validates the shape and renders a preview before apply or save.'
|
|
1033
3818
|
)
|
|
1034
3819
|
}}
|
|
1035
3820
|
</p>
|
|
@@ -1044,7 +3829,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
1044
3829
|
</button>
|
|
1045
3830
|
</div>
|
|
1046
3831
|
</section>
|
|
1047
|
-
`, styles: [":host{display:block;min-width:0}.prx-rich-editor{display:grid;gap:18px;color:var(--md-sys-color-on-surface, #1f2937)}.prx-rich-editor__header{display:flex;gap:16px;align-items:flex-start;justify-content:space-between}.prx-rich-editor__header h2{margin:0;font-size:1.25rem}.prx-rich-editor__header p{margin:6px 0 0;color:var(--md-sys-color-on-surface-variant, #5f6673)}.prx-rich-editor__eyebrow{margin:0 0 4px;text-transform:uppercase;letter-spacing:.08em;font-size:.72rem;color:var(--md-sys-color-primary, #3154e7)}.prx-rich-editor__status{flex:0 0 auto;padding:6px 10px;border-radius:999px;background:color-mix(in srgb,#16a34a 12%,transparent);color:#166534;font-size:.78rem;font-weight:700}.prx-rich-editor__status.invalid{background:color-mix(in srgb,#dc2626 12%,transparent);color:#991b1b}.prx-rich-editor__grid{display:grid;gap:14px;grid-template-columns:minmax(0,180px) minmax(0,1fr)}label,.prx-rich-editor__document{display:grid;gap:7px;font-weight:700}input,select,textarea{box-sizing:border-box;width:100%;border:1px solid var(--md-sys-color-outline-variant, #d7dce5);border-radius:12px;padding:10px 12px;font:inherit;color:inherit;background:var(--md-sys-color-surface, #fff)}textarea{min-height:360px;resize:vertical;font-family:Cascadia Code,Fira Code,Consolas,monospace;font-size:.86rem;line-height:1.45}.prx-rich-editor__help{margin:-8px 0 0;color:var(--md-sys-color-on-surface-variant, #5f6673);font-size:.85rem}.prx-rich-editor__error{margin:0;padding:10px 12px;border-radius:
|
|
3832
|
+
`, styles: [":host{display:block;min-width:0}.prx-rich-editor{display:grid;gap:18px;color:var(--md-sys-color-on-surface, #1f2937)}.prx-rich-editor__header{display:flex;gap:16px;align-items:flex-start;justify-content:space-between}.prx-rich-editor__header h2{margin:0;font-size:1.25rem}.prx-rich-editor__header p{margin:6px 0 0;color:var(--md-sys-color-on-surface-variant, #5f6673)}.prx-rich-editor__eyebrow{margin:0 0 4px;text-transform:uppercase;letter-spacing:.08em;font-size:.72rem;color:var(--md-sys-color-primary, #3154e7)}.prx-rich-editor__status{flex:0 0 auto;padding:6px 10px;border-radius:999px;background:color-mix(in srgb,#16a34a 12%,transparent);color:#166534;font-size:.78rem;font-weight:700}.prx-rich-editor__status.invalid{background:color-mix(in srgb,#dc2626 12%,transparent);color:#991b1b}.prx-rich-editor__grid{display:grid;gap:14px;grid-template-columns:minmax(0,180px) minmax(0,1fr)}.prx-rich-editor__blocks{display:grid;gap:14px}.prx-rich-editor__section-header,.prx-rich-editor__node-header{display:flex;gap:16px;align-items:flex-start;justify-content:space-between}.prx-rich-editor__section-header h3,.prx-rich-editor__node-header h4{margin:0}.prx-rich-editor__section-header p,.prx-rich-editor__node-note,.prx-rich-editor__empty{margin:6px 0 0;color:var(--md-sys-color-on-surface-variant, #5f6673);font-size:.88rem}.prx-rich-editor__add-block,.prx-rich-editor__node-actions{display:flex;flex-wrap:wrap;gap:10px;align-items:flex-end}.prx-rich-editor__add-block label{min-width:180px}.prx-rich-editor__node-list{display:grid;gap:12px}.prx-rich-editor__node-card{display:grid;gap:14px;border:1px solid var(--md-sys-color-outline-variant, #d7dce5);border-radius:8px;padding:14px;background:var(--md-sys-color-surface-container-lowest, #fff)}.prx-rich-editor__node-eyebrow{margin:0 0 4px;color:var(--md-sys-color-on-surface-variant, #5f6673);font-size:.75rem;font-weight:700;text-transform:uppercase}.prx-rich-editor__node-grid{display:grid;gap:12px;grid-template-columns:repeat(2,minmax(0,1fr))}.prx-rich-editor__wide-field,.prx-rich-editor__node-note{grid-column:1 / -1}.prx-rich-editor__field-group,.prx-rich-editor__nested-editor,.prx-rich-editor__nested-node{grid-column:1 / -1;display:grid;gap:12px}.prx-rich-editor__field-group,.prx-rich-editor__nested-node{margin:0;border:1px solid var(--md-sys-color-outline-variant, #d7dce5);border-radius:8px;padding:12px}.prx-rich-editor__field-group{grid-template-columns:repeat(2,minmax(0,1fr))}.prx-rich-editor__field-group legend{padding:0 4px;font-weight:700}.prx-rich-editor__nested-header,.prx-rich-editor__nested-actions{display:flex;flex-wrap:wrap;gap:10px;align-items:center;justify-content:space-between}.prx-rich-editor__node-grid textarea{min-height:96px;font-family:inherit}label,.prx-rich-editor__document{display:grid;gap:7px;font-weight:700}input,select,textarea{box-sizing:border-box;width:100%;border:1px solid var(--md-sys-color-outline-variant, #d7dce5);border-radius:12px;padding:10px 12px;font:inherit;color:inherit;background:var(--md-sys-color-surface, #fff)}textarea{min-height:360px;resize:vertical;font-family:Cascadia Code,Fira Code,Consolas,monospace;font-size:.86rem;line-height:1.45}.prx-rich-editor__workbench{display:grid;gap:14px;grid-template-columns:minmax(0,1.2fr) minmax(260px,.8fr)}.prx-rich-editor__inspector{display:grid;align-content:start;gap:14px;min-width:0;border:1px solid var(--md-sys-color-outline-variant, #d7dce5);border-radius:8px;padding:12px;background:var(--md-sys-color-surface-container-lowest, #fff)}.prx-rich-editor__inspector h3{margin:0 0 8px;font-size:.95rem}.prx-rich-editor__inspector dl{display:grid;gap:8px;margin:0}.prx-rich-editor__inspector dl div{display:grid;gap:2px}.prx-rich-editor__inspector dt{color:var(--md-sys-color-on-surface-variant, #5f6673);font-size:.78rem;font-weight:700}.prx-rich-editor__inspector dd{margin:0;overflow-wrap:anywhere;font-weight:600}.prx-rich-editor__preview{max-height:260px;overflow:auto;border:1px solid var(--md-sys-color-outline-variant, #d7dce5);border-radius:8px;padding:12px;background:var(--md-sys-color-surface, #fff)}.prx-rich-editor__help{margin:-8px 0 0;color:var(--md-sys-color-on-surface-variant, #5f6673);font-size:.85rem}.prx-rich-editor__error{margin:0;padding:10px 12px;border-radius:8px;color:#991b1b;background:color-mix(in srgb,#dc2626 9%,transparent)}.prx-rich-editor__error p{margin:0}.prx-rich-editor__error ul{display:grid;gap:6px;margin:8px 0 0;padding-inline-start:18px}.prx-rich-editor__error code{margin-right:4px;font-family:Cascadia Code,Fira Code,Consolas,monospace}.prx-rich-editor__actions{display:flex;flex-wrap:wrap;gap:10px}button{border:1px solid var(--md-sys-color-outline-variant, #d7dce5);border-radius:8px;padding:8px 14px;background:var(--md-sys-color-surface, #fff);color:var(--md-sys-color-primary, #3154e7);font-weight:700;cursor:pointer}button:disabled{cursor:not-allowed;opacity:.55}@media(max-width:760px){.prx-rich-editor__header,.prx-rich-editor__section-header,.prx-rich-editor__node-header,.prx-rich-editor__grid,.prx-rich-editor__field-group,.prx-rich-editor__node-grid,.prx-rich-editor__workbench{grid-template-columns:1fr;display:grid}}\n"] }]
|
|
1048
3833
|
}], propDecorators: { inputs: [{
|
|
1049
3834
|
type: Input
|
|
1050
3835
|
}] } });
|
|
@@ -1148,6 +3933,7 @@ const RICH_CONTENT_AI_CAPABILITIES = {
|
|
|
1148
3933
|
'text',
|
|
1149
3934
|
'icon',
|
|
1150
3935
|
'image',
|
|
3936
|
+
'link',
|
|
1151
3937
|
'badge',
|
|
1152
3938
|
'avatar',
|
|
1153
3939
|
'metric',
|
|
@@ -1183,7 +3969,7 @@ const RICH_CONTENT_AI_CAPABILITIES = {
|
|
|
1183
3969
|
{ path: 'document.context', category: 'richContent', valueKind: 'object', description: 'Config de escopos e aliases de contexto para bindings.' },
|
|
1184
3970
|
{ path: 'document.nodes', category: 'richNode', valueKind: 'array', description: 'Lista declarativa de nodes ricos.' },
|
|
1185
3971
|
{ path: 'document.nodes[]', category: 'richNode', valueKind: 'object', description: 'Node rich-content; o shape depende de document.nodes[].type.' },
|
|
1186
|
-
{ path: 'document.nodes[].type', category: 'richNode', valueKind: 'enum', allowedValues: ['text', 'icon', 'image', 'badge', 'avatar', 'metric', 'progress', 'compose', 'card', 'mediaBlock', 'timeline', 'preset'], description: 'Tipo canonico do node.' },
|
|
3972
|
+
{ path: 'document.nodes[].type', category: 'richNode', valueKind: 'enum', allowedValues: ['text', 'icon', 'image', 'link', 'badge', 'avatar', 'metric', 'progress', 'compose', 'card', 'mediaBlock', 'timeline', 'preset'], description: 'Tipo canonico do node.' },
|
|
1187
3973
|
{ path: 'document.nodes[].id', category: 'richNode', valueKind: 'string', description: 'Identificador estavel opcional para diffs, testes e telemetria.' },
|
|
1188
3974
|
{ path: 'document.nodes[].testId', category: 'richNode', valueKind: 'string', description: 'Test id opcional para E2E.' },
|
|
1189
3975
|
{ path: 'document.nodes[].className', category: 'richNode', valueKind: 'string', description: 'Classe CSS declarativa aplicada ao node.' },
|
|
@@ -1199,6 +3985,9 @@ const RICH_CONTENT_AI_CAPABILITIES = {
|
|
|
1199
3985
|
{ path: 'document.nodes[].src', category: 'richNode', valueKind: 'string', description: 'URL literal para image.' },
|
|
1200
3986
|
{ path: 'document.nodes[].srcExpr', category: 'richNode', valueKind: 'string', description: 'Path simples resolvido contra o contexto para image src.' },
|
|
1201
3987
|
{ path: 'document.nodes[].alt', category: 'richNode', valueKind: 'string', description: 'Texto alternativo literal para image.' },
|
|
3988
|
+
{ path: 'document.nodes[].href', category: 'richNode', valueKind: 'string', description: 'URL segura para node link.' },
|
|
3989
|
+
{ path: 'document.nodes[].target', category: 'richNode', valueKind: 'enum', allowedValues: ['_blank', '_self'], description: 'Destino permitido para node link.' },
|
|
3990
|
+
{ path: 'document.nodes[].rel', category: 'richNode', valueKind: 'string', description: 'Atributo rel opcional para node link.' },
|
|
1202
3991
|
{ path: 'document.nodes[].name', category: 'richNode', valueKind: 'string', description: 'Nome literal para avatar.' },
|
|
1203
3992
|
{ path: 'document.nodes[].nameExpr', category: 'richNode', valueKind: 'string', description: 'Path simples resolvido contra o contexto para avatar.' },
|
|
1204
3993
|
{ path: 'document.nodes[].valueExpr', category: 'richNode', valueKind: 'string', description: 'Path simples resolvido contra o contexto para metric/progress.' },
|
|
@@ -1213,6 +4002,302 @@ const RICH_CONTENT_AI_CAPABILITIES = {
|
|
|
1213
4002
|
],
|
|
1214
4003
|
};
|
|
1215
4004
|
|
|
4005
|
+
const nodeTypeEnum = [
|
|
4006
|
+
'text',
|
|
4007
|
+
'icon',
|
|
4008
|
+
'image',
|
|
4009
|
+
'link',
|
|
4010
|
+
'badge',
|
|
4011
|
+
'avatar',
|
|
4012
|
+
'metric',
|
|
4013
|
+
'progress',
|
|
4014
|
+
'compose',
|
|
4015
|
+
'card',
|
|
4016
|
+
'mediaBlock',
|
|
4017
|
+
'timeline',
|
|
4018
|
+
'preset',
|
|
4019
|
+
];
|
|
4020
|
+
const layoutEnum = ['block', 'inline'];
|
|
4021
|
+
const linkTargetEnum = ['_self', '_blank'];
|
|
4022
|
+
const richContentDocumentSchema = {
|
|
4023
|
+
type: 'object',
|
|
4024
|
+
required: ['kind', 'version', 'nodes'],
|
|
4025
|
+
properties: {
|
|
4026
|
+
kind: { const: 'praxis.rich-content' },
|
|
4027
|
+
version: { const: '1.0.0' },
|
|
4028
|
+
context: { type: 'object' },
|
|
4029
|
+
nodes: { type: 'array', items: { type: 'object' } },
|
|
4030
|
+
},
|
|
4031
|
+
};
|
|
4032
|
+
const blockPatchSchema = {
|
|
4033
|
+
type: 'object',
|
|
4034
|
+
minProperties: 1,
|
|
4035
|
+
properties: {
|
|
4036
|
+
id: { type: 'string' },
|
|
4037
|
+
testId: { type: 'string' },
|
|
4038
|
+
className: { type: 'string' },
|
|
4039
|
+
style: { type: 'object' },
|
|
4040
|
+
visibleWhen: { type: 'object' },
|
|
4041
|
+
text: { type: 'string' },
|
|
4042
|
+
textExpr: { type: 'string' },
|
|
4043
|
+
label: { type: 'string' },
|
|
4044
|
+
labelExpr: { type: 'string' },
|
|
4045
|
+
href: { type: 'string' },
|
|
4046
|
+
target: { enum: linkTargetEnum },
|
|
4047
|
+
rel: { type: 'string' },
|
|
4048
|
+
src: { type: 'string' },
|
|
4049
|
+
alt: { type: 'string' },
|
|
4050
|
+
title: { type: 'string' },
|
|
4051
|
+
subtitle: { type: 'string' },
|
|
4052
|
+
},
|
|
4053
|
+
};
|
|
4054
|
+
const PRAXIS_RICH_CONTENT_AUTHORING_MANIFEST = {
|
|
4055
|
+
schemaVersion: '1.0.0',
|
|
4056
|
+
componentId: 'praxis-rich-content',
|
|
4057
|
+
ownerPackage: '@praxisui/rich-content',
|
|
4058
|
+
configSchemaId: 'RichContentDocument',
|
|
4059
|
+
manifestVersion: '1.0.0',
|
|
4060
|
+
runtimeInputs: [
|
|
4061
|
+
{ name: 'document', type: 'RichContentDocument | null', description: 'Canonical praxis.rich-content document rendered by the component.' },
|
|
4062
|
+
{ name: 'nodes', type: 'RichBlockNode[] | null', description: 'Direct node list used only when the host controls the document envelope.' },
|
|
4063
|
+
{ name: 'context', type: 'JsonLogicDataRecord | null', description: 'External context used by expression paths and Json Logic rules.' },
|
|
4064
|
+
{ name: 'hostCapabilities', type: 'RichBlockHostCapabilities | null', description: 'Host-mediated preset, action, data and embed capabilities; functions are not serialized in public contracts.' },
|
|
4065
|
+
{ name: 'layout', type: "'block' | 'inline'", allowedValues: layoutEnum, description: 'Renderer layout mode.' },
|
|
4066
|
+
{ name: 'rootClassName', type: 'string', description: 'CSS class applied to the renderer root.' },
|
|
4067
|
+
],
|
|
4068
|
+
editableTargets: [
|
|
4069
|
+
{ kind: 'document', resolver: 'rich-content-document-root', description: 'The canonical RichContentDocument input envelope.' },
|
|
4070
|
+
{ kind: 'block', resolver: 'rich-block-by-id-or-index', description: 'A top-level document.nodes[] block with stable id preferred over index.' },
|
|
4071
|
+
{ kind: 'text', resolver: 'rich-text-node-by-id-or-path', description: 'Text-bearing nodes, including text, badge, metric, progress, card and timeline labels.' },
|
|
4072
|
+
{ kind: 'link', resolver: 'rich-link-node-by-id-or-path', description: 'Canonical link node with label, href, target and rel.' },
|
|
4073
|
+
{ kind: 'media', resolver: 'rich-media-node-by-id-or-path', description: 'Image, avatar image and mediaBlock child nodes.' },
|
|
4074
|
+
{ kind: 'preset', resolver: 'rich-block-preset-ref', description: 'Preset nodes resolved through PRAXIS_RICH_BLOCK_PRESETS or hostCapabilities.resolvePreset.' },
|
|
4075
|
+
{ kind: 'sanitizationPolicy', resolver: 'rich-content-validation-policy', description: 'Document validation policy enforced by validateRichContentDocument.' },
|
|
4076
|
+
{ kind: 'display', resolver: 'rich-content-layout-and-root-class', description: 'Layout and rootClassName runtime inputs.' },
|
|
4077
|
+
],
|
|
4078
|
+
operations: [
|
|
4079
|
+
{
|
|
4080
|
+
operationId: 'document.set',
|
|
4081
|
+
title: 'Set rich content document',
|
|
4082
|
+
scope: 'global',
|
|
4083
|
+
targetKind: 'document',
|
|
4084
|
+
target: { kind: 'document', resolver: 'rich-content-document-root', ambiguityPolicy: 'fail', required: false },
|
|
4085
|
+
inputSchema: richContentDocumentSchema,
|
|
4086
|
+
effects: [{ kind: 'set-value', path: 'document' }],
|
|
4087
|
+
validators: ['document-shape-canonical', 'document-version-supported', 'node-types-supported', 'unsafe-url-rejected', 'unsafe-style-rejected', 'editor-runtime-round-trip'],
|
|
4088
|
+
affectedPaths: ['document'],
|
|
4089
|
+
submissionImpact: false,
|
|
4090
|
+
preconditions: ['config-initialized'],
|
|
4091
|
+
},
|
|
4092
|
+
{
|
|
4093
|
+
operationId: 'block.add',
|
|
4094
|
+
title: 'Add rich content block',
|
|
4095
|
+
scope: 'templating',
|
|
4096
|
+
targetKind: 'block',
|
|
4097
|
+
target: { kind: 'block', resolver: 'document-nodes-array', ambiguityPolicy: 'fail', required: false },
|
|
4098
|
+
inputSchema: {
|
|
4099
|
+
type: 'object',
|
|
4100
|
+
required: ['type'],
|
|
4101
|
+
properties: {
|
|
4102
|
+
type: { enum: nodeTypeEnum },
|
|
4103
|
+
node: { type: 'object' },
|
|
4104
|
+
afterBlockId: { type: 'string' },
|
|
4105
|
+
beforeBlockId: { type: 'string' },
|
|
4106
|
+
},
|
|
4107
|
+
},
|
|
4108
|
+
effects: [{ kind: 'compile-domain-patch', handler: 'rich-content-block-add', handlerContract: {
|
|
4109
|
+
reads: ['document.nodes[]'],
|
|
4110
|
+
writes: ['document.nodes[]'],
|
|
4111
|
+
identityKeys: ['document.nodes[].id'],
|
|
4112
|
+
inputSchema: { type: 'object', required: ['type'], properties: { type: { enum: nodeTypeEnum }, node: { type: 'object' }, afterBlockId: { type: 'string' }, beforeBlockId: { type: 'string' } } },
|
|
4113
|
+
failureModes: ['unsupported-node-type', 'duplicate-node-id', 'invalid-node-shape', 'unsafe-url', 'unsafe-style'],
|
|
4114
|
+
description: 'Creates a supported RichBlockNode, validates it with validateRichContentDocument semantics and inserts it by stable neighbor id when provided.',
|
|
4115
|
+
} }],
|
|
4116
|
+
validators: ['node-types-supported', 'block-id-unique', 'document-shape-canonical', 'unsafe-url-rejected', 'unsafe-style-rejected', 'editor-runtime-round-trip'],
|
|
4117
|
+
affectedPaths: ['document.nodes[]'],
|
|
4118
|
+
submissionImpact: false,
|
|
4119
|
+
preconditions: ['config-initialized'],
|
|
4120
|
+
},
|
|
4121
|
+
{
|
|
4122
|
+
operationId: 'block.remove',
|
|
4123
|
+
title: 'Remove rich content block',
|
|
4124
|
+
scope: 'templating',
|
|
4125
|
+
targetKind: 'block',
|
|
4126
|
+
target: { kind: 'block', resolver: 'rich-block-by-id-or-index', ambiguityPolicy: 'fail', required: true },
|
|
4127
|
+
inputSchema: { type: 'object', properties: { blockId: { type: 'string' }, replacementBlockId: { type: 'string' } } },
|
|
4128
|
+
effects: [{ kind: 'remove-by-key', path: 'document.nodes[]', key: 'id' }],
|
|
4129
|
+
destructive: true,
|
|
4130
|
+
requiresConfirmation: true,
|
|
4131
|
+
validators: ['block-exists', 'destructive-removal-confirmed', 'document-not-empty-when-required', 'editor-runtime-round-trip'],
|
|
4132
|
+
affectedPaths: ['document.nodes[]'],
|
|
4133
|
+
submissionImpact: false,
|
|
4134
|
+
preconditions: ['config-initialized', 'target-block-exists', 'confirmation-collected'],
|
|
4135
|
+
},
|
|
4136
|
+
{
|
|
4137
|
+
operationId: 'block.order.set',
|
|
4138
|
+
title: 'Reorder rich content blocks',
|
|
4139
|
+
scope: 'templating',
|
|
4140
|
+
targetKind: 'block',
|
|
4141
|
+
target: { kind: 'block', resolver: 'rich-block-by-id-or-index', ambiguityPolicy: 'fail', required: true },
|
|
4142
|
+
inputSchema: { type: 'object', required: ['blockId'], properties: { blockId: { type: 'string' }, beforeBlockId: { type: 'string' }, afterBlockId: { type: 'string' } } },
|
|
4143
|
+
effects: [{ kind: 'reorder-by-key', path: 'document.nodes[]', key: 'id' }],
|
|
4144
|
+
validators: ['block-exists', 'block-order-deterministic', 'document-shape-canonical', 'editor-runtime-round-trip'],
|
|
4145
|
+
affectedPaths: ['document.nodes[]'],
|
|
4146
|
+
submissionImpact: false,
|
|
4147
|
+
preconditions: ['config-initialized', 'target-block-exists'],
|
|
4148
|
+
},
|
|
4149
|
+
{
|
|
4150
|
+
operationId: 'text.update',
|
|
4151
|
+
title: 'Update rich content text',
|
|
4152
|
+
scope: 'templating',
|
|
4153
|
+
targetKind: 'text',
|
|
4154
|
+
target: { kind: 'text', resolver: 'rich-text-node-by-id-or-path', ambiguityPolicy: 'fail', required: true },
|
|
4155
|
+
inputSchema: { type: 'object', minProperties: 1, properties: { text: { type: 'string' }, textExpr: { type: 'string' }, label: { type: 'string' }, labelExpr: { type: 'string' }, title: { type: 'string' }, subtitle: { type: 'string' } } },
|
|
4156
|
+
effects: [{ kind: 'merge-by-key', path: 'document.nodes[]', key: 'id' }],
|
|
4157
|
+
validators: ['block-exists', 'text-target-supports-field', 'expression-path-safe', 'document-shape-canonical', 'editor-runtime-round-trip'],
|
|
4158
|
+
affectedPaths: ['document.nodes[].text', 'document.nodes[].textExpr', 'document.nodes[].label', 'document.nodes[].labelExpr', 'document.nodes[].title', 'document.nodes[].subtitle'],
|
|
4159
|
+
submissionImpact: false,
|
|
4160
|
+
preconditions: ['config-initialized', 'target-block-exists'],
|
|
4161
|
+
},
|
|
4162
|
+
{
|
|
4163
|
+
operationId: 'link.add',
|
|
4164
|
+
title: 'Add link block',
|
|
4165
|
+
scope: 'templating',
|
|
4166
|
+
targetKind: 'link',
|
|
4167
|
+
target: { kind: 'link', resolver: 'document-nodes-array', ambiguityPolicy: 'fail', required: false },
|
|
4168
|
+
inputSchema: { type: 'object', required: ['label', 'href'], properties: { id: { type: 'string' }, label: { type: 'string' }, labelExpr: { type: 'string' }, href: { type: 'string' }, target: { enum: linkTargetEnum }, rel: { type: 'string' } } },
|
|
4169
|
+
effects: [{ kind: 'append-unique', path: 'document.nodes[]', key: 'id' }],
|
|
4170
|
+
validators: ['link-url-safe', 'link-policy-explicit', 'node-types-supported', 'block-id-unique', 'editor-runtime-round-trip'],
|
|
4171
|
+
affectedPaths: ['document.nodes[]', 'document.nodes[].href', 'document.nodes[].target', 'document.nodes[].rel'],
|
|
4172
|
+
submissionImpact: false,
|
|
4173
|
+
preconditions: ['config-initialized'],
|
|
4174
|
+
},
|
|
4175
|
+
{
|
|
4176
|
+
operationId: 'link.remove',
|
|
4177
|
+
title: 'Remove link from rich content',
|
|
4178
|
+
scope: 'templating',
|
|
4179
|
+
targetKind: 'link',
|
|
4180
|
+
target: { kind: 'link', resolver: 'rich-link-node-by-id-or-path', ambiguityPolicy: 'fail', required: true },
|
|
4181
|
+
inputSchema: { type: 'object', properties: { linkId: { type: 'string' }, preserveLabelAsText: { type: 'boolean' } } },
|
|
4182
|
+
effects: [{ kind: 'compile-domain-patch', handler: 'rich-content-link-remove', handlerContract: {
|
|
4183
|
+
reads: ['document.nodes[]', 'document.nodes[].id', 'document.nodes[].type', 'document.nodes[].label'],
|
|
4184
|
+
writes: ['document.nodes[]'],
|
|
4185
|
+
identityKeys: ['document.nodes[].id'],
|
|
4186
|
+
inputSchema: { type: 'object', properties: { linkId: { type: 'string' }, preserveLabelAsText: { type: 'boolean' } } },
|
|
4187
|
+
failureModes: ['link-not-found', 'ambiguous-link-target', 'replacement-text-invalid'],
|
|
4188
|
+
description: 'Removes a link node by stable id or path and optionally replaces it with a text node containing the prior link label.',
|
|
4189
|
+
} }],
|
|
4190
|
+
destructive: true,
|
|
4191
|
+
requiresConfirmation: true,
|
|
4192
|
+
validators: ['link-target-exists', 'destructive-removal-confirmed', 'document-shape-canonical', 'editor-runtime-round-trip'],
|
|
4193
|
+
affectedPaths: ['document.nodes[]'],
|
|
4194
|
+
submissionImpact: false,
|
|
4195
|
+
preconditions: ['config-initialized', 'target-link-exists', 'confirmation-collected'],
|
|
4196
|
+
},
|
|
4197
|
+
{
|
|
4198
|
+
operationId: 'preset.apply',
|
|
4199
|
+
title: 'Apply rich block preset',
|
|
4200
|
+
scope: 'templating',
|
|
4201
|
+
targetKind: 'preset',
|
|
4202
|
+
target: { kind: 'preset', resolver: 'rich-block-preset-ref', ambiguityPolicy: 'fail', required: false },
|
|
4203
|
+
inputSchema: { type: 'object', required: ['ref'], properties: { ref: { type: 'object' }, inputs: { type: 'object' }, replaceBlockId: { type: 'string' } } },
|
|
4204
|
+
effects: [{ kind: 'compile-domain-patch', handler: 'rich-content-preset-apply', handlerContract: {
|
|
4205
|
+
reads: ['PRAXIS_RICH_BLOCK_PRESETS', 'hostCapabilities.resolvePreset', 'document.nodes[]'],
|
|
4206
|
+
writes: ['document.nodes[]'],
|
|
4207
|
+
identityKeys: ['ref.kind', 'ref.namespace', 'ref.presetId', 'ref.version'],
|
|
4208
|
+
inputSchema: { type: 'object', required: ['ref'], properties: { ref: { type: 'object' }, inputs: { type: 'object' }, replaceBlockId: { type: 'string' } } },
|
|
4209
|
+
failureModes: ['preset-not-found', 'preset-kind-invalid', 'preset-document-invalid', 'preset-cycle-detected'],
|
|
4210
|
+
description: 'Resolves a rich-block preset through the governed registry/host mediation and inserts or replaces a preset reference without serializing functions.',
|
|
4211
|
+
} }],
|
|
4212
|
+
validators: ['preset-ref-valid', 'preset-exists-or-host-mediated', 'host-capabilities-serializable', 'document-shape-canonical', 'editor-runtime-round-trip'],
|
|
4213
|
+
affectedPaths: ['document.nodes[]', 'document.nodes[].ref', 'document.nodes[].inputs', 'hostCapabilities'],
|
|
4214
|
+
submissionImpact: false,
|
|
4215
|
+
preconditions: ['config-initialized'],
|
|
4216
|
+
},
|
|
4217
|
+
{
|
|
4218
|
+
operationId: 'sanitizationPolicy.set',
|
|
4219
|
+
title: 'Set rich content sanitization policy',
|
|
4220
|
+
scope: 'global',
|
|
4221
|
+
targetKind: 'sanitizationPolicy',
|
|
4222
|
+
target: { kind: 'sanitizationPolicy', resolver: 'rich-content-validation-policy', ambiguityPolicy: 'fail', required: false },
|
|
4223
|
+
inputSchema: { type: 'object', properties: { allowHtml: { const: false }, allowedUrlProtocols: { type: 'array', items: { type: 'string' } }, allowImageDataUrls: { type: 'boolean' }, maxNodeDepth: { type: 'number' }, maxNodeCount: { type: 'number' } } },
|
|
4224
|
+
effects: [{ kind: 'compile-domain-patch', handler: 'rich-content-sanitization-policy', handlerContract: {
|
|
4225
|
+
reads: ['document', 'validateRichContentDocument'],
|
|
4226
|
+
writes: ['diagnostics'],
|
|
4227
|
+
identityKeys: ['document.kind', 'document.version'],
|
|
4228
|
+
inputSchema: { type: 'object', properties: { allowHtml: { const: false }, allowedUrlProtocols: { type: 'array', items: { type: 'string' } }, allowImageDataUrls: { type: 'boolean' }, maxNodeDepth: { type: 'number' }, maxNodeCount: { type: 'number' } } },
|
|
4229
|
+
failureModes: ['html-not-supported', 'unsafe-protocol', 'max-depth-too-high', 'max-node-count-too-high'],
|
|
4230
|
+
description: 'Validates requested policy against the fixed validator guardrails; rich-content authoring remains structured JSON and does not allow arbitrary HTML.',
|
|
4231
|
+
} }],
|
|
4232
|
+
destructive: true,
|
|
4233
|
+
requiresConfirmation: true,
|
|
4234
|
+
validators: ['sanitization-policy-explicit', 'unsafe-html-rejected', 'unsafe-url-rejected', 'security-change-confirmed', 'editor-runtime-round-trip'],
|
|
4235
|
+
affectedPaths: ['document', 'diagnostics'],
|
|
4236
|
+
submissionImpact: false,
|
|
4237
|
+
preconditions: ['config-initialized', 'confirmation-collected'],
|
|
4238
|
+
},
|
|
4239
|
+
{
|
|
4240
|
+
operationId: 'display.mode.set',
|
|
4241
|
+
title: 'Set rich content display mode',
|
|
4242
|
+
scope: 'global',
|
|
4243
|
+
targetKind: 'display',
|
|
4244
|
+
target: { kind: 'display', resolver: 'rich-content-layout-and-root-class', ambiguityPolicy: 'fail', required: false },
|
|
4245
|
+
inputSchema: { type: 'object', properties: { layout: { enum: layoutEnum }, rootClassName: { type: 'string' } } },
|
|
4246
|
+
effects: [{ kind: 'set-value', path: 'layout' }, { kind: 'set-value', path: 'rootClassName' }],
|
|
4247
|
+
validators: ['layout-valid', 'root-class-safe', 'editor-runtime-round-trip'],
|
|
4248
|
+
affectedPaths: ['layout', 'rootClassName'],
|
|
4249
|
+
submissionImpact: false,
|
|
4250
|
+
preconditions: ['config-initialized'],
|
|
4251
|
+
},
|
|
4252
|
+
],
|
|
4253
|
+
validators: [
|
|
4254
|
+
{ validatorId: 'document-shape-canonical', level: 'error', code: 'PRC001', description: 'Documents must satisfy RichContentDocument with kind praxis.rich-content, version 1.0.0 and a nodes array.' },
|
|
4255
|
+
{ validatorId: 'document-version-supported', level: 'error', code: 'PRC002', description: 'Only document version 1.0.0 is supported.' },
|
|
4256
|
+
{ validatorId: 'node-types-supported', level: 'error', code: 'PRC003', description: 'Nodes must use supported rich-content node types, including link as a structured node rather than arbitrary HTML.' },
|
|
4257
|
+
{ validatorId: 'block-id-unique', level: 'warning', code: 'PRC004', description: 'Stable block ids should be unique when present; operations should prefer ids over array indexes.' },
|
|
4258
|
+
{ validatorId: 'block-exists', level: 'error', code: 'PRC005', description: 'Target block must exist before patching, reordering or removing it.' },
|
|
4259
|
+
{ validatorId: 'block-order-deterministic', level: 'error', code: 'PRC006', description: 'Block ordering must use stable ids or explicit before/after placement and not rely on transient generated indexes.' },
|
|
4260
|
+
{ validatorId: 'destructive-removal-confirmed', level: 'error', code: 'PRC007', description: 'Block and link removal are destructive and require confirmation.' },
|
|
4261
|
+
{ validatorId: 'document-not-empty-when-required', level: 'warning', code: 'PRC008', description: 'Removal should not leave an empty document unless the user explicitly requested an empty rich content document.' },
|
|
4262
|
+
{ validatorId: 'text-target-supports-field', level: 'error', code: 'PRC009', description: 'Text updates may only target fields supported by the selected node type.' },
|
|
4263
|
+
{ validatorId: 'expression-path-safe', level: 'warning', code: 'PRC010', description: 'Expression paths must remain simple context paths or Json Logic expressions, not string DSL code.' },
|
|
4264
|
+
{ validatorId: 'link-url-safe', level: 'error', code: 'PRC011', description: 'Link href values must reject javascript:, vbscript:, file: and unsafe data URLs.' },
|
|
4265
|
+
{ validatorId: 'link-policy-explicit', level: 'error', code: 'PRC012', description: 'Links must declare safe target/rel behavior; _blank links default to noopener noreferrer.' },
|
|
4266
|
+
{ validatorId: 'link-target-exists', level: 'error', code: 'PRC013', description: 'Link removal must resolve an existing link node by stable id or path.' },
|
|
4267
|
+
{ validatorId: 'unsafe-url-rejected', level: 'error', code: 'PRC014', description: 'Image, avatar and link URL fields must reject unsafe protocols and null bytes.' },
|
|
4268
|
+
{ validatorId: 'unsafe-style-rejected', level: 'error', code: 'PRC015', description: 'Inline style values must reject script, import, binding and data URL expressions.' },
|
|
4269
|
+
{ validatorId: 'unsafe-html-rejected', level: 'error', code: 'PRC016', description: 'Rich-content authoring must remain structured document JSON and must not accept arbitrary HTML/script patches.' },
|
|
4270
|
+
{ validatorId: 'sanitization-policy-explicit', level: 'error', code: 'PRC017', description: 'Security policy requests must keep HTML disabled and URL/style guardrails explicit.' },
|
|
4271
|
+
{ validatorId: 'security-change-confirmed', level: 'error', code: 'PRC018', description: 'Sanitization policy changes require explicit confirmation.' },
|
|
4272
|
+
{ validatorId: 'preset-ref-valid', level: 'error', code: 'PRC019', description: 'Preset refs must use kind rich-block plus namespace, presetId and optional version.' },
|
|
4273
|
+
{ validatorId: 'preset-exists-or-host-mediated', level: 'error', code: 'PRC020', description: 'Preset application must resolve through PRAXIS_RICH_BLOCK_PRESETS or a host-mediated resolver.' },
|
|
4274
|
+
{ validatorId: 'host-capabilities-serializable', level: 'error', code: 'PRC021', description: 'Public contracts must not serialize host capability functions; only refs and inputs are persisted.' },
|
|
4275
|
+
{ validatorId: 'layout-valid', level: 'error', code: 'PRC022', description: 'Layout must be block or inline.' },
|
|
4276
|
+
{ validatorId: 'root-class-safe', level: 'warning', code: 'PRC023', description: 'rootClassName must contain safe CSS class tokens.' },
|
|
4277
|
+
{ validatorId: 'editor-runtime-round-trip', level: 'error', code: 'PRC024', description: 'Config editor, Settings Panel payload and runtime renderer must preserve document structure, layout and rootClassName.' },
|
|
4278
|
+
],
|
|
4279
|
+
roundTripRequirements: [
|
|
4280
|
+
'The editor must produce the same widget input envelope consumed by page-builder: inputs.document, inputs.layout and inputs.rootClassName.',
|
|
4281
|
+
'Rich content must remain structured RichContentDocument JSON; arbitrary HTML and script URL patches are rejected before persistence.',
|
|
4282
|
+
'Block identity should prefer document.nodes[].id; array index is a resolver fallback only and cannot be the canonical identity for generated patches.',
|
|
4283
|
+
'Link authoring uses canonical link nodes with safe href/target/rel instead of markdown or raw HTML embedded in text nodes.',
|
|
4284
|
+
'Preset authoring persists only rich-block refs and inputs; host capability functions remain runtime-mediated and are not serialized.',
|
|
4285
|
+
],
|
|
4286
|
+
examples: [
|
|
4287
|
+
{ id: 'set-empty-document', request: 'Start with an empty rich content document.', operationId: 'document.set', params: { kind: 'praxis.rich-content', version: '1.0.0', nodes: [] }, isPositive: true },
|
|
4288
|
+
{ id: 'add-text-block', request: 'Add a text block saying Welcome.', operationId: 'block.add', params: { type: 'text', node: { id: 'welcome', type: 'text', text: 'Welcome' } }, isPositive: true },
|
|
4289
|
+
{ id: 'remove-obsolete-block', request: 'Remove the obsolete block.', operationId: 'block.remove', target: 'obsolete', params: { blockId: 'obsolete' }, isPositive: true },
|
|
4290
|
+
{ id: 'move-summary-before-details', request: 'Move summary before details.', operationId: 'block.order.set', target: 'summary', params: { blockId: 'summary', beforeBlockId: 'details' }, isPositive: true },
|
|
4291
|
+
{ id: 'update-title-text', request: 'Change the hero title to Customer overview.', operationId: 'text.update', target: 'hero-title', params: { text: 'Customer overview' }, isPositive: true },
|
|
4292
|
+
{ id: 'add-docs-link', request: 'Add a link to the documentation.', operationId: 'link.add', params: { id: 'docs-link', label: 'Documentation', href: '/docs', target: '_self' }, isPositive: true },
|
|
4293
|
+
{ id: 'reject-script-link', request: 'Add a link to javascript:alert(1).', operationId: 'link.add', params: { label: 'Bad link', href: 'javascript:alert(1)' }, isPositive: false },
|
|
4294
|
+
{ id: 'remove-docs-link', request: 'Remove the documentation link but keep its label as text.', operationId: 'link.remove', target: 'docs-link', params: { linkId: 'docs-link', preserveLabelAsText: true }, isPositive: true },
|
|
4295
|
+
{ id: 'apply-profile-preset', request: 'Apply the profile summary rich-block preset.', operationId: 'preset.apply', params: { ref: { kind: 'rich-block', namespace: 'praxis.rich-content', presetId: 'profile-summary', version: '1.0.0' } }, isPositive: true },
|
|
4296
|
+
{ id: 'reject-html-policy', request: 'Allow arbitrary HTML in this rich content document.', operationId: 'sanitizationPolicy.set', params: { allowHtml: true }, isPositive: false },
|
|
4297
|
+
{ id: 'compact-inline-layout', request: 'Render this rich content inline with root class status-line.', operationId: 'display.mode.set', params: { layout: 'inline', rootClassName: 'status-line' }, isPositive: true },
|
|
4298
|
+
],
|
|
4299
|
+
};
|
|
4300
|
+
|
|
1216
4301
|
/*
|
|
1217
4302
|
* Public API Surface of praxis-rich-content
|
|
1218
4303
|
*/
|
|
@@ -1221,4 +4306,4 @@ const RICH_CONTENT_AI_CAPABILITIES = {
|
|
|
1221
4306
|
* Generated bundle index. Do not edit.
|
|
1222
4307
|
*/
|
|
1223
4308
|
|
|
1224
|
-
export { PRAXIS_RICH_BLOCK_PRESETS, PRAXIS_RICH_CONTENT_COMPONENT_METADATA, PraxisRichContent, PraxisRichContentConfigEditor, RICH_CONTENT_AI_CAPABILITIES, RichContentPresetRegistryService, createPraxisRichContentI18nConfig, providePraxisRichContentI18n, providePraxisRichContentMetadata, resolvePraxisRichContentText };
|
|
4309
|
+
export { PRAXIS_RICH_BLOCK_PRESETS, PRAXIS_RICH_CONTENT_AUTHORING_MANIFEST, PRAXIS_RICH_CONTENT_COMPONENT_METADATA, PraxisRichContent, PraxisRichContentConfigEditor, RICH_CONTENT_AI_CAPABILITIES, RichContentPresetRegistryService, createPraxisRichContentI18nConfig, isValidRichContentDocument, providePraxisRichContentI18n, providePraxisRichContentMetadata, resolvePraxisRichContentText, validateRichContentDocument };
|