wysiwyg.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843
  1. /**
  2. * Simple WYSIWYG Editor
  3. * Lightweight rich text editor for publication content
  4. */
  5. class WYSIWYGEditor {
  6. constructor(textareaId, options = {}) {
  7. this.textarea = document.getElementById(textareaId);
  8. this.options = {
  9. toolbar: ['bold', 'italic', 'underline', '|', 'link', 'image', '|', 'ul', 'ol', '|', 'h1', 'h2', 'h3'],
  10. ...options
  11. };
  12. this.selectedImages = [];
  13. this.galleryImages = [];
  14. this.resizingImage = null;
  15. this.resizeData = null;
  16. this.aspectRatioLocked = true;
  17. this.isRawMode = false;
  18. this.rawTextarea = null;
  19. this.createEditor();
  20. this.bindEvents();
  21. this.setupImageUpload();
  22. this.setupImageResizing();
  23. }
  24. createEditor() {
  25. // Create editor container
  26. this.container = document.createElement('div');
  27. this.container.className = 'wysiwyg-container';
  28. // Create toolbar
  29. this.toolbar = document.createElement('div');
  30. this.toolbar.className = 'wysiwyg-toolbar';
  31. this.createToolbarButtons();
  32. // Create content area
  33. this.content = document.createElement('div');
  34. this.content.className = 'wysiwyg-content';
  35. this.content.contentEditable = true;
  36. // Create raw textarea
  37. this.rawTextarea = document.createElement('textarea');
  38. this.rawTextarea.className = 'wysiwyg-raw-textarea';
  39. this.rawTextarea.style.display = 'none'; // Hidden by default
  40. // Create character count
  41. this.charCount = document.createElement('div');
  42. this.charCount.className = 'wysiwyg-char-count';
  43. // Assemble editor
  44. this.container.appendChild(this.toolbar);
  45. this.container.appendChild(this.content);
  46. this.container.appendChild(this.rawTextarea);
  47. this.container.appendChild(this.charCount);
  48. // Replace textarea
  49. this.textarea.parentNode.insertBefore(this.container, this.textarea);
  50. this.textarea.style.display = 'none';
  51. // Initialize content
  52. this.content.innerHTML = this.textarea.value;
  53. this.updateCharCount();
  54. }
  55. createToolbarButtons() {
  56. this.options.toolbar.forEach(item => {
  57. if (item === '|') {
  58. const separator = document.createElement('div');
  59. separator.className = 'wysiwyg-separator';
  60. this.toolbar.appendChild(separator);
  61. } else {
  62. const button = document.createElement('button');
  63. button.className = 'wysiwyg-btn';
  64. button.type = 'button';
  65. button.innerHTML = this.getButtonLabel(item);
  66. button.dataset.command = item;
  67. button.addEventListener('click', () => this.execCommand(item));
  68. this.toolbar.appendChild(button);
  69. }
  70. });
  71. // Add raw edit toggle button
  72. const rawToggle = document.createElement('button');
  73. rawToggle.className = 'wysiwyg-btn wysiwyg-raw-toggle';
  74. rawToggle.type = 'button';
  75. rawToggle.innerHTML = '</>';
  76. rawToggle.title = 'Toggle Raw HTML Edit';
  77. rawToggle.addEventListener('click', () => this.toggleRawMode());
  78. this.toolbar.appendChild(rawToggle);
  79. }
  80. getButtonLabel(command) {
  81. const labels = {
  82. 'bold': 'B',
  83. 'italic': 'I',
  84. 'underline': 'U',
  85. 'link': '🔗',
  86. 'image': '🖼️',
  87. 'ul': '• List',
  88. 'ol': '1. List',
  89. 'h1': 'H1',
  90. 'h2': 'H2',
  91. 'h3': 'H3'
  92. };
  93. return labels[command] || command;
  94. }
  95. execCommand(command) {
  96. switch (command) {
  97. case 'bold':
  98. document.execCommand('bold', false, null);
  99. break;
  100. case 'italic':
  101. document.execCommand('italic', false, null);
  102. break;
  103. case 'underline':
  104. document.execCommand('underline', false, null);
  105. break;
  106. case 'link':
  107. this.insertLink();
  108. break;
  109. case 'image':
  110. this.insertImage();
  111. break;
  112. case 'ul':
  113. document.execCommand('insertUnorderedList', false, null);
  114. break;
  115. case 'ol':
  116. document.execCommand('insertOrderedList', false, null);
  117. break;
  118. case 'h1':
  119. this.formatHeading('h1');
  120. break;
  121. case 'h2':
  122. this.formatHeading('h2');
  123. break;
  124. case 'h3':
  125. this.formatHeading('h3');
  126. break;
  127. }
  128. this.content.focus();
  129. }
  130. insertLink() {
  131. const selection = window.getSelection();
  132. const url = prompt('Enter URL:');
  133. if (url) {
  134. document.execCommand('createLink', false, url);
  135. }
  136. }
  137. insertImage() {
  138. this.openImageGallery();
  139. }
  140. openImageGallery() {
  141. this.selectedImages = [];
  142. this.loadGalleryImages();
  143. document.getElementById('imageGallery').classList.add('open');
  144. }
  145. closeImageGallery() {
  146. document.getElementById('imageGallery').classList.remove('open');
  147. this.selectedImages = [];
  148. this.updateInsertButton();
  149. }
  150. showGalleryTab(tab) {
  151. // Hide all tabs
  152. document.getElementById('galleryBrowse').style.display = 'none';
  153. document.getElementById('galleryUpload').style.display = 'none';
  154. // Remove active class from all tabs
  155. document.querySelectorAll('.gallery-tab').forEach(tab => {
  156. tab.classList.remove('active');
  157. });
  158. // Show selected tab and add active class
  159. if (tab === 'browse') {
  160. document.getElementById('galleryBrowse').style.display = 'block';
  161. document.querySelectorAll('.gallery-tab')[0].classList.add('active');
  162. } else if (tab === 'upload') {
  163. document.getElementById('galleryUpload').style.display = 'block';
  164. document.querySelectorAll('.gallery-tab')[1].classList.add('active');
  165. }
  166. }
  167. async loadGalleryImages() {
  168. try {
  169. const response = await fetch('upload_image.php');
  170. const data = await response.json();
  171. if (data.success) {
  172. // Store images for later use
  173. this.galleryImages = data.images;
  174. this.renderGalleryImages(data.images);
  175. } else {
  176. console.error('Failed to load images:', data.error);
  177. this.galleryImages = [];
  178. }
  179. } catch (error) {
  180. console.error('Error loading images:', error);
  181. this.galleryImages = [];
  182. }
  183. }
  184. renderGalleryImages(images) {
  185. const grid = document.getElementById('galleryGrid');
  186. grid.innerHTML = '';
  187. images.forEach(image => {
  188. const item = document.createElement('div');
  189. item.className = 'gallery-item';
  190. item.dataset.imageId = image.id;
  191. item.dataset.imageUrl = image.url;
  192. item.dataset.imageName = image.original_name;
  193. item.innerHTML = `
  194. <div class="gallery-item-container">
  195. <img src="${image.thumbnail_url}" alt="${image.original_name}" class="gallery-image">
  196. <button class="gallery-delete-btn" onclick="wysiwygEditor.deleteImage(${image.id}, event)" title="Delete image">×</button>
  197. <div class="gallery-info">
  198. <div class="gallery-name">${image.original_name}</div>
  199. <div class="gallery-size">${this.formatFileSize(image.size)}</div>
  200. </div>
  201. </div>
  202. `;
  203. item.addEventListener('click', () => this.selectImage(item));
  204. grid.appendChild(item);
  205. });
  206. }
  207. selectImage(item) {
  208. const imageId = item.dataset.imageId;
  209. if (item.classList.contains('selected')) {
  210. item.classList.remove('selected');
  211. this.selectedImages = this.selectedImages.filter(id => id !== imageId);
  212. } else {
  213. item.classList.add('selected');
  214. this.selectedImages.push(imageId);
  215. }
  216. this.updateInsertButton();
  217. }
  218. updateInsertButton() {
  219. const insertBtn = document.getElementById('galleryInsertBtn');
  220. insertBtn.disabled = this.selectedImages.length === 0;
  221. }
  222. insertSelectedImages() {
  223. if (this.selectedImages.length === 0) return;
  224. // Insert thumbnail links into content
  225. this.selectedImages.forEach(imageId => {
  226. const image = this.galleryImages.find(img => img.id == imageId);
  227. if (image) {
  228. // Create link wrapper
  229. const link = document.createElement('a');
  230. link.href = image.url;
  231. link.target = '_blank';
  232. link.rel = 'noopener noreferrer';
  233. link.className = 'thumbnail-link';
  234. // Create thumbnail image
  235. const img = document.createElement('img');
  236. img.src = image.thumbnail_url;
  237. img.alt = image.original_name;
  238. img.className = 'thumbnail-image';
  239. img.style.maxWidth = '300px';
  240. img.style.height = 'auto';
  241. img.style.borderRadius = '4px';
  242. img.style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)';
  243. img.style.transition = 'transform 0.2s ease, box-shadow 0.2s ease';
  244. // Add hover effect
  245. img.addEventListener('mouseenter', () => {
  246. img.style.transform = 'scale(1.02)';
  247. img.style.boxShadow = '0 4px 16px rgba(0,0,0,0.15)';
  248. });
  249. img.addEventListener('mouseleave', () => {
  250. img.style.transform = 'scale(1)';
  251. img.style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)';
  252. });
  253. // Add click prevention to avoid WYSIWYG selection
  254. img.addEventListener('click', (e) => e.preventDefault());
  255. link.addEventListener('click', (e) => e.preventDefault());
  256. // Assemble the link with image
  257. link.appendChild(img);
  258. // Insert at cursor or end of content
  259. if (this.selection) {
  260. this.selection.deleteContents();
  261. this.selection.insertNode(link);
  262. this.selection.collapse(false);
  263. } else {
  264. this.content.appendChild(link);
  265. }
  266. }
  267. });
  268. // Update textarea and close gallery
  269. this.textarea.value = this.content.innerHTML;
  270. this.closeImageGallery();
  271. }
  272. // Alias function for backward compatibility
  273. insertSelectedImage() {
  274. return this.insertSelectedImages();
  275. }
  276. toggleRawMode() {
  277. this.isRawMode = !this.isRawMode;
  278. if (this.isRawMode) {
  279. // Switch to raw mode
  280. this.syncToRaw();
  281. this.content.style.display = 'none';
  282. this.rawTextarea.style.display = 'block';
  283. this.rawTextarea.focus();
  284. // Update button state
  285. const rawToggle = this.container.querySelector('.wysiwyg-raw-toggle');
  286. if (rawToggle) {
  287. rawToggle.classList.add('active');
  288. rawToggle.style.backgroundColor = '#007bff';
  289. rawToggle.style.color = 'white';
  290. }
  291. // Disable toolbar buttons (except raw toggle)
  292. this.disableToolbar(true);
  293. } else {
  294. // Switch to visual mode
  295. this.syncToVisual();
  296. this.rawTextarea.style.display = 'none';
  297. this.content.style.display = 'block';
  298. this.content.focus();
  299. // Update button state
  300. const rawToggle = this.container.querySelector('.wysiwyg-raw-toggle');
  301. if (rawToggle) {
  302. rawToggle.classList.remove('active');
  303. rawToggle.style.backgroundColor = '';
  304. rawToggle.style.color = '';
  305. }
  306. // Enable toolbar buttons
  307. this.disableToolbar(false);
  308. }
  309. }
  310. syncToRaw() {
  311. // Sync visual content to raw textarea
  312. this.rawTextarea.value = this.content.innerHTML;
  313. this.textarea.value = this.content.innerHTML;
  314. }
  315. syncToVisual() {
  316. // Sync raw textarea to visual content
  317. const rawContent = this.rawTextarea.value;
  318. this.content.innerHTML = rawContent;
  319. this.textarea.value = rawContent;
  320. this.updateCharCount();
  321. }
  322. disableToolbar(disable) {
  323. const buttons = this.toolbar.querySelectorAll('.wysiwyg-btn:not(.wysiwyg-raw-toggle)');
  324. buttons.forEach(button => {
  325. button.disabled = disable;
  326. button.style.opacity = disable ? '0.5' : '1';
  327. button.style.cursor = disable ? 'not-allowed' : 'pointer';
  328. });
  329. }
  330. async deleteImage(imageId, event) {
  331. event.stopPropagation(); // Prevent image selection
  332. if (!confirm('Are you sure you want to delete this image?')) {
  333. return;
  334. }
  335. try {
  336. const response = await fetch(`upload_image.php?id=${imageId}`, {
  337. method: 'DELETE'
  338. });
  339. const result = await response.json();
  340. if (result.success) {
  341. // Remove the image from gallery
  342. const galleryItem = document.querySelector(`[data-image-id="${imageId}"]`);
  343. if (galleryItem) {
  344. galleryItem.remove();
  345. }
  346. // Show success message
  347. this.showNotification('Image deleted successfully', 'success');
  348. } else {
  349. this.showNotification('Failed to delete image', 'error');
  350. }
  351. } catch (error) {
  352. console.error('Error deleting image:', error);
  353. this.showNotification('Error deleting image', 'error');
  354. }
  355. }
  356. showNotification(message, type = 'info') {
  357. // Create notification element
  358. const notification = document.createElement('div');
  359. notification.className = `notification notification-${type}`;
  360. notification.textContent = message;
  361. // Add to page
  362. document.body.appendChild(notification);
  363. // Remove after 3 seconds
  364. setTimeout(() => {
  365. notification.remove();
  366. }, 3000);
  367. }
  368. setupImageUpload() {
  369. const uploadArea = document.getElementById('uploadArea');
  370. const fileInput = document.getElementById('fileInput');
  371. // Click to upload
  372. uploadArea.addEventListener('click', () => {
  373. fileInput.click();
  374. });
  375. // File selection
  376. fileInput.addEventListener('change', (e) => {
  377. this.handleFileUpload(e.target.files);
  378. });
  379. // Drag and drop
  380. uploadArea.addEventListener('dragover', (e) => {
  381. e.preventDefault();
  382. uploadArea.classList.add('dragover');
  383. });
  384. uploadArea.addEventListener('dragleave', () => {
  385. uploadArea.classList.remove('dragover');
  386. });
  387. uploadArea.addEventListener('drop', (e) => {
  388. e.preventDefault();
  389. uploadArea.classList.remove('dragover');
  390. this.handleFileUpload(e.dataTransfer.files);
  391. });
  392. }
  393. async handleFileUpload(files) {
  394. const formData = new FormData();
  395. for (let i = 0; i < files.length; i++) {
  396. formData.append('images[]', files[i]);
  397. }
  398. try {
  399. this.showUploadProgress();
  400. const response = await fetch('upload_image.php', {
  401. method: 'POST',
  402. body: formData
  403. });
  404. const data = await response.json();
  405. if (data.success) {
  406. this.showUploadSuccess('Images uploaded successfully!');
  407. // Refresh gallery
  408. this.loadGalleryImages();
  409. // Switch to browse tab
  410. this.showGalleryTab('browse');
  411. } else {
  412. this.showUploadError('Upload failed: ' + data.errors.join(', '));
  413. }
  414. } catch (error) {
  415. this.showUploadError('Upload failed: ' + error.message);
  416. } finally {
  417. this.hideUploadProgress();
  418. }
  419. }
  420. showUploadProgress() {
  421. document.getElementById('uploadProgress').classList.add('active');
  422. document.getElementById('uploadStatus').textContent = 'Uploading...';
  423. document.getElementById('progressFill').style.width = '50%';
  424. }
  425. hideUploadProgress() {
  426. document.getElementById('uploadProgress').classList.remove('active');
  427. document.getElementById('progressFill').style.width = '0%';
  428. }
  429. showUploadSuccess(message) {
  430. const status = document.getElementById('uploadStatus');
  431. status.textContent = message;
  432. status.className = 'upload-status success';
  433. setTimeout(() => {
  434. this.hideUploadProgress();
  435. }, 3000);
  436. }
  437. showUploadError(message) {
  438. const status = document.getElementById('uploadStatus');
  439. status.textContent = message;
  440. status.className = 'upload-status error';
  441. setTimeout(() => {
  442. this.hideUploadProgress();
  443. }, 5000);
  444. }
  445. formatFileSize(bytes) {
  446. if (bytes === 0) return '0 Bytes';
  447. const k = 1024;
  448. const sizes = ['Bytes', 'KB', 'MB', 'GB'];
  449. const i = Math.floor(Math.log(bytes) / Math.log(k));
  450. return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
  451. }
  452. setupImageResizing() {
  453. this.content.addEventListener('click', (e) => {
  454. if (e.target.tagName === 'IMG') {
  455. this.selectImageForResize(e.target);
  456. } else if (e.target.closest('.resize-container')) {
  457. this.selectImageForResize(e.target.closest('.resize-container').querySelector('img'));
  458. } else {
  459. this.deselectImageForResize();
  460. }
  461. });
  462. // Handle resize handle mouse events
  463. this.content.addEventListener('mousedown', (e) => {
  464. if (e.target.classList.contains('resize-handle')) {
  465. e.preventDefault();
  466. this.startResize(e);
  467. }
  468. });
  469. // Handle global mouse events for resizing
  470. document.addEventListener('mousemove', (e) => {
  471. if (this.resizingImage) {
  472. this.handleResize(e);
  473. }
  474. });
  475. document.addEventListener('mouseup', () => {
  476. if (this.resizingImage) {
  477. this.stopResize();
  478. }
  479. });
  480. }
  481. selectImageForResize(img) {
  482. this.deselectImageForResize();
  483. // Wrap image in resize container if not already wrapped
  484. if (!img.closest('.resize-container')) {
  485. const container = document.createElement('div');
  486. container.className = 'resize-container';
  487. img.parentNode.insertBefore(container, img);
  488. container.appendChild(img);
  489. }
  490. const container = img.closest('.resize-container');
  491. container.classList.add('resizing');
  492. // Add resize handles
  493. this.addResizeHandles(container);
  494. // Add aspect ratio toggle
  495. this.addAspectRatioToggle(container);
  496. // Add size display
  497. this.addSizeDisplay(container);
  498. this.updateSizeDisplay(container);
  499. }
  500. deselectImageForResize() {
  501. // Remove all resize containers and handles
  502. this.content.querySelectorAll('.resize-container').forEach(container => {
  503. const img = container.querySelector('img');
  504. if (img) {
  505. container.parentNode.insertBefore(img, container);
  506. }
  507. container.remove();
  508. });
  509. this.resizingImage = null;
  510. this.resizeData = null;
  511. }
  512. addResizeHandles(container) {
  513. const handles = ['nw', 'ne', 'sw', 'se', 'n', 's', 'w', 'e'];
  514. handles.forEach(position => {
  515. const handle = document.createElement('div');
  516. handle.className = `resize-handle ${position}`;
  517. handle.dataset.position = position;
  518. container.appendChild(handle);
  519. });
  520. }
  521. addAspectRatioToggle(container) {
  522. const toggle = document.createElement('button');
  523. toggle.className = 'aspect-ratio-toggle';
  524. toggle.textContent = this.aspectRatioLocked ? 'Locked' : 'Free';
  525. toggle.title = 'Toggle aspect ratio lock';
  526. // Set initial locked state
  527. if (this.aspectRatioLocked) {
  528. toggle.classList.add('locked');
  529. }
  530. toggle.addEventListener('click', (e) => {
  531. e.stopPropagation();
  532. this.aspectRatioLocked = !this.aspectRatioLocked;
  533. toggle.textContent = this.aspectRatioLocked ? 'Locked' : 'Free';
  534. toggle.classList.toggle('locked', this.aspectRatioLocked);
  535. });
  536. container.appendChild(toggle);
  537. }
  538. addSizeDisplay(container) {
  539. const display = document.createElement('div');
  540. display.className = 'size-display';
  541. container.appendChild(display);
  542. }
  543. updateSizeDisplay(container) {
  544. const img = container.querySelector('img');
  545. const display = container.querySelector('.size-display');
  546. if (img && display) {
  547. display.textContent = `${img.offsetWidth} × ${img.offsetHeight}`;
  548. }
  549. }
  550. startResize(e) {
  551. const handle = e.target;
  552. const container = handle.closest('.resize-container');
  553. const img = container.querySelector('img');
  554. this.resizingImage = img;
  555. this.resizeData = {
  556. container: container,
  557. handle: handle,
  558. position: handle.dataset.position,
  559. startX: e.clientX,
  560. startY: e.clientY,
  561. startWidth: img.offsetWidth,
  562. startHeight: img.offsetHeight,
  563. aspectRatio: img.offsetWidth / img.offsetHeight
  564. };
  565. }
  566. handleResize(e) {
  567. if (!this.resizeData) return;
  568. const deltaX = e.clientX - this.resizeData.startX;
  569. const deltaY = e.clientY - this.resizeData.startY;
  570. const position = this.resizeData.position;
  571. let newWidth = this.resizeData.startWidth;
  572. let newHeight = this.resizeData.startHeight;
  573. switch (position) {
  574. case 'se':
  575. newWidth = this.resizeData.startWidth + deltaX;
  576. newHeight = this.aspectRatioLocked
  577. ? newWidth / this.resizeData.aspectRatio
  578. : this.resizeData.startHeight + deltaY;
  579. break;
  580. case 'sw':
  581. newWidth = this.resizeData.startWidth - deltaX;
  582. newHeight = this.aspectRatioLocked
  583. ? newWidth / this.resizeData.aspectRatio
  584. : this.resizeData.startHeight + deltaY;
  585. break;
  586. case 'ne':
  587. newWidth = this.resizeData.startWidth + deltaX;
  588. newHeight = this.aspectRatioLocked
  589. ? newWidth / this.resizeData.aspectRatio
  590. : this.resizeData.startHeight - deltaY;
  591. break;
  592. case 'nw':
  593. newWidth = this.resizeData.startWidth - deltaX;
  594. newHeight = this.aspectRatioLocked
  595. ? newWidth / this.resizeData.aspectRatio
  596. : this.resizeData.startHeight - deltaY;
  597. break;
  598. case 'n':
  599. newHeight = this.resizeData.startHeight - deltaY;
  600. if (this.aspectRatioLocked) {
  601. newWidth = newHeight * this.resizeData.aspectRatio;
  602. }
  603. break;
  604. case 's':
  605. newHeight = this.resizeData.startHeight + deltaY;
  606. if (this.aspectRatioLocked) {
  607. newWidth = newHeight * this.resizeData.aspectRatio;
  608. }
  609. break;
  610. case 'w':
  611. newWidth = this.resizeData.startWidth - deltaX;
  612. if (this.aspectRatioLocked) {
  613. newHeight = newWidth / this.resizeData.aspectRatio;
  614. }
  615. break;
  616. case 'e':
  617. newWidth = this.resizeData.startWidth + deltaX;
  618. if (this.aspectRatioLocked) {
  619. newHeight = newWidth / this.resizeData.aspectRatio;
  620. }
  621. break;
  622. }
  623. // Apply minimum size constraints
  624. newWidth = Math.max(50, newWidth);
  625. newHeight = Math.max(50, newHeight);
  626. // Apply new dimensions
  627. this.resizingImage.style.width = newWidth + 'px';
  628. this.resizingImage.style.height = newHeight + 'px';
  629. // Update size display
  630. this.updateSizeDisplay(this.resizeData.container);
  631. }
  632. stopResize() {
  633. if (this.resizeData) {
  634. // Update textarea content
  635. this.textarea.value = this.content.innerHTML;
  636. this.updateCharCount();
  637. }
  638. this.resizingImage = null;
  639. this.resizeData = null;
  640. }
  641. formatHeading(tag) {
  642. const selection = window.getSelection();
  643. const range = selection.getRangeAt(0);
  644. const heading = document.createElement(tag);
  645. heading.textContent = range.toString();
  646. range.deleteContents();
  647. range.insertNode(heading);
  648. }
  649. bindEvents() {
  650. // Update textarea when content changes
  651. this.content.addEventListener('input', () => {
  652. this.textarea.value = this.content.innerHTML;
  653. this.updateCharCount();
  654. });
  655. // Update content when textarea changes (for form submission)
  656. this.textarea.addEventListener('input', () => {
  657. this.content.innerHTML = this.textarea.value;
  658. this.updateCharCount();
  659. });
  660. // Handle paste events
  661. this.content.addEventListener('paste', (e) => {
  662. e.preventDefault();
  663. const text = e.clipboardData.getData('text/html') || e.clipboardData.getData('text/plain');
  664. document.execCommand('insertHTML', false, text);
  665. });
  666. // Keyboard shortcuts
  667. this.content.addEventListener('keydown', (e) => {
  668. if (e.ctrlKey || e.metaKey) {
  669. switch (e.key) {
  670. case 'b':
  671. e.preventDefault();
  672. this.execCommand('bold');
  673. break;
  674. case 'i':
  675. e.preventDefault();
  676. this.execCommand('italic');
  677. break;
  678. case 'u':
  679. e.preventDefault();
  680. this.execCommand('underline');
  681. break;
  682. }
  683. }
  684. });
  685. }
  686. updateCharCount() {
  687. const text = this.content.innerText || this.content.textContent || '';
  688. const count = text.length;
  689. this.charCount.textContent = `Characters: ${count}`;
  690. }
  691. getContent() {
  692. return this.content.innerHTML;
  693. }
  694. setContent(html) {
  695. this.content.innerHTML = html;
  696. this.textarea.value = html;
  697. this.updateCharCount();
  698. }
  699. destroy() {
  700. this.textarea.style.display = 'block';
  701. this.textarea.value = this.content.innerHTML;
  702. this.container.remove();
  703. }
  704. }
  705. // Initialize editor when DOM is ready
  706. document.addEventListener('DOMContentLoaded', () => {
  707. const editor = new WYSIWYGEditor('content');
  708. // Make editor globally accessible
  709. window.wysiwygEditor = editor;
  710. // Handle form submission
  711. const form = document.querySelector('.publication-form');
  712. if (form) {
  713. form.addEventListener('submit', () => {
  714. // Ensure textarea has latest content
  715. document.getElementById('content').value = editor.getContent();
  716. });
  717. }
  718. });