diff --git a/lib/src/styles/insertr.css b/lib/src/styles/insertr.css index cc07563..bfcefbf 100644 --- a/lib/src/styles/insertr.css +++ b/lib/src/styles/insertr.css @@ -702,4 +702,310 @@ body:not(.insertr-edit-mode) .insertr-editing-hover::after { margin-right: 0; order: -1; } +}/** + * Styles for StyleAwareEditor + * Clean, modern interface that integrates with existing Insertr styling + */ + +/* Main editor container */ +.insertr-style-aware-editor { + background: white; + border: 1px solid #d1d5db; + border-radius: 8px; + padding: 1rem; + min-width: 400px; + max-width: 600px; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; +} + +/* Style toolbar */ +.insertr-style-toolbar { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem; + background: #f9fafb; + border: 1px solid #e5e7eb; + border-radius: 6px; + margin-bottom: 1rem; + flex-wrap: wrap; +} + +.insertr-toolbar-title { + font-size: 0.875rem; + font-weight: 500; + color: #6b7280; + margin-right: 0.5rem; +} + +.insertr-style-btn { + background: white; + border: 1px solid #d1d5db; + border-radius: 4px; + padding: 0.375rem 0.75rem; + font-size: 0.875rem; + font-weight: 500; + color: #374151; + cursor: pointer; + transition: all 0.2s ease; +} + +.insertr-style-btn:hover { + background: #f3f4f6; + border-color: #9ca3af; +} + +.insertr-style-btn:active { + background: #e5e7eb; + transform: translateY(1px); +} + +/* Simple text editor */ +.insertr-simple-editor { + width: 100%; + border: 1px solid #d1d5db; + border-radius: 6px; + padding: 0.75rem; + font-size: 0.875rem; + line-height: 1.5; + resize: vertical; + font-family: inherit; + margin-bottom: 1rem; +} + +.insertr-simple-editor:focus { + outline: none; + border-color: #3b82f6; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +/* Rich text editor */ +.insertr-rich-editor { + width: 100%; + min-height: 100px; + border: 1px solid #d1d5db; + border-radius: 6px; + padding: 0.75rem; + font-size: 0.875rem; + line-height: 1.5; + margin-bottom: 1rem; + overflow-y: auto; +} + +.insertr-rich-editor:focus { + outline: none; + border-color: #3b82f6; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +/* Multi-property editor */ +.insertr-multi-property-editor { + margin-bottom: 1rem; +} + +.insertr-styled-element-form { + background: #f9fafb; + border: 1px solid #e5e7eb; + border-radius: 6px; + padding: 1rem; + margin-bottom: 1rem; +} + +.insertr-styled-element-form:last-child { + margin-bottom: 0; +} + +.insertr-form-header { + margin: 0 0 0.75rem 0; + font-size: 1rem; + font-weight: 600; + color: #374151; +} + +/* Property inputs */ +.insertr-property-input, +.insertr-text-input { + margin-bottom: 0.75rem; +} + +.insertr-property-input:last-child, +.insertr-text-input:last-child { + margin-bottom: 0; +} + +.insertr-property-label, +.insertr-input-label { + display: block; + font-size: 0.875rem; + font-weight: 500; + color: #374151; + margin-bottom: 0.25rem; +} + +.insertr-property-field, +.insertr-text-field { + width: 100%; + border: 1px solid #d1d5db; + border-radius: 4px; + padding: 0.5rem; + font-size: 0.875rem; + line-height: 1.4; + font-family: inherit; + transition: border-color 0.2s ease; +} + +.insertr-property-field:focus, +.insertr-text-field:focus { + outline: none; + border-color: #3b82f6; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +/* URL inputs */ +.insertr-property-field[type="url"] { + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 0.8125rem; +} + +/* Form actions */ +.insertr-form-actions { + display: flex; + gap: 0.75rem; + justify-content: flex-end; + padding-top: 1rem; + border-top: 1px solid #e5e7eb; +} + +.insertr-btn-save, +.insertr-btn-cancel { + padding: 0.5rem 1rem; + font-size: 0.875rem; + font-weight: 500; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s ease; + border: 1px solid; +} + +.insertr-btn-save { + background: #3b82f6; + border-color: #3b82f6; + color: white; +} + +.insertr-btn-save:hover { + background: #2563eb; + border-color: #2563eb; +} + +.insertr-btn-cancel { + background: white; + border-color: #d1d5db; + color: #374151; +} + +.insertr-btn-cancel:hover { + background: #f9fafb; + border-color: #9ca3af; +} + +/* Fallback editor */ +.insertr-fallback-editor { + background: white; + border: 1px solid #d1d5db; + border-radius: 8px; + padding: 1rem; + min-width: 400px; + max-width: 500px; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); +} + +.insertr-fallback-textarea { + width: 100%; + border: 1px solid #d1d5db; + border-radius: 6px; + padding: 0.75rem; + font-size: 0.875rem; + line-height: 1.5; + resize: vertical; + font-family: inherit; + margin-bottom: 1rem; +} + +.insertr-fallback-textarea:focus { + outline: none; + border-color: #3b82f6; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +/* Responsive adjustments */ +@media (max-width: 640px) { + .insertr-style-aware-editor, + .insertr-fallback-editor { + min-width: 300px; + max-width: calc(100vw - 2rem); + margin: 1rem; + } + + .insertr-style-toolbar { + padding: 0.5rem; + gap: 0.375rem; + } + + .insertr-style-btn { + padding: 0.25rem 0.5rem; + font-size: 0.8125rem; + } + + .insertr-form-actions { + flex-direction: column; + } + + .insertr-btn-save, + .insertr-btn-cancel { + width: 100%; + } +} + +/* Dark mode support (if needed) */ +@media (prefers-color-scheme: dark) { + .insertr-style-aware-editor, + .insertr-fallback-editor { + background: #1f2937; + border-color: #374151; + color: #f9fafb; + } + + .insertr-style-toolbar, + .insertr-styled-element-form { + background: #111827; + border-color: #374151; + } + + .insertr-style-btn { + background: #374151; + border-color: #4b5563; + color: #f9fafb; + } + + .insertr-style-btn:hover { + background: #4b5563; + border-color: #6b7280; + } + + .insertr-property-field, + .insertr-text-field, + .insertr-simple-editor, + .insertr-fallback-textarea { + background: #374151; + border-color: #4b5563; + color: #f9fafb; + } + + .insertr-rich-editor { + background: #374151; + border-color: #4b5563; + color: #f9fafb; + } } \ No newline at end of file diff --git a/lib/src/styles/style-aware-editor.css b/lib/src/styles/style-aware-editor.css new file mode 100644 index 0000000..7bccba3 --- /dev/null +++ b/lib/src/styles/style-aware-editor.css @@ -0,0 +1,307 @@ +/** + * Styles for StyleAwareEditor + * Clean, modern interface that integrates with existing Insertr styling + */ + +/* Main editor container */ +.insertr-style-aware-editor { + background: white; + border: 1px solid #d1d5db; + border-radius: 8px; + padding: 1rem; + min-width: 400px; + max-width: 600px; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; +} + +/* Style toolbar */ +.insertr-style-toolbar { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem; + background: #f9fafb; + border: 1px solid #e5e7eb; + border-radius: 6px; + margin-bottom: 1rem; + flex-wrap: wrap; +} + +.insertr-toolbar-title { + font-size: 0.875rem; + font-weight: 500; + color: #6b7280; + margin-right: 0.5rem; +} + +.insertr-style-btn { + background: white; + border: 1px solid #d1d5db; + border-radius: 4px; + padding: 0.375rem 0.75rem; + font-size: 0.875rem; + font-weight: 500; + color: #374151; + cursor: pointer; + transition: all 0.2s ease; +} + +.insertr-style-btn:hover { + background: #f3f4f6; + border-color: #9ca3af; +} + +.insertr-style-btn:active { + background: #e5e7eb; + transform: translateY(1px); +} + +/* Simple text editor */ +.insertr-simple-editor { + width: 100%; + border: 1px solid #d1d5db; + border-radius: 6px; + padding: 0.75rem; + font-size: 0.875rem; + line-height: 1.5; + resize: vertical; + font-family: inherit; + margin-bottom: 1rem; +} + +.insertr-simple-editor:focus { + outline: none; + border-color: #3b82f6; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +/* Rich text editor */ +.insertr-rich-editor { + width: 100%; + min-height: 100px; + border: 1px solid #d1d5db; + border-radius: 6px; + padding: 0.75rem; + font-size: 0.875rem; + line-height: 1.5; + margin-bottom: 1rem; + overflow-y: auto; +} + +.insertr-rich-editor:focus { + outline: none; + border-color: #3b82f6; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +/* Multi-property editor */ +.insertr-multi-property-editor { + margin-bottom: 1rem; +} + +.insertr-styled-element-form { + background: #f9fafb; + border: 1px solid #e5e7eb; + border-radius: 6px; + padding: 1rem; + margin-bottom: 1rem; +} + +.insertr-styled-element-form:last-child { + margin-bottom: 0; +} + +.insertr-form-header { + margin: 0 0 0.75rem 0; + font-size: 1rem; + font-weight: 600; + color: #374151; +} + +/* Property inputs */ +.insertr-property-input, +.insertr-text-input { + margin-bottom: 0.75rem; +} + +.insertr-property-input:last-child, +.insertr-text-input:last-child { + margin-bottom: 0; +} + +.insertr-property-label, +.insertr-input-label { + display: block; + font-size: 0.875rem; + font-weight: 500; + color: #374151; + margin-bottom: 0.25rem; +} + +.insertr-property-field, +.insertr-text-field { + width: 100%; + border: 1px solid #d1d5db; + border-radius: 4px; + padding: 0.5rem; + font-size: 0.875rem; + line-height: 1.4; + font-family: inherit; + transition: border-color 0.2s ease; +} + +.insertr-property-field:focus, +.insertr-text-field:focus { + outline: none; + border-color: #3b82f6; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +/* URL inputs */ +.insertr-property-field[type="url"] { + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 0.8125rem; +} + +/* Form actions */ +.insertr-form-actions { + display: flex; + gap: 0.75rem; + justify-content: flex-end; + padding-top: 1rem; + border-top: 1px solid #e5e7eb; +} + +.insertr-btn-save, +.insertr-btn-cancel { + padding: 0.5rem 1rem; + font-size: 0.875rem; + font-weight: 500; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s ease; + border: 1px solid; +} + +.insertr-btn-save { + background: #3b82f6; + border-color: #3b82f6; + color: white; +} + +.insertr-btn-save:hover { + background: #2563eb; + border-color: #2563eb; +} + +.insertr-btn-cancel { + background: white; + border-color: #d1d5db; + color: #374151; +} + +.insertr-btn-cancel:hover { + background: #f9fafb; + border-color: #9ca3af; +} + +/* Fallback editor */ +.insertr-fallback-editor { + background: white; + border: 1px solid #d1d5db; + border-radius: 8px; + padding: 1rem; + min-width: 400px; + max-width: 500px; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); +} + +.insertr-fallback-textarea { + width: 100%; + border: 1px solid #d1d5db; + border-radius: 6px; + padding: 0.75rem; + font-size: 0.875rem; + line-height: 1.5; + resize: vertical; + font-family: inherit; + margin-bottom: 1rem; +} + +.insertr-fallback-textarea:focus { + outline: none; + border-color: #3b82f6; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +/* Responsive adjustments */ +@media (max-width: 640px) { + .insertr-style-aware-editor, + .insertr-fallback-editor { + min-width: 300px; + max-width: calc(100vw - 2rem); + margin: 1rem; + } + + .insertr-style-toolbar { + padding: 0.5rem; + gap: 0.375rem; + } + + .insertr-style-btn { + padding: 0.25rem 0.5rem; + font-size: 0.8125rem; + } + + .insertr-form-actions { + flex-direction: column; + } + + .insertr-btn-save, + .insertr-btn-cancel { + width: 100%; + } +} + +/* Dark mode support (if needed) */ +@media (prefers-color-scheme: dark) { + .insertr-style-aware-editor, + .insertr-fallback-editor { + background: #1f2937; + border-color: #374151; + color: #f9fafb; + } + + .insertr-style-toolbar, + .insertr-styled-element-form { + background: #111827; + border-color: #374151; + } + + .insertr-style-btn { + background: #374151; + border-color: #4b5563; + color: #f9fafb; + } + + .insertr-style-btn:hover { + background: #4b5563; + border-color: #6b7280; + } + + .insertr-property-field, + .insertr-text-field, + .insertr-simple-editor, + .insertr-fallback-textarea { + background: #374151; + border-color: #4b5563; + color: #f9fafb; + } + + .insertr-rich-editor { + background: #374151; + border-color: #4b5563; + color: #f9fafb; + } +} \ No newline at end of file diff --git a/lib/src/ui/editor.js b/lib/src/ui/editor.js index 97a9799..ec66045 100644 --- a/lib/src/ui/editor.js +++ b/lib/src/ui/editor.js @@ -1,57 +1,139 @@ /** - * Editor - Handles all content types with markdown-first approach + * Editor - Handles all content types with style-aware approach */ -import { markdownConverter } from '../utils/markdown.js'; +import { StyleAwareEditor } from './style-aware-editor.js'; import { Previewer } from './previewer.js'; export class Editor { constructor() { this.currentOverlay = null; + this.currentStyleEditor = null; this.previewer = new Previewer(); } /** - * Edit any content element with markdown interface + * Edit any content element with style-aware interface * @param {Object} meta - Element metadata {element, contentId, contentType} * @param {string|Object} currentContent - Current content value * @param {Function} onSave - Save callback * @param {Function} onCancel - Cancel callback */ - edit(meta, currentContent, onSave, onCancel) { + async edit(meta, currentContent, onSave, onCancel) { const { element } = meta; // Handle both single elements and groups uniformly const elements = Array.isArray(element) ? element : [element]; - const context = new EditContext(elements, currentContent); + const primaryElement = elements[0]; // Close any existing editor this.close(); - // Create editor form - const form = this.createForm(context, meta); + try { + // Create style-aware editor + const styleEditor = new StyleAwareEditor(primaryElement, { + onSave: (content) => { + onSave(content); + this.close(); + }, + onCancel: () => { + onCancel(); + this.close(); + }, + onChange: (content) => { + // Optional: trigger live preview + this.handlePreviewChange(primaryElement, content); + } + }); + + // Initialize the editor + await styleEditor.initialize(); + + // Create overlay and position + const editorElement = styleEditor.getEditorElement(); + const overlay = this.createOverlay(editorElement); + + // Position relative to primary element + this.positionForm(primaryElement, overlay); + + // Show editor + document.body.appendChild(overlay); + this.currentOverlay = overlay; + this.currentStyleEditor = styleEditor; + + // Focus first input + const firstInput = editorElement.querySelector('textarea, input[type="text"], [contenteditable="true"]'); + if (firstInput) { + setTimeout(() => firstInput.focus(), 100); + } + + return overlay; + + } catch (error) { + console.error('Failed to create style-aware editor:', error); + // Fallback to simple text editing + return this.createFallbackEditor(primaryElement, onSave, onCancel); + } + } + + /** + * Create fallback editor for simple text editing when style-aware editor fails + */ + createFallbackEditor(element, onSave, onCancel) { + const form = document.createElement('div'); + form.className = 'insertr-fallback-editor'; + + const textarea = document.createElement('textarea'); + textarea.className = 'insertr-fallback-textarea'; + textarea.value = element.textContent; + textarea.rows = 4; + textarea.placeholder = 'Enter content...'; + + const actions = document.createElement('div'); + actions.className = 'insertr-form-actions'; + + const saveBtn = document.createElement('button'); + saveBtn.textContent = 'Save'; + saveBtn.className = 'insertr-btn-save'; + saveBtn.onclick = () => { + element.textContent = textarea.value; + onSave({ type: 'text', content: textarea.value }); + this.close(); + }; + + const cancelBtn = document.createElement('button'); + cancelBtn.textContent = 'Cancel'; + cancelBtn.className = 'insertr-btn-cancel'; + cancelBtn.onclick = () => { + onCancel(); + this.close(); + }; + + actions.appendChild(saveBtn); + actions.appendChild(cancelBtn); + form.appendChild(textarea); + form.appendChild(actions); + const overlay = this.createOverlay(form); + this.positionForm(element, overlay); - // Position relative to primary element - this.positionForm(context.primaryElement, overlay); - - // Setup event handlers - this.setupEventHandlers(form, overlay, context, { onSave, onCancel }); - - // Show editor document.body.appendChild(overlay); this.currentOverlay = overlay; - // Focus textarea - const textarea = form.querySelector('textarea'); - if (textarea) { - setTimeout(() => textarea.focus(), 100); - } + setTimeout(() => textarea.focus(), 100); return overlay; } /** - * Create editing form for any content type + * Handle preview changes from style-aware editor + */ + handlePreviewChange(element, content) { + // Implement live preview if needed + // For now, we'll skip this to avoid complexity + } + + /** + * Create editing form for any content type (legacy - keeping for compatibility) */ createForm(context, meta) { const config = this.getFieldConfig(context); @@ -349,6 +431,11 @@ export class Editor { this.previewer.clearPreview(); } + if (this.currentStyleEditor) { + this.currentStyleEditor.destroy(); + this.currentStyleEditor = null; + } + if (this.currentOverlay) { this.currentOverlay.remove(); this.currentOverlay = null; diff --git a/lib/src/ui/style-aware-editor.js b/lib/src/ui/style-aware-editor.js new file mode 100644 index 0000000..cc864a0 --- /dev/null +++ b/lib/src/ui/style-aware-editor.js @@ -0,0 +1,673 @@ +/** + * StyleAwareEditor - Rich text editor with style-specific formatting options + * + * Implements the sophisticated style preservation system described in CLASSES.md: + * - Automatic style detection from nested elements + * - Formatting toolbar with developer-style-based options + * - Multi-property editing for complex elements (links, images, etc.) + * - Perfect attribute preservation during editing + */ + +import { styleDetectionEngine } from '../utils/style-detection.js'; +import { htmlPreservationEngine } from '../utils/html-preservation.js'; + +export class StyleAwareEditor { + constructor(element, options = {}) { + this.element = element; + this.options = { + showToolbar: true, + enableMultiProperty: true, + preserveAttributes: true, + ...options + }; + + // Core system components + this.styleEngine = styleDetectionEngine; + this.htmlEngine = htmlPreservationEngine; + + // Editor state + this.isInitialized = false; + this.editorContainer = null; + this.contentEditor = null; + this.toolbar = null; + this.detectedStyles = null; + this.contentStructure = null; + this.originalContent = null; + + // Event callbacks + this.onSave = options.onSave || (() => {}); + this.onCancel = options.onCancel || (() => {}); + this.onChange = options.onChange || (() => {}); + } + + /** + * Initialize the style-aware editor + * Analyzes element, detects styles, creates appropriate editing interface + */ + async initialize() { + if (this.isInitialized) { + return; + } + + try { + // Analyze element for styles and structure + const analysis = this.analyzeElement(); + + // Create editor interface based on analysis + this.createEditorInterface(analysis); + + // Set up event handlers + this.setupEventHandlers(); + + this.isInitialized = true; + + } catch (error) { + console.error('Failed to initialize StyleAwareEditor:', error); + throw error; + } + } + + /** + * Analyze element to determine editing strategy + * + * @returns {Object} - Analysis results with styles, structure, and editing strategy + */ + analyzeElement() { + // Extract content with preservation metadata + this.originalContent = this.htmlEngine.extractForEditing(this.element); + + // Detect styles and structure + const detection = this.styleEngine.detectStylesAndStructure(this.element); + this.detectedStyles = detection.styles; + this.contentStructure = detection.structure; + + // Determine editing strategy based on complexity + const editingStrategy = this.determineEditingStrategy(detection); + + return { + styles: detection.styles, + structure: detection.structure, + strategy: editingStrategy, + hasMultiPropertyElements: this.hasMultiPropertyElements(detection.structure) + }; + } + + /** + * Determine the best editing strategy based on content complexity + * + * @param {Object} detection - Style detection results + * @returns {string} - Editing strategy: 'simple', 'rich', 'multi-property' + */ + determineEditingStrategy(detection) { + if (detection.structure.length === 0) { + return 'simple'; // No nested elements + } + + const hasStyledElements = detection.structure.some(piece => piece.type === 'styled'); + const hasMultiProperty = this.hasMultiPropertyElements(detection.structure); + + if (hasMultiProperty) { + return 'multi-property'; // Complex elements with multiple editable properties + } else if (hasStyledElements) { + return 'rich'; // Rich text with styling + } else { + return 'simple'; // Plain text + } + } + + /** + * 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 + * + * @param {Object} analysis - Analysis results + */ + createEditorInterface(analysis) { + // Create main editor container + this.editorContainer = document.createElement('div'); + this.editorContainer.className = 'insertr-style-aware-editor'; + + // Create appropriate editor based on strategy + switch (analysis.strategy) { + case 'simple': + this.createSimpleEditor(); + break; + case 'rich': + this.createRichEditor(analysis); + break; + case 'multi-property': + this.createMultiPropertyEditor(analysis); + break; + } + + // Add toolbar if enabled and we have detected styles + if (this.options.showToolbar && analysis.styles.size > 0) { + this.createStyleToolbar(analysis.styles); + } + + // Add form actions + this.createFormActions(); + } + + /** + * Create simple text editor for plain content + */ + createSimpleEditor() { + const textarea = document.createElement('textarea'); + textarea.className = 'insertr-simple-editor'; + textarea.value = this.originalContent.text; + textarea.rows = 3; + textarea.placeholder = 'Enter content...'; + + this.contentEditor = textarea; + this.editorContainer.appendChild(textarea); + } + + /** + * Create rich text editor with style preservation + * + * @param {Object} analysis - Analysis results + */ + createRichEditor(analysis) { + // Create contentEditable div + const editor = document.createElement('div'); + editor.className = 'insertr-rich-editor'; + editor.contentEditable = true; + editor.innerHTML = this.originalContent.html; + + this.contentEditor = editor; + this.editorContainer.appendChild(editor); + } + + /** + * Create multi-property editor for complex elements + * + * @param {Object} analysis - Analysis results + */ + createMultiPropertyEditor(analysis) { + const container = document.createElement('div'); + container.className = 'insertr-multi-property-editor'; + + // Create fields for each structure piece + analysis.structure.forEach((piece, index) => { + if (piece.type === 'text') { + // Simple text input + const input = this.createTextInput(piece.content, `Text ${index + 1}`); + input.dataset.structureIndex = index; + container.appendChild(input); + } else if (piece.type === 'styled') { + // Multi-property form for styled element + const form = this.createStyledElementForm(piece, index); + container.appendChild(form); + } + }); + + this.contentEditor = container; + this.editorContainer.appendChild(container); + } + + /** + * Create form for editing styled element with multiple properties + * + * @param {Object} piece - Structure piece for styled element + * @param {number} index - Index in structure array + * @returns {HTMLElement} - Form element + */ + createStyledElementForm(piece, index) { + const form = document.createElement('div'); + form.className = 'insertr-styled-element-form'; + form.dataset.structureIndex = index; + form.dataset.styleId = piece.styleId; + + const style = this.detectedStyles.get(piece.styleId); + if (!style) { + return form; + } + + // Form header + const header = document.createElement('h4'); + header.textContent = style.name; + header.className = 'insertr-form-header'; + form.appendChild(header); + + // Create input for each editable property + Object.entries(piece.properties).forEach(([property, value]) => { + const input = this.createPropertyInput(property, value, style.tagName); + form.appendChild(input); + }); + + return form; + } + + /** + * Create input field for a specific property + * + * @param {string} property - Property name (content, href, src, etc.) + * @param {string} value - Current property value + * @param {string} tagName - Element tag name for context + * @returns {HTMLElement} - Input field container + */ + createPropertyInput(property, value, tagName) { + const container = document.createElement('div'); + container.className = 'insertr-property-input'; + + const label = document.createElement('label'); + label.textContent = this.getPropertyLabel(property, tagName); + label.className = 'insertr-property-label'; + + let input; + + switch (property) { + case 'content': + input = document.createElement('textarea'); + input.rows = 2; + break; + case 'href': + input = document.createElement('input'); + input.type = 'url'; + input.placeholder = 'https://example.com'; + break; + case 'src': + input = document.createElement('input'); + input.type = 'url'; + input.placeholder = 'https://example.com/image.jpg'; + break; + case 'alt': + case 'title': + input = document.createElement('input'); + input.type = 'text'; + break; + default: + input = document.createElement('input'); + input.type = 'text'; + break; + } + + input.className = 'insertr-property-field'; + input.name = property; + input.value = value || ''; + + container.appendChild(label); + container.appendChild(input); + + return container; + } + + /** + * Get human-readable label for property + * + * @param {string} property - Property name + * @param {string} tagName - Element tag name + * @returns {string} - Human-readable label + */ + getPropertyLabel(property, tagName) { + const labels = { + content: tagName === 'img' ? 'Description' : 'Text', + href: 'URL', + src: 'Image URL', + alt: 'Alt Text', + title: 'Title', + target: 'Target', + placeholder: 'Placeholder' + }; + + return labels[property] || property.charAt(0).toUpperCase() + property.slice(1); + } + + /** + * Create simple text input + * + * @param {string} content - Current content + * @param {string} label - Input label + * @returns {HTMLElement} - Input container + */ + createTextInput(content, label) { + const container = document.createElement('div'); + container.className = 'insertr-text-input'; + + const labelEl = document.createElement('label'); + labelEl.textContent = label; + labelEl.className = 'insertr-input-label'; + + const input = document.createElement('textarea'); + input.className = 'insertr-text-field'; + input.value = content; + input.rows = 2; + + container.appendChild(labelEl); + container.appendChild(input); + + return container; + } + + /** + * Create formatting toolbar with detected style buttons + * + * @param {Map} styles - Detected styles + */ + createStyleToolbar(styles) { + const toolbar = document.createElement('div'); + toolbar.className = 'insertr-style-toolbar'; + + const title = document.createElement('span'); + title.textContent = 'Formatting:'; + title.className = 'insertr-toolbar-title'; + toolbar.appendChild(title); + + // Add button for each detected style + for (const [styleId, styleInfo] of styles) { + const button = this.createStyleButton(styleId, styleInfo); + toolbar.appendChild(button); + } + + this.toolbar = toolbar; + this.editorContainer.insertBefore(toolbar, this.contentEditor); + } + + /** + * Create button for applying detected style + * + * @param {string} styleId - Style identifier + * @param {Object} styleInfo - Style information + * @returns {HTMLElement} - Style button + */ + createStyleButton(styleId, styleInfo) { + 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; + + // Add click handler for style application + button.addEventListener('click', (e) => { + e.preventDefault(); + this.applyStyle(styleId); + }); + + return button; + } + + /** + * Create form action buttons (Save, Cancel) + */ + createFormActions() { + const actions = document.createElement('div'); + actions.className = 'insertr-form-actions'; + + const saveBtn = document.createElement('button'); + saveBtn.type = 'button'; + saveBtn.className = 'insertr-btn-save'; + saveBtn.textContent = 'Save'; + + const cancelBtn = document.createElement('button'); + cancelBtn.type = 'button'; + cancelBtn.className = 'insertr-btn-cancel'; + cancelBtn.textContent = 'Cancel'; + + actions.appendChild(saveBtn); + actions.appendChild(cancelBtn); + + this.editorContainer.appendChild(actions); + } + + /** + * Set up event handlers for editor interaction + */ + setupEventHandlers() { + // Save button + const saveBtn = this.editorContainer.querySelector('.insertr-btn-save'); + saveBtn?.addEventListener('click', () => this.handleSave()); + + // Cancel button + const cancelBtn = this.editorContainer.querySelector('.insertr-btn-cancel'); + cancelBtn?.addEventListener('click', () => this.handleCancel()); + + // Content change events + if (this.contentEditor) { + this.contentEditor.addEventListener('input', () => this.handleChange()); + } + + // Multi-property form changes + const propertyFields = this.editorContainer.querySelectorAll('.insertr-property-field'); + propertyFields.forEach(field => { + field.addEventListener('input', () => this.handleChange()); + }); + } + + /** + * Apply detected style to selected text or current position + * + * @param {string} styleId - Style to apply + */ + applyStyle(styleId) { + const styleInfo = this.detectedStyles.get(styleId); + if (!styleInfo) { + return; + } + + if (this.contentEditor.contentEditable === 'true') { + // Rich text editor - apply style to selection + this.applyStyleToSelection(styleInfo); + } else { + // Other editor types - could expand functionality here + console.log('Style application for this editor type not yet implemented'); + } + } + + /** + * Apply style to current selection in rich text editor + * + * @param {Object} styleInfo - Style information + */ + applyStyleToSelection(styleInfo) { + const selection = window.getSelection(); + if (selection.rangeCount === 0) { + return; + } + + const range = selection.getRangeAt(0); + const selectedText = range.toString(); + + if (selectedText) { + // Create styled element + const styledElement = this.styleEngine.createElementFromTemplate( + styleInfo, + { content: selectedText } + ); + + // Replace selection with styled element + range.deleteContents(); + range.insertNode(styledElement); + + // Clear selection + selection.removeAllRanges(); + + // Trigger change event + this.handleChange(); + } + } + + /** + * Handle save action + */ + handleSave() { + try { + const content = this.extractContent(); + const success = this.applyContentToElement(content); + + if (success) { + this.onSave(content); + } + } catch (error) { + console.error('Save failed:', error); + } + } + + /** + * Handle cancel action + */ + handleCancel() { + this.onCancel(); + } + + /** + * Handle content change + */ + handleChange() { + try { + const content = this.extractContent(); + this.onChange(content); + } catch (error) { + console.error('Change handling failed:', error); + } + } + + /** + * Extract current content from editor + * + * @returns {Object} - Extracted content + */ + extractContent() { + if (this.contentEditor.className.includes('simple-editor')) { + // Simple text editor + return { + type: 'text', + content: this.contentEditor.value + }; + } else if (this.contentEditor.className.includes('rich-editor')) { + // Rich text editor + return { + type: 'html', + content: this.contentEditor.innerHTML + }; + } else if (this.contentEditor.className.includes('multi-property-editor')) { + // Multi-property editor + return this.extractMultiPropertyContent(); + } + + return null; + } + + /** + * Extract content from multi-property editor + * + * @returns {Object} - Structured content with updated properties + */ + extractMultiPropertyContent() { + const updatedStructure = [...this.contentStructure]; + const updatedProperties = {}; + + // Extract text inputs + const textInputs = this.contentEditor.querySelectorAll('.insertr-text-input textarea'); + textInputs.forEach(input => { + const index = parseInt(input.closest('.insertr-text-input').dataset.structureIndex); + if (!isNaN(index)) { + updatedStructure[index] = { + ...updatedStructure[index], + content: input.value + }; + } + }); + + // Extract styled element forms + const styledForms = this.contentEditor.querySelectorAll('.insertr-styled-element-form'); + styledForms.forEach(form => { + const index = parseInt(form.dataset.structureIndex); + const styleId = form.dataset.styleId; + + if (!isNaN(index)) { + const properties = {}; + const propertyFields = form.querySelectorAll('.insertr-property-field'); + + propertyFields.forEach(field => { + properties[field.name] = field.value; + }); + + updatedProperties[index] = { properties }; + updatedStructure[index] = { + ...updatedStructure[index], + properties + }; + } + }); + + return { + type: 'structured', + structure: updatedStructure, + updatedProperties: updatedProperties + }; + } + + /** + * Apply extracted content to the original element + * + * @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; + } + } catch (error) { + console.error('Failed to apply content:', error); + return false; + } + } + + /** + * Get the editor container element + * + * @returns {HTMLElement} - Editor container + */ + getEditorElement() { + return this.editorContainer; + } + + /** + * Destroy the editor and clean up + */ + destroy() { + if (this.editorContainer && this.editorContainer.parentNode) { + this.editorContainer.parentNode.removeChild(this.editorContainer); + } + + this.isInitialized = false; + this.editorContainer = null; + this.contentEditor = null; + this.toolbar = null; + } +} \ No newline at end of file diff --git a/test-multi-property-elements.html b/test-multi-property-elements.html new file mode 100644 index 0000000..fc0e3fc --- /dev/null +++ b/test-multi-property-elements.html @@ -0,0 +1,162 @@ + + +
+ + +Testing our enhanced system that handles elements with multiple editable properties like links (href + content).
+ +Testing if our new system preserves WHERE styles are positioned, not just WHAT styles exist.
+ +This page tests our new StyleDetectionEngine and HTMLPreservationEngine with actual DOM elements.
+ +