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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user