Mercurial
comparison rich_editor/rich_editor.js @ 206:240337164a80
[Seobeo] SSL should be used for large file as well lol.
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Sun, 15 Feb 2026 11:41:53 -0800 |
| parents | e5aed6c36672 |
| children | 5d3e116dd745 |
comparison
equal
deleted
inserted
replaced
| 205:e07b4b5a66bb | 206:240337164a80 |
|---|---|
| 158 .rich-editor-content img { | 158 .rich-editor-content img { |
| 159 max-width: 100%; | 159 max-width: 100%; |
| 160 height: auto; | 160 height: auto; |
| 161 border-radius: 4px; | 161 border-radius: 4px; |
| 162 margin: 8px 0; | 162 margin: 8px 0; |
| 163 cursor: pointer; | |
| 164 } | |
| 165 | |
| 166 .rich-editor-content img.selected { | |
| 167 outline: 2px solid #0078ff; | |
| 168 outline-offset: 2px; | |
| 169 } | |
| 170 | |
| 171 .rich-editor-resize-wrapper { | |
| 172 position: relative; | |
| 173 display: inline-block; | |
| 174 margin: 8px 0; | |
| 175 } | |
| 176 | |
| 177 .rich-editor-resize-handle { | |
| 178 position: absolute; | |
| 179 width: 12px; | |
| 180 height: 12px; | |
| 181 background: #0078ff; | |
| 182 border: 2px solid white; | |
| 183 border-radius: 50%; | |
| 184 cursor: nwse-resize; | |
| 185 z-index: 10; | |
| 186 } | |
| 187 | |
| 188 .rich-editor-resize-handle.nw { | |
| 189 top: -6px; | |
| 190 left: -6px; | |
| 191 cursor: nwse-resize; | |
| 192 } | |
| 193 | |
| 194 .rich-editor-resize-handle.ne { | |
| 195 top: -6px; | |
| 196 right: -6px; | |
| 197 cursor: nesw-resize; | |
| 198 } | |
| 199 | |
| 200 .rich-editor-resize-handle.sw { | |
| 201 bottom: -6px; | |
| 202 left: -6px; | |
| 203 cursor: nesw-resize; | |
| 204 } | |
| 205 | |
| 206 .rich-editor-resize-handle.se { | |
| 207 bottom: -6px; | |
| 208 right: -6px; | |
| 209 cursor: nwse-resize; | |
| 210 } | |
| 211 | |
| 212 .rich-editor-size-input { | |
| 213 position: absolute; | |
| 214 top: -35px; | |
| 215 left: 50%; | |
| 216 transform: translateX(-50%); | |
| 217 background: white; | |
| 218 border: 1px solid #ccc; | |
| 219 padding: 4px 8px; | |
| 220 border-radius: 4px; | |
| 221 font-size: 12px; | |
| 222 display: flex; | |
| 223 gap: 8px; | |
| 224 align-items: center; | |
| 225 box-shadow: 0 2px 8px rgba(0,0,0,0.1); | |
| 226 z-index: 11; | |
| 227 } | |
| 228 | |
| 229 .rich-editor-size-input input { | |
| 230 width: 60px; | |
| 231 padding: 2px 4px; | |
| 232 border: 1px solid #ccc; | |
| 233 border-radius: 2px; | |
| 234 font-size: 12px; | |
| 235 } | |
| 236 | |
| 237 .rich-editor-size-input label { | |
| 238 font-size: 11px; | |
| 239 color: #666; | |
| 163 } | 240 } |
| 164 | 241 |
| 165 .rich-editor-content a { | 242 .rich-editor-content a { |
| 166 color: #0078ff; | 243 color: #0078ff; |
| 167 text-decoration: none; | 244 text-decoration: none; |
| 240 | 317 |
| 241 class Editor { | 318 class Editor { |
| 242 constructor(elementId, options) { | 319 constructor(elementId, options) { |
| 243 this.options = { ...DEFAULT_OPTIONS, ...options }; | 320 this.options = { ...DEFAULT_OPTIONS, ...options }; |
| 244 this.container = document.getElementById(elementId); | 321 this.container = document.getElementById(elementId); |
| 245 this.state = { readOnly: false }; | 322 this.state = { |
| 323 readOnly: false, | |
| 324 selectedImage: null, | |
| 325 resizing: false | |
| 326 }; | |
| 246 | 327 |
| 247 if (!this.container) { | 328 if (!this.container) { |
| 248 throw new Error(`Element with id "${elementId}" not found`); | 329 throw new Error(`Element with id "${elementId}" not found`); |
| 249 } | 330 } |
| 250 | 331 |
| 389 this.save(); | 470 this.save(); |
| 390 break; | 471 break; |
| 391 } | 472 } |
| 392 } | 473 } |
| 393 }); | 474 }); |
| 475 | |
| 476 // Handle image clicks for resizing | |
| 477 this.content.addEventListener('click', (e) => { | |
| 478 if (e.target.tagName === 'IMG') { | |
| 479 e.preventDefault(); | |
| 480 this.selectImage(e.target); | |
| 481 } else if (!e.target.closest('.rich-editor-resize-wrapper')) { | |
| 482 this.deselectImage(); | |
| 483 } | |
| 484 }); | |
| 485 | |
| 486 // Deselect image when clicking outside | |
| 487 document.addEventListener('click', (e) => { | |
| 488 if (!this.content.contains(e.target)) { | |
| 489 this.deselectImage(); | |
| 490 } | |
| 491 }); | |
| 394 } | 492 } |
| 395 | 493 |
| 396 showDropOverlay() { | 494 showDropOverlay() { |
| 397 if (this.dropOverlay) return; | 495 if (this.dropOverlay) return; |
| 398 | 496 |
| 466 // Replace placeholder with actual content | 564 // Replace placeholder with actual content |
| 467 if (file.type.startsWith('image/')) { | 565 if (file.type.startsWith('image/')) { |
| 468 const img = document.createElement('img'); | 566 const img = document.createElement('img'); |
| 469 img.src = result.url; | 567 img.src = result.url; |
| 470 img.alt = file.name; | 568 img.alt = file.name; |
| 569 | |
| 570 // Store original dimensions when image loads | |
| 571 img.onload = () => { | |
| 572 img.dataset.originalWidth = img.naturalWidth; | |
| 573 img.dataset.originalHeight = img.naturalHeight; | |
| 574 }; | |
| 575 | |
| 471 placeholder.replaceWith(img); | 576 placeholder.replaceWith(img); |
| 472 } else { | 577 } else { |
| 473 const link = document.createElement('a'); | 578 const link = document.createElement('a'); |
| 474 link.href = result.url; | 579 link.href = result.url; |
| 475 link.textContent = file.name; | 580 link.textContent = file.name; |
| 634 } | 739 } |
| 635 | 740 |
| 636 focus() { | 741 focus() { |
| 637 this.content.focus(); | 742 this.content.focus(); |
| 638 } | 743 } |
| 744 | |
| 745 selectImage(img) { | |
| 746 if (this.state.readOnly) return; | |
| 747 | |
| 748 // Deselect previous image | |
| 749 this.deselectImage(); | |
| 750 | |
| 751 this.state.selectedImage = img; | |
| 752 img.classList.add('selected'); | |
| 753 | |
| 754 // Wrap image in resize container if not already wrapped | |
| 755 if (!img.parentElement.classList.contains('rich-editor-resize-wrapper')) { | |
| 756 const wrapper = document.createElement('div'); | |
| 757 wrapper.className = 'rich-editor-resize-wrapper'; | |
| 758 wrapper.contentEditable = false; | |
| 759 | |
| 760 // Store original dimensions if not set | |
| 761 if (!img.dataset.originalWidth) { | |
| 762 img.dataset.originalWidth = img.naturalWidth; | |
| 763 img.dataset.originalHeight = img.naturalHeight; | |
| 764 } | |
| 765 | |
| 766 img.parentNode.insertBefore(wrapper, img); | |
| 767 wrapper.appendChild(img); | |
| 768 } | |
| 769 | |
| 770 const wrapper = img.parentElement; | |
| 771 | |
| 772 // Add resize handles | |
| 773 const corners = ['nw', 'ne', 'sw', 'se']; | |
| 774 corners.forEach(corner => { | |
| 775 const handle = document.createElement('div'); | |
| 776 handle.className = `rich-editor-resize-handle ${corner}`; | |
| 777 handle.dataset.corner = corner; | |
| 778 | |
| 779 handle.addEventListener('mousedown', (e) => { | |
| 780 e.preventDefault(); | |
| 781 e.stopPropagation(); | |
| 782 this.startResize(e, img, corner); | |
| 783 }); | |
| 784 | |
| 785 wrapper.appendChild(handle); | |
| 786 }); | |
| 787 | |
| 788 // Add size input | |
| 789 const sizeInput = document.createElement('div'); | |
| 790 sizeInput.className = 'rich-editor-size-input'; | |
| 791 sizeInput.innerHTML = ` | |
| 792 <label>Width:</label> | |
| 793 <input type="number" class="width-input" value="${img.width}" min="50" max="2000"> | |
| 794 <label>px</label> | |
| 795 `; | |
| 796 | |
| 797 const widthInput = sizeInput.querySelector('.width-input'); | |
| 798 widthInput.addEventListener('input', (e) => { | |
| 799 const newWidth = parseInt(e.target.value); | |
| 800 if (newWidth && newWidth > 0) { | |
| 801 this.resizeImage(img, newWidth); | |
| 802 } | |
| 803 }); | |
| 804 | |
| 805 wrapper.appendChild(sizeInput); | |
| 806 } | |
| 807 | |
| 808 deselectImage() { | |
| 809 if (!this.state.selectedImage) return; | |
| 810 | |
| 811 const img = this.state.selectedImage; | |
| 812 img.classList.remove('selected'); | |
| 813 | |
| 814 const wrapper = img.parentElement; | |
| 815 if (wrapper && wrapper.classList.contains('rich-editor-resize-wrapper')) { | |
| 816 // Remove resize handles and size input | |
| 817 wrapper.querySelectorAll('.rich-editor-resize-handle, .rich-editor-size-input').forEach(el => el.remove()); | |
| 818 | |
| 819 // Unwrap image | |
| 820 wrapper.parentNode.insertBefore(img, wrapper); | |
| 821 wrapper.remove(); | |
| 822 } | |
| 823 | |
| 824 this.state.selectedImage = null; | |
| 825 } | |
| 826 | |
| 827 startResize(e, img, corner) { | |
| 828 this.state.resizing = true; | |
| 829 | |
| 830 const startX = e.clientX; | |
| 831 const startY = e.clientY; | |
| 832 const startWidth = img.width; | |
| 833 const startHeight = img.height; | |
| 834 const aspectRatio = startWidth / startHeight; | |
| 835 | |
| 836 const onMouseMove = (e) => { | |
| 837 if (!this.state.resizing) return; | |
| 838 | |
| 839 let deltaX = e.clientX - startX; | |
| 840 let deltaY = e.clientY - startY; | |
| 841 | |
| 842 // Adjust delta based on corner | |
| 843 if (corner === 'nw' || corner === 'sw') { | |
| 844 deltaX = -deltaX; | |
| 845 } | |
| 846 if (corner === 'nw' || corner === 'ne') { | |
| 847 deltaY = -deltaY; | |
| 848 } | |
| 849 | |
| 850 // Use the larger delta to maintain aspect ratio | |
| 851 const delta = Math.abs(deltaX) > Math.abs(deltaY) ? deltaX : deltaY; | |
| 852 const newWidth = Math.max(50, startWidth + delta); | |
| 853 | |
| 854 this.resizeImage(img, newWidth, aspectRatio); | |
| 855 }; | |
| 856 | |
| 857 const onMouseUp = () => { | |
| 858 this.state.resizing = false; | |
| 859 document.removeEventListener('mousemove', onMouseMove); | |
| 860 document.removeEventListener('mouseup', onMouseUp); | |
| 861 | |
| 862 // Update width input | |
| 863 const wrapper = img.parentElement; | |
| 864 const widthInput = wrapper.querySelector('.width-input'); | |
| 865 if (widthInput) { | |
| 866 widthInput.value = img.width; | |
| 867 } | |
| 868 | |
| 869 if (this.debouncedSave) this.debouncedSave(); | |
| 870 }; | |
| 871 | |
| 872 document.addEventListener('mousemove', onMouseMove); | |
| 873 document.addEventListener('mouseup', onMouseUp); | |
| 874 } | |
| 875 | |
| 876 resizeImage(img, newWidth, aspectRatio = null) { | |
| 877 if (!aspectRatio) { | |
| 878 const originalWidth = parseInt(img.dataset.originalWidth) || img.naturalWidth; | |
| 879 const originalHeight = parseInt(img.dataset.originalHeight) || img.naturalHeight; | |
| 880 aspectRatio = originalWidth / originalHeight; | |
| 881 } | |
| 882 | |
| 883 const newHeight = newWidth / aspectRatio; | |
| 884 | |
| 885 img.width = newWidth; | |
| 886 img.height = newHeight; | |
| 887 img.style.width = newWidth + 'px'; | |
| 888 img.style.height = newHeight + 'px'; | |
| 889 } | |
| 639 } | 890 } |
| 640 | 891 |
| 641 return { | 892 return { |
| 642 init: function(elementId, options) { | 893 init: function(elementId, options) { |
| 643 return new Editor(elementId, options); | 894 return new Editor(elementId, options); |