feat: implement container expansion and group editing functionality

- Add container element detection and child expansion in InsertrCore
- Implement .insertr descendant expansion matching CLI behavior
- Add .insertr-group collective editing with markdown editor
- Fix UX issue where div.insertr got text input instead of proper child editors
- Add comprehensive test cases for both features in about.html
- Enable live preview for group editing with proper content splitting
This commit is contained in:
2025-09-07 20:43:43 +02:00
parent 53762645e0
commit fdf9e1bb7e
3 changed files with 363 additions and 10 deletions

View File

@@ -10,23 +10,137 @@ export class InsertrCore {
};
}
// Find all enhanced elements on the page
// Find all enhanced elements on the page with container expansion
findEnhancedElements() {
return document.querySelectorAll('.insertr');
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;
}
// 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 contains only text content (no nested HTML elements)
hasOnlyTextContent(element) {
for (const child of element.children) {
// Found nested HTML element - not text-only
return false;
}
// Only text nodes (and whitespace) - 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());
}
// Get element metadata
getElementMetadata(element) {
return {
contentId: element.getAttribute('data-content-id'),
contentType: element.getAttribute('data-content-type'),
contentId: element.getAttribute('data-content-id') || this.generateTempId(element),
contentType: element.getAttribute('data-content-type') || this.detectContentType(element),
element: element
};
}
// Get all elements with their metadata
// Generate temporary ID for elements without data-content-id
generateTempId(element) {
const tag = element.tagName.toLowerCase();
const text = element.textContent.trim().substring(0, 20).replace(/\s+/g, '-').toLowerCase();
return `${tag}-${text}-${Date.now()}`;
}
// Detect content type for elements without data-content-type
detectContentType(element) {
const tag = element.tagName.toLowerCase();
if (element.classList.contains('insertr-group')) {
return 'markdown';
}
switch (tag) {
case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6':
return 'text';
case 'p':
return 'textarea';
case 'a': case 'button':
return 'link';
case 'div': case 'section':
return 'markdown';
default:
return 'text';
}
}
// Get all elements with their metadata, including group elements
getAllElements() {
const elements = this.findEnhancedElements();
return Array.from(elements).map(el => this.getElementMetadata(el));
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));
}
}