wysiwyg.js 26 KB

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