@dignite/ng.site 0.0.0-stage → 1.0.1

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,782 @@
1
+ import * as i4 from '@abp/ng.core';
2
+ import { LocalizationService, CoreModule } from '@abp/ng.core';
3
+ import { ToasterService, ThemeSharedModule } from '@abp/ng.theme.shared';
4
+ import * as i0 from '@angular/core';
5
+ import { inject, signal, ChangeDetectionStrategy, Component } from '@angular/core';
6
+ import * as i1 from '@angular/forms';
7
+ import { FormBuilder, Validators, FormGroup, ReactiveFormsModule } from '@angular/forms';
8
+ import * as i2 from '@angular/router';
9
+ import { ActivatedRoute, Router, RouterLink } from '@angular/router';
10
+ import { FlexFieldControlComponent } from '@dignite/ng.flex-fields';
11
+ import { forkJoin, of, switchMap, catchError } from 'rxjs';
12
+ import { C as ContentAdminService, c as ContentTypeAdminService, P as PageAdminService, S as SiteReferenceDataService, d as ContentStatus } from './dignite-ng.site-dignite-ng.site-CsKxqjY9.mjs';
13
+ import * as i3 from '@ngx-validate/core';
14
+
15
+ /**
16
+ * Maps a mounted flex field's own control errors to the same localized text, regardless of which
17
+ * field type produced them or how deep the control is nested (top-level, a Table column, a Matrix
18
+ * sub-field): every built-in field type only ever raises `required`/`min`/`max`/`maxlength`, since
19
+ * that's the full set `NumberControlComponent`/`TextControlComponent`/etc. push in `@dignite/ng.flex-fields`.
20
+ * Gated on `touched` so a fresh, still-empty required field doesn't show red before the user reaches it.
21
+ */
22
+ function flexFieldErrorMessage(control, localization) {
23
+ const errors = control?.touched ? control.errors : null;
24
+ if (!errors) {
25
+ return null;
26
+ }
27
+ if (errors['required']) {
28
+ return localization.instant('AbpValidation::ThisFieldIsRequired');
29
+ }
30
+ if (errors['min']) {
31
+ return localization.instant('FlexFieldsSite::Validate:MinValue', String(errors['min'].min));
32
+ }
33
+ if (errors['max']) {
34
+ return localization.instant('FlexFieldsSite::Validate:MaxValue', String(errors['max'].max));
35
+ }
36
+ if (errors['maxlength']) {
37
+ return localization.instant('FlexFieldsSite::Validate:MaxLength', String(errors['maxlength'].requiredLength));
38
+ }
39
+ return null;
40
+ }
41
+
42
+ const MAX_SLUG_LENGTH = 256;
43
+ const OPTIONAL_SLUG_TOKEN = '{slug?}';
44
+ const REQUIRED_SLUG_TOKEN = '{slug}';
45
+ /** Mirrors `SlugNormalizer.AllowedCharactersPattern` (`src/Dignite.Site.Domain.Shared/SlugNormalizer.cs`). */
46
+ const SLUG_PATTERN = /^[\p{L}\p{Nd}_-]*$/u;
47
+ /**
48
+ * Creating and editing one content, as a page of its own rather than a dialog.
49
+ *
50
+ * The two halves answer different questions and are sized accordingly: the left is the content itself -
51
+ * however many fields its content type defines, which can be a long form - and the right is the handful
52
+ * of system fields that place and schedule it. A dialog forced both into one scrolling column.
53
+ *
54
+ * Switching the content type rebuilds the left half, because the fields *are* the content type's
55
+ * definition. Values already typed under fields the new type does not have are dropped, which is the
56
+ * honest outcome: they have nowhere to be stored.
57
+ *
58
+ * **Deliberately no CKEditor theme bridge here.** This component used to carry a global
59
+ * `::ng-deep :root` block remapping CKEditor 5's `--ck-color-base-*` tokens onto the host theme -
60
+ * CKEditor ships one fixed light palette with no dark-mode switch, so the editor stayed light in a
61
+ * dark-themed host. `@dignite/ng.flex-fields-ckeditor` >= 10.0.0-rc.13 ships that bridge itself, with
62
+ * a longer fallback chain than this copy had (LeptonX token -> the Bootstrap 5.3 token every ABP
63
+ * Angular theme defines -> CKEditor's own stock literal) plus fixes this copy never had. Re-adding a
64
+ * copy here would not add anything and would fight it at equal `:root` specificity, decided by style
65
+ * injection order alone.
66
+ */
67
+ class ContentEditorComponent {
68
+ constructor() {
69
+ this.contentService = inject(ContentAdminService);
70
+ this.contentTypeService = inject(ContentTypeAdminService);
71
+ this.pageService = inject(PageAdminService);
72
+ this.referenceData = inject(SiteReferenceDataService);
73
+ this.route = inject(ActivatedRoute);
74
+ this.router = inject(Router);
75
+ this.fb = inject(FormBuilder);
76
+ this.toaster = inject(ToasterService);
77
+ this.localization = inject(LocalizationService);
78
+ this.ContentStatus = ContentStatus;
79
+ this.statuses = [ContentStatus.Draft, ContentStatus.Published, ContentStatus.Archived];
80
+ this.contentId = this.route.snapshot.paramMap.get('id');
81
+ this.isEdit = !!this.contentId;
82
+ // State that changes in an HTTP callback is a signal, not a plain field: the host layout's ancestors
83
+ // (`abp-router-outlet`, the LeptonX layouts) are OnPush, and a plain-field write in a response
84
+ // callback never marks them for check, so the form would stay blank (or go stale) until some
85
+ // unrelated event happened to.
86
+ this.page = signal(undefined, /* @ts-ignore */
87
+ ...(ngDevMode ? [{ debugName: "page" }] : /* istanbul ignore next */ []));
88
+ this.contentTypes = signal([], /* @ts-ignore */
89
+ ...(ngDevMode ? [{ debugName: "contentTypes" }] : /* istanbul ignore next */ []));
90
+ this.languages = signal([], /* @ts-ignore */
91
+ ...(ngDevMode ? [{ debugName: "languages" }] : /* istanbul ignore next */ []));
92
+ this.defaultLanguage = '';
93
+ /**
94
+ * Whether the page's own address (an empty slug) is already taken by another content, for the
95
+ * currently selected language - only meaningful, and only checked, when {@link slugState} is
96
+ * `'optional'`. Read by the template to decide whether the "use the page's own address" checkbox is
97
+ * offered at all.
98
+ */
99
+ this.emptySlotTaken = signal(false, /* @ts-ignore */
100
+ ...(ngDevMode ? [{ debugName: "emptySlotTaken" }] : /* istanbul ignore next */ []));
101
+ this.fieldsById = new Map();
102
+ this.arrangementByContentType = new Map();
103
+ /**
104
+ * The current content type's fields, as stable `FlexFieldValue`s. Rebuilt only when the content type
105
+ * changes - `ff-flex-field-control` compares this input by object reference, and handing it a freshly
106
+ * allocated array on every change-detection pass would make it tear down and recreate its dynamic
107
+ * child forever (`NG0103`).
108
+ */
109
+ this.fields = signal([], /* @ts-ignore */
110
+ ...(ngDevMode ? [{ debugName: "fields" }] : /* istanbul ignore next */ []));
111
+ this.isBusy = signal(false, /* @ts-ignore */
112
+ ...(ngDevMode ? [{ debugName: "isBusy" }] : /* istanbul ignore next */ []));
113
+ this.isLoading = signal(true, /* @ts-ignore */
114
+ ...(ngDevMode ? [{ debugName: "isLoading" }] : /* istanbul ignore next */ []));
115
+ const pageId = this.route.snapshot.queryParamMap.get('pageId');
116
+ forkJoin({
117
+ schema: this.referenceData.getSchema(),
118
+ fieldsById: this.referenceData.getFieldsById(),
119
+ content: this.contentId ? this.contentService.get(this.contentId) : of(undefined),
120
+ })
121
+ .pipe(switchMap(loaded => {
122
+ const languages = loaded.schema.enabledLanguages ?? [];
123
+ this.languages.set(languages);
124
+ this.defaultLanguage = loaded.schema.defaultLanguage ?? languages[0] ?? '';
125
+ this.fieldsById = loaded.fieldsById;
126
+ this.content = loaded.content;
127
+ const resolvedPageId = loaded.content?.pageId ?? pageId ?? '';
128
+ return forkJoin({
129
+ page: resolvedPageId ? this.pageService.get(resolvedPageId) : of(undefined),
130
+ contentTypes: resolvedPageId
131
+ ? this.contentTypeService.getListByPage(resolvedPageId)
132
+ : of({ items: [] }),
133
+ });
134
+ }))
135
+ .subscribe(({ page, contentTypes }) => {
136
+ this.page.set(page);
137
+ this.contentTypes.set(contentTypes.items ?? []);
138
+ this.rebuildArrangements();
139
+ this.form = this.buildForm();
140
+ this.rebuildFields();
141
+ this.isLoading.set(false);
142
+ this.refreshEmptySlotAvailability();
143
+ });
144
+ }
145
+ get hasMultipleContentTypes() {
146
+ return this.contentTypes().length > 1;
147
+ }
148
+ /**
149
+ * What this page's route (总体设计 §3.3) says about the slug beneath it - the front-end mirror of
150
+ * `PageRoute.HasSlug`/`PageRoute.IsSlugOptional`. `'forbidden'`: no `{slug}`/`{slug?}` at all, so this
151
+ * page has nothing beneath it but its own address. `'required'`: `{slug}` or `{slug:REGEX}`, so every
152
+ * content needs one. `'optional'`: `{slug?}`, so an empty slug is also allowed, subject to
153
+ * {@link emptySlotTaken}.
154
+ */
155
+ get slugState() {
156
+ const route = this.page()?.route ?? '';
157
+ if (route.includes(OPTIONAL_SLUG_TOKEN)) {
158
+ return 'optional';
159
+ }
160
+ return route.includes(REQUIRED_SLUG_TOKEN) || this.hasSlugWithRegex(route) ? 'required' : 'forbidden';
161
+ }
162
+ /**
163
+ * Whether `route` carries `{slug:REGEX}` - a slug placeholder with a regex and no format, the one
164
+ * decorated form `PageRoute.HasSlug` recognizes. Whether a given slug matches that regex is left to the
165
+ * server (`PageRoute.IsSlugAllowed`): a .NET pattern does not always mean the same thing in JavaScript.
166
+ */
167
+ hasSlugWithRegex(route) {
168
+ for (let i = route.indexOf('{'); i >= 0; i = route.indexOf('{', i + 1)) {
169
+ const placeholder = this.readPlaceholder(route, i);
170
+ if (placeholder && placeholder.name.toLowerCase() === 'slug' && placeholder.format === null) {
171
+ return true;
172
+ }
173
+ if (placeholder) {
174
+ i = placeholder.end - 1;
175
+ }
176
+ }
177
+ return false;
178
+ }
179
+ get usePageOwnAddress() {
180
+ return !!this.form?.get('usePageOwnAddress')?.value;
181
+ }
182
+ /**
183
+ * The address this content will actually be served at, recomputed on every read so the template can
184
+ * bind it directly and have it track the slug field as the user types. An approximation, not the
185
+ * source of truth: {@link resolvePlaceholder} resolves every placeholder generically off the form and
186
+ * the loaded content, falling back to the raw token when it cannot - `Page.BuildContentPath` on the
187
+ * server is what actually decides the URL.
188
+ *
189
+ * The empty-slug branch mirrors `Page.BuildContentPath` itself, not a naming convention: an empty
190
+ * slug is short-circuited to the page's own path there regardless of what the rest of the route
191
+ * contains, so the preview has to do the same rather than run it through the placeholder replacement
192
+ * below.
193
+ */
194
+ get slugPreview() {
195
+ const route = this.page()?.route;
196
+ if (!route) {
197
+ return '';
198
+ }
199
+ const slug = (this.form?.get('slug')?.value ?? '').trim();
200
+ const path = slug ? this.buildPreviewPath(route) : this.pageOwnPath(route);
201
+ return this.applyCulturePrefix(path);
202
+ }
203
+ get isDraft() {
204
+ return this.form?.getRawValue().status === ContentStatus.Draft;
205
+ }
206
+ statusKeyOf(status) {
207
+ return `Site::Enum:ContentStatus:${ContentStatus[status]}`;
208
+ }
209
+ fieldErrorMessage(fieldValue) {
210
+ return flexFieldErrorMessage(this.form?.get('flexFields')?.get(fieldValue.field.name), this.localization);
211
+ }
212
+ rebuildArrangements() {
213
+ this.arrangementByContentType = new Map(this.contentTypes().map(contentType => [
214
+ contentType.id,
215
+ (contentType.fields ?? [])
216
+ .map(entry => {
217
+ const definition = entry.fieldId ? this.fieldsById.get(entry.fieldId) : undefined;
218
+ if (!definition) {
219
+ return undefined;
220
+ }
221
+ const fieldData = {
222
+ id: definition.id,
223
+ name: definition.name,
224
+ displayName: entry.displayName || definition.displayName,
225
+ description: definition.description ?? undefined,
226
+ fieldTypeName: definition.fieldTypeName,
227
+ configuration: (definition.configuration ?? {}),
228
+ };
229
+ return {
230
+ fieldId: definition.id,
231
+ name: definition.name,
232
+ displayName: fieldData.displayName,
233
+ fieldTypeName: definition.fieldTypeName,
234
+ required: !!entry.required,
235
+ searchable: !!entry.searchable,
236
+ showInList: !!entry.showInList,
237
+ order: entry.order ?? 0,
238
+ fieldData,
239
+ };
240
+ })
241
+ .filter((field) => !!field)
242
+ .sort((a, b) => a.order - b.order),
243
+ ]));
244
+ }
245
+ /** Called on load and whenever the content type changes. See {@link fields}. */
246
+ rebuildFields() {
247
+ const contentTypeId = this.form?.get('contentTypeId')?.value;
248
+ const arranged = this.arrangementByContentType.get(contentTypeId ?? '') ?? [];
249
+ const values = this.content?.fieldValues ?? {};
250
+ this.fields.set(arranged.map(field => ({
251
+ field: field.fieldData,
252
+ required: field.required,
253
+ searchable: field.searchable,
254
+ value: values[field.name],
255
+ })));
256
+ }
257
+ onContentTypeChange() {
258
+ // The value bag is keyed by field name, so controls for the old type's fields have to go before the
259
+ // new type's are registered - otherwise the payload carries both.
260
+ this.form?.setControl('flexFields', this.fb.group({}));
261
+ this.rebuildFields();
262
+ }
263
+ onStatusChange() {
264
+ this.applyPublishTimeAvailability();
265
+ }
266
+ onCultureNameChange() {
267
+ this.refreshEmptySlotAvailability();
268
+ }
269
+ onUsePageOwnAddressChange() {
270
+ this.applySlugAvailability();
271
+ }
272
+ save() {
273
+ if (!this.form || this.form.invalid || this.isBusy()) {
274
+ return;
275
+ }
276
+ this.isBusy.set(true);
277
+ const value = this.form.getRawValue();
278
+ const fieldValues = (this.form.get('flexFields')?.getRawValue() ?? {});
279
+ const slug = value.slug ?? '';
280
+ const request$ = this.content
281
+ ? this.contentService.update(this.content.id, {
282
+ contentTypeId: value.contentTypeId,
283
+ slug,
284
+ publishTime: value.publishTime,
285
+ status: value.status,
286
+ fieldValues,
287
+ })
288
+ : this.contentService.create({
289
+ contentTypeId: value.contentTypeId,
290
+ cultureName: value.cultureName,
291
+ slug,
292
+ publishTime: value.publishTime,
293
+ status: value.status,
294
+ fieldValues,
295
+ });
296
+ request$.subscribe({
297
+ next: () => {
298
+ this.isBusy.set(false);
299
+ this.toaster.success('AbpUi::SavedSuccessfully');
300
+ this.backToList();
301
+ },
302
+ error: () => this.isBusy.set(false),
303
+ });
304
+ }
305
+ backToList() {
306
+ this.router.navigate(['/site/contents']);
307
+ }
308
+ buildForm() {
309
+ const content = this.content;
310
+ const form = this.fb.group({
311
+ contentTypeId: [
312
+ content?.contentTypeId ?? this.contentTypes()[0]?.id ?? '',
313
+ Validators.required,
314
+ ],
315
+ cultureName: [
316
+ {
317
+ value: content?.cultureName ??
318
+ this.route.snapshot.queryParamMap.get('cultureName') ??
319
+ this.languages()[0] ??
320
+ '',
321
+ // Fixed after creation: the language is half the natural key that groups a content's
322
+ // translations, so changing it would silently move the content to another translation set.
323
+ disabled: !!content,
324
+ },
325
+ Validators.required,
326
+ ],
327
+ // Editing a content that is already the page's own (an empty slug) starts the checkbox checked -
328
+ // otherwise applySlugAvailability would immediately overwrite its empty slug with a forced-required
329
+ // value the moment the form builds.
330
+ usePageOwnAddress: [!!content && !content.slug],
331
+ slug: [content?.slug ?? '', [Validators.maxLength(MAX_SLUG_LENGTH), Validators.pattern(SLUG_PATTERN)]],
332
+ publishTime: [this.toLocalDateTime(content?.publishTime), Validators.required],
333
+ status: [content?.status ?? ContentStatus.Published],
334
+ flexFields: this.fb.group({}),
335
+ });
336
+ this.form = form;
337
+ this.applySlugAvailability();
338
+ this.applyPublishTimeAvailability();
339
+ return form;
340
+ }
341
+ /**
342
+ * Enables/disables and (in)validates the `slug` control from {@link slugState} - and, in the
343
+ * `'optional'` case, from whether the "use the page's own address" checkbox is ticked. The same
344
+ * three-way split `ContentManager.CheckSlugRequirementAsync` enforces server-side; this only decides
345
+ * what the form looks like before that ever runs.
346
+ */
347
+ applySlugAvailability() {
348
+ const slug = this.form?.get('slug');
349
+ if (!slug) {
350
+ return;
351
+ }
352
+ const forceOwnAddress = this.slugState === 'forbidden' ||
353
+ (this.slugState === 'optional' && this.usePageOwnAddress);
354
+ if (forceOwnAddress) {
355
+ slug.setValue('');
356
+ slug.clearValidators();
357
+ slug.disable();
358
+ }
359
+ else {
360
+ slug.enable();
361
+ slug.setValidators([
362
+ Validators.required,
363
+ Validators.maxLength(MAX_SLUG_LENGTH),
364
+ Validators.pattern(SLUG_PATTERN),
365
+ ]);
366
+ }
367
+ slug.updateValueAndValidity();
368
+ }
369
+ /**
370
+ * Looks up whether this page's own address (an empty slug) is already taken for the selected
371
+ * language, so the "use the page's own address" option can be withheld before the editor ever submits
372
+ * and hits `Site:040001`. A no-op outside the `'optional'` state, where the question does not arise.
373
+ *
374
+ * Goes through `getList` rather than the more direct `findBySlug`, which exists for exactly this
375
+ * lookup - `IContentAdminAppService.FindBySlugAsync(pageId, cultureName, "")` is the same query
376
+ * `SiteRouteResolver` uses to resolve a bare page match. But ABP's `RestService.getParams` silently
377
+ * drops any query parameter whose value is an empty string, so a `slug` of `""` never reaches the
378
+ * request at all; the server then sees a *missing* parameter, not an intentionally empty one, and
379
+ * rejects it exactly as `Should_Reject_A_Null_Slug` says it must. Listing this page's contents for the
380
+ * language and filtering for an empty slug client-side sidesteps that without weakening the server's
381
+ * null-vs-empty distinction.
382
+ */
383
+ refreshEmptySlotAvailability() {
384
+ const pageId = this.page()?.id;
385
+ if (this.slugState !== 'optional' || !pageId) {
386
+ this.emptySlotTaken.set(false);
387
+ return;
388
+ }
389
+ const cultureName = this.form?.get('cultureName')?.value;
390
+ if (!cultureName) {
391
+ return;
392
+ }
393
+ this.contentService
394
+ .getList({ pageId, cultureName, maxResultCount: 1000 })
395
+ .pipe(catchError(() => of({ items: [], totalCount: 0 })))
396
+ .subscribe(result => {
397
+ const existing = (result.items ?? []).find(item => item.slug === '');
398
+ const taken = !!existing && existing.id !== this.content?.id;
399
+ this.emptySlotTaken.set(taken);
400
+ // The slot this checkbox promised is gone (e.g. the language was switched to one where it is
401
+ // already taken) - un-tick it rather than leave a checked box the next save would reject.
402
+ if (taken && this.usePageOwnAddress) {
403
+ this.form?.get('usePageOwnAddress')?.setValue(false);
404
+ this.applySlugAvailability();
405
+ }
406
+ });
407
+ }
408
+ /**
409
+ * A draft is never public, so a publish time on one is not a schedule anything acts on - the server
410
+ * rejects the combination outright (`Site:040003`). Scheduling is Published plus a future time.
411
+ */
412
+ applyPublishTimeAvailability() {
413
+ const publishTime = this.form?.get('publishTime');
414
+ const status = this.form?.get('status')?.value;
415
+ if (!publishTime) {
416
+ return;
417
+ }
418
+ if (status === ContentStatus.Draft) {
419
+ publishTime.setValue(this.toLocalDateTime());
420
+ publishTime.disable();
421
+ }
422
+ else {
423
+ publishTime.enable();
424
+ }
425
+ }
426
+ /** `<input type="datetime-local">` wants `yyyy-MM-ddTHH:mm`, not an ISO string with a zone. */
427
+ toLocalDateTime(value) {
428
+ const date = value ? new Date(value) : new Date();
429
+ const offset = date.getTimezoneOffset() * 60_000;
430
+ return new Date(date.getTime() - offset).toISOString().slice(0, 16);
431
+ }
432
+ /**
433
+ * Mirrors `SiteUrlContext.ApplyCulturePrefix` (总体设计 §5.5): every enabled language carries a
434
+ * `/cultureName` prefix except the default one, which is served unprefixed. `cultureName` here is
435
+ * already one of `languages` (or the loaded content's own, already-normalized value), so - unlike the
436
+ * server, which normalizes through `CultureNameNormalizer` first - a plain equality check against
437
+ * {@link defaultLanguage} is enough to tell the two apart.
438
+ */
439
+ applyCulturePrefix(path) {
440
+ const cultureName = this.form?.get('cultureName')?.value;
441
+ if (!cultureName || cultureName === this.defaultLanguage) {
442
+ return path;
443
+ }
444
+ return path === '/' ? `/${cultureName}` : `/${cultureName}${path}`;
445
+ }
446
+ /** Mirrors `PageRoute.GetPath`: everything up to the segment that first carries a placeholder. */
447
+ pageOwnPath(route) {
448
+ const index = route.indexOf('{');
449
+ if (index < 0) {
450
+ return route;
451
+ }
452
+ const boundary = route.lastIndexOf('/', index);
453
+ return boundary <= 0 ? '/' : route.slice(0, boundary);
454
+ }
455
+ /**
456
+ * A rough, client-side mirror of `PageRoute.Build` - see {@link slugPreview}'s remarks. Every
457
+ * placeholder, `{slug}` included, goes through the same generic {@link resolvePlaceholder} - no
458
+ * per-name branch here, deliberately: the server does not have one either, it reflects over `Content`
459
+ * (`Page.ResolveFieldValue`), and a name-by-name `if` ladder here would just be that same mistake
460
+ * moved to TypeScript instead of removed.
461
+ * <p>
462
+ * Placeholders are located with {@link readPlaceholder}'s brace-depth-aware scan, not a single regex -
463
+ * a `/\{...\}/` pattern cannot tell a placeholder's own closing brace apart from a balanced one inside
464
+ * its `:REGEX` segment (a quantifier like `\d{4}`, e.g. in `{publishTime:yyyy-MM:^\d{4}-(0[1-9]|1[0-2])$}`),
465
+ * the same reason `PageRoute.TryParseTemplate` on the server had to stop using ABP's own tokenizer.
466
+ */
467
+ buildPreviewPath(route) {
468
+ const canonical = route.replace(OPTIONAL_SLUG_TOKEN, REQUIRED_SLUG_TOKEN);
469
+ let result = '';
470
+ let i = 0;
471
+ while (i < canonical.length) {
472
+ if (canonical[i] !== '{') {
473
+ result += canonical[i];
474
+ i++;
475
+ continue;
476
+ }
477
+ const placeholder = this.readPlaceholder(canonical, i);
478
+ if (!placeholder) {
479
+ // Not a well-formed placeholder start - a route that reaches this component was already
480
+ // validated by `PageRoute.IsValid` when it was saved, so this is only ever a defensive fallback:
481
+ // treat '{' as a literal character rather than throwing out of what is meant to be a best-effort
482
+ // preview.
483
+ result += canonical[i];
484
+ i++;
485
+ continue;
486
+ }
487
+ result += this.resolvePlaceholder(placeholder.name, placeholder.format) ?? placeholder.token;
488
+ i = placeholder.end;
489
+ }
490
+ return result;
491
+ }
492
+ /**
493
+ * Reads one `{name}` / `{name:FORMAT}` / `{name:REGEX}` / `{name:FORMAT:REGEX}` placeholder starting at
494
+ * `route[start]` (must be `'{'`), mirroring `PageRoute.TryParseTemplate`'s single-placeholder case
495
+ * (`src/Dignite.Site.Domain.Shared/Pages/PageRoute.cs`): brace depth is tracked once inside the
496
+ * `:`-segment so an embedded, balanced `{`/`}` does not get mistaken for the placeholder's own closing
497
+ * brace, and a backslash escapes the next character so an explicitly-escaped `\{`/`\}` never perturbs
498
+ * the count either. Returns `null` for anything that does not parse as a well-formed placeholder -
499
+ * {@link buildPreviewPath} treats that as a literal `{` rather than failing the whole preview.
500
+ * <p>
501
+ * Only `name` and `format` are extracted, never the regex itself - a placeholder's regex constrains
502
+ * what may be read back out of a request path, and has nothing to say about a value being written into
503
+ * one, the same reason `PageRoute.Build` on the server discards it too (see that method's remarks).
504
+ */
505
+ readPlaceholder(route, start) {
506
+ let i = start + 1; // consume '{'
507
+ const nameStart = i;
508
+ // Mirrors `PageRoute.IsNameStartChar`/`IsNameChar`: every `IdentifierName` a field can have, plus
509
+ // uppercase for `Content`'s camel-cased system properties (`publishTime`).
510
+ if (i >= route.length || !/[a-zA-Z0-9]/.test(route[i])) {
511
+ return null;
512
+ }
513
+ i++;
514
+ while (i < route.length && /[a-zA-Z0-9_-]/.test(route[i])) {
515
+ i++;
516
+ }
517
+ const name = route.slice(nameStart, i);
518
+ if (i >= route.length) {
519
+ return null;
520
+ }
521
+ if (route[i] === '}') {
522
+ return { token: route.slice(start, i + 1), end: i + 1, name, format: null };
523
+ }
524
+ if (route[i] !== ':') {
525
+ return null;
526
+ }
527
+ i++; // consume ':'
528
+ const innerStart = i;
529
+ let depth = 0;
530
+ let closed = false;
531
+ while (i < route.length) {
532
+ const c = route[i];
533
+ if (c === '\\' && i + 1 < route.length) {
534
+ i += 2;
535
+ continue;
536
+ }
537
+ if (c === '{') {
538
+ depth++;
539
+ }
540
+ else if (c === '}') {
541
+ if (depth === 0) {
542
+ closed = true;
543
+ break;
544
+ }
545
+ depth--;
546
+ }
547
+ i++;
548
+ }
549
+ if (!closed) {
550
+ return null;
551
+ }
552
+ const inner = route.slice(innerStart, i);
553
+ const end = i + 1; // consume the closing '}'
554
+ return { token: route.slice(start, end), end, name, format: this.disambiguateFormat(inner) };
555
+ }
556
+ /**
557
+ * Mirrors `PageRoute.TryDisambiguate`, reduced to the half a preview needs: a placeholder's already
558
+ * brace-depth-isolated inner text (everything after the first `:`) splits into a `:FORMAT` prefix plus a
559
+ * regex remainder when the prefix is format-shaped (letters/digits/`.`/`_`/`-` only - a real format
560
+ * string, by construction, can never also look like a regex worth compiling, which is what keeps every
561
+ * `{name:FORMAT}` route classified exactly as it always was); otherwise the inner text is a bare format
562
+ * on its own if it matches that shape whole, or else it is a regex with no format at all.
563
+ */
564
+ disambiguateFormat(inner) {
565
+ const formatCharacters = /^[a-zA-Z0-9_.-]+$/;
566
+ const colon = inner.indexOf(':');
567
+ if (colon >= 0) {
568
+ const candidate = inner.slice(0, colon);
569
+ if (candidate && formatCharacters.test(candidate)) {
570
+ return candidate;
571
+ }
572
+ }
573
+ return formatCharacters.test(inner) ? inner : null;
574
+ }
575
+ /**
576
+ * Resolves one placeholder's preview text purely from *where* `name` is actually found, never from
577
+ * what it is called. Angular forms have no runtime type to reflect over the way `Content` does server
578
+ * side - `ContentDto` is a TypeScript interface, erased at compile time - so the nearest equivalent is
579
+ * a generic, case-insensitive walk of what is actually on the form and the loaded content, checked in
580
+ * the same order the server would find them: a real property first, `FlexFields` second.
581
+ * 1. A top-level form control - every system field the editor can change lives here (`cultureName`,
582
+ * `status`, `publishTime`, `contentTypeId`, `slug`...), whatever it happens to be named.
583
+ * 2. A control inside `flexFields` - a content type's own business fields.
584
+ * 3. A property already on the loaded `this.content` DTO - covers the read-only audit trail
585
+ * (`id`, `creationTime`, ...) `FullAuditedEntityDto` carries but no form control exists for.
586
+ * Naturally empty until this content has actually been saved once - which is honest, since the
587
+ * server has nothing to reflect there yet either.
588
+ *
589
+ * One deliberate, named exception to "no name-based branching": {@link usePageOwnAddress} is a UI-only
590
+ * control with no server-side counterpart, and step 1 cannot tell that apart from a real field by
591
+ * shape alone - a route that actually referenced `{usePageOwnAddress}` would resolve to "true"/"false"
592
+ * here. Excluding it by name would be exactly the kind of branch this method exists to avoid, and the
593
+ * failure mode is harmless: `Page.BuildContentPath` has no such property either, so a route that made
594
+ * this mistake for real still fails loudly server-side the first time a URL is actually built from it.
595
+ */
596
+ resolvePlaceholder(name, format) {
597
+ const raw = this.findValue(name);
598
+ if (raw === null || raw === undefined || raw === '') {
599
+ return null;
600
+ }
601
+ if (format) {
602
+ const formatted = this.tryFormatByShape(raw, format);
603
+ if (formatted !== null) {
604
+ return formatted;
605
+ }
606
+ }
607
+ return String(raw);
608
+ }
609
+ /**
610
+ * Applies `format` to `raw` purely from what `raw` looks like, never from which placeholder produced
611
+ * it - the same dispatch-by-shape rule {@link resolvePlaceholder} follows, extended to every shape
612
+ * this preview knows how to format. Mirrors `Page.FormatFieldValue`'s `value is IFormattable` check,
613
+ * just done by sniffing a form value's shape with a regex instead of asking the CLR for its runtime
614
+ * type - there is no such type to ask here, `raw` is already unwrapped to a plain JS value by the time
615
+ * it reaches this method.
616
+ * <p>
617
+ * Each shape is tried in turn; the first one whose regex matches `raw` owns the answer from there,
618
+ * whether or not it actually recognizes `format` - a date-shaped value with a nonsense format is a
619
+ * date that failed to format, not a number or a Guid that happened to also look like one. Recognizing
620
+ * the shape but not the specific `format` asked of it (a Guid's `X`, a numeric format outside
621
+ * {@link formatNumber}'s subset) returns null, same as a shape nothing here recognizes at all - both
622
+ * fall back to the unformatted value in {@link resolvePlaceholder}, deliberately: this is a preview,
623
+ * and a plausible but possibly-wrong guess at an unimplemented format would be worse than showing the
624
+ * plain value.
625
+ */
626
+ tryFormatByShape(raw, format) {
627
+ const date = this.tryParseDate(raw);
628
+ if (date) {
629
+ return this.formatDate(date, format);
630
+ }
631
+ const guid = this.tryParseGuid(raw);
632
+ if (guid) {
633
+ return this.formatGuid(guid, format);
634
+ }
635
+ const number = this.tryParseNumber(raw);
636
+ if (number !== null) {
637
+ return this.formatNumber(number, format);
638
+ }
639
+ return null;
640
+ }
641
+ findValue(name) {
642
+ const lowerName = name.toLowerCase();
643
+ const topLevelControls = this.form?.controls;
644
+ const topLevelKey = topLevelControls && Object.keys(topLevelControls).find(key => key.toLowerCase() === lowerName);
645
+ if (topLevelKey) {
646
+ return this.form.get(topLevelKey).value;
647
+ }
648
+ const flexFields = this.form?.get('flexFields');
649
+ if (flexFields instanceof FormGroup) {
650
+ const flexKey = Object.keys(flexFields.controls).find(key => key.toLowerCase() === lowerName);
651
+ if (flexKey) {
652
+ return flexFields.get(flexKey).value;
653
+ }
654
+ }
655
+ if (this.content) {
656
+ const contentRecord = this.content;
657
+ const contentKey = Object.keys(contentRecord).find(key => key.toLowerCase() === lowerName);
658
+ if (contentKey !== undefined) {
659
+ return contentRecord[contentKey];
660
+ }
661
+ }
662
+ return undefined;
663
+ }
664
+ /** Whether `value` is shaped like a `datetime-local` input's own output (`yyyy-MM-ddTHH:mm`). */
665
+ tryParseDate(value) {
666
+ if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(value)) {
667
+ return null;
668
+ }
669
+ const date = new Date(value);
670
+ return isNaN(date.getTime()) ? null : date;
671
+ }
672
+ /** Only the three specifiers a `:FORMAT` suffix may actually contain (总体设计 §3.3) - no '/' among them. */
673
+ formatDate(date, format) {
674
+ return format
675
+ .replace(/yyyy/g, String(date.getFullYear()))
676
+ .replace(/yy/g, String(date.getFullYear()).slice(-2))
677
+ .replace(/MM/g, String(date.getMonth() + 1).padStart(2, '0'))
678
+ .replace(/dd/g, String(date.getDate()).padStart(2, '0'));
679
+ }
680
+ /** Whether `value` is shaped like the canonical, hyphenated form of a Guid. */
681
+ tryParseGuid(value) {
682
+ return typeof value === 'string' &&
683
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value)
684
+ ? value
685
+ : null;
686
+ }
687
+ /**
688
+ * `Guid.ToString(format)`'s four straightforward specifiers - N (no separators), D (hyphenated, the
689
+ * shape already checked by {@link tryParseGuid}), B (braced) and P (parenthesized). `X` deliberately
690
+ * not implemented: it renders as a C#-syntax initializer list, not a URL-shaped string, and nothing
691
+ * about a route makes that a plausible thing to have ever asked for.
692
+ */
693
+ formatGuid(guid, format) {
694
+ const digits = guid.replace(/-/g, '').toLowerCase();
695
+ const dashed = `${digits.slice(0, 8)}-${digits.slice(8, 12)}-${digits.slice(12, 16)}-${digits.slice(16, 20)}-${digits.slice(20)}`;
696
+ switch (format.toUpperCase()) {
697
+ case 'N':
698
+ return digits;
699
+ case 'D':
700
+ return dashed;
701
+ case 'B':
702
+ return `{${dashed}}`;
703
+ case 'P':
704
+ return `(${dashed})`;
705
+ default:
706
+ return null;
707
+ }
708
+ }
709
+ /** Whether `value` is shaped like a plain, optionally negative, optionally decimal number. */
710
+ tryParseNumber(value) {
711
+ if (typeof value === 'number' && isFinite(value)) {
712
+ return value;
713
+ }
714
+ return typeof value === 'string' && /^-?\d+(\.\d+)?$/.test(value) ? Number(value) : null;
715
+ }
716
+ /**
717
+ * A representative slice of .NET's numeric format strings - the ones a URL path segment plausibly
718
+ * asks for. Custom zero-padding (`"0000"`, unchanged from before) plus four standard specifiers, each
719
+ * with an optional 1-2 digit precision: `D` (zero-padded, integers only - the standard-format spelling
720
+ * of the same thing `"0000"` does), `X`/`x` (hexadecimal, integers only, letter case controls digit
721
+ * case, negative not attempted - see {@link formatHex}'s remarks), `F` (fixed-point) and `N`
722
+ * (fixed-point with thousands grouping). Deliberately not `C`/`P` (currency/percent): both put a
723
+ * character that means something else in a URL - a locale-dependent currency symbol, or `%`, the URL
724
+ * escape character itself - into the path, which nothing about routing would ever actually want. Not
725
+ * `E`/`G`/`R` either: those are about round-trip print fidelity for an arbitrary number, not a shape
726
+ * any real route has a reason to ask for.
727
+ */
728
+ formatNumber(value, format) {
729
+ if (/^0+$/.test(format)) {
730
+ return Number.isInteger(value) ? this.padInteger(value, format.length) : null;
731
+ }
732
+ const standard = /^([DdXxFfNn])(\d{0,2})$/.exec(format);
733
+ if (!standard) {
734
+ return null;
735
+ }
736
+ const [, specifier, precisionText] = standard;
737
+ const precision = precisionText === '' ? null : Number(precisionText);
738
+ switch (specifier.toUpperCase()) {
739
+ case 'D':
740
+ return Number.isInteger(value) ? this.padInteger(value, precision ?? 0) : null;
741
+ case 'X':
742
+ return Number.isInteger(value) && value >= 0 ? this.formatHex(value, precision ?? 0, specifier === 'X') : null;
743
+ case 'F':
744
+ return value.toFixed(precision ?? 2);
745
+ case 'N':
746
+ return this.groupThousands(value.toFixed(precision ?? 2));
747
+ default:
748
+ return null;
749
+ }
750
+ }
751
+ padInteger(value, width) {
752
+ const negative = value < 0;
753
+ const padded = Math.abs(value).toString().padStart(width, '0');
754
+ return negative ? `-${padded}` : padded;
755
+ }
756
+ /**
757
+ * Negative values are rejected before this is ever called - .NET's `X` renders a negative signed
758
+ * integer as its two's-complement bit pattern, which depends on the source type's own width (`int` vs
759
+ * `long` vs `byte` produce different digits for the same value), information a form control's plain
760
+ * number does not carry. Guessing a width would risk a preview that looks plausible and is wrong;
761
+ * not attempting it is the same call already made for a Guid's `X` and for `C`/`P`/`E`/`G`/`R`.
762
+ */
763
+ formatHex(value, width, uppercase) {
764
+ const hex = value.toString(16).padStart(width, '0');
765
+ return uppercase ? hex.toUpperCase() : hex;
766
+ }
767
+ groupThousands(fixedValue) {
768
+ const negative = fixedValue.startsWith('-');
769
+ const [integerPart, fractionPart] = (negative ? fixedValue.slice(1) : fixedValue).split('.');
770
+ const grouped = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
771
+ return `${negative ? '-' : ''}${grouped}${fractionPart ? '.' + fractionPart : ''}`;
772
+ }
773
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: ContentEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
774
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.2.2", type: ContentEditorComponent, isStandalone: true, selector: "site-content-editor", ngImport: i0, template: "@if (!isLoading() && form) {\n <form [formGroup]=\"form\" (submit)=\"save()\">\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <div>\n <h5 class=\"mb-0\">\n {{ (isEdit ? 'Site::EditContent' : 'Site::NewContent') | abpLocalization }}\n </h5>\n @if (page(); as currentPage) {\n <small class=\"text-muted\">{{ currentPage.displayName }}</small>\n }\n </div>\n <div>\n <a class=\"btn btn-secondary me-2\" [routerLink]=\"['/site/contents']\">\n {{ 'AbpUi::Cancel' | abpLocalization }}\n </a>\n <!-- A plain submit button, not `abp-button`: that component declares its own `click` output,\n so `(click)` binds to an EventEmitter instead of the DOM event, and it takes `buttonType`\n rather than `type`. Both make it easy to end up with a button that quietly never submits.\n Inside a form on a page, the native element says what it does. -->\n <button type=\"submit\" class=\"btn btn-primary\" [disabled]=\"!form.valid || isBusy()\">\n @if (isBusy()) {\n <span class=\"spinner-border spinner-border-sm me-1\" role=\"status\"></span>\n } @else {\n <i class=\"fa fa-check me-1\"></i>\n }\n {{ 'AbpUi::Save' | abpLocalization }}\n </button>\n </div>\n </div>\n\n <div class=\"row g-3\">\n <!-- Left: the content itself - whatever fields its content type defines. -->\n <div class=\"col-lg-8\">\n <div class=\"card h-100\">\n <div class=\"card-body\">\n @if (hasMultipleContentTypes) {\n <div class=\"mb-3 pb-3 border-bottom\">\n <label class=\"form-label\">\n {{ 'Site::DisplayName:Content.ContentType' | abpLocalization }}\n </label>\n <select\n class=\"form-select\"\n formControlName=\"contentTypeId\"\n (change)=\"onContentTypeChange()\"\n >\n @for (contentType of contentTypes(); track contentType.id) {\n <option [value]=\"contentType.id\">{{ contentType.displayName }}</option>\n }\n </select>\n <small class=\"form-text text-muted\">\n {{ 'Site::ContentTypeSwitchHint' | abpLocalization }}\n </small>\n </div>\n }\n\n @if (fields().length > 0) {\n @for (fieldValue of fields(); track fieldValue.field.name) {\n <ff-flex-field-control\n [fields]=\"fieldValue\"\n [entity]=\"form\"\n parentFieldName=\"flexFields\"\n [selected]=\"fieldValue.value\"\n ></ff-flex-field-control>\n @if (fieldErrorMessage(fieldValue); as message) {\n <div class=\"invalid-feedback d-block mb-3\">{{ message }}</div>\n }\n }\n } @else {\n <p class=\"text-muted mb-0\">{{ 'Site::NoFieldsArranged' | abpLocalization }}</p>\n }\n </div>\n </div>\n </div>\n\n <!-- Right: the system fields that place and schedule the content. -->\n <div class=\"col-lg-4\">\n <div class=\"card\">\n <div class=\"card-body\">\n @if (!hasMultipleContentTypes && contentTypes().length === 1) {\n <div class=\"mb-3\">\n <label class=\"form-label\">\n {{ 'Site::DisplayName:Content.ContentType' | abpLocalization }}\n </label>\n <input type=\"text\" class=\"form-control\" [value]=\"contentTypes()[0].displayName\" disabled />\n </div>\n }\n\n <div class=\"mb-3\">\n <label class=\"form-label\">\n {{ 'Site::DisplayName:Content.CultureName' | abpLocalization }}\n </label>\n <select class=\"form-select\" formControlName=\"cultureName\" (change)=\"onCultureNameChange()\">\n @for (language of languages(); track language) {\n <option [value]=\"language\">{{ language }}</option>\n }\n </select>\n </div>\n\n <!-- The page's route (\u603B\u4F53\u8BBE\u8BA1 \u00A73.3) decides this, not the editor: no {slug}/{slug?} at all\n means nothing is shown here - there is nothing to choose. -->\n @if (slugState !== 'forbidden') {\n <div class=\"mb-3\">\n <label class=\"form-label\">{{ 'Site::DisplayName:Content.Slug' | abpLocalization }}</label>\n\n @if (slugState === 'optional' && !emptySlotTaken()) {\n <div class=\"form-check mb-2\">\n <input\n type=\"checkbox\"\n class=\"form-check-input\"\n id=\"site-use-page-own-address\"\n formControlName=\"usePageOwnAddress\"\n (change)=\"onUsePageOwnAddressChange()\"\n />\n <label class=\"form-check-label\" for=\"site-use-page-own-address\">\n {{ 'Site::UsePageOwnAddress' | abpLocalization }}\n </label>\n </div>\n }\n\n @if (!usePageOwnAddress) {\n <input type=\"text\" class=\"form-control\" formControlName=\"slug\" />\n <small class=\"form-text text-muted d-block mt-1\">\n {{ 'Site::Description:Content.Slug' | abpLocalization }}\n </small>\n }\n\n @if (slugState === 'optional' && emptySlotTaken()) {\n <small class=\"form-text text-muted d-block mt-1\">\n {{ 'Site::SlugSlotTaken' | abpLocalization }}\n </small>\n }\n\n <small class=\"form-text text-muted d-block mt-1\">\n {{ 'Site::SlugPreview' | abpLocalization: slugPreview }}\n </small>\n </div>\n }\n\n <div class=\"mb-3\">\n <label class=\"form-label\">{{ 'Site::DisplayName:Content.Status' | abpLocalization }}</label>\n <select class=\"form-select\" formControlName=\"status\" (change)=\"onStatusChange()\">\n @for (status of statuses; track status) {\n <option [ngValue]=\"status\">{{ statusKeyOf(status) | abpLocalization }}</option>\n }\n </select>\n </div>\n\n <div class=\"mb-0\">\n <label class=\"form-label\">\n {{ 'Site::DisplayName:Content.PublishTime' | abpLocalization }}\n </label>\n <input type=\"datetime-local\" class=\"form-control\" formControlName=\"publishTime\" />\n @if (isDraft) {\n <small class=\"form-text text-muted\">\n {{ 'Site::DraftPublishTimeDisabledHint' | abpLocalization }}\n </small>\n }\n </div>\n </div>\n </div>\n </div>\n </div>\n </form>\n}\n", dependencies: [{ kind: "ngmodule", type: CoreModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.CheckboxControlValueAccessor, selector: "input[type=checkbox]:not([ngNoCva])[formControlName],input[type=checkbox]:not([ngNoCva])[formControl],input[type=checkbox]:not([ngNoCva])[ngModel]" }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple]):not([ngNoCva])[formControlName],select:not([multiple]):not([ngNoCva])[formControl],select:not([multiple]):not([ngNoCva])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: i2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "directive", type: i3.ValidationGroupDirective, selector: "[formGroup],[formGroupName]", exportAs: ["validationGroup"] }, { kind: "directive", type: i3.ValidationDirective, selector: "[formControl],[formControlName]", exportAs: ["validationDirective"] }, { kind: "ngmodule", type: ThemeSharedModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "component", type: FlexFieldControlComponent, selector: "ff-flex-field-control", inputs: ["fields", "entity", "parentFieldName", "selected"] }, { kind: "pipe", type: i4.LocalizationPipe, name: "abpLocalization" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
775
+ }
776
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: ContentEditorComponent, decorators: [{
777
+ type: Component,
778
+ args: [{ selector: 'site-content-editor', changeDetection: ChangeDetectionStrategy.OnPush, imports: [CoreModule, ThemeSharedModule, ReactiveFormsModule, RouterLink, FlexFieldControlComponent], template: "@if (!isLoading() && form) {\n <form [formGroup]=\"form\" (submit)=\"save()\">\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <div>\n <h5 class=\"mb-0\">\n {{ (isEdit ? 'Site::EditContent' : 'Site::NewContent') | abpLocalization }}\n </h5>\n @if (page(); as currentPage) {\n <small class=\"text-muted\">{{ currentPage.displayName }}</small>\n }\n </div>\n <div>\n <a class=\"btn btn-secondary me-2\" [routerLink]=\"['/site/contents']\">\n {{ 'AbpUi::Cancel' | abpLocalization }}\n </a>\n <!-- A plain submit button, not `abp-button`: that component declares its own `click` output,\n so `(click)` binds to an EventEmitter instead of the DOM event, and it takes `buttonType`\n rather than `type`. Both make it easy to end up with a button that quietly never submits.\n Inside a form on a page, the native element says what it does. -->\n <button type=\"submit\" class=\"btn btn-primary\" [disabled]=\"!form.valid || isBusy()\">\n @if (isBusy()) {\n <span class=\"spinner-border spinner-border-sm me-1\" role=\"status\"></span>\n } @else {\n <i class=\"fa fa-check me-1\"></i>\n }\n {{ 'AbpUi::Save' | abpLocalization }}\n </button>\n </div>\n </div>\n\n <div class=\"row g-3\">\n <!-- Left: the content itself - whatever fields its content type defines. -->\n <div class=\"col-lg-8\">\n <div class=\"card h-100\">\n <div class=\"card-body\">\n @if (hasMultipleContentTypes) {\n <div class=\"mb-3 pb-3 border-bottom\">\n <label class=\"form-label\">\n {{ 'Site::DisplayName:Content.ContentType' | abpLocalization }}\n </label>\n <select\n class=\"form-select\"\n formControlName=\"contentTypeId\"\n (change)=\"onContentTypeChange()\"\n >\n @for (contentType of contentTypes(); track contentType.id) {\n <option [value]=\"contentType.id\">{{ contentType.displayName }}</option>\n }\n </select>\n <small class=\"form-text text-muted\">\n {{ 'Site::ContentTypeSwitchHint' | abpLocalization }}\n </small>\n </div>\n }\n\n @if (fields().length > 0) {\n @for (fieldValue of fields(); track fieldValue.field.name) {\n <ff-flex-field-control\n [fields]=\"fieldValue\"\n [entity]=\"form\"\n parentFieldName=\"flexFields\"\n [selected]=\"fieldValue.value\"\n ></ff-flex-field-control>\n @if (fieldErrorMessage(fieldValue); as message) {\n <div class=\"invalid-feedback d-block mb-3\">{{ message }}</div>\n }\n }\n } @else {\n <p class=\"text-muted mb-0\">{{ 'Site::NoFieldsArranged' | abpLocalization }}</p>\n }\n </div>\n </div>\n </div>\n\n <!-- Right: the system fields that place and schedule the content. -->\n <div class=\"col-lg-4\">\n <div class=\"card\">\n <div class=\"card-body\">\n @if (!hasMultipleContentTypes && contentTypes().length === 1) {\n <div class=\"mb-3\">\n <label class=\"form-label\">\n {{ 'Site::DisplayName:Content.ContentType' | abpLocalization }}\n </label>\n <input type=\"text\" class=\"form-control\" [value]=\"contentTypes()[0].displayName\" disabled />\n </div>\n }\n\n <div class=\"mb-3\">\n <label class=\"form-label\">\n {{ 'Site::DisplayName:Content.CultureName' | abpLocalization }}\n </label>\n <select class=\"form-select\" formControlName=\"cultureName\" (change)=\"onCultureNameChange()\">\n @for (language of languages(); track language) {\n <option [value]=\"language\">{{ language }}</option>\n }\n </select>\n </div>\n\n <!-- The page's route (\u603B\u4F53\u8BBE\u8BA1 \u00A73.3) decides this, not the editor: no {slug}/{slug?} at all\n means nothing is shown here - there is nothing to choose. -->\n @if (slugState !== 'forbidden') {\n <div class=\"mb-3\">\n <label class=\"form-label\">{{ 'Site::DisplayName:Content.Slug' | abpLocalization }}</label>\n\n @if (slugState === 'optional' && !emptySlotTaken()) {\n <div class=\"form-check mb-2\">\n <input\n type=\"checkbox\"\n class=\"form-check-input\"\n id=\"site-use-page-own-address\"\n formControlName=\"usePageOwnAddress\"\n (change)=\"onUsePageOwnAddressChange()\"\n />\n <label class=\"form-check-label\" for=\"site-use-page-own-address\">\n {{ 'Site::UsePageOwnAddress' | abpLocalization }}\n </label>\n </div>\n }\n\n @if (!usePageOwnAddress) {\n <input type=\"text\" class=\"form-control\" formControlName=\"slug\" />\n <small class=\"form-text text-muted d-block mt-1\">\n {{ 'Site::Description:Content.Slug' | abpLocalization }}\n </small>\n }\n\n @if (slugState === 'optional' && emptySlotTaken()) {\n <small class=\"form-text text-muted d-block mt-1\">\n {{ 'Site::SlugSlotTaken' | abpLocalization }}\n </small>\n }\n\n <small class=\"form-text text-muted d-block mt-1\">\n {{ 'Site::SlugPreview' | abpLocalization: slugPreview }}\n </small>\n </div>\n }\n\n <div class=\"mb-3\">\n <label class=\"form-label\">{{ 'Site::DisplayName:Content.Status' | abpLocalization }}</label>\n <select class=\"form-select\" formControlName=\"status\" (change)=\"onStatusChange()\">\n @for (status of statuses; track status) {\n <option [ngValue]=\"status\">{{ statusKeyOf(status) | abpLocalization }}</option>\n }\n </select>\n </div>\n\n <div class=\"mb-0\">\n <label class=\"form-label\">\n {{ 'Site::DisplayName:Content.PublishTime' | abpLocalization }}\n </label>\n <input type=\"datetime-local\" class=\"form-control\" formControlName=\"publishTime\" />\n @if (isDraft) {\n <small class=\"form-text text-muted\">\n {{ 'Site::DraftPublishTimeDisabledHint' | abpLocalization }}\n </small>\n }\n </div>\n </div>\n </div>\n </div>\n </div>\n </form>\n}\n" }]
779
+ }], ctorParameters: () => [] });
780
+
781
+ export { ContentEditorComponent };
782
+ //# sourceMappingURL=dignite-ng.site-content-editor.component-iFWDeYJ9.mjs.map