Improve collection management: fix template selection UI and item positioning

This commit addresses multiple collection management issues to improve user experience:

## Template Selection Modal Improvements
- Replace inline styles with CSS classes for reliable visual feedback
- Fix default template selection conflicts that showed multiple templates as selected
- Add styled template previews that show actual CSS styling differences
- Improve modal responsiveness and visual hierarchy

## Collection Item Creation Fixes
- Fix empty collection items with no content/height that were unclickable
- Preserve template placeholder content during item creation instead of clearing it
- Implement proper positioning system using GetMaxPosition to place new items at collection end
- Add position calculation logic to prevent new items from jumping to beginning

## Backend Positioning System
- Add GetMaxPosition method to all repository implementations (SQLite, PostgreSQL, HTTPClient)
- Update CreateCollectionItemFromTemplate to calculate correct position (maxPos + 1)
- Maintain reconstruction ordering by position ASC for consistent item placement

## Frontend Template Selection
- CSS class-based selection states replace problematic inline style manipulation
- Template previews now render actual HTML with real page styling
- Improved hover states and selection visual feedback
- Fixed auto-selection interference with user interaction

These changes ensure collection items appear in expected order and template selection
provides clear visual feedback with actual styling previews.
This commit is contained in:
2025-10-30 22:06:44 +01:00
parent 00255cb105
commit 900f91bc25
7 changed files with 313 additions and 89 deletions

View File

@@ -509,117 +509,86 @@ export class CollectionManager {
// Create modal overlay
const overlay = document.createElement('div');
overlay.className = 'insertr-modal-overlay';
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 999999;
`;
// Create modal content
const modal = document.createElement('div');
modal.className = 'insertr-template-selector';
modal.style.cssText = `
background: white;
border-radius: 8px;
padding: 24px;
max-width: 500px;
width: 90%;
max-height: 70vh;
overflow-y: auto;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
`;
// Create modal HTML
// Create modal HTML using CSS classes
modal.innerHTML = `
<h3 style="margin: 0 0 16px 0; font-size: 18px; font-weight: 600;">Choose Template</h3>
<div class="template-options">
<h3>Choose Template</h3>
<div class="insertr-template-options">
${templates.map(template => `
<div class="template-option" data-template-id="${template.template_id}" style="
border: 2px solid #e5e7eb;
border-radius: 6px;
padding: 12px;
margin-bottom: 12px;
cursor: pointer;
transition: all 0.2s;
${template.is_default ? 'border-color: #3b82f6; background: #eff6ff;' : ''}
">
<div style="font-weight: 500; margin-bottom: 4px;">
<div class="insertr-template-option ${template.is_default ? 'insertr-template-default' : ''}"
data-template-id="${template.template_id}">
<div class="insertr-template-name">
${template.name}${template.is_default ? ' (default)' : ''}
</div>
<div style="font-size: 14px; color: #6b7280; font-family: monospace; background: #f9fafb; padding: 8px; border-radius: 4px; overflow: hidden;">
${this.truncateHtml(template.html_template, 100)}
<div class="insertr-template-preview-container">
${this.createStyledTemplatePreview(template.html_template)}
</div>
</div>
`).join('')}
</div>
<div style="display: flex; gap: 12px; justify-content: flex-end; margin-top: 20px;">
<button class="cancel-btn" style="
padding: 8px 16px;
border: 1px solid #d1d5db;
background: white;
border-radius: 4px;
cursor: pointer;
">Cancel</button>
<button class="select-btn" style="
padding: 8px 16px;
background: #3b82f6;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
opacity: 0.5;
" disabled>Add Item</button>
<div class="insertr-template-actions">
<button class="insertr-template-btn insertr-template-btn-cancel">Cancel</button>
<button class="insertr-template-btn insertr-template-btn-select" disabled>Add Item</button>
</div>
`;
let selectedTemplate = null;
let userHasInteracted = false;
// Add event listeners
modal.querySelectorAll('.template-option').forEach(option => {
option.addEventListener('click', () => {
// Remove previous selection
modal.querySelectorAll('.template-option').forEach(opt => {
opt.style.borderColor = '#e5e7eb';
opt.style.background = 'white';
// Add event listeners for template selection
modal.querySelectorAll('.insertr-template-option').forEach(option => {
option.addEventListener('click', (e) => {
// Mark that user has interacted
userHasInteracted = true;
// Remove previous selection from all options
modal.querySelectorAll('.insertr-template-option').forEach(opt => {
opt.classList.remove('insertr-template-selected');
});
// Apply selection styling
option.style.borderColor = '#3b82f6';
option.style.background = '#eff6ff';
// Add selection class to clicked option
option.classList.add('insertr-template-selected');
// Find selected template
const templateId = parseInt(option.dataset.templateId);
selectedTemplate = templates.find(t => t.template_id === templateId);
// Enable select button
const selectBtn = modal.querySelector('.select-btn');
const selectBtn = modal.querySelector('.insertr-template-btn-select');
selectBtn.disabled = false;
selectBtn.style.opacity = '1';
console.log('🎯 Template selected by user:', selectedTemplate.name);
});
});
// Auto-select default template after all event listeners are set up
// Auto-select default template only if user hasn't interacted
const defaultTemplate = templates.find(t => t.is_default);
if (defaultTemplate) {
// Set initial selection without triggering click event
selectedTemplate = defaultTemplate;
const defaultOption = modal.querySelector(`[data-template-id="${defaultTemplate.template_id}"]`);
if (defaultOption) {
defaultOption.click();
// Apply visual selection without click event
defaultOption.classList.add('insertr-template-selected');
const selectBtn = modal.querySelector('.insertr-template-btn-select');
selectBtn.disabled = false;
console.log('🎯 Default template pre-selected:', defaultTemplate.name);
}
}
modal.querySelector('.cancel-btn').addEventListener('click', () => {
// Cancel button handler
modal.querySelector('.insertr-template-btn-cancel').addEventListener('click', () => {
document.body.removeChild(overlay);
resolve(null);
});
modal.querySelector('.select-btn').addEventListener('click', () => {
// Select button handler
modal.querySelector('.insertr-template-btn-select').addEventListener('click', () => {
console.log('✅ Adding item with template:', selectedTemplate ? selectedTemplate.name : 'none');
document.body.removeChild(overlay);
resolve(selectedTemplate);
});
@@ -638,14 +607,59 @@ export class CollectionManager {
}
/**
* Truncate HTML for preview
* Create safe template preview text (no HTML truncation)
* @param {string} html - HTML string
* @param {number} maxLength - Maximum character length
* @returns {string} Truncated HTML
* @returns {string} Safe preview text
*/
truncateHtml(html, maxLength) {
if (html.length <= maxLength) return html;
return html.substring(0, maxLength) + '...';
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>`;
}
}
/**