wysiwyg.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  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.createEditor();
  14. this.bindEvents();
  15. this.setupImageUpload();
  16. }
  17. createEditor() {
  18. // Create editor container
  19. this.container = document.createElement('div');
  20. this.container.className = 'wysiwyg-container';
  21. // Create toolbar
  22. this.toolbar = document.createElement('div');
  23. this.toolbar.className = 'wysiwyg-toolbar';
  24. this.createToolbarButtons();
  25. // Create content area
  26. this.content = document.createElement('div');
  27. this.content.className = 'wysiwyg-content';
  28. this.content.contentEditable = true;
  29. // Create character count
  30. this.charCount = document.createElement('div');
  31. this.charCount.className = 'wysiwyg-char-count';
  32. // Assemble editor
  33. this.container.appendChild(this.toolbar);
  34. this.container.appendChild(this.content);
  35. this.container.appendChild(this.charCount);
  36. // Replace textarea
  37. this.textarea.parentNode.insertBefore(this.container, this.textarea);
  38. this.textarea.style.display = 'none';
  39. // Initialize content
  40. this.content.innerHTML = this.textarea.value;
  41. this.updateCharCount();
  42. }
  43. createToolbarButtons() {
  44. this.options.toolbar.forEach(item => {
  45. if (item === '|') {
  46. const separator = document.createElement('div');
  47. separator.className = 'wysiwyg-separator';
  48. this.toolbar.appendChild(separator);
  49. } else {
  50. const button = document.createElement('button');
  51. button.className = 'wysiwyg-btn';
  52. button.type = 'button';
  53. button.innerHTML = this.getButtonLabel(item);
  54. button.dataset.command = item;
  55. button.addEventListener('click', () => this.execCommand(item));
  56. this.toolbar.appendChild(button);
  57. }
  58. });
  59. }
  60. getButtonLabel(command) {
  61. const labels = {
  62. 'bold': 'B',
  63. 'italic': 'I',
  64. 'underline': 'U',
  65. 'link': '🔗',
  66. 'image': '🖼️',
  67. 'ul': '• List',
  68. 'ol': '1. List',
  69. 'h1': 'H1',
  70. 'h2': 'H2',
  71. 'h3': 'H3'
  72. };
  73. return labels[command] || command;
  74. }
  75. execCommand(command) {
  76. switch (command) {
  77. case 'bold':
  78. document.execCommand('bold', false, null);
  79. break;
  80. case 'italic':
  81. document.execCommand('italic', false, null);
  82. break;
  83. case 'underline':
  84. document.execCommand('underline', false, null);
  85. break;
  86. case 'link':
  87. this.insertLink();
  88. break;
  89. case 'image':
  90. this.insertImage();
  91. break;
  92. case 'ul':
  93. document.execCommand('insertUnorderedList', false, null);
  94. break;
  95. case 'ol':
  96. document.execCommand('insertOrderedList', false, null);
  97. break;
  98. case 'h1':
  99. this.formatHeading('h1');
  100. break;
  101. case 'h2':
  102. this.formatHeading('h2');
  103. break;
  104. case 'h3':
  105. this.formatHeading('h3');
  106. break;
  107. }
  108. this.content.focus();
  109. }
  110. insertLink() {
  111. const selection = window.getSelection();
  112. const url = prompt('Enter URL:');
  113. if (url) {
  114. document.execCommand('createLink', false, url);
  115. }
  116. }
  117. insertImage() {
  118. this.openImageGallery();
  119. }
  120. openImageGallery() {
  121. this.selectedImages = [];
  122. this.loadGalleryImages();
  123. document.getElementById('imageGallery').classList.add('open');
  124. }
  125. closeImageGallery() {
  126. document.getElementById('imageGallery').classList.remove('open');
  127. this.selectedImages = [];
  128. this.updateInsertButton();
  129. }
  130. showGalleryTab(tab) {
  131. // Hide all tabs
  132. document.getElementById('galleryBrowse').style.display = 'none';
  133. document.getElementById('galleryUpload').style.display = 'none';
  134. // Remove active class from all tabs
  135. document.querySelectorAll('.gallery-tab').forEach(tab => {
  136. tab.classList.remove('active');
  137. });
  138. // Show selected tab and add active class
  139. if (tab === 'browse') {
  140. document.getElementById('galleryBrowse').style.display = 'block';
  141. document.querySelectorAll('.gallery-tab')[0].classList.add('active');
  142. } else if (tab === 'upload') {
  143. document.getElementById('galleryUpload').style.display = 'block';
  144. document.querySelectorAll('.gallery-tab')[1].classList.add('active');
  145. }
  146. }
  147. async loadGalleryImages() {
  148. try {
  149. const response = await fetch('upload_image.php');
  150. const data = await response.json();
  151. if (data.success) {
  152. this.renderGalleryImages(data.images);
  153. } else {
  154. console.error('Failed to load images:', data.error);
  155. }
  156. } catch (error) {
  157. console.error('Error loading images:', error);
  158. }
  159. }
  160. renderGalleryImages(images) {
  161. const grid = document.getElementById('galleryGrid');
  162. grid.innerHTML = '';
  163. images.forEach(image => {
  164. const item = document.createElement('div');
  165. item.className = 'gallery-item';
  166. item.dataset.imageId = image.id;
  167. item.dataset.imageUrl = image.url;
  168. item.dataset.imageName = image.original_name;
  169. item.innerHTML = `
  170. <img src="${image.thumbnail_url}" alt="${image.original_name}" class="gallery-image">
  171. <div class="gallery-info">
  172. <div class="gallery-name">${image.original_name}</div>
  173. <div class="gallery-size">${this.formatFileSize(image.size)}</div>
  174. </div>
  175. `;
  176. item.addEventListener('click', () => this.selectImage(item));
  177. grid.appendChild(item);
  178. });
  179. }
  180. selectImage(item) {
  181. const imageId = item.dataset.imageId;
  182. if (item.classList.contains('selected')) {
  183. item.classList.remove('selected');
  184. this.selectedImages = this.selectedImages.filter(id => id !== imageId);
  185. } else {
  186. item.classList.add('selected');
  187. this.selectedImages.push(imageId);
  188. }
  189. this.updateInsertButton();
  190. }
  191. updateInsertButton() {
  192. const insertBtn = document.getElementById('galleryInsertBtn');
  193. insertBtn.disabled = this.selectedImages.length === 0;
  194. }
  195. insertSelectedImage() {
  196. if (this.selectedImages.length === 0) return;
  197. const selectedItems = document.querySelectorAll('.gallery-item.selected');
  198. const images = [];
  199. selectedItems.forEach(item => {
  200. images.push({
  201. url: item.dataset.imageUrl,
  202. name: item.dataset.imageName
  203. });
  204. });
  205. // Insert images into editor
  206. images.forEach(image => {
  207. const img = document.createElement('img');
  208. img.src = image.url;
  209. img.alt = image.name;
  210. img.style.maxWidth = '100%';
  211. img.style.height = 'auto';
  212. // Insert at cursor position
  213. const selection = window.getSelection();
  214. if (selection.rangeCount > 0) {
  215. const range = selection.getRangeAt(0);
  216. range.insertNode(img);
  217. range.collapse(false);
  218. } else {
  219. this.content.appendChild(img);
  220. }
  221. });
  222. // Update textarea and close gallery
  223. this.textarea.value = this.content.innerHTML;
  224. this.closeImageGallery();
  225. }
  226. setupImageUpload() {
  227. const uploadArea = document.getElementById('uploadArea');
  228. const fileInput = document.getElementById('fileInput');
  229. // Click to upload
  230. uploadArea.addEventListener('click', () => {
  231. fileInput.click();
  232. });
  233. // File selection
  234. fileInput.addEventListener('change', (e) => {
  235. this.handleFileUpload(e.target.files);
  236. });
  237. // Drag and drop
  238. uploadArea.addEventListener('dragover', (e) => {
  239. e.preventDefault();
  240. uploadArea.classList.add('dragover');
  241. });
  242. uploadArea.addEventListener('dragleave', () => {
  243. uploadArea.classList.remove('dragover');
  244. });
  245. uploadArea.addEventListener('drop', (e) => {
  246. e.preventDefault();
  247. uploadArea.classList.remove('dragover');
  248. this.handleFileUpload(e.dataTransfer.files);
  249. });
  250. }
  251. async handleFileUpload(files) {
  252. const formData = new FormData();
  253. for (let i = 0; i < files.length; i++) {
  254. formData.append('images[]', files[i]);
  255. }
  256. try {
  257. this.showUploadProgress();
  258. const response = await fetch('upload_image.php', {
  259. method: 'POST',
  260. body: formData
  261. });
  262. const data = await response.json();
  263. if (data.success) {
  264. this.showUploadSuccess('Images uploaded successfully!');
  265. // Refresh gallery
  266. this.loadGalleryImages();
  267. // Switch to browse tab
  268. this.showGalleryTab('browse');
  269. } else {
  270. this.showUploadError('Upload failed: ' + data.errors.join(', '));
  271. }
  272. } catch (error) {
  273. this.showUploadError('Upload failed: ' + error.message);
  274. } finally {
  275. this.hideUploadProgress();
  276. }
  277. }
  278. showUploadProgress() {
  279. document.getElementById('uploadProgress').classList.add('active');
  280. document.getElementById('uploadStatus').textContent = 'Uploading...';
  281. document.getElementById('progressFill').style.width = '50%';
  282. }
  283. hideUploadProgress() {
  284. document.getElementById('uploadProgress').classList.remove('active');
  285. document.getElementById('progressFill').style.width = '0%';
  286. }
  287. showUploadSuccess(message) {
  288. const status = document.getElementById('uploadStatus');
  289. status.textContent = message;
  290. status.className = 'upload-status success';
  291. setTimeout(() => {
  292. this.hideUploadProgress();
  293. }, 3000);
  294. }
  295. showUploadError(message) {
  296. const status = document.getElementById('uploadStatus');
  297. status.textContent = message;
  298. status.className = 'upload-status error';
  299. setTimeout(() => {
  300. this.hideUploadProgress();
  301. }, 5000);
  302. }
  303. formatFileSize(bytes) {
  304. if (bytes === 0) return '0 Bytes';
  305. const k = 1024;
  306. const sizes = ['Bytes', 'KB', 'MB', 'GB'];
  307. const i = Math.floor(Math.log(bytes) / Math.log(k));
  308. return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
  309. }
  310. formatHeading(tag) {
  311. const selection = window.getSelection();
  312. const range = selection.getRangeAt(0);
  313. const heading = document.createElement(tag);
  314. heading.textContent = range.toString();
  315. range.deleteContents();
  316. range.insertNode(heading);
  317. }
  318. bindEvents() {
  319. // Update textarea when content changes
  320. this.content.addEventListener('input', () => {
  321. this.textarea.value = this.content.innerHTML;
  322. this.updateCharCount();
  323. });
  324. // Update content when textarea changes (for form submission)
  325. this.textarea.addEventListener('input', () => {
  326. this.content.innerHTML = this.textarea.value;
  327. this.updateCharCount();
  328. });
  329. // Handle paste events
  330. this.content.addEventListener('paste', (e) => {
  331. e.preventDefault();
  332. const text = e.clipboardData.getData('text/html') || e.clipboardData.getData('text/plain');
  333. document.execCommand('insertHTML', false, text);
  334. });
  335. // Keyboard shortcuts
  336. this.content.addEventListener('keydown', (e) => {
  337. if (e.ctrlKey || e.metaKey) {
  338. switch (e.key) {
  339. case 'b':
  340. e.preventDefault();
  341. this.execCommand('bold');
  342. break;
  343. case 'i':
  344. e.preventDefault();
  345. this.execCommand('italic');
  346. break;
  347. case 'u':
  348. e.preventDefault();
  349. this.execCommand('underline');
  350. break;
  351. }
  352. }
  353. });
  354. }
  355. updateCharCount() {
  356. const text = this.content.innerText || this.content.textContent || '';
  357. const count = text.length;
  358. this.charCount.textContent = `Characters: ${count}`;
  359. }
  360. getContent() {
  361. return this.content.innerHTML;
  362. }
  363. setContent(html) {
  364. this.content.innerHTML = html;
  365. this.textarea.value = html;
  366. this.updateCharCount();
  367. }
  368. destroy() {
  369. this.textarea.style.display = 'block';
  370. this.textarea.value = this.content.innerHTML;
  371. this.container.remove();
  372. }
  373. }
  374. // Initialize editor when DOM is ready
  375. document.addEventListener('DOMContentLoaded', () => {
  376. const editor = new WYSIWYGEditor('content');
  377. // Make editor globally accessible
  378. window.wysiwygEditor = editor;
  379. // Handle form submission
  380. const form = document.querySelector('.publication-form');
  381. if (form) {
  382. form.addEventListener('submit', () => {
  383. // Ensure textarea has latest content
  384. document.getElementById('content').value = editor.getContent();
  385. });
  386. }
  387. });