@acorex-platform/module-content-management-client 0.0.21

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.
@@ -0,0 +1,694 @@
1
+ import * as i7 from '@acorex/components/button';
2
+ import { AXButtonModule } from '@acorex/components/button';
3
+ import * as i1 from '@acorex/components/decorators';
4
+ import { AXDecoratorModule } from '@acorex/components/decorators';
5
+ import * as i8 from '@acorex/components/dropdown';
6
+ import { AXDropdownModule } from '@acorex/components/dropdown';
7
+ import { AXFormModule } from '@acorex/components/form';
8
+ import * as i3 from '@acorex/components/tabs';
9
+ import { AXTabsModule } from '@acorex/components/tabs';
10
+ import * as i4 from '@acorex/components/text-area';
11
+ import { AXTextAreaModule } from '@acorex/components/text-area';
12
+ import * as i5 from '@acorex/components/text-box';
13
+ import { AXTextBoxModule } from '@acorex/components/text-box';
14
+ import { AXToastService } from '@acorex/components/toast';
15
+ import * as i6 from '@acorex/components/wysiwyg';
16
+ import { AXWysiwygModule } from '@acorex/components/wysiwyg';
17
+ import * as i2$1 from '@acorex/core/translation';
18
+ import { AXTranslationService, AXTranslationModule } from '@acorex/core/translation';
19
+ import { AXValidationModule } from '@acorex/core/validation';
20
+ import { AXPSettingsService, AXPTokenDefinitionService } from '@acorex-platform/framework-client/common';
21
+ import { AXPEntityService } from '@acorex-platform/framework-client/layout/entity';
22
+ import { AXPValueWidgetComponent } from '@acorex-platform/framework-client/layout/widget-core';
23
+ import { AXPCommandExecutor } from '@acorex-platform/framework-client/runtime';
24
+ import * as i1$1 from '@angular/common';
25
+ import { CommonModule } from '@angular/common';
26
+ import * as i0 from '@angular/core';
27
+ import { inject, DestroyRef, ElementRef, viewChild, signal, computed, effect, ChangeDetectionStrategy, Component } from '@angular/core';
28
+ import { DomSanitizer } from '@angular/platform-browser';
29
+ import * as i2 from '@angular/forms';
30
+ import { FormsModule } from '@angular/forms';
31
+ import { runtimeCommandPayloadData } from '@acorex-platform/framework-shared/core';
32
+ import { AXM_TEXT_TEMPLATE_KIND, AXM_TEXT_TEMPLATE_CONTENT_SLOT, resolveTextTemplateLocalizedString, AXM_TEXT_TEMPLATE_PREVIEW_COMMAND_KEY, collectTextTemplatePlaceholderNames, RootConfig, AXM_TEXT_TEMPLATE_MAX_PARENT_DEPTH, composeTextTemplateBodyChain, replaceTextTemplatePlaceholders } from '@acorex-platform/module-content-management-client/contracts';
33
+ import { AXP_TEXT_TEMPLATE_CHANNEL_DEFINITION_PROVIDER } from './acorex-platform-module-content-management-client.mjs';
34
+
35
+ class AXMTemplateContentEditorWidgetEditComponent extends AXPValueWidgetComponent {
36
+ /** Returns icon class for a variable/token based on kind and dataType */
37
+ getVariableIcon(v) {
38
+ const kindMap = {
39
+ Static: 'fa-light fa-database',
40
+ Context: 'fa-light fa-layer-group',
41
+ Event: 'fa-light fa-bolt',
42
+ Computed: 'fa-light fa-calculator',
43
+ Secret: 'fa-light fa-key',
44
+ };
45
+ const dataTypeMap = {
46
+ String: 'fa-light fa-font',
47
+ Integer: 'fa-light fa-hashtag',
48
+ Boolean: 'fa-light fa-toggle-on',
49
+ Decimal: 'fa-light fa-calculator',
50
+ DateTime: 'fa-light fa-clock',
51
+ Json: 'fa-light fa-braces',
52
+ };
53
+ const byKind = v.kind && kindMap[v.kind];
54
+ if (byKind)
55
+ return byKind;
56
+ const byDataType = v.dataType && dataTypeMap[v.dataType];
57
+ if (byDataType)
58
+ return byDataType;
59
+ return 'fa-light fa-code';
60
+ }
61
+ /** Returns icon class for a channel based on name and type */
62
+ getChannelIcon(ch) {
63
+ const nameMap = {
64
+ email: 'fa-light fa-envelope',
65
+ sms: 'fa-light fa-comment-sms',
66
+ whatsapp: 'fa-brands fa-whatsapp',
67
+ push: 'fa-light fa-bell',
68
+ telegram: 'fa-brands fa-telegram',
69
+ 'in-app': 'fa-light fa-inbox',
70
+ };
71
+ const byName = nameMap[ch.name?.toLowerCase() ?? ''];
72
+ if (byName)
73
+ return byName;
74
+ return ch.type === 'rich' ? 'fa-light fa-file-lines' : 'fa-light fa-align-left';
75
+ }
76
+ getTemplateLocale() {
77
+ return this.translationService.getActiveLang() ?? 'en-US';
78
+ }
79
+ /**
80
+ * Builds a mutable locale map from a stored channel field (string legacy or ML object).
81
+ */
82
+ normalizeChannelFieldToMl(existing) {
83
+ if (existing == null || existing === '') {
84
+ return {};
85
+ }
86
+ if (typeof existing === 'string') {
87
+ const s = existing;
88
+ return { 'en-US': s, 'fa-IR': s };
89
+ }
90
+ return { ...existing };
91
+ }
92
+ constructor() {
93
+ super();
94
+ /** Stored cursor/selection state before Insert Variable dropdown opens */
95
+ this.storedCursor = null;
96
+ this.channelProviders = inject(AXP_TEXT_TEMPLATE_CHANNEL_DEFINITION_PROVIDER, {
97
+ optional: true,
98
+ });
99
+ this.settingsService = inject(AXPSettingsService);
100
+ this.translationService = inject(AXTranslationService);
101
+ this.entityService = inject(AXPEntityService);
102
+ this.tokenDefinitionService = inject(AXPTokenDefinitionService, { optional: true });
103
+ this.commandExecutor = inject(AXPCommandExecutor);
104
+ this.toastService = inject(AXToastService);
105
+ this.sanitizer = inject(DomSanitizer);
106
+ this.destroyRef = inject(DestroyRef);
107
+ this.hostRef = inject((ElementRef));
108
+ /** Dedupes in-flight preview loads when effect re-runs. */
109
+ this.previewRequestSeq = 0;
110
+ this.wysiwygRef = viewChild('wysiwygRef', /* @ts-ignore */
111
+ ...(ngDevMode ? [{ debugName: "wysiwygRef" }] : /* istanbul ignore next */ []));
112
+ this.bodyTextareaRef = viewChild('bodyTextarea', { ...(ngDevMode ? { debugName: "bodyTextareaRef" } : /* istanbul ignore next */ {}), read: (ElementRef) });
113
+ this.availableChannels = signal([], /* @ts-ignore */
114
+ ...(ngDevMode ? [{ debugName: "availableChannels" }] : /* istanbul ignore next */ []));
115
+ this.channelConfigured = signal({}, /* @ts-ignore */
116
+ ...(ngDevMode ? [{ debugName: "channelConfigured" }] : /* istanbul ignore next */ []));
117
+ this.availableVariables = signal([], /* @ts-ignore */
118
+ ...(ngDevMode ? [{ debugName: "availableVariables" }] : /* istanbul ignore next */ []));
119
+ this.activeTabIndex = signal(0, /* @ts-ignore */
120
+ ...(ngDevMode ? [{ debugName: "activeTabIndex" }] : /* istanbul ignore next */ []));
121
+ this.viewMode = signal('edit', /* @ts-ignore */
122
+ ...(ngDevMode ? [{ debugName: "viewMode" }] : /* istanbul ignore next */ []));
123
+ /** Resolved values for local preview fallback: expression/token evaluation, then sampleValue */
124
+ this.resolvedPreviewValues = signal({}, /* @ts-ignore */
125
+ ...(ngDevMode ? [{ debugName: "resolvedPreviewValues" }] : /* istanbul ignore next */ []));
126
+ /** Parent-chain bodies (leaf → root) for the active locale, keyed by channel (local fallback). */
127
+ this.composedPreviewBodies = signal({}, /* @ts-ignore */
128
+ ...(ngDevMode ? [{ debugName: "composedPreviewBodies" }] : /* istanbul ignore next */ []));
129
+ /** Server Preview results keyed by channel (preferred over local compose). */
130
+ this.serverPreviewByChannel = signal({}, /* @ts-ignore */
131
+ ...(ngDevMode ? [{ debugName: "serverPreviewByChannel" }] : /* istanbul ignore next */ []));
132
+ this.activeChannel = computed(() => {
133
+ const idx = this.activeTabIndex();
134
+ const chs = this.availableChannels();
135
+ return chs[idx] ?? null;
136
+ }, /* @ts-ignore */
137
+ ...(ngDevMode ? [{ debugName: "activeChannel" }] : /* istanbul ignore next */ []));
138
+ /** True when the host entity form has Kind = Master. */
139
+ this.isMasterKind = computed(() => {
140
+ this.contextService.changeEvent();
141
+ const kind = this.contextService.getValue('kind');
142
+ return kind === AXM_TEXT_TEMPLATE_KIND.Master;
143
+ }, /* @ts-ignore */
144
+ ...(ngDevMode ? [{ debugName: "isMasterKind" }] : /* istanbul ignore next */ []));
145
+ this.otherChannelsWithContent = (currentName) => this.availableChannels().filter((c) => c.name !== currentName && (this.getChannelBody(c.name) || (c.hasSubject && this.getChannelSubject(c.name))));
146
+ effect(() => {
147
+ this.viewMode();
148
+ this.activeTabIndex();
149
+ this.getValue();
150
+ this.contextService.changeEvent();
151
+ if (this.viewMode() === 'preview') {
152
+ void this.loadServerPreview();
153
+ }
154
+ else {
155
+ this.previewRequestSeq += 1;
156
+ this.resolvedPreviewValues.set({});
157
+ this.composedPreviewBodies.set({});
158
+ this.serverPreviewByChannel.set({});
159
+ }
160
+ });
161
+ this.settingsService.onChanged.subscribe(() => {
162
+ void this.refreshChannelConfiguration();
163
+ });
164
+ }
165
+ isChannelConfigured(name) {
166
+ return this.channelConfigured()[name] ?? true;
167
+ }
168
+ async openChannelConfigure(channel) {
169
+ const configure = channel.configureSettings;
170
+ if (!configure) {
171
+ return;
172
+ }
173
+ const saved = await this.settingsService.showSectionPopup({
174
+ scope: configure.scope,
175
+ section: configure.section,
176
+ group: configure.group,
177
+ });
178
+ if (saved) {
179
+ await this.refreshChannelConfiguration();
180
+ }
181
+ }
182
+ async refreshChannelConfiguration() {
183
+ const channels = this.availableChannels();
184
+ const next = {};
185
+ for (const channel of channels) {
186
+ next[channel.name] = await this.isChannelConfiguredInternal(channel);
187
+ }
188
+ this.channelConfigured.set(next);
189
+ }
190
+ async isChannelConfiguredInternal(channel) {
191
+ const requiredKeys = channel.requiredSettingKeys ?? [];
192
+ if (!requiredKeys.length) {
193
+ return true;
194
+ }
195
+ const scopeKey = channel.configureSettings?.scope ?? 'T';
196
+ const scoped = this.settingsService.scope(scopeKey);
197
+ for (const key of requiredKeys) {
198
+ const value = await scoped.get(key);
199
+ if (!this.hasNonEmptySettingValue(value)) {
200
+ return false;
201
+ }
202
+ }
203
+ return true;
204
+ }
205
+ hasNonEmptySettingValue(value) {
206
+ if (value == null) {
207
+ return false;
208
+ }
209
+ if (typeof value === 'string') {
210
+ return value.trim().length > 0;
211
+ }
212
+ if (typeof value === 'number') {
213
+ return Number.isFinite(value);
214
+ }
215
+ if (typeof value === 'boolean') {
216
+ return true;
217
+ }
218
+ if (typeof value === 'object' && 'value' in value) {
219
+ return this.hasNonEmptySettingValue(value.value);
220
+ }
221
+ return String(value).trim().length > 0;
222
+ }
223
+ ngAfterViewInit() {
224
+ const el = this.hostRef.nativeElement;
225
+ const handleMousedown = (e) => {
226
+ const target = e.target;
227
+ if (!target || !el.contains(target))
228
+ return;
229
+ const trigger = target.closest?.('[data-insert-var-trigger]');
230
+ if (!trigger)
231
+ return;
232
+ this.captureCursorBeforeInsert();
233
+ };
234
+ el.addEventListener('mousedown', handleMousedown, { capture: true });
235
+ this.destroyRef.onDestroy(() => el.removeEventListener('mousedown', handleMousedown, { capture: true }));
236
+ }
237
+ async ngOnInit() {
238
+ super.ngOnInit();
239
+ const rawCh = this.channelProviders;
240
+ const chProviders = rawCh == null ? [] : Array.isArray(rawCh) ? rawCh : [rawCh];
241
+ const allChannels = [];
242
+ for (const p of chProviders) {
243
+ if (p?.getChannels)
244
+ allChannels.push(...p.getChannels());
245
+ }
246
+ this.availableChannels.set(Array.from(new Map(allChannels.map((c) => [c.name, c])).values()));
247
+ await this.refreshChannelConfiguration();
248
+ try {
249
+ const tokensAccessor = this.entityService.withEntity('PlatformManagement', 'Tokens').data();
250
+ const result = await tokensAccessor.query({ skip: 0, take: 500 });
251
+ const items = (result.items ?? []);
252
+ const vars = items
253
+ .map((t) => ({
254
+ name: String(t.name ?? ''),
255
+ title: String(t.title ?? t.name ?? ''),
256
+ sampleValue: t.sampleValue != null ? String(t.sampleValue) : undefined,
257
+ kind: t.kind,
258
+ dataType: t.dataType,
259
+ }))
260
+ .filter((v) => v.name);
261
+ this.availableVariables.set(Array.from(new Map(vars.map((v) => [v.name, v])).values()));
262
+ }
263
+ catch {
264
+ this.availableVariables.set([]);
265
+ }
266
+ const value = this.getValue();
267
+ if (value && typeof value === 'object' && Object.keys(value).length > 0) {
268
+ const names = Object.keys(value);
269
+ const chs = this.availableChannels();
270
+ const idx = chs.findIndex((c) => names.includes(c.name));
271
+ if (idx >= 0)
272
+ this.activeTabIndex.set(idx);
273
+ }
274
+ }
275
+ /** Capture cursor/selection before dropdown opens (runs in mousedown capture phase) */
276
+ captureCursorBeforeInsert() {
277
+ const ch = this.activeChannel();
278
+ if (!ch)
279
+ return;
280
+ this.storedCursor = null;
281
+ const active = document.activeElement;
282
+ if (!active)
283
+ return;
284
+ if (ch.type === 'plain') {
285
+ const textarea = this.getTextareaElement(active);
286
+ if (textarea) {
287
+ this.storedCursor = {
288
+ channelName: ch.name,
289
+ isPlain: true,
290
+ selectionStart: textarea.selectionStart,
291
+ selectionEnd: textarea.selectionEnd,
292
+ };
293
+ }
294
+ }
295
+ else {
296
+ const editable = this.getContentEditableElement(active);
297
+ if (editable) {
298
+ const sel = document.getSelection();
299
+ if (sel && sel.rangeCount > 0) {
300
+ const range = sel.getRangeAt(0).cloneRange();
301
+ this.storedCursor = {
302
+ channelName: ch.name,
303
+ isPlain: false,
304
+ range,
305
+ };
306
+ }
307
+ }
308
+ }
309
+ }
310
+ getTextareaElement(el) {
311
+ if (el instanceof HTMLTextAreaElement)
312
+ return el;
313
+ return el.querySelector?.('textarea') ?? null;
314
+ }
315
+ getContentEditableElement(el) {
316
+ if (el.isContentEditable)
317
+ return el;
318
+ return el.closest?.('[contenteditable="true"]') ?? null;
319
+ }
320
+ insertVariable(channelName, varName) {
321
+ this.insertToken(channelName, `{{${varName}}}`);
322
+ }
323
+ insertContentSlot(channelName) {
324
+ this.insertToken(channelName, AXM_TEXT_TEMPLATE_CONTENT_SLOT);
325
+ }
326
+ insertToken(channelName, token) {
327
+ const current = this.getChannelBody(channelName);
328
+ const stored = this.storedCursor;
329
+ this.storedCursor = null;
330
+ if (stored && stored.channelName === channelName) {
331
+ if (stored.isPlain) {
332
+ const start = stored.selectionStart ?? current.length;
333
+ const end = stored.selectionEnd ?? start;
334
+ const before = current.slice(0, start);
335
+ const after = current.slice(end);
336
+ const newBody = before + token + after;
337
+ this.setChannelBody(channelName, newBody);
338
+ const newCursor = start + token.length;
339
+ queueMicrotask(() => this.restoreTextareaCursor(newCursor));
340
+ }
341
+ else if (stored.range) {
342
+ try {
343
+ const startNode = stored.range.startContainer;
344
+ const elem = startNode.nodeType === Node.TEXT_NODE
345
+ ? startNode.parentElement
346
+ : startNode;
347
+ const editable = elem?.closest?.('[contenteditable="true"]');
348
+ const wysiwyg = this.wysiwygRef();
349
+ const target = editable ?? wysiwyg?.getHostElement()?.querySelector?.('[contenteditable="true"]');
350
+ if (target) {
351
+ target.focus();
352
+ const sel = document.getSelection();
353
+ if (sel) {
354
+ sel.removeAllRanges();
355
+ sel.addRange(stored.range);
356
+ document.execCommand('insertText', false, token);
357
+ }
358
+ }
359
+ else {
360
+ this.setChannelBody(channelName, current + token);
361
+ }
362
+ }
363
+ catch {
364
+ this.setChannelBody(channelName, current + token);
365
+ }
366
+ }
367
+ }
368
+ else {
369
+ this.setChannelBody(channelName, current + token);
370
+ }
371
+ }
372
+ restoreTextareaCursor(position) {
373
+ const ref = this.bodyTextareaRef();
374
+ const el = ref?.nativeElement;
375
+ const textarea = el?.querySelector?.('textarea') ?? (el instanceof HTMLTextAreaElement ? el : null);
376
+ if (textarea) {
377
+ textarea.focus();
378
+ textarea.setSelectionRange(position, position);
379
+ }
380
+ }
381
+ copyFromChannel(targetName, sourceName) {
382
+ const ch = this.availableChannels().find((c) => c.name === sourceName);
383
+ if (!ch)
384
+ return;
385
+ const hasSubject = ch.hasSubject;
386
+ if (hasSubject) {
387
+ this.setChannelSubject(targetName, this.getChannelSubject(sourceName));
388
+ }
389
+ this.setChannelBody(targetName, this.getChannelBody(sourceName));
390
+ }
391
+ getChannelSubject(name) {
392
+ const v = this.getValue()?.[name];
393
+ const raw = v && typeof v === 'object' && 'subject' in v ? v.subject : undefined;
394
+ return resolveTextTemplateLocalizedString(raw, this.getTemplateLocale());
395
+ }
396
+ setChannelSubject(name, value) {
397
+ const current = this.getValue() || {};
398
+ const ch = current[name] || { body: '' };
399
+ const locale = this.getTemplateLocale();
400
+ const ml = this.normalizeChannelFieldToMl(ch.subject);
401
+ ml[locale] = value ?? '';
402
+ this.setValue({ ...current, [name]: { ...ch, subject: ml } });
403
+ }
404
+ getChannelBody(name) {
405
+ const v = this.getValue()?.[name];
406
+ const raw = v && typeof v === 'object' && 'body' in v ? v.body : undefined;
407
+ return resolveTextTemplateLocalizedString(raw, this.getTemplateLocale());
408
+ }
409
+ handleWysiwygChange(name, e) {
410
+ const val = typeof e === 'object' && e !== null && 'value' in e
411
+ ? e.value
412
+ : String(e ?? '');
413
+ this.setChannelBody(name, val);
414
+ }
415
+ setChannelBody(name, value) {
416
+ const current = this.getValue() || {};
417
+ const ch = current[name] || { body: '' };
418
+ const hasSubject = this.availableChannels().find((c) => c.name === name)?.hasSubject ?? false;
419
+ const locale = this.getTemplateLocale();
420
+ const ml = this.normalizeChannelFieldToMl(ch.body);
421
+ ml[locale] = value ?? '';
422
+ if (hasSubject) {
423
+ this.setValue({ ...current, [name]: { ...ch, body: ml } });
424
+ }
425
+ else {
426
+ this.setValue({ ...current, [name]: { body: ml } });
427
+ }
428
+ }
429
+ /**
430
+ * Prefer server draft Preview (Context tokens + Event samples + parent composition).
431
+ * Falls back to local compose when the command fails.
432
+ */
433
+ async loadServerPreview() {
434
+ const requestSeq = ++this.previewRequestSeq;
435
+ const value = this.getValue();
436
+ const channel = this.activeChannel()?.name?.trim() || '';
437
+ if (!value || typeof value !== 'object' || !channel) {
438
+ this.serverPreviewByChannel.set({});
439
+ this.composedPreviewBodies.set({});
440
+ return;
441
+ }
442
+ const locale = this.getTemplateLocale();
443
+ const parentIdRaw = this.contextService.getValue('parentId');
444
+ const parentId = typeof parentIdRaw === 'string' ? parentIdRaw.trim() : '';
445
+ const kindRaw = this.contextService.getValue('kind');
446
+ const kind = typeof kindRaw === 'string' ? kindRaw.trim() : undefined;
447
+ const request = {
448
+ channels: value,
449
+ channel,
450
+ locale,
451
+ ...(parentId ? { parentId } : {}),
452
+ ...(kind ? { kind } : {}),
453
+ };
454
+ try {
455
+ const result = await this.commandExecutor.execute(AXM_TEXT_TEMPLATE_PREVIEW_COMMAND_KEY, request);
456
+ if (requestSeq !== this.previewRequestSeq) {
457
+ return;
458
+ }
459
+ const preview = runtimeCommandPayloadData(result);
460
+ if (preview?.body != null) {
461
+ this.serverPreviewByChannel.update((prev) => ({
462
+ ...prev,
463
+ [channel]: {
464
+ ...(preview.subject != null ? { subject: preview.subject } : {}),
465
+ body: preview.body,
466
+ },
467
+ }));
468
+ return;
469
+ }
470
+ const message = typeof result?.message?.text === 'string' && result.message.text.trim()
471
+ ? result.message.text.trim()
472
+ : undefined;
473
+ await this.fallbackToLocalPreview(channel, message);
474
+ }
475
+ catch (error) {
476
+ if (requestSeq !== this.previewRequestSeq) {
477
+ return;
478
+ }
479
+ const message = error instanceof Error ? error.message : undefined;
480
+ await this.fallbackToLocalPreview(channel, message);
481
+ }
482
+ }
483
+ async fallbackToLocalPreview(channel, errorMessage) {
484
+ const title = await this.translationService.translateAsync('@content-management:templates.messages.preview-failed.title');
485
+ const content = await this.translationService.translateAsync('@content-management:templates.messages.preview-failed.content');
486
+ this.toastService.show({
487
+ color: 'warning',
488
+ title,
489
+ content: errorMessage ? `${content} ${errorMessage}` : content,
490
+ closeButton: true,
491
+ timeOut: 5000,
492
+ });
493
+ this.serverPreviewByChannel.update((prev) => {
494
+ const next = { ...prev };
495
+ delete next[channel];
496
+ return next;
497
+ });
498
+ await this.resolveComposedPreviewBodies();
499
+ }
500
+ /** Resolve variable values for local preview fallback */
501
+ async resolvePreviewVariables() {
502
+ const value = this.getValue();
503
+ if (!value || typeof value !== 'object')
504
+ return;
505
+ const locale = this.getTemplateLocale();
506
+ const parts = [];
507
+ for (const v of Object.values(value)) {
508
+ if (v != null && typeof v === 'object' && 'body' in v) {
509
+ const o = v;
510
+ parts.push(resolveTextTemplateLocalizedString(o.subject, locale));
511
+ parts.push(resolveTextTemplateLocalizedString(o.body, locale));
512
+ }
513
+ }
514
+ for (const body of Object.values(this.composedPreviewBodies())) {
515
+ parts.push(body);
516
+ }
517
+ const allText = parts.join('');
518
+ const varNames = collectTextTemplatePlaceholderNames(allText);
519
+ if (varNames.length === 0)
520
+ return;
521
+ const vars = this.availableVariables();
522
+ const resolved = {};
523
+ const svc = this.tokenDefinitionService;
524
+ for (const name of varNames) {
525
+ try {
526
+ if (svc) {
527
+ const val = await svc.getValue(name);
528
+ resolved[name] = val != null ? String(val) : '';
529
+ }
530
+ }
531
+ catch {
532
+ /* use fallback */
533
+ }
534
+ if (!(name in resolved) || !resolved[name]) {
535
+ const v = vars.find((x) => x.name === name);
536
+ if (v?.sampleValue != null) {
537
+ resolved[name] = v.sampleValue;
538
+ }
539
+ else if (!(name in resolved)) {
540
+ resolved[name] = '';
541
+ }
542
+ }
543
+ }
544
+ this.resolvedPreviewValues.update((prev) => ({ ...prev, ...resolved }));
545
+ }
546
+ /**
547
+ * Local fallback: loads optional parent masters and stores leaf→root body chains per channel.
548
+ */
549
+ async resolveComposedPreviewBodies() {
550
+ const value = this.getValue();
551
+ if (!value || typeof value !== 'object') {
552
+ this.composedPreviewBodies.set({});
553
+ return;
554
+ }
555
+ const parentIdRaw = this.contextService.getValue('parentId');
556
+ const parentId = typeof parentIdRaw === 'string' ? parentIdRaw.trim() : '';
557
+ const locale = this.getTemplateLocale();
558
+ const next = {};
559
+ if (!parentId) {
560
+ for (const channelName of Object.keys(value)) {
561
+ next[channelName] = this.getChannelBody(channelName);
562
+ }
563
+ this.composedPreviewBodies.set(next);
564
+ await this.resolvePreviewVariables();
565
+ return;
566
+ }
567
+ const parentBodiesByChannel = {};
568
+ try {
569
+ const accessor = this.entityService
570
+ .withEntity(RootConfig.module.name, RootConfig.entities.textTemplate.name)
571
+ .data();
572
+ const visited = new Set();
573
+ const selfIdRaw = this.contextService.getValue('id');
574
+ const selfId = typeof selfIdRaw === 'string' ? selfIdRaw.trim() : '';
575
+ if (selfId) {
576
+ visited.add(selfId);
577
+ }
578
+ let currentId = parentId;
579
+ let depth = 1;
580
+ while (currentId && depth <= AXM_TEXT_TEMPLATE_MAX_PARENT_DEPTH) {
581
+ if (visited.has(currentId)) {
582
+ break;
583
+ }
584
+ visited.add(currentId);
585
+ const parent = await accessor.byKey(currentId);
586
+ if (!parent?.channels) {
587
+ break;
588
+ }
589
+ for (const [channelName, channelDef] of Object.entries(parent.channels)) {
590
+ const body = resolveTextTemplateLocalizedString(channelDef?.body, locale);
591
+ if (!body)
592
+ continue;
593
+ (parentBodiesByChannel[channelName] ??= []).push(body);
594
+ }
595
+ currentId = parent.parentId?.trim() || null;
596
+ depth += 1;
597
+ }
598
+ }
599
+ catch {
600
+ /* keep leaf-only preview */
601
+ }
602
+ for (const channelName of Object.keys(value)) {
603
+ const leafBody = this.getChannelBody(channelName);
604
+ const parents = parentBodiesByChannel[channelName] ?? [];
605
+ next[channelName] = composeTextTemplateBodyChain([leafBody, ...parents], {});
606
+ }
607
+ this.composedPreviewBodies.set(next);
608
+ await this.resolvePreviewVariables();
609
+ }
610
+ /** Get preview value for variable: expression/token first, then sampleValue */
611
+ getPreviewValue(varName) {
612
+ const resolved = this.resolvedPreviewValues()[varName];
613
+ if (resolved !== undefined && resolved !== '')
614
+ return resolved;
615
+ const v = this.availableVariables().find((x) => x.name === varName);
616
+ return v?.sampleValue ?? `{{${varName}}}`;
617
+ }
618
+ /**
619
+ * Merge map for {@link replaceTextTemplatePlaceholders}: one entry per `{{name}}` in `text`.
620
+ * When `escapeMergeValuesForHtml` is true, substitution values are escaped for safe `innerHTML` fragments.
621
+ */
622
+ previewMergeFor(text, escapeMergeValuesForHtml) {
623
+ const out = {};
624
+ for (const name of collectTextTemplatePlaceholderNames(text)) {
625
+ let v = this.getPreviewValue(name);
626
+ if (escapeMergeValuesForHtml) {
627
+ v = v.replace(/</g, '&lt;').replace(/>/g, '&gt;');
628
+ }
629
+ out[name] = v;
630
+ }
631
+ return out;
632
+ }
633
+ previewSubject(channelName) {
634
+ const server = this.serverPreviewByChannel()[channelName];
635
+ if (server?.subject != null) {
636
+ return server.subject;
637
+ }
638
+ return this.previewText(this.getChannelSubject(channelName));
639
+ }
640
+ previewText(text) {
641
+ return replaceTextTemplatePlaceholders(text, this.previewMergeFor(text, false));
642
+ }
643
+ /**
644
+ * Email/rich preview must bypass Angular's default innerHTML sanitizer;
645
+ * otherwise inline `style` (colors, gradients, header bar) is stripped and
646
+ * the preview diverges from the real sent email.
647
+ */
648
+ previewBodyHtml(channelName) {
649
+ const ch = this.availableChannels().find((c) => c.name === channelName);
650
+ const server = this.serverPreviewByChannel()[channelName];
651
+ if (server?.body != null) {
652
+ if (ch?.type === 'rich') {
653
+ return this.sanitizer.bypassSecurityTrustHtml(server.body);
654
+ }
655
+ return server.body
656
+ .replace(/</g, '&lt;')
657
+ .replace(/>/g, '&gt;')
658
+ .replace(/\n/g, '<br/>');
659
+ }
660
+ const composed = this.composedPreviewBodies()[channelName];
661
+ const body = composed ?? this.getChannelBody(channelName);
662
+ // Local fallback: do not escape merge for rich HTML (logo mark / styled shells).
663
+ const merged = replaceTextTemplatePlaceholders(body, this.previewMergeFor(body, ch?.type !== 'rich'));
664
+ if (ch?.type === 'rich') {
665
+ return this.sanitizer.bypassSecurityTrustHtml(merged);
666
+ }
667
+ return merged
668
+ .replace(/</g, '&lt;')
669
+ .replace(/>/g, '&gt;')
670
+ .replace(/\n/g, '<br/>');
671
+ }
672
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMTemplateContentEditorWidgetEditComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
673
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: AXMTemplateContentEditorWidgetEditComponent, isStandalone: true, selector: "axm-template-content-editor-widget-edit", viewQueries: [{ propertyName: "wysiwygRef", first: true, predicate: ["wysiwygRef"], descendants: true, isSignal: true }, { propertyName: "bodyTextareaRef", first: true, predicate: ["bodyTextarea"], descendants: true, read: ElementRef, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<!-- Template Content Editor: Channel tabs and content per channel -->\n<div class=\"flex flex-col gap-4 w-full\">\n <!-- Channel tabs navigation -->\n <div class=\"flex items-center gap-2 flex-wrap\">\n <ax-tabs\n [look]=\"'with-line'\"\n (onActiveTabChanged)=\"activeTabIndex.set($event.index)\"\n class=\"flex-1 min-w-0\"\n >\n @for (ch of availableChannels(); track ch.name; let i = $index) {\n <ax-tab-item\n [text]=\"(ch.title | translate | async) ?? ch.name\"\n [key]=\"i.toString()\"\n [active]=\"activeTabIndex() === i\"\n ></ax-tab-item>\n }\n </ax-tabs>\n </div>\n\n @if (activeChannel(); as ch) {\n <!-- Channel content area: Edit mode or Preview mode -->\n <div class=\"mb-4 p-4\">\n @if (!isChannelConfigured(ch.name) && ch.notConfiguredMessageKey) {\n <div\n class=\"mb-4 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 p-3 rounded border border-amber-300 bg-amber-50 dark:border-amber-700 dark:bg-amber-950/30\"\n >\n <p class=\"text-sm text-amber-900 dark:text-amber-100\">\n {{ ch.notConfiguredMessageKey | translate | async }}\n </p>\n @if (ch.configureSettings) {\n <ax-button\n [text]=\"\n '@content-management:templates.actions.configure-channel'\n | translate\n | async\n \"\n look=\"outline\"\n color=\"primary\"\n size=\"sm\"\n (onClick)=\"openChannelConfigure(ch)\"\n >\n <ax-prefix>\n <ax-icon icon=\"fa-light fa-gear\"></ax-icon>\n </ax-prefix>\n </ax-button>\n }\n </div>\n }\n\n @if (viewMode() === 'edit') {\n <!-- Edit mode: Subject field (for channels that support it, e.g. Email) -->\n @if (ch.hasSubject) {\n <div class=\"mb-3\">\n <label class=\"block text-sm mb-1\">\n {{\n '@content-management:templates.entities.text-template.fields.subject.title'\n | translate\n | async\n }}\n </label>\n <ax-text-box\n [ngModel]=\"getChannelSubject(ch.name)\"\n (ngModelChange)=\"setChannelSubject(ch.name, $event)\"\n [placeholder]=\"\n ('@content-management:templates.entities.text-template.fields.subject.placeholder'\n | translate\n | async)!\n \"\n ></ax-text-box>\n </div>\n }\n\n <!-- Edit mode: Body field (plain text or rich WYSIWYG) -->\n <div>\n <label class=\"block text-sm mb-1\">\n {{\n '@content-management:templates.entities.text-template.fields.body.title'\n | translate\n | async\n }}\n </label>\n @if (ch.type === 'plain') {\n <ax-text-area\n #bodyTextarea\n [ngModel]=\"getChannelBody(ch.name)\"\n (ngModelChange)=\"setChannelBody(ch.name, $event)\"\n [rows]=\"8\"\n [placeholder]=\"\n ('@content-management:templates.entities.text-template.fields.body-placeholder.plain'\n | translate\n | async)!\n \"\n ></ax-text-area>\n } @else {\n <ax-wysiwyg-container\n #wysiwygRef\n [placeHolder]=\"\n ('@content-management:templates.entities.text-template.fields.body-placeholder.rich'\n | translate\n | async)!\n \"\n [ngModel]=\"getChannelBody(ch.name)\"\n (onValueChanged)=\"handleWysiwygChange(ch.name, $event)\"\n [look]=\"'solid'\"\n >\n <ax-wysiwyg-view [style.min-height.px]=\"200\"></ax-wysiwyg-view>\n <ax-wysiwyg-toolbar>\n <ax-prefix>\n <ax-wysiwyg-history></ax-wysiwyg-history>\n <ax-wysiwyg-font-style></ax-wysiwyg-font-style>\n <ax-wysiwyg-list></ax-wysiwyg-list>\n </ax-prefix>\n </ax-wysiwyg-toolbar>\n </ax-wysiwyg-container>\n }\n </div>\n } @else {\n <!-- Preview mode: Rendered content with variable placeholders replaced by sample values -->\n <div class=\"space-y-2\">\n @if (ch.hasSubject && previewSubject(ch.name)) {\n <div>\n <span class=\"text-xs text-gray-500\"\n >{{\n '@content-management:templates.entities.text-template.fields.subject.title'\n | translate\n | async\n }}:\n </span>\n <span class=\"text-sm\">{{ previewSubject(ch.name) }}</span>\n </div>\n }\n <!-- ----- Rich email keeps inline styles; plain channels use pre-wrap ----- -->\n <div\n class=\"text-sm p-3 bg-gray-100 dark:bg-gray-900 rounded border overflow-auto\"\n [class.whitespace-pre-wrap]=\"ch.type !== 'rich'\"\n [innerHTML]=\"previewBodyHtml(ch.name)\"\n ></div>\n </div>\n }\n </div>\n\n <!-- Footer: Action buttons (left) + Edit/Preview toggle (right) in single row -->\n <div\n class=\"flex flex-nowrap items-center justify-between gap-3 pt-3 mt-1 border-t border-gray-200 dark:border-gray-700\"\n >\n <div class=\"flex flex-nowrap items-center gap-2 min-w-0\">\n @if (viewMode() === 'edit') {\n <!-- ----- Insert variable dropdown from platform tokens ----- -->\n <div data-insert-var-trigger>\n <ax-button\n [text]=\"\n '@content-management:templates.actions.insert-variable'\n | translate\n | async\n \"\n look=\"twotone\"\n color=\"default\"\n size=\"sm\"\n >\n <ax-prefix>\n <ax-icon icon=\"fa-light fa-code\"></ax-icon>\n </ax-prefix>\n <ax-dropdown-panel>\n <ax-button-item-list>\n @for (v of availableVariables(); track v.name) {\n <ax-button-item\n look=\"blank\"\n color=\"default\"\n [text]=\"(v.title || v.name) + ' ({{' + v.name + '}})'\"\n (onClick)=\"insertVariable(ch.name, v.name)\"\n >\n <ax-prefix>\n <ax-icon [icon]=\"getVariableIcon(v)\"></ax-icon>\n </ax-prefix>\n </ax-button-item>\n }\n </ax-button-item-list>\n </ax-dropdown-panel>\n <ax-suffix>\n <ax-icon icon=\"fa-light fa-chevron-down\"></ax-icon>\n </ax-suffix>\n </ax-button>\n </div>\n <!--->\n <!-- ----- Insert reserved [content] slot for master templates ----- -->\n @if (isMasterKind()) {\n <div data-insert-var-trigger>\n <ax-button\n [text]=\"\n '@content-management:templates.actions.insert-content-slot'\n | translate\n | async\n \"\n look=\"twotone\"\n color=\"default\"\n size=\"sm\"\n (onClick)=\"insertContentSlot(ch.name)\"\n >\n <ax-prefix>\n <ax-icon icon=\"fa-light fa-layer-group\"></ax-icon>\n </ax-prefix>\n </ax-button>\n </div>\n }\n <!--->\n <!-- ----- Copy subject/body from another channel ----- -->\n @if (otherChannelsWithContent(ch.name).length > 0) {\n <ax-button\n [text]=\"\n '@content-management:templates.actions.copy-from'\n | translate\n | async\n \"\n look=\"twotone\"\n color=\"default\"\n size=\"sm\"\n >\n <ax-prefix>\n <ax-icon icon=\"fa-light fa-copy\"></ax-icon>\n </ax-prefix>\n <ax-suffix>\n <ax-icon icon=\"fa-light fa-chevron-down\"></ax-icon>\n </ax-suffix>\n <ax-dropdown-panel>\n <ax-button-item-list>\n @for (\n src of otherChannelsWithContent(ch.name);\n track src.name\n ) {\n <ax-button-item\n look=\"blank\"\n color=\"default\"\n [text]=\"(src.title | translate | async) ?? src.name\"\n (onClick)=\"copyFromChannel(ch.name, src.name)\"\n >\n <ax-prefix>\n <ax-icon [icon]=\"getChannelIcon(src)\"></ax-icon>\n </ax-prefix>\n </ax-button-item>\n }\n </ax-button-item-list>\n </ax-dropdown-panel>\n </ax-button>\n }\n }\n </div>\n <div>\n <ax-tabs [look]=\"'default'\">\n <ax-tab-item\n [text]=\"\n ('@content-management:templates.actions.edit'\n | translate\n | async) ?? 'Edit'\n \"\n [active]=\"viewMode() === 'edit'\"\n (onClick)=\"viewMode.set('edit')\"\n >\n <ax-prefix>\n <ax-icon icon=\"fa-light fa-pen\" class=\"text-current\"></ax-icon>\n </ax-prefix>\n </ax-tab-item>\n <ax-tab-item\n [text]=\"\n ('@content-management:templates.actions.preview'\n | translate\n | async) ?? 'Preview'\n \"\n [active]=\"viewMode() === 'preview'\"\n (onClick)=\"viewMode.set('preview')\"\n >\n <ax-prefix>\n <ax-icon icon=\"fa-light fa-eye\" class=\"text-current\"></ax-icon>\n </ax-prefix>\n </ax-tab-item>\n </ax-tabs>\n </div>\n </div>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: AXDecoratorModule }, { kind: "component", type: i1.AXDecoratorIconComponent, selector: "ax-icon", inputs: ["icon"] }, { kind: "component", type: i1.AXDecoratorGenericComponent, selector: "ax-footer, ax-header, ax-content, ax-divider, ax-form-hint, ax-prefix, ax-suffix, ax-text, ax-title, ax-subtitle, ax-placeholder, ax-overlay" }, { kind: "ngmodule", type: FormsModule }, { 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: "ngmodule", type: AXFormModule }, { kind: "ngmodule", type: AXTabsModule }, { kind: "component", type: i3.AXTabsComponent, selector: "ax-tabs", inputs: ["look", "location", "fitParent", "minWidth", "content"], outputs: ["onActiveTabChanged"] }, { kind: "component", type: i3.AXTabItemComponent, selector: "ax-tab-item", inputs: ["disabled", "text", "key", "headerTemplate", "active"], outputs: ["disabledChange", "onClick", "onBlur", "onFocus", "activeChange"] }, { kind: "ngmodule", type: AXTextAreaModule }, { kind: "component", type: i4.AXTextAreaComponent, selector: "ax-text-area", inputs: ["disabled", "tabIndex", "readonly", "value", "state", "name", "placeholder", "maxLength", "look", "rows", "allowResize", "showCounter", "class"], outputs: ["onBlur", "onFocus", "valueChange", "stateChange", "onValueChanged", "readonlyChange", "disabledChange", "onKeyDown", "onKeyUp", "onKeyPress"] }, { kind: "ngmodule", type: AXTextBoxModule }, { kind: "component", type: i5.AXTextBoxComponent, selector: "ax-text-box", inputs: ["disabled", "tabIndex", "readonly", "value", "state", "name", "id", "placeholder", "maxLength", "allowNull", "type", "autoComplete", "look", "maskPattern", "customTokens", "class"], outputs: ["onBlur", "onFocus", "valueChange", "stateChange", "onValueChanged", "readonlyChange", "disabledChange", "onKeyDown", "onKeyUp", "onKeyPress", "onMaskChanged"] }, { kind: "ngmodule", type: AXWysiwygModule }, { kind: "component", type: i6.AXWysiwygContainerComponent, selector: "ax-wysiwyg-container", inputs: ["look", "readonly", "disabled", "value", "placeHolder", "initialLine"], outputs: ["onValueChanged"] }, { kind: "component", type: i6.AXWysiwygViewComponent, selector: "ax-wysiwyg-view" }, { kind: "component", type: i6.AXWysiwygFontStyleComponent, selector: "ax-wysiwyg-font-style" }, { kind: "component", type: i6.AXWysiwygHistoryComponent, selector: "ax-wysiwyg-history" }, { kind: "component", type: i6.AXWysiwygListComponent, selector: "ax-wysiwyg-list" }, { kind: "component", type: i6.AXWysiwygToolbarComponent, selector: "ax-wysiwyg-toolbar" }, { kind: "ngmodule", type: AXValidationModule }, { kind: "ngmodule", type: AXTranslationModule }, { kind: "ngmodule", type: AXButtonModule }, { kind: "component", type: i7.AXButtonComponent, selector: "ax-button", inputs: ["disabled", "size", "tabIndex", "color", "look", "text", "toggleable", "selected", "iconOnly", "type", "loadingText"], outputs: ["onBlur", "onFocus", "onClick", "selectedChange", "toggleableChange", "lookChange", "colorChange", "disabledChange", "loadingTextChange"] }, { kind: "component", type: i7.AXButtonItemComponent, selector: "ax-button-item", inputs: ["color", "disabled", "text", "selected", "divided", "data", "name"], outputs: ["onClick", "onFocus", "onBlur", "disabledChange"] }, { kind: "component", type: i7.AXButtonItemListComponent, selector: "ax-button-item-list", inputs: ["items", "closeParentOnClick", "lockOnLoading"], outputs: ["onItemClick"] }, { kind: "ngmodule", type: AXDropdownModule }, { kind: "component", type: i8.AXDropdownPanelComponent, selector: "ax-dropdown-panel", inputs: ["isOpen", "fitParent", "dropdownWidth", "position", "placement", "_target", "adaptivityEnabled"], outputs: ["onOpened", "onClosed"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "pipe", type: i2$1.AXTranslatorPipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
674
+ }
675
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMTemplateContentEditorWidgetEditComponent, decorators: [{
676
+ type: Component,
677
+ args: [{ selector: 'axm-template-content-editor-widget-edit', changeDetection: ChangeDetectionStrategy.OnPush, imports: [
678
+ CommonModule,
679
+ AXDecoratorModule,
680
+ FormsModule,
681
+ AXFormModule,
682
+ AXTabsModule,
683
+ AXTextAreaModule,
684
+ AXTextBoxModule,
685
+ AXWysiwygModule,
686
+ AXValidationModule,
687
+ AXTranslationModule,
688
+ AXButtonModule,
689
+ AXDropdownModule,
690
+ ], template: "<!-- Template Content Editor: Channel tabs and content per channel -->\n<div class=\"flex flex-col gap-4 w-full\">\n <!-- Channel tabs navigation -->\n <div class=\"flex items-center gap-2 flex-wrap\">\n <ax-tabs\n [look]=\"'with-line'\"\n (onActiveTabChanged)=\"activeTabIndex.set($event.index)\"\n class=\"flex-1 min-w-0\"\n >\n @for (ch of availableChannels(); track ch.name; let i = $index) {\n <ax-tab-item\n [text]=\"(ch.title | translate | async) ?? ch.name\"\n [key]=\"i.toString()\"\n [active]=\"activeTabIndex() === i\"\n ></ax-tab-item>\n }\n </ax-tabs>\n </div>\n\n @if (activeChannel(); as ch) {\n <!-- Channel content area: Edit mode or Preview mode -->\n <div class=\"mb-4 p-4\">\n @if (!isChannelConfigured(ch.name) && ch.notConfiguredMessageKey) {\n <div\n class=\"mb-4 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 p-3 rounded border border-amber-300 bg-amber-50 dark:border-amber-700 dark:bg-amber-950/30\"\n >\n <p class=\"text-sm text-amber-900 dark:text-amber-100\">\n {{ ch.notConfiguredMessageKey | translate | async }}\n </p>\n @if (ch.configureSettings) {\n <ax-button\n [text]=\"\n '@content-management:templates.actions.configure-channel'\n | translate\n | async\n \"\n look=\"outline\"\n color=\"primary\"\n size=\"sm\"\n (onClick)=\"openChannelConfigure(ch)\"\n >\n <ax-prefix>\n <ax-icon icon=\"fa-light fa-gear\"></ax-icon>\n </ax-prefix>\n </ax-button>\n }\n </div>\n }\n\n @if (viewMode() === 'edit') {\n <!-- Edit mode: Subject field (for channels that support it, e.g. Email) -->\n @if (ch.hasSubject) {\n <div class=\"mb-3\">\n <label class=\"block text-sm mb-1\">\n {{\n '@content-management:templates.entities.text-template.fields.subject.title'\n | translate\n | async\n }}\n </label>\n <ax-text-box\n [ngModel]=\"getChannelSubject(ch.name)\"\n (ngModelChange)=\"setChannelSubject(ch.name, $event)\"\n [placeholder]=\"\n ('@content-management:templates.entities.text-template.fields.subject.placeholder'\n | translate\n | async)!\n \"\n ></ax-text-box>\n </div>\n }\n\n <!-- Edit mode: Body field (plain text or rich WYSIWYG) -->\n <div>\n <label class=\"block text-sm mb-1\">\n {{\n '@content-management:templates.entities.text-template.fields.body.title'\n | translate\n | async\n }}\n </label>\n @if (ch.type === 'plain') {\n <ax-text-area\n #bodyTextarea\n [ngModel]=\"getChannelBody(ch.name)\"\n (ngModelChange)=\"setChannelBody(ch.name, $event)\"\n [rows]=\"8\"\n [placeholder]=\"\n ('@content-management:templates.entities.text-template.fields.body-placeholder.plain'\n | translate\n | async)!\n \"\n ></ax-text-area>\n } @else {\n <ax-wysiwyg-container\n #wysiwygRef\n [placeHolder]=\"\n ('@content-management:templates.entities.text-template.fields.body-placeholder.rich'\n | translate\n | async)!\n \"\n [ngModel]=\"getChannelBody(ch.name)\"\n (onValueChanged)=\"handleWysiwygChange(ch.name, $event)\"\n [look]=\"'solid'\"\n >\n <ax-wysiwyg-view [style.min-height.px]=\"200\"></ax-wysiwyg-view>\n <ax-wysiwyg-toolbar>\n <ax-prefix>\n <ax-wysiwyg-history></ax-wysiwyg-history>\n <ax-wysiwyg-font-style></ax-wysiwyg-font-style>\n <ax-wysiwyg-list></ax-wysiwyg-list>\n </ax-prefix>\n </ax-wysiwyg-toolbar>\n </ax-wysiwyg-container>\n }\n </div>\n } @else {\n <!-- Preview mode: Rendered content with variable placeholders replaced by sample values -->\n <div class=\"space-y-2\">\n @if (ch.hasSubject && previewSubject(ch.name)) {\n <div>\n <span class=\"text-xs text-gray-500\"\n >{{\n '@content-management:templates.entities.text-template.fields.subject.title'\n | translate\n | async\n }}:\n </span>\n <span class=\"text-sm\">{{ previewSubject(ch.name) }}</span>\n </div>\n }\n <!-- ----- Rich email keeps inline styles; plain channels use pre-wrap ----- -->\n <div\n class=\"text-sm p-3 bg-gray-100 dark:bg-gray-900 rounded border overflow-auto\"\n [class.whitespace-pre-wrap]=\"ch.type !== 'rich'\"\n [innerHTML]=\"previewBodyHtml(ch.name)\"\n ></div>\n </div>\n }\n </div>\n\n <!-- Footer: Action buttons (left) + Edit/Preview toggle (right) in single row -->\n <div\n class=\"flex flex-nowrap items-center justify-between gap-3 pt-3 mt-1 border-t border-gray-200 dark:border-gray-700\"\n >\n <div class=\"flex flex-nowrap items-center gap-2 min-w-0\">\n @if (viewMode() === 'edit') {\n <!-- ----- Insert variable dropdown from platform tokens ----- -->\n <div data-insert-var-trigger>\n <ax-button\n [text]=\"\n '@content-management:templates.actions.insert-variable'\n | translate\n | async\n \"\n look=\"twotone\"\n color=\"default\"\n size=\"sm\"\n >\n <ax-prefix>\n <ax-icon icon=\"fa-light fa-code\"></ax-icon>\n </ax-prefix>\n <ax-dropdown-panel>\n <ax-button-item-list>\n @for (v of availableVariables(); track v.name) {\n <ax-button-item\n look=\"blank\"\n color=\"default\"\n [text]=\"(v.title || v.name) + ' ({{' + v.name + '}})'\"\n (onClick)=\"insertVariable(ch.name, v.name)\"\n >\n <ax-prefix>\n <ax-icon [icon]=\"getVariableIcon(v)\"></ax-icon>\n </ax-prefix>\n </ax-button-item>\n }\n </ax-button-item-list>\n </ax-dropdown-panel>\n <ax-suffix>\n <ax-icon icon=\"fa-light fa-chevron-down\"></ax-icon>\n </ax-suffix>\n </ax-button>\n </div>\n <!--->\n <!-- ----- Insert reserved [content] slot for master templates ----- -->\n @if (isMasterKind()) {\n <div data-insert-var-trigger>\n <ax-button\n [text]=\"\n '@content-management:templates.actions.insert-content-slot'\n | translate\n | async\n \"\n look=\"twotone\"\n color=\"default\"\n size=\"sm\"\n (onClick)=\"insertContentSlot(ch.name)\"\n >\n <ax-prefix>\n <ax-icon icon=\"fa-light fa-layer-group\"></ax-icon>\n </ax-prefix>\n </ax-button>\n </div>\n }\n <!--->\n <!-- ----- Copy subject/body from another channel ----- -->\n @if (otherChannelsWithContent(ch.name).length > 0) {\n <ax-button\n [text]=\"\n '@content-management:templates.actions.copy-from'\n | translate\n | async\n \"\n look=\"twotone\"\n color=\"default\"\n size=\"sm\"\n >\n <ax-prefix>\n <ax-icon icon=\"fa-light fa-copy\"></ax-icon>\n </ax-prefix>\n <ax-suffix>\n <ax-icon icon=\"fa-light fa-chevron-down\"></ax-icon>\n </ax-suffix>\n <ax-dropdown-panel>\n <ax-button-item-list>\n @for (\n src of otherChannelsWithContent(ch.name);\n track src.name\n ) {\n <ax-button-item\n look=\"blank\"\n color=\"default\"\n [text]=\"(src.title | translate | async) ?? src.name\"\n (onClick)=\"copyFromChannel(ch.name, src.name)\"\n >\n <ax-prefix>\n <ax-icon [icon]=\"getChannelIcon(src)\"></ax-icon>\n </ax-prefix>\n </ax-button-item>\n }\n </ax-button-item-list>\n </ax-dropdown-panel>\n </ax-button>\n }\n }\n </div>\n <div>\n <ax-tabs [look]=\"'default'\">\n <ax-tab-item\n [text]=\"\n ('@content-management:templates.actions.edit'\n | translate\n | async) ?? 'Edit'\n \"\n [active]=\"viewMode() === 'edit'\"\n (onClick)=\"viewMode.set('edit')\"\n >\n <ax-prefix>\n <ax-icon icon=\"fa-light fa-pen\" class=\"text-current\"></ax-icon>\n </ax-prefix>\n </ax-tab-item>\n <ax-tab-item\n [text]=\"\n ('@content-management:templates.actions.preview'\n | translate\n | async) ?? 'Preview'\n \"\n [active]=\"viewMode() === 'preview'\"\n (onClick)=\"viewMode.set('preview')\"\n >\n <ax-prefix>\n <ax-icon icon=\"fa-light fa-eye\" class=\"text-current\"></ax-icon>\n </ax-prefix>\n </ax-tab-item>\n </ax-tabs>\n </div>\n </div>\n }\n</div>\n" }]
691
+ }], ctorParameters: () => [], propDecorators: { wysiwygRef: [{ type: i0.ViewChild, args: ['wysiwygRef', { isSignal: true }] }], bodyTextareaRef: [{ type: i0.ViewChild, args: ['bodyTextarea', { ...{ read: (ElementRef) }, isSignal: true }] }] } });
692
+
693
+ export { AXMTemplateContentEditorWidgetEditComponent };
694
+ //# sourceMappingURL=acorex-platform-module-content-management-client-template-content-editor-widget-edit.component-CApNVTfc.mjs.map