| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676 |
- /**
- * 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 = false;
-
- 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 = `
- <img src="${image.thumbnail_url}" alt="${image.original_name}" class="gallery-image">
- <div class="gallery-info">
- <div class="gallery-name">${image.original_name}</div>
- <div class="gallery-size">${this.formatFileSize(image.size)}</div>
- </div>
- `;
-
- 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';
-
- 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();
- });
- }
- });
|