/**
* 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}
${this.formatFileSize(image.size)}
`;
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();
});
}
});