Files
insertr/lib/src/ui/collection-manager.js
Joakim 163cbf7eea Implement live collection preview system with contextual template selection
Replace isolated template previews with live collection reconstruction:
- Frontend now reconstructs collection container with all template variants
- Users click directly on rendered templates in proper CSS context
- Perfect preservation of grid/flex layouts and responsive behavior
- Simplified API: preview endpoint returns container_html + templates for frontend reconstruction
- Enhanced UX: WYSIWYG template selection shows exactly what will be added
- Removed redundant templates endpoint in favor of unified preview approach

Backend changes:
- Add GET /api/collections/{id}/preview endpoint
- Remove GET /api/collections/{id}/templates endpoint
- Return container HTML + templates for frontend reconstruction

Frontend changes:
- Replace isolated template modal with live collection preview
- Add generateLivePreview() method for container reconstruction
- Update CollectionManager to use preview API
- Add interactive CSS styling for template selection

This provides true contextual template selection where CSS inheritance,
grid layouts, and responsive design work perfectly in preview mode.
2025-10-31 22:41:12 +01:00

999 lines
36 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* CollectionManager - Dynamic content collection management for .insertr-add elements
*
* Handles:
* - Template detection from existing children
* - Add/remove/reorder UI controls
* - Collection data management
* - Integration with existing .insertr editing system
*/
import { InsertrFormRenderer } from './form-renderer.js';
export class CollectionManager {
constructor(meta, apiClient, auth) {
this.meta = meta;
this.container = meta.element;
this.apiClient = apiClient;
this.auth = auth;
// Extract collection ID from container
this.collectionId = this.container.getAttribute('data-collection-id');
if (!this.collectionId) {
console.error('❌ Collection container missing data-collection-id attribute');
return;
}
// Collection state
this.template = null;
this.items = [];
this.isActive = false;
this.cachedPreview = null; // Cache for collection preview data
// UI elements
this.addButton = null;
this.itemControls = new Map(); // Map item element to its controls
console.log('🔄 CollectionManager initialized for:', this.container, 'Collection ID:', this.collectionId);
}
/**
* Initialize the collection manager
*/
async initialize() {
if (this.isActive) return;
console.log('🚀 Starting collection management for:', this.container.className);
// Analyze existing content to detect template
this.analyzeTemplate();
// Sync with backend to map existing items to collection item IDs
await this.syncWithBackend();
// Add collection management UI only when in edit mode
this.setupEditModeDetection();
this.isActive = true;
}
/**
* Set up detection for when edit mode is activated
*/
setupEditModeDetection() {
// Check current auth state
if (this.auth.isAuthenticated() && this.auth.isEditMode()) {
this.activateCollectionUI();
}
// Listen for auth state changes (assuming the auth object has events)
// For now, we'll poll - in a real implementation we'd use events
this.authCheckInterval = setInterval(() => {
const shouldBeActive = this.auth.isAuthenticated() && this.auth.isEditMode();
if (shouldBeActive && !this.hasCollectionUI()) {
this.activateCollectionUI();
} else if (!shouldBeActive && this.hasCollectionUI()) {
this.deactivateCollectionUI();
}
}, 1000);
}
/**
* Check if collection UI is currently active
*/
hasCollectionUI() {
return this.addButton && this.addButton.parentNode;
}
/**
* Activate collection UI when in edit mode
*/
activateCollectionUI() {
console.log('✅ Activating collection UI');
// Add visual indicator to container
this.container.classList.add('insertr-collection-active');
// Add the "+ Add" button (top right of container per spec)
this.createAddButton();
// Add control buttons to each existing item
this.addControlsToExistingItems();
}
/**
* Deactivate collection UI when not in edit mode
*/
deactivateCollectionUI() {
console.log('❌ Deactivating collection UI');
// Remove visual indicator
this.container.classList.remove('insertr-collection-active');
// Remove add button
if (this.addButton) {
this.addButton.remove();
this.addButton = null;
}
// Remove all item controls
this.itemControls.forEach((controls, item) => {
controls.remove();
});
this.itemControls.clear();
}
/**
* Analyze existing children to detect template pattern
*/
analyzeTemplate() {
const children = Array.from(this.container.children);
if (children.length === 0) {
console.warn('⚠️ No children found for template analysis');
return;
}
// Use first child as template baseline
const firstChild = children[0];
this.template = {
structure: this.extractElementStructure(firstChild),
editableFields: this.findEditableElements(firstChild),
htmlTemplate: firstChild.outerHTML
};
console.log('📋 Template detected:', this.template);
// Store reference to current items
// For existing items, try to extract collection item IDs if they exist
this.items = children.map((child, index) => ({
element: child,
index: index,
id: this.generateItemId(index),
collectionItemId: this.extractCollectionItemId(child)
}));
}
/**
* Extract the structural pattern of an element
*/
extractElementStructure(element) {
return {
tagName: element.tagName,
classes: Array.from(element.classList),
attributes: this.getRelevantAttributes(element),
childStructure: this.analyzeChildStructure(element)
};
}
/**
* Get relevant attributes (excluding data-content-id which will be unique)
*/
getRelevantAttributes(element) {
const relevantAttrs = {};
for (const attr of element.attributes) {
if (attr.name !== 'data-content-id') {
relevantAttrs[attr.name] = attr.value;
}
}
return relevantAttrs;
}
/**
* Analyze child structure for template replication
*/
analyzeChildStructure(element) {
return Array.from(element.children).map(child => ({
tagName: child.tagName,
classes: Array.from(child.classList),
hasInsertrClass: child.classList.contains('insertr'),
content: child.classList.contains('insertr') ? '' : child.textContent
}));
}
/**
* Find editable elements within a container
*/
findEditableElements(container) {
return Array.from(container.querySelectorAll('.insertr')).map(el => ({
selector: this.generateRelativeSelector(el, container),
type: this.determineFieldType(el),
placeholder: this.generatePlaceholder(el)
}));
}
/**
* Generate a relative selector for an element within a container
*/
generateRelativeSelector(element, container) {
// Simple approach: use tag name and classes
const tagName = element.tagName.toLowerCase();
const classes = Array.from(element.classList).join('.');
return classes ? `${tagName}.${classes}` : tagName;
}
/**
* Determine the type of field for editing
*/
determineFieldType(element) {
const tagName = element.tagName.toLowerCase();
if (tagName === 'a') return 'link';
if (tagName === 'img') return 'image';
return 'text';
}
/**
* Generate placeholder text for empty fields
*/
generatePlaceholder(element) {
const tagName = element.tagName.toLowerCase();
if (tagName === 'h1' || tagName === 'h2') return 'Enter heading...';
if (tagName === 'blockquote') return 'Enter quote...';
if (tagName === 'cite') return 'Enter author...';
return 'Enter text...';
}
/**
* Generate unique ID for new items
*/
generateItemId(index) {
return `item-${Date.now()}-${index}`;
}
/**
* Extract collection item ID from existing DOM element
* This is used for existing items that were reconstructed from database
*/
extractCollectionItemId(element) {
// Look for data-item-id attribute first (from server enhancement)
let itemId = element.getAttribute('data-item-id');
if (itemId) {
return itemId;
}
// For existing items reconstructed from database, try to infer from data-content-id
// The backend should have generated collection item IDs based on collection ID
const contentId = element.getAttribute('data-content-id');
if (contentId && this.collectionId) {
// This is a heuristic - we'll need to fetch the actual mapping from the backend
// For now, return null and let the backend operations handle missing IDs
return null;
}
return null;
}
/**
* Sync frontend state with backend collection items
* This maps existing DOM elements to their collection item IDs
*/
async syncWithBackend() {
if (!this.collectionId) {
console.warn('⚠️ Cannot sync with backend: no collection ID');
return;
}
try {
// Fetch current collection items from backend
const backendItems = await this.apiClient.getCollectionItems(this.collectionId);
console.log('📋 Backend collection items:', backendItems);
// Items already have data-item-id from server enhancement
// Just update the collectionItemId in our internal items array
backendItems.forEach((backendItem, index) => {
if (this.items[index]) {
this.items[index].collectionItemId = backendItem.item_id;
console.log(`🔗 Mapped DOM element ${index} to collection item ${backendItem.item_id}`);
}
});
console.log('✅ Frontend-backend sync completed');
} catch (error) {
console.error('❌ Failed to sync with backend:', error);
// Continue without backend sync - collection management will still work for new items
}
}
/**
* Create the "+ Add" button positioned in top right of container
*/
createAddButton() {
if (this.addButton) return; // Already exists
this.addButton = document.createElement('button');
this.addButton.className = 'insertr-add-btn';
this.addButton.innerHTML = '+ Add Item';
this.addButton.title = 'Add new item to collection';
// Position in top right of container as per spec
this.container.style.position = 'relative';
this.addButton.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
this.addNewItem();
});
this.container.appendChild(this.addButton);
console.log(' Add button created');
}
/**
* Add control buttons to all existing items
*/
addControlsToExistingItems() {
this.items.forEach((item, index) => {
this.addItemControls(item.element, index);
});
}
/**
* Add management controls to an item (remove, reorder)
*/
addItemControls(itemElement, index) {
if (!this.itemControls || this.itemControls.has(itemElement)) return; // Already has controls or not initialized
const controls = document.createElement('div');
controls.className = 'insertr-item-controls';
// Remove button (always present)
const removeBtn = this.createControlButton('×', 'Remove item', () =>
this.removeItem(itemElement)
);
// Move up button (if not first item)
if (index > 0) {
const upBtn = this.createControlButton('↑', 'Move up', () =>
this.moveItem(itemElement, 'up')
);
controls.appendChild(upBtn);
}
// Move down button (if not last item)
if (index < this.items.length - 1) {
const downBtn = this.createControlButton('↓', 'Move down', () =>
this.moveItem(itemElement, 'down')
);
controls.appendChild(downBtn);
}
controls.appendChild(removeBtn);
// Position in top right corner of item as per spec
itemElement.style.position = 'relative';
itemElement.appendChild(controls);
// Store reference
this.itemControls.set(itemElement, controls);
// Add hover behavior
this.setupItemHoverBehavior(itemElement, controls);
}
/**
* Create a control button
*/
createControlButton(text, title, onClick) {
const button = document.createElement('button');
button.className = 'insertr-control-btn';
button.textContent = text;
button.title = title;
button.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
onClick();
});
return button;
}
/**
* Set up hover behavior for item controls
*/
setupItemHoverBehavior(itemElement, controls) {
itemElement.addEventListener('mouseenter', () => {
controls.style.opacity = '1';
});
itemElement.addEventListener('mouseleave', () => {
controls.style.opacity = '0';
});
}
/**
* Add a new item to the collection (backend-first approach)
*/
async addNewItem() {
console.log(' Adding new item to collection');
if (!this.template || !this.collectionId) {
console.error('❌ No template or collection ID available for creating new items');
return;
}
try {
// 1. Get collection preview data
const previewData = await this.getCollectionPreview();
if (!previewData || !previewData.templates || previewData.templates.length === 0) {
console.error('❌ No templates available for collection:', this.collectionId);
alert('No templates available for this collection. Please refresh the page.');
return;
}
// 2. Select template (auto-select if only one, otherwise show live preview)
const selectedTemplate = await this.selectTemplate(previewData);
if (!selectedTemplate) {
console.log('Template selection cancelled by user');
return;
}
// 3. Create collection item in database first (backend-first approach)
const collectionItem = await this.apiClient.createCollectionItem(this.collectionId, selectedTemplate.template_id);
// 4. Create DOM element from the returned collection item data
const newItem = this.createItemFromCollectionData(collectionItem);
// 5. Add to DOM
this.container.insertBefore(newItem, this.addButton);
// 6. Update items array with backend data
const newItemData = {
element: newItem,
index: this.items.length,
id: collectionItem.item_id,
collectionItem: collectionItem
};
this.items.push(newItemData);
// 7. Add controls to new item
this.addItemControls(newItem, this.items.length - 1);
// 8. Re-initialize any .insertr elements in the new item
this.initializeInsertrElements(newItem);
// 9. Update all item controls (indices may have changed)
this.updateAllItemControls();
// 10. Trigger site enhancement to update static files
await this.apiClient.enhanceSite();
console.log('✅ New item added successfully:', collectionItem.item_id);
} catch (error) {
console.error('❌ Failed to add new collection item:', error);
alert('Failed to add new item. Please try again.');
}
}
/**
* Get collection preview data (container + templates)
* @returns {Promise<Object>} Object with collection_id, container_html, and templates
*/
async getCollectionPreview() {
try {
if (!this.cachedPreview) {
console.log('🔍 Fetching preview for collection:', this.collectionId);
this.cachedPreview = await this.apiClient.getCollectionPreview(this.collectionId);
console.log('📋 Preview fetched:', this.cachedPreview);
}
return this.cachedPreview;
} catch (error) {
console.error('❌ Failed to fetch preview for collection:', this.collectionId, error);
return null;
}
}
/**
* Select a template for creating new items
* @param {Object} previewData - Preview data with container_html and templates
* @returns {Promise<Object|null>} Selected template or null if cancelled
*/
async selectTemplate(previewData) {
const templates = previewData.templates;
// Auto-select if only one template
if (templates.length === 1) {
console.log('🎯 Auto-selecting single template:', templates[0].name);
return templates[0];
}
// Present live collection preview for multiple templates
console.log('🎨 Multiple templates available, showing live preview');
return this.showLiveCollectionPreview(previewData);
}
/**
* Show live collection preview for template selection
* @param {Object} previewData - Preview data with container_html and templates
* @returns {Promise<Object|null>} Selected template or null if cancelled
*/
async showLiveCollectionPreview(previewData) {
return new Promise((resolve) => {
// Create modal overlay
const overlay = document.createElement('div');
overlay.className = 'insertr-modal-overlay';
// Create modal content
const modal = document.createElement('div');
modal.className = 'insertr-collection-preview-modal';
// Generate live preview by reconstructing collection with all templates
const previewHTML = this.generateLivePreview(previewData.container_html, previewData.templates);
modal.innerHTML = `
<div class="insertr-preview-header">
<h3>Choose Template</h3>
<p>Click on the item you want to add</p>
</div>
<div class="insertr-preview-container">
${previewHTML}
</div>
<div class="insertr-preview-actions">
<button class="insertr-template-btn insertr-template-btn-cancel">Cancel</button>
</div>
`;
// Handle template selection by clicking on preview items
modal.addEventListener('click', (e) => {
const previewItem = e.target.closest('.insertr-preview-item');
if (previewItem) {
const templateId = parseInt(previewItem.dataset.templateId);
const selectedTemplate = previewData.templates.find(t => t.template_id === templateId);
if (selectedTemplate) {
console.log('🎯 Template selected from preview:', selectedTemplate.name);
document.body.removeChild(overlay);
resolve(selectedTemplate);
}
}
});
// Cancel button handler
modal.querySelector('.insertr-template-btn-cancel').addEventListener('click', () => {
document.body.removeChild(overlay);
resolve(null);
});
// Close on overlay click
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
document.body.removeChild(overlay);
resolve(null);
}
});
overlay.appendChild(modal);
document.body.appendChild(overlay);
});
}
/**
* Create safe template preview text (no HTML truncation)
* @param {string} html - HTML string
* @param {number} maxLength - Maximum character length
* @returns {string} Safe preview text
*/
createTemplatePreview(html, maxLength = 60) {
try {
// Create a temporary DOM element to safely extract text
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
// Extract just the text content
const textContent = tempDiv.textContent || tempDiv.innerText || '';
// Truncate the text (not HTML)
if (textContent.length <= maxLength) {
return textContent;
}
return textContent.substring(0, maxLength).trim() + '...';
} catch (error) {
// Fallback to safer extraction
console.warn('Template preview extraction failed:', error);
return html.replace(/<[^>]*>/g, '').substring(0, maxLength) + '...';
}
}
/**
* Create styled template preview that shows actual template styling
* @param {string} html - HTML template string
* @returns {string} HTML preview with actual styles
*/
createStyledTemplatePreview(html) {
try {
// Clean the HTML and replace .insertr elements with placeholder content
let previewHtml = html
.replace(/class="insertr"/g, 'class="insertr-preview-content"')
.replace(/class="([^"]*\s+)?insertr(\s+[^"]*)?"/g, 'class="$1insertr-preview-content$2"')
.replace(/>([^<]{0,50})</g, (match, content) => {
// Replace long content with placeholder text
if (content.trim().length > 30) {
return '>Sample content...<';
}
return match;
});
// Wrap in a preview container with scaling
return `<div class="insertr-template-preview-render">${previewHtml}</div>`;
} catch (error) {
console.warn('Styled template preview failed:', error);
// Fallback to text preview
return `<div class="insertr-template-preview-fallback">${this.createTemplatePreview(html, 50)}</div>`;
}
}
/**
* Create a DOM element from collection item data returned by backend
* Backend is the source of truth - use its HTML content directly
*/
createItemFromCollectionData(collectionItem) {
// Use backend HTML content directly (database is source of truth)
if (collectionItem.html_content && collectionItem.html_content.trim()) {
const tempContainer = document.createElement('div');
tempContainer.innerHTML = collectionItem.html_content;
const newItem = tempContainer.firstElementChild;
// Set the collection item ID as data attribute for future reference
newItem.setAttribute('data-item-id', collectionItem.item_id);
return newItem;
} else {
// Fallback: create from frontend template if backend content is empty
const tempContainer = document.createElement('div');
tempContainer.innerHTML = this.template.htmlTemplate;
const newItem = tempContainer.firstElementChild;
// Set the collection item ID as data attribute for future reference
newItem.setAttribute('data-item-id', collectionItem.item_id);
return newItem;
}
}
/**
* Create a new item from the template (legacy method, kept for compatibility)
*/
createItemFromTemplate() {
// Create element from template HTML
const tempContainer = document.createElement('div');
tempContainer.innerHTML = this.template.htmlTemplate;
const newItem = tempContainer.firstElementChild;
// Clear content from editable fields
this.template.editableFields.forEach(field => {
const element = newItem.querySelector(field.selector);
if (element) {
this.clearElementContent(element, field.type);
// Add placeholder text
if (field.type === 'text') {
element.textContent = field.placeholder;
element.style.color = '#999';
element.style.fontStyle = 'italic';
// Remove placeholder styling when user starts editing
element.addEventListener('focus', () => {
if (element.textContent === field.placeholder) {
element.textContent = '';
element.style.color = '';
element.style.fontStyle = '';
}
});
}
}
});
// Generate unique data-content-id for the item
newItem.setAttribute('data-content-id', this.generateItemId(Date.now()));
return newItem;
}
/**
* Clear content from an element based on its type
*/
clearElementContent(element, type) {
if (type === 'link') {
element.textContent = '';
element.removeAttribute('href');
} else if (type === 'image') {
element.removeAttribute('src');
element.removeAttribute('alt');
} else {
element.textContent = '';
}
}
/**
* Initialize .insertr elements within a new item
* This integrates with the existing editing system
*/
initializeInsertrElements(container) {
const insertrElements = container.querySelectorAll('.insertr');
insertrElements.forEach(element => {
// Add click handler for editing (same as existing system)
element.addEventListener('click', (e) => {
// Only allow editing if authenticated and in edit mode
if (!this.auth.isAuthenticated() || !this.auth.isEditMode()) {
return;
}
e.preventDefault();
e.stopPropagation();
// Use the existing form renderer
const formRenderer = new InsertrFormRenderer(this.apiClient);
const meta = {
contentId: element.getAttribute('data-content-id'),
element: element,
htmlMarkup: element.outerHTML
};
const currentContent = this.extractCurrentContent(element);
formRenderer.showEditForm(
meta,
currentContent,
(formData) => this.handleItemSave(meta, formData),
() => formRenderer.closeForm()
);
});
});
}
/**
* Extract current content (simplified version of editor.js method)
*/
extractCurrentContent(element) {
if (element.tagName.toLowerCase() === 'a') {
return {
text: element.textContent.trim(),
url: element.getAttribute('href') || ''
};
}
return element.textContent.trim();
}
/**
* Handle saving of individual item content
*/
async handleItemSave(meta, formData) {
console.log('💾 Saving item content:', meta.contentId, formData);
try {
let contentValue;
if (typeof formData === 'string') {
contentValue = formData;
} else if (formData.content) {
contentValue = formData.content;
} else if (formData.text) {
contentValue = formData.text;
} else {
contentValue = formData;
}
let result;
if (meta.contentId) {
result = await this.apiClient.updateContent(meta.contentId, contentValue);
} else {
result = await this.apiClient.createContent(contentValue, meta.htmlMarkup);
}
if (result) {
meta.element.setAttribute('data-content-id', result.id);
console.log(`✅ Item content saved: ${result.id}`);
} else {
console.error('❌ Failed to save item content to server');
}
} catch (error) {
console.error('❌ Error saving item content:', error);
}
}
/**
* Remove an item from the collection (backend-first approach)
*/
async removeItem(itemElement) {
if (!confirm('Are you sure you want to remove this item?')) {
return;
}
console.log('🗑️ Removing item from collection');
try {
// 1. Get the collection item ID from the element
const collectionItemId = itemElement.getAttribute('data-item-id');
if (!collectionItemId) {
console.error('❌ Cannot remove item: missing data-item-id attribute');
return;
}
// 2. Delete from database first (backend-first approach)
const success = await this.apiClient.deleteCollectionItem(this.collectionId, collectionItemId);
if (!success) {
alert('Failed to remove item from database. Please try again.');
return;
}
// 3. Remove controls
const controls = this.itemControls.get(itemElement);
if (controls) {
controls.remove();
this.itemControls.delete(itemElement);
}
// 4. Remove from items array
this.items = this.items.filter(item => item.element !== itemElement);
// 5. Remove from DOM
itemElement.remove();
// 6. Update all item controls (indices changed)
this.updateAllItemControls();
// 7. Trigger site enhancement to update static files
await this.apiClient.enhanceSite();
console.log('✅ Item removed successfully:', collectionItemId);
} catch (error) {
console.error('❌ Failed to remove collection item:', error);
alert('Failed to remove item. Please try again.');
}
}
/**
* Move an item up or down in the collection (backend-first approach)
*/
async moveItem(itemElement, direction) {
console.log(`🔄 Moving item ${direction}`);
const currentIndex = this.items.findIndex(item => item.element === itemElement);
if (currentIndex === -1) return;
let newIndex;
if (direction === 'up' && currentIndex > 0) {
newIndex = currentIndex - 1;
} else if (direction === 'down' && currentIndex < this.items.length - 1) {
newIndex = currentIndex + 1;
} else {
return; // Can't move in that direction
}
try {
// 1. Get the collection item ID
const collectionItemId = itemElement.getAttribute('data-item-id');
if (!collectionItemId) {
console.error('❌ Cannot move item: missing data-item-id attribute');
return;
}
// 2. Store original state for potential rollback
const originalItems = [...this.items];
// 3. Perform DOM move (optimistic UI)
const targetItem = this.items[newIndex];
if (direction === 'up') {
this.container.insertBefore(itemElement, targetItem.element);
} else {
this.container.insertBefore(itemElement, targetItem.element.nextSibling);
}
// 4. Update items array
[this.items[currentIndex], this.items[newIndex]] = [this.items[newIndex], this.items[currentIndex]];
this.items[currentIndex].index = currentIndex;
this.items[newIndex].index = newIndex;
// 5. Update all item controls
this.updateAllItemControls();
// 6. Build bulk reorder payload from current DOM state
const itemOrder = this.items.map((item, index) => ({
itemId: item.element.getAttribute('data-item-id'),
position: index + 1 // 1-based positions
}));
// 7. Send bulk reorder to backend
const success = await this.apiClient.reorderCollection(this.collectionId, itemOrder);
if (!success) {
// Rollback DOM changes
this.items = originalItems;
this.items.forEach(item => {
this.container.appendChild(item.element);
});
this.updateAllItemControls();
alert('Failed to update item position in database. Please try again.');
return;
}
// 8. Trigger site enhancement to update static files
await this.apiClient.enhanceSite();
console.log('✅ Item moved successfully:', collectionItemId, '→ position', newIndex + 1);
} catch (error) {
console.error('❌ Failed to move collection item:', error);
alert('Failed to move item. Please try again.');
}
}
/**
* Update controls for all items (called after reordering)
*/
updateAllItemControls() {
// Remove all existing controls
this.itemControls.forEach((controls, item) => {
controls.remove();
});
this.itemControls.clear();
// Re-add controls with correct up/down button states
this.items.forEach((item, index) => {
this.addItemControls(item.element, index);
});
}
/**
* Cleanup when the collection manager is destroyed
*/
destroy() {
if (this.authCheckInterval) {
clearInterval(this.authCheckInterval);
}
this.deactivateCollectionUI();
this.isActive = false;
console.log('🧹 CollectionManager destroyed');
}
/**
* Generate live collection preview by reconstructing container with all template variants
* @param {string} containerHTML - Collection container HTML
* @param {Array} templates - Array of template objects
* @returns {string} HTML string with reconstructed collection
*/
generateLivePreview(containerHTML, templates) {
try {
// Parse the container HTML
const tempContainer = document.createElement('div');
tempContainer.innerHTML = containerHTML;
const collectionContainer = tempContainer.querySelector('.insertr-add');
if (!collectionContainer) {
console.error('❌ No .insertr-add container found in collection HTML');
return '<div>Preview generation failed</div>';
}
// Clear existing children
collectionContainer.innerHTML = '';
// Add one instance of each template with preview classes
templates.forEach(template => {
// Parse template HTML
const templateContainer = document.createElement('div');
templateContainer.innerHTML = template.html_template;
const templateElement = templateContainer.firstElementChild;
if (templateElement) {
// Clone the template element
const previewElement = templateElement.cloneNode(true);
// Add preview classes and data attributes for selection
previewElement.classList.add('insertr-preview-item');
previewElement.setAttribute('data-template-id', template.template_id);
previewElement.setAttribute('data-template-name', template.name);
// Add the preview element to the collection container
collectionContainer.appendChild(previewElement);
console.log(`✅ Added template ${template.template_id} (${template.name}) to preview`);
}
});
// Return the complete collection HTML
return tempContainer.innerHTML;
} catch (error) {
console.error('❌ Failed to generate live preview:', error);
return '<div>Preview generation failed</div>';
}
}
}