feat: implement Phase 3 container transformation with CLASSES.md compliance

- Add backend container transformation in engine.go following syntactic sugar specification
- Containers with .insertr get class removed and viable children get .insertr added
- Remove incorrect frontend container expansion - frontend only finds enhanced elements
- Fix StyleAwareEditor hasMultiPropertyElements runtime error
- Add addClass/removeClass methods to ContentEngine for class manipulation
- Update frontend to match HTML-first approach with no runtime container logic
- Test verified: container <section class='insertr'> transforms to individual h1.insertr, p.insertr, button.insertr

This completes the container expansion functionality per CLASSES.md:
Developer convenience (one .insertr enables section editing) + granular control (individual element editing)
This commit is contained in:
2025-09-21 19:17:12 +02:00
parent 4ef032cad6
commit b5e601d09f
16 changed files with 568 additions and 1407 deletions

View File

@@ -88,43 +88,41 @@ export class StyleAwareEditor {
styles: detection.styles,
structure: detection.structure,
strategy: editingStrategy,
hasMultiPropertyElements: this.hasMultiPropertyElements(detection.structure)
hasMultiPropertyElements: false // Removed - not needed for HTML-first approach
};
}
/**
* Determine the best editing strategy based on content complexity
* Determine the best editing interface based on element type
* HTML-first approach: detect behavior from element tag, not attributes
*
* @param {Object} detection - Style detection results
* @returns {string} - Editing strategy: 'simple', 'rich', 'multi-property'
* @returns {string} - Editing interface: 'direct' or 'rich'
*/
determineEditingStrategy(detection) {
if (detection.structure.length === 0) {
return 'simple'; // No nested elements
const tagName = this.element.tagName.toLowerCase();
// Multi-property elements get direct editing interface
if (this.isMultiPropertyElement(tagName)) {
return 'direct';
}
const hasStyledElements = detection.structure.some(piece => piece.type === 'styled');
if (hasStyledElements) {
return 'rich'; // Rich text with styling (handles all styled content including links)
} else {
return 'simple'; // Plain text
}
// All other elements get rich HTML editing with style preservation
return 'rich';
}
/**
* Check if element is a multi-property element requiring direct editing
*
* @param {string} tagName - Element tag name
* @returns {boolean} - True if multi-property element
*/
isMultiPropertyElement(tagName) {
const multiPropertyTags = new Set(['a', 'button', 'img']);
return multiPropertyTags.has(tagName);
}
/**
* Check if structure contains multi-property elements
*
* @param {Array} structure - Content structure array
* @returns {boolean} - True if has multi-property elements
*/
hasMultiPropertyElements(structure) {
return structure.some(piece =>
piece.type === 'styled' &&
piece.properties &&
Object.keys(piece.properties).length > 1
);
}
/**
* Create editor interface based on analysis
@@ -138,16 +136,16 @@ export class StyleAwareEditor {
// Create appropriate editor based on strategy
switch (analysis.strategy) {
case 'simple':
this.createSimpleEditor();
case 'direct':
this.createDirectEditor(analysis);
break;
case 'rich':
this.createRichEditor(analysis);
break;
}
// Add toolbar if enabled and we have any styled content
if (this.options.showToolbar && (analysis.styles.size > 0 || analysis.hasMultiPropertyElements)) {
// Add toolbar if enabled and we have styled content (rich editor only)
if (this.options.showToolbar && analysis.strategy === 'rich' && analysis.styles.size > 0) {
this.createStyleToolbar(analysis.styles, analysis.structure);
}
@@ -156,17 +154,114 @@ export class StyleAwareEditor {
}
/**
* Create simple text editor for plain content
* Create direct property editor for multi-property elements (links, buttons, images)
*/
createSimpleEditor() {
const textarea = document.createElement('textarea');
textarea.className = 'insertr-simple-editor';
textarea.value = this.originalContent.text;
textarea.rows = 3;
textarea.placeholder = 'Enter content...';
createDirectEditor(analysis) {
const tagName = this.element.tagName.toLowerCase();
this.contentEditor = textarea;
this.editorContainer.appendChild(textarea);
if (tagName === 'a') {
this.createLinkEditor();
} else if (tagName === 'button') {
this.createButtonEditor();
} else if (tagName === 'img') {
this.createImageEditor();
} else {
// Fallback to rich editor
this.createRichEditor(analysis);
}
}
/**
* Create link editor with URL and text fields
*/
createLinkEditor() {
const form = document.createElement('div');
form.className = 'insertr-direct-editor insertr-link-editor';
// Text field
const textGroup = document.createElement('div');
textGroup.className = 'insertr-form-group';
textGroup.innerHTML = `
<label class="insertr-form-label">Link Text</label>
<input type="text" class="insertr-form-input" id="link-text" value="${this.element.textContent}" placeholder="Link text">
`;
// URL field
const urlGroup = document.createElement('div');
urlGroup.className = 'insertr-form-group';
urlGroup.innerHTML = `
<label class="insertr-form-label">URL</label>
<input type="url" class="insertr-form-input" id="link-url" value="${this.element.href || ''}" placeholder="https://example.com">
`;
// Target field
const targetGroup = document.createElement('div');
targetGroup.className = 'insertr-form-group';
targetGroup.innerHTML = `
<label class="insertr-form-label">Target</label>
<select class="insertr-form-select" id="link-target">
<option value="">Same window</option>
<option value="_blank" ${this.element.target === '_blank' ? 'selected' : ''}>New window</option>
</select>
`;
form.appendChild(textGroup);
form.appendChild(urlGroup);
form.appendChild(targetGroup);
this.contentEditor = form;
this.editorContainer.appendChild(form);
}
/**
* Create button editor with text field
*/
createButtonEditor() {
const form = document.createElement('div');
form.className = 'insertr-direct-editor insertr-button-editor';
// Text field
const textGroup = document.createElement('div');
textGroup.className = 'insertr-form-group';
textGroup.innerHTML = `
<label class="insertr-form-label">Button Text</label>
<input type="text" class="insertr-form-input" id="button-text" value="${this.element.textContent}" placeholder="Button text">
`;
form.appendChild(textGroup);
this.contentEditor = form;
this.editorContainer.appendChild(form);
}
/**
* Create image editor with src and alt fields
*/
createImageEditor() {
const form = document.createElement('div');
form.className = 'insertr-direct-editor insertr-image-editor';
// Source field
const srcGroup = document.createElement('div');
srcGroup.className = 'insertr-form-group';
srcGroup.innerHTML = `
<label class="insertr-form-label">Image URL</label>
<input type="url" class="insertr-form-input" id="image-src" value="${this.element.src || ''}" placeholder="https://example.com/image.jpg">
`;
// Alt text field
const altGroup = document.createElement('div');
altGroup.className = 'insertr-form-group';
altGroup.innerHTML = `
<label class="insertr-form-label">Alt Text</label>
<input type="text" class="insertr-form-input" id="image-alt" value="${this.element.alt || ''}" placeholder="Image description">
`;
form.appendChild(srcGroup);
form.appendChild(altGroup);
this.contentEditor = form;
this.editorContainer.appendChild(form);
}
/**
@@ -242,46 +337,31 @@ export class StyleAwareEditor {
const button = document.createElement('button');
button.type = 'button';
button.className = 'insertr-style-btn';
button.textContent = styleInfo.name;
button.title = `Apply ${styleInfo.name} style`;
button.dataset.styleId = styleId;
// Apply preview styling by copying computed styles from the original element
// Create preview content container
const previewContent = document.createElement('span');
previewContent.className = 'insertr-preview-content';
previewContent.textContent = styleInfo.name;
// Apply the original classes to the preview content (not the button)
if (styleInfo.element && styleInfo.classes && styleInfo.classes.length > 0) {
// Add the detected classes first
styleInfo.classes.forEach(className => {
button.classList.add(className);
});
// Add special button class
// Add special preview button class to the button
button.classList.add('insertr-style-preview');
// Copy specific style properties from the original element to ensure they show
const computedStyle = window.getComputedStyle(styleInfo.element);
// Copy color (most important for visual preview)
if (computedStyle.color && computedStyle.color !== 'rgb(0, 0, 0)') {
button.style.setProperty('color', computedStyle.color, 'important');
}
// Copy font-weight
if (computedStyle.fontWeight && computedStyle.fontWeight !== '400') {
button.style.setProperty('font-weight', computedStyle.fontWeight, 'important');
}
// Copy text-decoration (for underlines, etc.)
if (computedStyle.textDecoration && computedStyle.textDecoration !== 'none') {
button.style.setProperty('text-decoration', computedStyle.textDecoration, 'important');
}
// Copy text-transform (for uppercase, etc.)
if (computedStyle.textTransform && computedStyle.textTransform !== 'none') {
button.style.setProperty('text-transform', computedStyle.textTransform, 'important');
}
// Don't copy background-color to keep button appearance
// Add the detected classes to the preview content
styleInfo.classes.forEach(className => {
previewContent.classList.add(className);
});
} else {
// No meaningful styles detected - use fallback
previewContent.classList.add('insertr-fallback-style');
}
// Add the preview content to the button
button.appendChild(previewContent);
// Add click handler for style application
button.addEventListener('click', (e) => {
e.preventDefault();
@@ -449,18 +529,16 @@ export class StyleAwareEditor {
/**
* Extract current content from editor
* HTML-first approach: always return HTML content
*
* @returns {Object} - Extracted content
*/
extractContent() {
if (this.contentEditor.className.includes('simple-editor')) {
// Simple text editor
return {
type: 'text',
content: this.contentEditor.value
};
if (this.contentEditor.className.includes('direct-editor')) {
// Direct property editor - extract form values and generate HTML
return this.extractDirectEditorContent();
} else if (this.contentEditor.className.includes('rich-editor')) {
// Rich text editor
// Rich text editor - return HTML as-is
return {
type: 'html',
content: this.contentEditor.innerHTML
@@ -469,40 +547,74 @@ export class StyleAwareEditor {
return null;
}
/**
* Extract content from direct property editors
*
* @returns {Object} - Content with generated HTML
*/
extractDirectEditorContent() {
const tagName = this.element.tagName.toLowerCase();
if (tagName === 'a') {
const text = document.getElementById('link-text').value;
const url = document.getElementById('link-url').value;
const target = document.getElementById('link-target').value;
return {
type: 'html',
content: text,
properties: { href: url, target: target }
};
} else if (tagName === 'button') {
const text = document.getElementById('button-text').value;
return {
type: 'html',
content: text
};
} else if (tagName === 'img') {
const src = document.getElementById('image-src').value;
const alt = document.getElementById('image-alt').value;
return {
type: 'html',
content: '',
properties: { src: src, alt: alt }
};
}
return null;
}
/**
* Apply extracted content to the original element
* HTML-first approach: always use HTML preservation with optional property updates
*
* @param {Object} content - Content to apply
* @returns {boolean} - Success status
*/
applyContentToElement(content) {
try {
switch (content.type) {
case 'text':
// Simple text - just update textContent
this.element.textContent = content.content;
return true;
case 'html':
// Rich HTML - use HTML preservation engine
return this.htmlEngine.applyFromEditing(this.element, content.content);
case 'structured':
// Structured content - reconstruct using style engine
const reconstructedHTML = this.styleEngine.reconstructHTML(
content.structure,
this.detectedStyles,
content.updatedProperties
);
return this.htmlEngine.applyFromEditing(this.element, reconstructedHTML);
default:
console.error('Unknown content type:', content.type);
return false;
// Apply properties if specified (for direct editors)
if (content.properties) {
for (const [property, value] of Object.entries(content.properties)) {
if (value) {
this.element.setAttribute(property, value);
} else {
this.element.removeAttribute(property);
}
}
}
// Apply HTML content using preservation engine
if (content.content !== undefined) {
return this.htmlEngine.applyFromEditing(this.element, content.content);
}
return true;
} catch (error) {
console.error('Failed to apply content:', error);
return false;