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

@@ -10,111 +10,23 @@ export class InsertrCore {
};
}
// Find all enhanced elements on the page with container expansion
// Find all enhanced elements on the page
// Note: Container expansion is handled at build-time by the backend enhancer
// Frontend should only find elements that already have .insertr class
findEnhancedElements() {
const directElements = document.querySelectorAll('.insertr');
const expandedElements = [];
directElements.forEach(element => {
if (this.isContainer(element) && !element.classList.contains('insertr-group')) {
// Container element (.insertr) - expand to viable children
const children = this.findViableChildren(element);
expandedElements.push(...children);
} else {
// Regular element or group (.insertr-group)
expandedElements.push(element);
}
});
return expandedElements;
return document.querySelectorAll('.insertr');
}
// Check if element is a container that should expand to children
isContainer(element) {
const containerTags = new Set([
'div', 'section', 'article', 'header',
'footer', 'main', 'aside', 'nav'
]);
return containerTags.has(element.tagName.toLowerCase());
}
// Find viable children for editing (elements with only text content)
findViableChildren(containerElement) {
const viable = [];
for (const child of containerElement.children) {
// Skip elements that already have .insertr class
if (child.classList.contains('insertr')) {
continue;
}
// Skip self-closing elements
if (this.isSelfClosing(child)) {
continue;
}
// Check if element has only text content (no nested HTML elements)
if (this.hasOnlyTextContent(child)) {
viable.push(child);
}
}
return viable;
}
// Check if element is viable for editing (allows simple formatting)
hasOnlyTextContent(element) {
// Allow elements with simple formatting tags
const allowedTags = new Set(['strong', 'b', 'em', 'i', 'a', 'span', 'code']);
for (const child of element.children) {
const tagName = child.tagName.toLowerCase();
// If child is not an allowed formatting tag, reject
if (!allowedTags.has(tagName)) {
return false;
}
// If formatting tag has nested complex elements, reject
if (child.children.length > 0) {
// Recursively check nested content isn't too complex
for (const nestedChild of child.children) {
const nestedTag = nestedChild.tagName.toLowerCase();
if (!allowedTags.has(nestedTag)) {
return false;
}
}
}
}
// Element has only text and/or simple formatting - this is viable
return element.textContent.trim().length > 0;
}
// Check if element is self-closing
isSelfClosing(element) {
const selfClosingTags = new Set([
'img', 'input', 'br', 'hr', 'meta', 'link',
'area', 'base', 'col', 'embed', 'source', 'track', 'wbr'
]);
return selfClosingTags.has(element.tagName.toLowerCase());
}
// Note: All container expansion logic removed - handled by backend enhancer
// Frontend only works with elements that already have .insertr class
// Get element metadata
getElementMetadata(element) {
const existingId = element.getAttribute('data-content-id');
// Ensure element has insertr class for server processing
if (!element.classList.contains('insertr')) {
element.classList.add('insertr');
}
// Send HTML markup to server for unified ID generation
// HTML-first approach: no content type needed, just HTML markup for ID generation
return {
contentId: existingId, // null if new content, existing ID if updating
contentType: element.getAttribute('data-content-type') || this.detectContentType(element),
element: element,
htmlMarkup: element.outerHTML // Server will generate ID from this
};
@@ -130,38 +42,10 @@ export class InsertrCore {
return path.replace(/^\//, '');
}
// Detect content type for elements without data-content-type
detectContentType(element) {
const tag = element.tagName.toLowerCase();
// Only return database-valid types: 'text' or 'link'
if (tag === 'a' || tag === 'button') {
return 'link';
}
// All other elements are text content
return 'text';
}
// Get all elements with their metadata, including group elements
// Get all elements with their metadata
// Note: Container expansion handled by backend - frontend finds enhanced elements only
getAllElements() {
const directElements = document.querySelectorAll('.insertr, .insertr-group');
const processedElements = [];
directElements.forEach(element => {
if (element.classList.contains('insertr-group')) {
// Group element - treat as single editable unit
processedElements.push(element);
} else if (this.isContainer(element)) {
// Container element - expand to children
const children = this.findViableChildren(element);
processedElements.push(...children);
} else {
// Regular element
processedElements.push(element);
}
});
return Array.from(processedElements).map(el => this.getElementMetadata(el));
const elements = this.findEnhancedElements();
return Array.from(elements).map(el => this.getElementMetadata(el));
}
}