feat(back-end): rich text improvements
This commit is contained in:
@@ -711,7 +711,6 @@
|
|||||||
[attr.aria-label]="
|
[attr.aria-label]="
|
||||||
'Descrizione ' + languageLabels[activeContentLanguage]
|
'Descrizione ' + languageLabels[activeContentLanguage]
|
||||||
"
|
"
|
||||||
[innerHTML]="productForm.descriptions[activeContentLanguage]"
|
|
||||||
(input)="onDescriptionEditorInput($event)"
|
(input)="onDescriptionEditorInput($event)"
|
||||||
(blur)="onDescriptionEditorBlur($event)"
|
(blur)="onDescriptionEditorBlur($event)"
|
||||||
></div>
|
></div>
|
||||||
|
|||||||
@@ -263,20 +263,19 @@
|
|||||||
gap: 0.4rem;
|
gap: 0.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rich-text-toolbar__button {
|
.admin-shop .rich-text-toolbar__button {
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border) !important;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm) !important;
|
||||||
background: #fff;
|
background: #fff !important;
|
||||||
color: var(--color-text);
|
color: var(--color-text) !important;
|
||||||
min-height: 2rem;
|
min-height: 2rem;
|
||||||
padding: 0.28rem 0.56rem;
|
padding: 0.28rem 0.56rem !important;
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.rich-text-toolbar__button:hover {
|
.admin-shop .rich-text-toolbar__button:hover:not(:disabled) {
|
||||||
border-color: #cbb88a;
|
border-color: #cbb88a !important;
|
||||||
background: #fffdf4;
|
background: #fffdf4 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rich-text-editor {
|
.rich-text-editor {
|
||||||
|
|||||||
@@ -140,10 +140,14 @@ const RICH_TEXT_ALLOWED_TAGS = new Set([
|
|||||||
export class AdminShopComponent implements OnInit, OnDestroy {
|
export class AdminShopComponent implements OnInit, OnDestroy {
|
||||||
private readonly adminShopService = inject(AdminShopService);
|
private readonly adminShopService = inject(AdminShopService);
|
||||||
private readonly adminOperationsService = inject(AdminOperationsService);
|
private readonly adminOperationsService = inject(AdminOperationsService);
|
||||||
|
private descriptionEditorElement: HTMLDivElement | null = null;
|
||||||
@ViewChild('workspaceRef')
|
@ViewChild('workspaceRef')
|
||||||
private readonly workspaceRef?: ElementRef<HTMLDivElement>;
|
private readonly workspaceRef?: ElementRef<HTMLDivElement>;
|
||||||
@ViewChild('descriptionEditorRef')
|
@ViewChild('descriptionEditorRef')
|
||||||
private readonly descriptionEditorRef?: ElementRef<HTMLDivElement>;
|
set descriptionEditorRef(value: ElementRef<HTMLDivElement> | undefined) {
|
||||||
|
this.descriptionEditorElement = value?.nativeElement ?? null;
|
||||||
|
this.renderActiveDescriptionInEditor();
|
||||||
|
}
|
||||||
|
|
||||||
readonly shopLanguages = SHOP_LANGUAGES;
|
readonly shopLanguages = SHOP_LANGUAGES;
|
||||||
readonly mediaLanguages = MEDIA_LANGUAGES;
|
readonly mediaLanguages = MEDIA_LANGUAGES;
|
||||||
@@ -318,6 +322,8 @@ export class AdminShopComponent implements OnInit, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.syncDescriptionFromEditor(this.descriptionEditorElement, true);
|
||||||
|
|
||||||
const validationError = this.validateProductForm();
|
const validationError = this.validateProductForm();
|
||||||
if (validationError) {
|
if (validationError) {
|
||||||
this.errorMessage = validationError;
|
this.errorMessage = validationError;
|
||||||
@@ -541,11 +547,9 @@ export class AdminShopComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setActiveContentLanguage(language: ShopLanguage): void {
|
setActiveContentLanguage(language: ShopLanguage): void {
|
||||||
this.syncDescriptionFromEditor(
|
this.syncDescriptionFromEditor(this.descriptionEditorElement, true);
|
||||||
this.descriptionEditorRef?.nativeElement ?? null,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
this.activeContentLanguage = language;
|
this.activeContentLanguage = language;
|
||||||
|
this.renderActiveDescriptionInEditor();
|
||||||
}
|
}
|
||||||
|
|
||||||
isContentLanguageComplete(language: ShopLanguage): boolean {
|
isContentLanguageComplete(language: ShopLanguage): boolean {
|
||||||
@@ -1268,6 +1272,7 @@ export class AdminShopComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
private resetProductForm(): void {
|
private resetProductForm(): void {
|
||||||
Object.assign(this.productForm, this.createEmptyProductForm());
|
Object.assign(this.productForm, this.createEmptyProductForm());
|
||||||
|
this.renderActiveDescriptionInEditor();
|
||||||
}
|
}
|
||||||
|
|
||||||
private createEmptyMaterialForm(
|
private createEmptyMaterialForm(
|
||||||
@@ -1324,6 +1329,7 @@ export class AdminShopComponent implements OnInit, OnDestroy {
|
|||||||
sortOrder: product.sortOrder ?? 0,
|
sortOrder: product.sortOrder ?? 0,
|
||||||
materials: this.toMaterialForms(product.variants),
|
materials: this.toMaterialForms(product.variants),
|
||||||
});
|
});
|
||||||
|
this.renderActiveDescriptionInEditor();
|
||||||
}
|
}
|
||||||
|
|
||||||
private toMaterialForms(
|
private toMaterialForms(
|
||||||
@@ -1841,8 +1847,19 @@ export class AdminShopComponent implements OnInit, OnDestroy {
|
|||||||
this.productForm.descriptions[currentLanguage] = editor.innerHTML;
|
this.productForm.descriptions[currentLanguage] = editor.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderActiveDescriptionInEditor(): void {
|
||||||
|
const editor = this.descriptionEditorElement;
|
||||||
|
if (!editor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const html = this.productForm.descriptions[this.activeContentLanguage] ?? '';
|
||||||
|
if (editor.innerHTML !== html) {
|
||||||
|
editor.innerHTML = html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private applyDescriptionExecCommand(command: string): void {
|
private applyDescriptionExecCommand(command: string): void {
|
||||||
const editor = this.descriptionEditorRef?.nativeElement ?? null;
|
const editor = this.descriptionEditorElement;
|
||||||
if (!editor) {
|
if (!editor) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -353,7 +353,7 @@
|
|||||||
<app-stl-viewer
|
<app-stl-viewer
|
||||||
[file]="modelPreviewFile"
|
[file]="modelPreviewFile"
|
||||||
[height]="420"
|
[height]="420"
|
||||||
[color]="selectedVariant()?.colorHex || '#facf0a'"
|
[color]="colorHex(selectedVariant())"
|
||||||
></app-stl-viewer>
|
></app-stl-viewer>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
|||||||
import { catchError, combineLatest, finalize, of, switchMap, tap } from 'rxjs';
|
import { catchError, combineLatest, finalize, of, switchMap, tap } from 'rxjs';
|
||||||
import { SeoService } from '../../core/services/seo.service';
|
import { SeoService } from '../../core/services/seo.service';
|
||||||
import { LanguageService } from '../../core/services/language.service';
|
import { LanguageService } from '../../core/services/language.service';
|
||||||
|
import { getColorHex } from '../../core/constants/colors.const';
|
||||||
import { AppButtonComponent } from '../../shared/components/app-button/app-button.component';
|
import { AppButtonComponent } from '../../shared/components/app-button/app-button.component';
|
||||||
import { AppCardComponent } from '../../shared/components/app-card/app-card.component';
|
import { AppCardComponent } from '../../shared/components/app-card/app-card.component';
|
||||||
import { StlViewerComponent } from '../../shared/components/stl-viewer/stl-viewer.component';
|
import { StlViewerComponent } from '../../shared/components/stl-viewer/stl-viewer.component';
|
||||||
@@ -52,6 +53,7 @@ interface ShopMaterialProperty {
|
|||||||
styleUrl: './product-detail.component.scss',
|
styleUrl: './product-detail.component.scss',
|
||||||
})
|
})
|
||||||
export class ProductDetailComponent {
|
export class ProductDetailComponent {
|
||||||
|
private static readonly HEX_COLOR_PATTERN = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
||||||
private readonly destroyRef = inject(DestroyRef);
|
private readonly destroyRef = inject(DestroyRef);
|
||||||
private readonly injector = inject(Injector);
|
private readonly injector = inject(Injector);
|
||||||
private readonly router = inject(Router);
|
private readonly router = inject(Router);
|
||||||
@@ -376,8 +378,18 @@ export class ProductDetailComponent {
|
|||||||
return variant.colorName || variant.variantLabel || '-';
|
return variant.colorName || variant.variantLabel || '-';
|
||||||
}
|
}
|
||||||
|
|
||||||
colorHex(variant: ShopProductVariantOption): string {
|
colorHex(variant: ShopProductVariantOption | null | undefined): string {
|
||||||
return variant.colorHex || '#d5d8de';
|
const normalizedHex = this.normalizeHexColor(variant?.colorHex);
|
||||||
|
if (normalizedHex) {
|
||||||
|
return normalizedHex;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallbackByName = this.colorHexFromName(variant?.colorName);
|
||||||
|
if (fallbackByName) {
|
||||||
|
return fallbackByName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '#d5d8de';
|
||||||
}
|
}
|
||||||
|
|
||||||
materialPriceLabel(material: ShopMaterialOption): number {
|
materialPriceLabel(material: ShopMaterialOption): number {
|
||||||
@@ -464,6 +476,34 @@ export class ProductDetailComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private normalizeHexColor(value: string | null | undefined): string | null {
|
||||||
|
const raw = String(value ?? '').trim();
|
||||||
|
if (!raw) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const withHash = raw.startsWith('#') ? raw : `#${raw}`;
|
||||||
|
if (!ProductDetailComponent.HEX_COLOR_PATTERN.test(withHash)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return withHash.toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
private colorHexFromName(value: string | null | undefined): string | null {
|
||||||
|
const colorName = String(value ?? '').trim();
|
||||||
|
if (!colorName) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallback = getColorHex(colorName);
|
||||||
|
if (!fallback || fallback === '#facf0a') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
private applySeo(product: ShopProductDetail): void {
|
private applySeo(product: ShopProductDetail): void {
|
||||||
const title = product.seoTitle || `${product.name} | 3D fab`;
|
const title = product.seoTitle || `${product.name} | 3D fab`;
|
||||||
const description =
|
const description =
|
||||||
|
|||||||
Reference in New Issue
Block a user