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;