/** * Simple WYSIWYG Editor * Lightweight rich text editor for publication content */ class WYSIWYGEditor { constructor(textareaId, options = {}) { this.textarea = document.getElementById(textareaId); this.options = { toolbar: ['bold', 'italic', 'underline', '|', 'link', 'image', '|', 'ul', 'ol', '|', 'h1', 'h2', 'h3'], ...options }; this.selectedImages = []; this.resizingImage = null; this.resizeData = null; this.aspectRatioLocked = true; this.createEditor(); this.bindEvents(); this.setupImageUpload(); this.setupImageResizing(); } createEditor() { // Create editor container this.container = document.createElement('div'); this.container.className = 'wysiwyg-container'; // Create toolbar this.toolbar = document.createElement('div'); this.toolbar.className = 'wysiwyg-toolbar'; this.createToolbarButtons(); // Create content area this.content = document.createElement('div'); this.content.className = 'wysiwyg-content'; this.content.contentEditable = true; // Create character count this.charCount = document.createElement('div'); this.charCount.className = 'wysiwyg-char-count'; // Assemble editor this.container.appendChild(this.toolbar); this.container.appendChild(this.content); this.container.appendChild(this.charCount); // Replace textarea this.textarea.parentNode.insertBefore(this.container, this.textarea); this.textarea.style.display = 'none'; // Initialize content this.content.innerHTML = this.textarea.value; this.updateCharCount(); } createToolbarButtons() { this.options.toolbar.forEach(item => { if (item === '|') { const separator = document.createElement('div'); separator.className = 'wysiwyg-separator'; this.toolbar.appendChild(separator); } else { const button = document.createElement('button'); button.className = 'wysiwyg-btn'; button.type = 'button'; button.innerHTML = this.getButtonLabel(item); button.dataset.command = item; button.addEventListener('click', () => this.execCommand(item)); this.toolbar.appendChild(button); } }); } getButtonLabel(command) { const labels = { 'bold': 'B', 'italic': 'I', 'underline': 'U', 'link': '🔗', 'image': '🖼️', 'ul': '• List', 'ol': '1. List', 'h1': 'H1', 'h2': 'H2', 'h3': 'H3' }; return labels[command] || command; } execCommand(command) { switch (command) { case 'bold': document.execCommand('bold', false, null); break; case 'italic': document.execCommand('italic', false, null); break; case 'underline': document.execCommand('underline', false, null); break; case 'link': this.insertLink(); break; case 'image': this.insertImage(); break; case 'ul': document.execCommand('insertUnorderedList', false, null); break; case 'ol': document.execCommand('insertOrderedList', false, null); break; case 'h1': this.formatHeading('h1'); break; case 'h2': this.formatHeading('h2'); break; case 'h3': this.formatHeading('h3'); break; } this.content.focus(); } insertLink() { const selection = window.getSelection(); const url = prompt('Enter URL:'); if (url) { document.execCommand('createLink', false, url); } } insertImage() { this.openImageGallery(); } openImageGallery() { this.selectedImages = []; this.loadGalleryImages(); document.getElementById('imageGallery').classList.add('open'); } closeImageGallery() { document.getElementById('imageGallery').classList.remove('open'); this.selectedImages = []; this.updateInsertButton(); } showGalleryTab(tab) { // Hide all tabs document.getElementById('galleryBrowse').style.display = 'none'; document.getElementById('galleryUpload').style.display = 'none'; // Remove active class from all tabs document.querySelectorAll('.gallery-tab').forEach(tab => { tab.classList.remove('active'); }); // Show selected tab and add active class if (tab === 'browse') { document.getElementById('galleryBrowse').style.display = 'block'; document.querySelectorAll('.gallery-tab')[0].classList.add('active'); } else if (tab === 'upload') { document.getElementById('galleryUpload').style.display = 'block'; document.querySelectorAll('.gallery-tab')[1].classList.add('active'); } } async loadGalleryImages() { try { const response = await fetch('upload_image.php'); const data = await response.json(); if (data.success) { this.renderGalleryImages(data.images); } else { console.error('Failed to load images:', data.error); } } catch (error) { console.error('Error loading images:', error); } } renderGalleryImages(images) { const grid = document.getElementById('galleryGrid'); grid.innerHTML = ''; images.forEach(image => { const item = document.createElement('div'); item.className = 'gallery-item'; item.dataset.imageId = image.id; item.dataset.imageUrl = image.url; item.dataset.imageName = image.original_name; item.innerHTML = ` ${image.original_name} `; item.addEventListener('click', () => this.selectImage(item)); grid.appendChild(item); }); } selectImage(item) { const imageId = item.dataset.imageId; if (item.classList.contains('selected')) { item.classList.remove('selected'); this.selectedImages = this.selectedImages.filter(id => id !== imageId); } else { item.classList.add('selected'); this.selectedImages.push(imageId); } this.updateInsertButton(); } updateInsertButton() { const insertBtn = document.getElementById('galleryInsertBtn'); insertBtn.disabled = this.selectedImages.length === 0; } insertSelectedImage() { if (this.selectedImages.length === 0) return; const selectedItems = document.querySelectorAll('.gallery-item.selected'); const images = []; selectedItems.forEach(item => { images.push({ url: item.dataset.imageUrl, name: item.dataset.imageName }); }); // Insert images into editor images.forEach(image => { const img = document.createElement('img'); img.src = image.url; img.alt = image.name; img.style.maxWidth = '100%'; img.style.height = 'auto'; // Insert at cursor position const selection = window.getSelection(); if (selection.rangeCount > 0) { const range = selection.getRangeAt(0); range.insertNode(img); range.collapse(false); } else { this.content.appendChild(img); } }); // Update textarea and close gallery this.textarea.value = this.content.innerHTML; this.closeImageGallery(); } setupImageUpload() { const uploadArea = document.getElementById('uploadArea'); const fileInput = document.getElementById('fileInput'); // Click to upload uploadArea.addEventListener('click', () => { fileInput.click(); }); // File selection fileInput.addEventListener('change', (e) => { this.handleFileUpload(e.target.files); }); // Drag and drop uploadArea.addEventListener('dragover', (e) => { e.preventDefault(); uploadArea.classList.add('dragover'); }); uploadArea.addEventListener('dragleave', () => { uploadArea.classList.remove('dragover'); }); uploadArea.addEventListener('drop', (e) => { e.preventDefault(); uploadArea.classList.remove('dragover'); this.handleFileUpload(e.dataTransfer.files); }); } async handleFileUpload(files) { const formData = new FormData(); for (let i = 0; i < files.length; i++) { formData.append('images[]', files[i]); } try { this.showUploadProgress(); const response = await fetch('upload_image.php', { method: 'POST', body: formData }); const data = await response.json(); if (data.success) { this.showUploadSuccess('Images uploaded successfully!'); // Refresh gallery this.loadGalleryImages(); // Switch to browse tab this.showGalleryTab('browse'); } else { this.showUploadError('Upload failed: ' + data.errors.join(', ')); } } catch (error) { this.showUploadError('Upload failed: ' + error.message); } finally { this.hideUploadProgress(); } } showUploadProgress() { document.getElementById('uploadProgress').classList.add('active'); document.getElementById('uploadStatus').textContent = 'Uploading...'; document.getElementById('progressFill').style.width = '50%'; } hideUploadProgress() { document.getElementById('uploadProgress').classList.remove('active'); document.getElementById('progressFill').style.width = '0%'; } showUploadSuccess(message) { const status = document.getElementById('uploadStatus'); status.textContent = message; status.className = 'upload-status success'; setTimeout(() => { this.hideUploadProgress(); }, 3000); } showUploadError(message) { const status = document.getElementById('uploadStatus'); status.textContent = message; status.className = 'upload-status error'; setTimeout(() => { this.hideUploadProgress(); }, 5000); } formatFileSize(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } setupImageResizing() { this.content.addEventListener('click', (e) => { if (e.target.tagName === 'IMG') { this.selectImageForResize(e.target); } else if (e.target.closest('.resize-container')) { this.selectImageForResize(e.target.closest('.resize-container').querySelector('img')); } else { this.deselectImageForResize(); } }); // Handle resize handle mouse events this.content.addEventListener('mousedown', (e) => { if (e.target.classList.contains('resize-handle')) { e.preventDefault(); this.startResize(e); } }); // Handle global mouse events for resizing document.addEventListener('mousemove', (e) => { if (this.resizingImage) { this.handleResize(e); } }); document.addEventListener('mouseup', () => { if (this.resizingImage) { this.stopResize(); } }); } selectImageForResize(img) { this.deselectImageForResize(); // Wrap image in resize container if not already wrapped if (!img.closest('.resize-container')) { const container = document.createElement('div'); container.className = 'resize-container'; img.parentNode.insertBefore(container, img); container.appendChild(img); } const container = img.closest('.resize-container'); container.classList.add('resizing'); // Add resize handles this.addResizeHandles(container); // Add aspect ratio toggle this.addAspectRatioToggle(container); // Add size display this.addSizeDisplay(container); this.updateSizeDisplay(container); } deselectImageForResize() { // Remove all resize containers and handles this.content.querySelectorAll('.resize-container').forEach(container => { const img = container.querySelector('img'); if (img) { container.parentNode.insertBefore(img, container); } container.remove(); }); this.resizingImage = null; this.resizeData = null; } addResizeHandles(container) { const handles = ['nw', 'ne', 'sw', 'se', 'n', 's', 'w', 'e']; handles.forEach(position => { const handle = document.createElement('div'); handle.className = `resize-handle ${position}`; handle.dataset.position = position; container.appendChild(handle); }); } addAspectRatioToggle(container) { const toggle = document.createElement('button'); toggle.className = 'aspect-ratio-toggle'; toggle.textContent = this.aspectRatioLocked ? 'Locked' : 'Free'; toggle.title = 'Toggle aspect ratio lock'; // Set initial locked state if (this.aspectRatioLocked) { toggle.classList.add('locked'); } toggle.addEventListener('click', (e) => { e.stopPropagation(); this.aspectRatioLocked = !this.aspectRatioLocked; toggle.textContent = this.aspectRatioLocked ? 'Locked' : 'Free'; toggle.classList.toggle('locked', this.aspectRatioLocked); }); container.appendChild(toggle); } addSizeDisplay(container) { const display = document.createElement('div'); display.className = 'size-display'; container.appendChild(display); } updateSizeDisplay(container) { const img = container.querySelector('img'); const display = container.querySelector('.size-display'); if (img && display) { display.textContent = `${img.offsetWidth} × ${img.offsetHeight}`; } } startResize(e) { const handle = e.target; const container = handle.closest('.resize-container'); const img = container.querySelector('img'); this.resizingImage = img; this.resizeData = { container: container, handle: handle, position: handle.dataset.position, startX: e.clientX, startY: e.clientY, startWidth: img.offsetWidth, startHeight: img.offsetHeight, aspectRatio: img.offsetWidth / img.offsetHeight }; } handleResize(e) { if (!this.resizeData) return; const deltaX = e.clientX - this.resizeData.startX; const deltaY = e.clientY - this.resizeData.startY; const position = this.resizeData.position; let newWidth = this.resizeData.startWidth; let newHeight = this.resizeData.startHeight; switch (position) { case 'se': newWidth = this.resizeData.startWidth + deltaX; newHeight = this.aspectRatioLocked ? newWidth / this.resizeData.aspectRatio : this.resizeData.startHeight + deltaY; break; case 'sw': newWidth = this.resizeData.startWidth - deltaX; newHeight = this.aspectRatioLocked ? newWidth / this.resizeData.aspectRatio : this.resizeData.startHeight + deltaY; break; case 'ne': newWidth = this.resizeData.startWidth + deltaX; newHeight = this.aspectRatioLocked ? newWidth / this.resizeData.aspectRatio : this.resizeData.startHeight - deltaY; break; case 'nw': newWidth = this.resizeData.startWidth - deltaX; newHeight = this.aspectRatioLocked ? newWidth / this.resizeData.aspectRatio : this.resizeData.startHeight - deltaY; break; case 'n': newHeight = this.resizeData.startHeight - deltaY; if (this.aspectRatioLocked) { newWidth = newHeight * this.resizeData.aspectRatio; } break; case 's': newHeight = this.resizeData.startHeight + deltaY; if (this.aspectRatioLocked) { newWidth = newHeight * this.resizeData.aspectRatio; } break; case 'w': newWidth = this.resizeData.startWidth - deltaX; if (this.aspectRatioLocked) { newHeight = newWidth / this.resizeData.aspectRatio; } break; case 'e': newWidth = this.resizeData.startWidth + deltaX; if (this.aspectRatioLocked) { newHeight = newWidth / this.resizeData.aspectRatio; } break; } // Apply minimum size constraints newWidth = Math.max(50, newWidth); newHeight = Math.max(50, newHeight); // Apply new dimensions this.resizingImage.style.width = newWidth + 'px'; this.resizingImage.style.height = newHeight + 'px'; // Update size display this.updateSizeDisplay(this.resizeData.container); } stopResize() { if (this.resizeData) { // Update textarea content this.textarea.value = this.content.innerHTML; this.updateCharCount(); } this.resizingImage = null; this.resizeData = null; } formatHeading(tag) { const selection = window.getSelection(); const range = selection.getRangeAt(0); const heading = document.createElement(tag); heading.textContent = range.toString(); range.deleteContents(); range.insertNode(heading); } bindEvents() { // Update textarea when content changes this.content.addEventListener('input', () => { this.textarea.value = this.content.innerHTML; this.updateCharCount(); }); // Update content when textarea changes (for form submission) this.textarea.addEventListener('input', () => { this.content.innerHTML = this.textarea.value; this.updateCharCount(); }); // Handle paste events this.content.addEventListener('paste', (e) => { e.preventDefault(); const text = e.clipboardData.getData('text/html') || e.clipboardData.getData('text/plain'); document.execCommand('insertHTML', false, text); }); // Keyboard shortcuts this.content.addEventListener('keydown', (e) => { if (e.ctrlKey || e.metaKey) { switch (e.key) { case 'b': e.preventDefault(); this.execCommand('bold'); break; case 'i': e.preventDefault(); this.execCommand('italic'); break; case 'u': e.preventDefault(); this.execCommand('underline'); break; } } }); } updateCharCount() { const text = this.content.innerText || this.content.textContent || ''; const count = text.length; this.charCount.textContent = `Characters: ${count}`; } getContent() { return this.content.innerHTML; } setContent(html) { this.content.innerHTML = html; this.textarea.value = html; this.updateCharCount(); } destroy() { this.textarea.style.display = 'block'; this.textarea.value = this.content.innerHTML; this.container.remove(); } } // Initialize editor when DOM is ready document.addEventListener('DOMContentLoaded', () => { const editor = new WYSIWYGEditor('content'); // Make editor globally accessible window.wysiwygEditor = editor; // Handle form submission const form = document.querySelector('.publication-form'); if (form) { form.addEventListener('submit', () => { // Ensure textarea has latest content document.getElementById('content').value = editor.getContent(); }); } });