diff --git a/src/app/features/project-editor/components/html-editor/marketplace-html-editor.component.html b/src/app/features/project-editor/components/html-editor/marketplace-html-editor.component.html index 3677e12..32a566c 100644 --- a/src/app/features/project-editor/components/html-editor/marketplace-html-editor.component.html +++ b/src/app/features/project-editor/components/html-editor/marketplace-html-editor.component.html @@ -6,13 +6,18 @@ - + @if (showCode()) { + + @if (codeError()) { +

{{ codeError() }}

+ } + }
  ' }, + { id: 'hr', label: 'HR', command: 'insertHorizontalRule' }, + { id: 'codeblock', label: 'Code', command: 'formatBlock', value: 'PRE' }, + { id: 'embed', label: 'Embed', command: 'insertEmbed' }, ]; @Component({ @@ -40,6 +44,7 @@ export class MarketplaceHtmlEditorComponent implements OnChanges, AfterViewInit readonly toolbar = HTML_EDITOR_TOOLBAR; readonly showCode = signal(false); readonly codeValue = signal(''); + readonly codeError = signal(null); ngOnChanges(changes: SimpleChanges): void { if (changes['html']) { @@ -73,6 +78,20 @@ export class MarketplaceHtmlEditorComponent implements OnChanges, AfterViewInit return; } document.execCommand('insertImage', false, url); + } else if (item.command === 'insertEmbed') { + const url = window.prompt(this.translate.t('builder.promptEmbedUrl')); + if (!url) { + return; + } + // Sandboxed iframe: no same-origin/script escalation into the editor + // page itself. The editor already emits raw, unsanitized HTML by design + // (FACTS PE-...-0004) - sanitization happens at storefront render time + // (StaticPageComponent runs everything through DomSanitizer). + document.execCommand( + 'insertHTML', + false, + ``, + ); } else { document.execCommand(item.command, false, item.value); } @@ -86,9 +105,19 @@ export class MarketplaceHtmlEditorComponent implements OnChanges, AfterViewInit toggleCode(): void { if (!this.showCode()) { this.codeValue.set(this.surface.nativeElement.innerHTML); + this.codeError.set(null); this.showCode.set(true); return; } + // Validate raw HTML before committing it back to the visual surface - + // malformed markup (an unclosed/mismatched tag) stays in code mode with + // an inline error rather than silently corrupting the visual editor. + const result = validateHtml(this.codeValue()); + if (!result.ok) { + this.codeError.set(result.error ?? this.translate.t('builder.htmlEditorInvalidHtml')); + return; + } + this.codeError.set(null); this.surface.nativeElement.innerHTML = this.codeValue(); this.showCode.set(false); this.emitChange(); @@ -96,6 +125,13 @@ export class MarketplaceHtmlEditorComponent implements OnChanges, AfterViewInit updateCode(value: string): void { this.codeValue.set(value); + if (this.codeError()) { + this.codeError.set(null); + } + } + + private escapeAttribute(value: string): string { + return value.replace(/&/g, '&').replace(/"/g, '"'); } private emitChange(): void { diff --git a/src/app/i18n/en.ts b/src/app/i18n/en.ts index 53629d0..a17d55a 100644 --- a/src/app/i18n/en.ts +++ b/src/app/i18n/en.ts @@ -551,6 +551,8 @@ export const en: Translations = { confirmLeaveUnsaved: 'You have unsaved changes. Leave anyway?', promptLinkUrl: 'URL', promptImageUrl: 'Image URL', + promptEmbedUrl: 'Embed URL', + htmlEditorInvalidHtml: 'This HTML has an unclosed or mismatched tag. Fix it before switching back to the visual editor.', htmlEditorCode: 'Code', htmlEditorPreview: 'Preview', lastSaved: 'Last saved', diff --git a/src/app/i18n/hy.ts b/src/app/i18n/hy.ts index 1e3f463..62c9eab 100644 --- a/src/app/i18n/hy.ts +++ b/src/app/i18n/hy.ts @@ -551,6 +551,8 @@ export const hy: Translations = { confirmLeaveUnsaved: 'Կան չպահված փոփոխություններ։ Այնուամենայնիվ դուրս գա՞լ։', promptLinkUrl: 'URL', promptImageUrl: 'Նկարի URL', + promptEmbedUrl: 'Ներդրման URL', + htmlEditorInvalidHtml: 'Այս HTML-ը ունի չփակված կամ չհամընկնող թեգ։ Ուղղեք նախքան տեսողական խմբագրիչին վերադառնալը։', htmlEditorCode: 'Կոդ', htmlEditorPreview: 'Նախադիտում', lastSaved: 'Վերջին պահպանումը', diff --git a/src/app/i18n/ru.ts b/src/app/i18n/ru.ts index 49f4744..201f08f 100644 --- a/src/app/i18n/ru.ts +++ b/src/app/i18n/ru.ts @@ -551,6 +551,8 @@ export const ru: Translations = { confirmLeaveUnsaved: 'Есть несохранённые изменения. Всё равно уйти?', promptLinkUrl: 'URL', promptImageUrl: 'URL изображения', + promptEmbedUrl: 'URL для встраивания', + htmlEditorInvalidHtml: 'В этом HTML есть незакрытый или несовпадающий тег. Исправьте перед возвратом к визуальному редактору.', htmlEditorCode: 'Код', htmlEditorPreview: 'Предпросмотр', lastSaved: 'Последнее сохранение', diff --git a/src/app/i18n/translations.ts b/src/app/i18n/translations.ts index 32fbb9b..33b933c 100644 --- a/src/app/i18n/translations.ts +++ b/src/app/i18n/translations.ts @@ -549,6 +549,8 @@ export interface Translations { confirmLeaveUnsaved: string; promptLinkUrl: string; promptImageUrl: string; + promptEmbedUrl: string; + htmlEditorInvalidHtml: string; htmlEditorCode: string; htmlEditorPreview: string; lastSaved: string;