Mercurial
comparison rich_editor/rich_editor.js @ 208:5d3e116dd745
[MrJuneJune] made it more mobile friendly.
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Sun, 15 Feb 2026 12:33:54 -0800 |
| parents | 240337164a80 |
| children |
comparison
equal
deleted
inserted
replaced
| 207:58d9b64d8dca | 208:5d3e116dd745 |
|---|---|
| 85 }); | 85 }); |
| 86 | 86 |
| 87 return toolbar; | 87 return toolbar; |
| 88 } | 88 } |
| 89 | 89 |
| 90 function ensureViewportMeta() { | |
| 91 // Check if viewport meta tag exists | |
| 92 let viewport = document.querySelector('meta[name="viewport"]'); | |
| 93 if (!viewport) { | |
| 94 viewport = document.createElement('meta'); | |
| 95 viewport.name = 'viewport'; | |
| 96 viewport.content = 'width=device-width, initial-scale=1.0, maximum-scale=5.0'; | |
| 97 document.head.appendChild(viewport); | |
| 98 } | |
| 99 } | |
| 100 | |
| 90 function createStyles() { | 101 function createStyles() { |
| 91 if (document.getElementById('rich-editor-styles')) return; | 102 if (document.getElementById('rich-editor-styles')) return; |
| 103 | |
| 104 ensureViewportMeta(); | |
| 92 | 105 |
| 93 const style = document.createElement('style'); | 106 const style = document.createElement('style'); |
| 94 style.id = 'rich-editor-styles'; | 107 style.id = 'rich-editor-styles'; |
| 95 style.textContent = ` | 108 style.textContent = ` |
| 96 .rich-editor-container { | 109 .rich-editor-container { |
| 292 justify-content: center; | 305 justify-content: center; |
| 293 font-size: 18px; | 306 font-size: 18px; |
| 294 color: #0078ff; | 307 color: #0078ff; |
| 295 pointer-events: none; | 308 pointer-events: none; |
| 296 z-index: 10; | 309 z-index: 10; |
| 310 padding: 20px; | |
| 311 text-align: center; | |
| 297 } | 312 } |
| 298 | 313 |
| 299 .rich-editor-status { | 314 .rich-editor-status { |
| 300 padding: 4px 8px; | 315 padding: 4px 8px; |
| 301 font-size: 12px; | 316 font-size: 12px; |
| 308 display: none; | 323 display: none; |
| 309 } | 324 } |
| 310 | 325 |
| 311 ol,ul { | 326 ol,ul { |
| 312 padding: 16px; | 327 padding: 16px; |
| 328 } | |
| 329 | |
| 330 /* Mobile optimizations */ | |
| 331 @media (max-width: 720px) { | |
| 332 .rich-editor-toolbar { | |
| 333 padding: 6px; | |
| 334 gap: 3px; | |
| 335 overflow-x: auto; | |
| 336 -webkit-overflow-scrolling: touch; | |
| 337 } | |
| 338 | |
| 339 .rich-editor-btn { | |
| 340 min-width: 44px; | |
| 341 min-height: 44px; | |
| 342 padding: 8px; | |
| 343 flex-shrink: 0; | |
| 344 } | |
| 345 | |
| 346 .rich-editor-content { | |
| 347 min-height: 200px; | |
| 348 padding: 12px; | |
| 349 font-size: 16px; /* Prevents iOS zoom */ | |
| 350 line-height: 1.6; | |
| 351 } | |
| 352 | |
| 353 .rich-editor-content h1 { | |
| 354 font-size: 1.75em; | |
| 355 } | |
| 356 | |
| 357 .rich-editor-content h2 { | |
| 358 font-size: 1.5em; | |
| 359 } | |
| 360 | |
| 361 .rich-editor-content h3 { | |
| 362 font-size: 1.25em; | |
| 363 } | |
| 364 | |
| 365 .rich-editor-status { | |
| 366 font-size: 11px; | |
| 367 padding: 6px 8px; | |
| 368 } | |
| 369 | |
| 370 /* Mobile-friendly image resizing */ | |
| 371 .rich-editor-resize-handle { | |
| 372 width: 20px; | |
| 373 height: 20px; | |
| 374 border-width: 3px; | |
| 375 } | |
| 376 | |
| 377 .rich-editor-resize-handle.nw { | |
| 378 top: -10px; | |
| 379 left: -10px; | |
| 380 } | |
| 381 | |
| 382 .rich-editor-resize-handle.ne { | |
| 383 top: -10px; | |
| 384 right: -10px; | |
| 385 } | |
| 386 | |
| 387 .rich-editor-resize-handle.sw { | |
| 388 bottom: -10px; | |
| 389 left: -10px; | |
| 390 } | |
| 391 | |
| 392 .rich-editor-resize-handle.se { | |
| 393 bottom: -10px; | |
| 394 right: -10px; | |
| 395 } | |
| 396 | |
| 397 .rich-editor-size-input { | |
| 398 top: -40px; | |
| 399 padding: 6px 10px; | |
| 400 } | |
| 401 | |
| 402 .rich-editor-size-input input { | |
| 403 width: 70px; | |
| 404 padding: 4px 6px; | |
| 405 font-size: 16px; /* Prevents iOS zoom */ | |
| 406 } | |
| 407 | |
| 408 /* Better touch targets for images */ | |
| 409 .rich-editor-content img { | |
| 410 margin: 12px 0; | |
| 411 } | |
| 412 | |
| 413 /* Hide drop overlay text on small screens */ | |
| 414 .rich-editor-upload-overlay { | |
| 415 font-size: 16px; | |
| 416 } | |
| 417 | |
| 418 /* Make link/note dialogs mobile-friendly */ | |
| 419 .rich-editor-content a { | |
| 420 padding: 2px 0; | |
| 421 } | |
| 422 } | |
| 423 | |
| 424 /* Extra small devices */ | |
| 425 @media (max-width: 480px) { | |
| 426 .rich-editor-toolbar { | |
| 427 padding: 4px; | |
| 428 gap: 2px; | |
| 429 } | |
| 430 | |
| 431 .rich-editor-btn { | |
| 432 min-width: 40px; | |
| 433 min-height: 40px; | |
| 434 padding: 6px; | |
| 435 } | |
| 436 | |
| 437 .rich-editor-content { | |
| 438 padding: 10px; | |
| 439 font-size: 16px; | |
| 440 } | |
| 441 | |
| 442 .rich-editor-size-input { | |
| 443 font-size: 11px; | |
| 444 } | |
| 445 | |
| 446 .rich-editor-size-input input { | |
| 447 width: 60px; | |
| 448 font-size: 14px; | |
| 449 } | |
| 313 } | 450 } |
| 314 `; | 451 `; |
| 315 document.head.appendChild(style); | 452 document.head.appendChild(style); |
| 316 } | 453 } |
| 317 | 454 |
| 774 corners.forEach(corner => { | 911 corners.forEach(corner => { |
| 775 const handle = document.createElement('div'); | 912 const handle = document.createElement('div'); |
| 776 handle.className = `rich-editor-resize-handle ${corner}`; | 913 handle.className = `rich-editor-resize-handle ${corner}`; |
| 777 handle.dataset.corner = corner; | 914 handle.dataset.corner = corner; |
| 778 | 915 |
| 916 // Mouse events | |
| 779 handle.addEventListener('mousedown', (e) => { | 917 handle.addEventListener('mousedown', (e) => { |
| 780 e.preventDefault(); | 918 e.preventDefault(); |
| 781 e.stopPropagation(); | 919 e.stopPropagation(); |
| 782 this.startResize(e, img, corner); | 920 this.startResize(e, img, corner); |
| 921 }); | |
| 922 | |
| 923 // Touch events for mobile | |
| 924 handle.addEventListener('touchstart', (e) => { | |
| 925 e.preventDefault(); | |
| 926 e.stopPropagation(); | |
| 927 const touch = e.touches[0]; | |
| 928 this.startResize(touch, img, corner, true); | |
| 783 }); | 929 }); |
| 784 | 930 |
| 785 wrapper.appendChild(handle); | 931 wrapper.appendChild(handle); |
| 786 }); | 932 }); |
| 787 | 933 |
| 822 } | 968 } |
| 823 | 969 |
| 824 this.state.selectedImage = null; | 970 this.state.selectedImage = null; |
| 825 } | 971 } |
| 826 | 972 |
| 827 startResize(e, img, corner) { | 973 startResize(e, img, corner, isTouch = false) { |
| 828 this.state.resizing = true; | 974 this.state.resizing = true; |
| 829 | 975 |
| 830 const startX = e.clientX; | 976 const startX = e.clientX; |
| 831 const startY = e.clientY; | 977 const startY = e.clientY; |
| 832 const startWidth = img.width; | 978 const startWidth = img.width; |
| 833 const startHeight = img.height; | 979 const startHeight = img.height; |
| 834 const aspectRatio = startWidth / startHeight; | 980 const aspectRatio = startWidth / startHeight; |
| 835 | 981 |
| 836 const onMouseMove = (e) => { | 982 const onMove = (e) => { |
| 837 if (!this.state.resizing) return; | 983 if (!this.state.resizing) return; |
| 838 | 984 |
| 839 let deltaX = e.clientX - startX; | 985 const clientX = isTouch ? e.touches[0].clientX : e.clientX; |
| 840 let deltaY = e.clientY - startY; | 986 const clientY = isTouch ? e.touches[0].clientY : e.clientY; |
| 987 | |
| 988 let deltaX = clientX - startX; | |
| 989 let deltaY = clientY - startY; | |
| 841 | 990 |
| 842 // Adjust delta based on corner | 991 // Adjust delta based on corner |
| 843 if (corner === 'nw' || corner === 'sw') { | 992 if (corner === 'nw' || corner === 'sw') { |
| 844 deltaX = -deltaX; | 993 deltaX = -deltaX; |
| 845 } | 994 } |
| 852 const newWidth = Math.max(50, startWidth + delta); | 1001 const newWidth = Math.max(50, startWidth + delta); |
| 853 | 1002 |
| 854 this.resizeImage(img, newWidth, aspectRatio); | 1003 this.resizeImage(img, newWidth, aspectRatio); |
| 855 }; | 1004 }; |
| 856 | 1005 |
| 857 const onMouseUp = () => { | 1006 const onEnd = () => { |
| 858 this.state.resizing = false; | 1007 this.state.resizing = false; |
| 859 document.removeEventListener('mousemove', onMouseMove); | 1008 |
| 860 document.removeEventListener('mouseup', onMouseUp); | 1009 if (isTouch) { |
| 1010 document.removeEventListener('touchmove', onMove); | |
| 1011 document.removeEventListener('touchend', onEnd); | |
| 1012 } else { | |
| 1013 document.removeEventListener('mousemove', onMove); | |
| 1014 document.removeEventListener('mouseup', onEnd); | |
| 1015 } | |
| 861 | 1016 |
| 862 // Update width input | 1017 // Update width input |
| 863 const wrapper = img.parentElement; | 1018 const wrapper = img.parentElement; |
| 864 const widthInput = wrapper.querySelector('.width-input'); | 1019 const widthInput = wrapper.querySelector('.width-input'); |
| 865 if (widthInput) { | 1020 if (widthInput) { |
| 867 } | 1022 } |
| 868 | 1023 |
| 869 if (this.debouncedSave) this.debouncedSave(); | 1024 if (this.debouncedSave) this.debouncedSave(); |
| 870 }; | 1025 }; |
| 871 | 1026 |
| 872 document.addEventListener('mousemove', onMouseMove); | 1027 if (isTouch) { |
| 873 document.addEventListener('mouseup', onMouseUp); | 1028 document.addEventListener('touchmove', onMove, { passive: false }); |
| 1029 document.addEventListener('touchend', onEnd); | |
| 1030 } else { | |
| 1031 document.addEventListener('mousemove', onMove); | |
| 1032 document.addEventListener('mouseup', onEnd); | |
| 1033 } | |
| 874 } | 1034 } |
| 875 | 1035 |
| 876 resizeImage(img, newWidth, aspectRatio = null) { | 1036 resizeImage(img, newWidth, aspectRatio = null) { |
| 877 if (!aspectRatio) { | 1037 if (!aspectRatio) { |
| 878 const originalWidth = parseInt(img.dataset.originalWidth) || img.naturalWidth; | 1038 const originalWidth = parseInt(img.dataset.originalWidth) || img.naturalWidth; |