wysiwyg.js 26 KB

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