Last active
December 15, 2025 09:42
-
-
Save ffkev/4ed5afc282a8bf886d9565008774643b to your computer and use it in GitHub Desktop.
Changes included in the HTML Editor to support Mulish font.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ====================================================================================================== | |
| Use the latest test_quill.html as a reference for the full implementation | |
| Create a folder under web/ as web/fonts/ and include all the Mulish fonts for offline access from | |
| the assets/ folder in the Flutter project | |
| ====================================================================================================== | |
| Refer line 351 | |
| // Register Mulish font with Quill | |
| const Font = Quill.import('formats/font'); | |
| Font.whitelist = ['sans-serif', 'serif', 'monospace', 'Mulish']; | |
| Quill.register(Font, true); | |
| ====================================================================================================== | |
| Refer line 494 | |
| // dropdown with defaults from theme | |
| [{ 'font': ['sans-serif', 'serif', 'monospace', 'Mulish'] }], | |
| ====================================================================================================== | |
| Refer line 20 | |
| /* Mulish Font Family */ | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-Regular.ttf') format('truetype'); | |
| font-weight: 400; | |
| font-style: normal; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-Italic.ttf') format('truetype'); | |
| font-weight: 400; | |
| font-style: italic; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-Light.ttf') format('truetype'); | |
| font-weight: 300; | |
| font-style: normal; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-LightItalic.ttf') format('truetype'); | |
| font-weight: 300; | |
| font-style: italic; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-Medium.ttf') format('truetype'); | |
| font-weight: 500; | |
| font-style: normal; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-MediumItalic.ttf') format('truetype'); | |
| font-weight: 500; | |
| font-style: italic; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-SemiBold.ttf') format('truetype'); | |
| font-weight: 600; | |
| font-style: normal; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-SemiBoldItalic.ttf') format('truetype'); | |
| font-weight: 600; | |
| font-style: italic; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-Bold.ttf') format('truetype'); | |
| font-weight: 700; | |
| font-style: normal; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-BoldItalic.ttf') format('truetype'); | |
| font-weight: 700; | |
| font-style: italic; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-ExtraBold.ttf') format('truetype'); | |
| font-weight: 800; | |
| font-style: normal; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-ExtraBoldItalic.ttf') format('truetype'); | |
| font-weight: 800; | |
| font-style: italic; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-Black.ttf') format('truetype'); | |
| font-weight: 900; | |
| font-style: normal; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-BlackItalic.ttf') format('truetype'); | |
| font-weight: 900; | |
| font-style: italic; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-ExtraLight.ttf') format('truetype'); | |
| font-weight: 200; | |
| font-style: normal; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-ExtraLightItalic.ttf') format('truetype'); | |
| font-weight: 200; | |
| font-style: italic; | |
| } | |
| ====================================================================================================== | |
| Refer line 331 | |
| /* Mulish font styling for Quill - must be specific to override Quill defaults */ | |
| /* Target all possible Quill inline elements with Mulish font class */ | |
| .ql-editor .ql-font-Mulish, | |
| .ql-font-Mulish, | |
| .ql-editor span.ql-font-Mulish, | |
| span.ql-font-Mulish, | |
| .ql-editor .ql-font-Mulish *, | |
| .ql-font-Mulish *, | |
| .ql-container .ql-editor .ql-font-Mulish, | |
| .ql-container .ql-editor .ql-font-Mulish p, | |
| .ql-container .ql-editor .ql-font-Mulish span, | |
| .ql-container .ql-editor .ql-font-Mulish div, | |
| .ql-snow .ql-editor .ql-font-Mulish, | |
| .ql-snow .ql-editor span.ql-font-Mulish { | |
| font-family: 'Mulish', sans-serif !important; | |
| } | |
| ====================================================================================================== | |
| /* Additional rule to ensure font applies to text nodes */ | Refer line 348 | |
| .ql-editor [class*="ql-font-Mulish"] { | |
| font-family: 'Mulish', sans-serif !important; | |
| } | |
| ====================================================================================================== | |
| /* Ensure font dropdown displays Mulish correctly */ | Refer line 336 | |
| .ql-picker.ql-font .ql-picker-item[data-value="Mulish"]::before, | |
| .ql-picker.ql-font .ql-picker-label[data-value="Mulish"]::before { | |
| content: 'Mulish'; | |
| font-family: 'Mulish', sans-serif; | |
| } | |
| /* Override default Quill font classes to use Mulish as fallback */ | |
| .ql-font-sans-serif { | |
| font-family: 'Mulish', sans-serif; | |
| } | |
| /* Ensure all font options in dropdown use proper font rendering */ | |
| .ql-picker.ql-font .ql-picker-item { | |
| font-size: 14px; | |
| } | |
| ====================================================================================================== |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ==================================================================================================== | |
| Include all references to the Mulish font as per the previous implementations in the exported htmlString | |
| ==================================================================================================== | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <link href="https://cdn.jsdelivr.net/npm/quill@2/dist/quill.snow.css" rel="stylesheet"> | |
| <link href="https://cdn.jsdelivr.net/npm/quill-table-better@1/dist/quill-table-better.css" rel="stylesheet"> | |
| <style> | |
| table, | |
| td, | |
| th { | |
| border: 1px solid black; | |
| border-collapse: collapse; | |
| } | |
| .ql-editor ol li { | |
| list-style-type: decimal; | |
| padding-left: 0; | |
| } | |
| .ql-editor ul li { | |
| list-style-type: disc; | |
| padding-left: 0; | |
| margin: 0; | |
| } | |
| .ql-editor ul { | |
| margin: 0; | |
| } | |
| table tbody tr td p { | |
| margin: 0; | |
| padding: 0; | |
| } | |
| p { | |
| margin: 0; | |
| padding: 0; | |
| } | |
| /* Mulish Font Family */ | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-Regular.ttf') format('truetype'); | |
| font-weight: 400; | |
| font-style: normal; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-Italic.ttf') format('truetype'); | |
| font-weight: 400; | |
| font-style: italic; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-Light.ttf') format('truetype'); | |
| font-weight: 300; | |
| font-style: normal; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-LightItalic.ttf') format('truetype'); | |
| font-weight: 300; | |
| font-style: italic; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-Medium.ttf') format('truetype'); | |
| font-weight: 500; | |
| font-style: normal; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-MediumItalic.ttf') format('truetype'); | |
| font-weight: 500; | |
| font-style: italic; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-SemiBold.ttf') format('truetype'); | |
| font-weight: 600; | |
| font-style: normal; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-SemiBoldItalic.ttf') format('truetype'); | |
| font-weight: 600; | |
| font-style: italic; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-Bold.ttf') format('truetype'); | |
| font-weight: 700; | |
| font-style: normal; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-BoldItalic.ttf') format('truetype'); | |
| font-weight: 700; | |
| font-style: italic; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-ExtraBold.ttf') format('truetype'); | |
| font-weight: 800; | |
| font-style: normal; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-ExtraBoldItalic.ttf') format('truetype'); | |
| font-weight: 800; | |
| font-style: italic; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-Black.ttf') format('truetype'); | |
| font-weight: 900; | |
| font-style: normal; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-BlackItalic.ttf') format('truetype'); | |
| font-weight: 900; | |
| font-style: italic; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-ExtraLight.ttf') format('truetype'); | |
| font-weight: 200; | |
| font-style: normal; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-ExtraLightItalic.ttf') format('truetype'); | |
| font-weight: 200; | |
| font-style: italic; | |
| } | |
| /* Mulish font styling for Quill - must be specific to override Quill defaults */ | |
| /* Target all possible Quill inline elements with Mulish font class */ | |
| .ql-editor .ql-font-Mulish, | |
| .ql-font-Mulish, | |
| .ql-editor span.ql-font-Mulish, | |
| span.ql-font-Mulish, | |
| .ql-editor .ql-font-Mulish *, | |
| .ql-font-Mulish *, | |
| .ql-container .ql-editor .ql-font-Mulish, | |
| .ql-container .ql-editor .ql-font-Mulish p, | |
| .ql-container .ql-editor .ql-font-Mulish span, | |
| .ql-container .ql-editor .ql-font-Mulish div, | |
| .ql-snow .ql-editor .ql-font-Mulish, | |
| .ql-snow .ql-editor span.ql-font-Mulish { | |
| font-family: 'Mulish', sans-serif !important; | |
| } | |
| /* Additional rule to ensure font applies to text nodes */ | |
| .ql-editor [class*="ql-font-Mulish"] { | |
| font-family: 'Mulish', sans-serif !important; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="ql-editor"> | |
| <p><span class="ql-font-sans-serif">asdasdsa </span></p> | |
| <p><span class="ql-font-monospace">asdlsakjdhksj</span></p> | |
| <p><span class="ql-font-Mulish">dfsljfoilksdjflk</span></p> | |
| </div> | |
| </body> | |
| </html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Quill Editor Test - Table Support</title> | |
| <!-- Quill.js CSS --> | |
| <link href="https://cdn.jsdelivr.net/npm/quill@2/dist/quill.snow.css" rel="stylesheet"> | |
| <!-- Quill Better Table CSS --> | |
| <link href="https://cdn.jsdelivr.net/npm/quill-table-better@1/dist/quill-table-better.css" rel="stylesheet"> | |
| <style> | |
| /* Mulish Font Family */ | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-Regular.ttf') format('truetype'); | |
| font-weight: 400; | |
| font-style: normal; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-Italic.ttf') format('truetype'); | |
| font-weight: 400; | |
| font-style: italic; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-Light.ttf') format('truetype'); | |
| font-weight: 300; | |
| font-style: normal; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-LightItalic.ttf') format('truetype'); | |
| font-weight: 300; | |
| font-style: italic; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-Medium.ttf') format('truetype'); | |
| font-weight: 500; | |
| font-style: normal; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-MediumItalic.ttf') format('truetype'); | |
| font-weight: 500; | |
| font-style: italic; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-SemiBold.ttf') format('truetype'); | |
| font-weight: 600; | |
| font-style: normal; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-SemiBoldItalic.ttf') format('truetype'); | |
| font-weight: 600; | |
| font-style: italic; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-Bold.ttf') format('truetype'); | |
| font-weight: 700; | |
| font-style: normal; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-BoldItalic.ttf') format('truetype'); | |
| font-weight: 700; | |
| font-style: italic; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-ExtraBold.ttf') format('truetype'); | |
| font-weight: 800; | |
| font-style: normal; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-ExtraBoldItalic.ttf') format('truetype'); | |
| font-weight: 800; | |
| font-style: italic; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-Black.ttf') format('truetype'); | |
| font-weight: 900; | |
| font-style: normal; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-BlackItalic.ttf') format('truetype'); | |
| font-weight: 900; | |
| font-style: italic; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-ExtraLight.ttf') format('truetype'); | |
| font-weight: 200; | |
| font-style: normal; | |
| } | |
| @font-face { | |
| font-family: 'Mulish'; | |
| src: url('fonts/Mulish-ExtraLightItalic.ttf') format('truetype'); | |
| font-weight: 200; | |
| font-style: italic; | |
| } | |
| body { | |
| font-family: Arial, sans-serif; | |
| margin: 0; | |
| padding: 0px; | |
| background-color: transparent; | |
| } | |
| .ql-container.ql-snow { | |
| height: 400px; | |
| } | |
| .container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| background: white; | |
| border-radius: 8px; | |
| box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); | |
| overflow: hidden; | |
| } | |
| .header { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| padding: 20px; | |
| text-align: center; | |
| } | |
| .header h1 { | |
| margin: 0; | |
| font-size: 2em; | |
| } | |
| .header p { | |
| margin: 10px 0 0 0; | |
| opacity: 0.9; | |
| } | |
| .toolbar-custom { | |
| background: #f8f9fa; | |
| border-bottom: 1px solid #e9ecef; | |
| padding: 10px 20px; | |
| display: flex; | |
| gap: 10px; | |
| align-items: center; | |
| } | |
| .btn { | |
| background: #007bff; | |
| color: white; | |
| border: none; | |
| padding: 8px 16px; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| font-size: 14px; | |
| transition: background-color 0.2s; | |
| } | |
| .btn:hover { | |
| background: #0056b3; | |
| } | |
| .btn.secondary { | |
| background: #6c757d; | |
| } | |
| .btn.secondary:hover { | |
| background: #545b62; | |
| } | |
| .btn.success { | |
| background: #28a745; | |
| } | |
| .btn.success:hover { | |
| background: #1e7e34; | |
| } | |
| .editor-container { | |
| padding: 20px; | |
| min-height: 400px; | |
| } | |
| .content-preview { | |
| margin-top: 20px; | |
| padding: 20px; | |
| background: #f8f9fa; | |
| border-radius: 4px; | |
| border-left: 4px solid #007bff; | |
| } | |
| .content-preview h3 { | |
| margin-top: 0; | |
| color: #495057; | |
| } | |
| .content-preview pre { | |
| background: white; | |
| padding: 15px; | |
| border-radius: 4px; | |
| border: 1px solid #dee2e6; | |
| overflow-x: auto; | |
| white-space: pre-wrap; | |
| word-wrap: break-word; | |
| } | |
| .stats { | |
| display: flex; | |
| gap: 20px; | |
| margin-top: 10px; | |
| font-size: 14px; | |
| color: #6c757d; | |
| } | |
| .feature-list { | |
| background: #e3f2fd; | |
| padding: 15px; | |
| border-radius: 4px; | |
| margin-bottom: 20px; | |
| } | |
| .feature-list h4 { | |
| margin-top: 0; | |
| color: #1976d2; | |
| } | |
| .feature-list ul { | |
| margin: 10px 0; | |
| padding-left: 20px; | |
| } | |
| .feature-list li { | |
| margin: 5px 0; | |
| } | |
| .table-demo { | |
| background: #fff3cd; | |
| padding: 15px; | |
| border-radius: 4px; | |
| margin-bottom: 20px; | |
| border-left: 4px solid #ffc107; | |
| } | |
| .table-demo h4 { | |
| margin-top: 0; | |
| color: #856404; | |
| } | |
| /* Format Painter button styling */ | |
| .ql-format-painter { | |
| position: relative; | |
| } | |
| .ql-format-painter.ql-active { | |
| background-color: #e3f2fd !important; | |
| } | |
| .ql-format-painter.ql-active::after { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background-color: rgba(33, 150, 243, 0.2); | |
| border-radius: 2px; | |
| pointer-events: none; | |
| } | |
| /* Format Painter icon (paintbrush SVG) */ | |
| .ql-format-painter svg { | |
| display: inline-block; | |
| vertical-align: middle; | |
| width: 18px; | |
| height: 18px; | |
| } | |
| .ql-format-painter::before { | |
| content: ''; | |
| } | |
| /* Image resize functionality */ | |
| .ql-editor img.ql-image-selected { | |
| outline: 2px solid #007bff !important; | |
| outline-offset: 2px; | |
| cursor: move; | |
| position: relative; | |
| } | |
| .ql-image-resize-handle { | |
| position: fixed; | |
| width: 14px; | |
| height: 14px; | |
| background: #007bff; | |
| border: 2px solid white; | |
| border-radius: 50%; | |
| cursor: nwse-resize; | |
| display: none; | |
| z-index: 10000; | |
| box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); | |
| transition: transform 0.2s, background 0.2s; | |
| pointer-events: auto; | |
| } | |
| .ql-image-resize-handle:hover { | |
| transform: scale(1.3); | |
| background: #0056b3; | |
| box-shadow: 0 3px 8px rgba(0, 0, 0, 0.4); | |
| } | |
| .ql-editor { | |
| position: relative; | |
| } | |
| /* Mulish font styling for Quill - must be specific to override Quill defaults */ | |
| /* Target all possible Quill inline elements with Mulish font class */ | |
| .ql-editor .ql-font-Mulish, | |
| .ql-font-Mulish, | |
| .ql-editor span.ql-font-Mulish, | |
| span.ql-font-Mulish, | |
| .ql-editor .ql-font-Mulish *, | |
| .ql-font-Mulish *, | |
| .ql-container .ql-editor .ql-font-Mulish, | |
| .ql-container .ql-editor .ql-font-Mulish p, | |
| .ql-container .ql-editor .ql-font-Mulish span, | |
| .ql-container .ql-editor .ql-font-Mulish div, | |
| .ql-snow .ql-editor .ql-font-Mulish, | |
| .ql-snow .ql-editor span.ql-font-Mulish { | |
| font-family: 'Mulish', sans-serif !important; | |
| } | |
| /* Additional rule to ensure font applies to text nodes */ | |
| .ql-editor [class*="ql-font-Mulish"] { | |
| font-family: 'Mulish', sans-serif !important; | |
| } | |
| /* Ensure font dropdown displays Mulish correctly */ | |
| .ql-picker.ql-font .ql-picker-item[data-value="Mulish"]::before, | |
| .ql-picker.ql-font .ql-picker-label[data-value="Mulish"]::before { | |
| content: 'Mulish'; | |
| font-family: 'Mulish', sans-serif; | |
| } | |
| /* Ensure all font options in dropdown use proper font rendering */ | |
| .ql-picker.ql-font .ql-picker-item { | |
| font-size: 14px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="root"></div> | |
| <br /> | |
| <script src="https://cdn.jsdelivr.net/npm/quill@2/dist/quill.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/quill-table-better@1/dist/quill-table-better.js"></script> | |
| <script> | |
| Quill.register({ | |
| 'modules/table-better': QuillTableBetter | |
| }, true); | |
| // Register Mulish font with Quill | |
| const Font = Quill.import('formats/font'); | |
| Font.whitelist = ['sans-serif', 'serif', 'monospace', 'Mulish']; | |
| Quill.register(Font, true); | |
| // Verify font registration | |
| console.log('Registered fonts:', Font.whitelist); | |
| // Format Painter custom handler | |
| const FormatPainter = { | |
| storedFormat: null, | |
| isActive: false, | |
| copyFormat() { | |
| const selection = editor.getSelection(true); | |
| if (!selection) { | |
| return false; | |
| } | |
| // Get the format at the selection | |
| const format = editor.getFormat(selection); | |
| // Store the format (filter out attributes we don't want to copy) | |
| this.storedFormat = {}; | |
| const attributesToCopy = [ | |
| 'bold', 'italic', 'underline', 'strike', | |
| 'color', 'background', | |
| 'font', 'size', | |
| 'script', 'link', | |
| 'header', 'align', | |
| 'blockquote', 'code-block', | |
| 'list', 'indent', 'direction' | |
| ]; | |
| attributesToCopy.forEach(attr => { | |
| if (format[attr] !== undefined && format[attr] !== null && format[attr] !== '') { | |
| this.storedFormat[attr] = format[attr]; | |
| } | |
| }); | |
| // If we have any format to copy, activate | |
| if (Object.keys(this.storedFormat).length > 0) { | |
| this.isActive = true; | |
| this.updateButtonState(); | |
| return true; | |
| } | |
| return false; | |
| }, | |
| applyFormat() { | |
| if (!this.storedFormat || !this.isActive) { | |
| return false; | |
| } | |
| const selection = editor.getSelection(true); | |
| if (!selection || selection.length === 0) { | |
| return false; | |
| } | |
| // Apply the stored format to the current selection | |
| Object.keys(this.storedFormat).forEach(attr => { | |
| const value = this.storedFormat[attr]; | |
| editor.formatText(selection.index, selection.length, attr, value, Quill.sources.USER); | |
| }); | |
| // Deactivate after applying (single-use mode) | |
| this.isActive = false; | |
| this.updateButtonState(); | |
| return true; | |
| }, | |
| updateButtonState() { | |
| setTimeout(() => { | |
| const button = document.querySelector('.ql-format-painter'); | |
| if (button) { | |
| if (this.isActive) { | |
| button.classList.add('ql-active'); | |
| button.title = 'Format Painter Active - Select text and click to apply format'; | |
| } else { | |
| button.classList.remove('ql-active'); | |
| button.title = 'Format Painter - Select formatted text and click to copy format'; | |
| } | |
| } | |
| }, 50); | |
| }, | |
| handleClick() { | |
| const selection = editor.getSelection(true); | |
| if (this.isActive) { | |
| // Format painter is active - apply format to current selection | |
| if (selection && selection.length > 0) { | |
| this.applyFormat(); | |
| } else { | |
| // No selection, deactivate | |
| this.isActive = false; | |
| this.updateButtonState(); | |
| } | |
| } else { | |
| // Format painter is not active - copy format from current selection | |
| if (selection && selection.length > 0) { | |
| this.copyFormat(); | |
| } else if (selection) { | |
| // No text selected, try to get format at cursor position | |
| const format = editor.getFormat(selection); | |
| if (Object.keys(format).length > 0) { | |
| this.storedFormat = {}; | |
| const attributesToCopy = [ | |
| 'bold', 'italic', 'underline', 'strike', | |
| 'color', 'background', 'font', 'size', | |
| 'script', 'link', 'header', 'align', | |
| 'blockquote', 'code-block', 'list', 'indent', 'direction' | |
| ]; | |
| attributesToCopy.forEach(attr => { | |
| if (format[attr] !== undefined && format[attr] !== null && format[attr] !== '') { | |
| this.storedFormat[attr] = format[attr]; | |
| } | |
| }); | |
| if (Object.keys(this.storedFormat).length > 0) { | |
| this.isActive = true; | |
| this.updateButtonState(); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| }; | |
| const toolbarOptions = [ | |
| ['bold', 'italic', 'underline', 'strike'], // toggled buttons | |
| ['format-painter'], // format painter button | |
| ['blockquote', 'code-block'], | |
| ['link', 'image', 'video', 'formula'], | |
| [{ 'header': 1 }, { 'header': 2 }], // custom button values | |
| [{ 'list': 'ordered' }, { 'list': 'bullet' }, { 'list': 'check' }], | |
| [{ 'script': 'sub' }, { 'script': 'super' }], // superscript/subscript | |
| [{ 'indent': '-1' }, { 'indent': '+1' }], // outdent/indent | |
| [{ 'direction': 'rtl' }], // text direction | |
| [{ 'size': ['small', false, 'large', 'huge'] }], // custom dropdown | |
| [{ 'header': [1, 2, 3, 4, 5, 6, false] }], | |
| [{ 'color': [] }, { 'background': [] }], // dropdown with defaults from theme | |
| [{ 'font': ['sans-serif', 'serif', 'monospace', 'Mulish'] }], | |
| [{ 'align': [] }], | |
| ['clean'], // remove formatting button | |
| ['table-better'] | |
| ]; | |
| const options = { | |
| theme: 'snow', | |
| modules: { | |
| toolbar: { | |
| container: toolbarOptions, | |
| handlers: { | |
| 'format-painter': function () { | |
| FormatPainter.handleClick(); | |
| } | |
| } | |
| }, | |
| table: false, | |
| 'table-better': { | |
| toolbarTable: true, | |
| menus: ['column', 'row', 'merge', 'table', 'cell', 'wrap', 'copy', 'delete'], | |
| }, | |
| keyboard: { | |
| bindings: QuillTableBetter.keyboardBindings | |
| } | |
| } | |
| }; | |
| const editor = new Quill('#root', options); | |
| // Add tooltips to all toolbar items | |
| function addTooltipsToToolbar() { | |
| const toolbar = editor.getModule('toolbar'); | |
| const toolbarElement = toolbar.container; | |
| // Function to add tooltip to an element | |
| function setTooltip(element, text) { | |
| if (element && !element.title) { | |
| element.title = text; | |
| } | |
| } | |
| // Add tooltips after a short delay to ensure DOM is ready | |
| setTimeout(() => { | |
| // Simple buttons with direct tooltips | |
| const buttonTooltips = { | |
| 'ql-bold': 'Bold (Ctrl+B)', | |
| 'ql-italic': 'Italic (Ctrl+I)', | |
| 'ql-underline': 'Underline (Ctrl+U)', | |
| 'ql-strike': 'Strikethrough', | |
| 'ql-format-painter': 'Format Painter', | |
| 'ql-blockquote': 'Blockquote', | |
| 'ql-code-block': 'Code Block', | |
| 'ql-link': 'Insert Link', | |
| 'ql-image': 'Insert Image', | |
| 'ql-video': 'Insert Video', | |
| 'ql-formula': 'Insert Formula', | |
| 'ql-clean': 'Remove Formatting', | |
| 'ql-table-better': 'Insert Table' | |
| }; | |
| // Add tooltips to simple buttons | |
| Object.keys(buttonTooltips).forEach(className => { | |
| toolbarElement.querySelectorAll(`button.${className}`).forEach(button => { | |
| setTooltip(button, buttonTooltips[className]); | |
| }); | |
| }); | |
| // Header buttons | |
| toolbarElement.querySelectorAll('button.ql-header').forEach(button => { | |
| const value = button.getAttribute('value'); | |
| if (value === '1') setTooltip(button, 'Heading 1'); | |
| else if (value === '2') setTooltip(button, 'Heading 2'); | |
| else setTooltip(button, 'Heading'); | |
| }); | |
| // List buttons | |
| toolbarElement.querySelectorAll('button.ql-list').forEach(button => { | |
| const value = button.getAttribute('value'); | |
| if (value === 'ordered') setTooltip(button, 'Numbered List'); | |
| else if (value === 'bullet') setTooltip(button, 'Bullet List'); | |
| else if (value === 'check') setTooltip(button, 'Check List'); | |
| else setTooltip(button, 'List'); | |
| }); | |
| // Script buttons (subscript/superscript) | |
| toolbarElement.querySelectorAll('button.ql-script').forEach(button => { | |
| const value = button.getAttribute('value'); | |
| if (value === 'sub') setTooltip(button, 'Subscript'); | |
| else if (value === 'super') setTooltip(button, 'Superscript'); | |
| }); | |
| // Indent buttons | |
| toolbarElement.querySelectorAll('button.ql-indent').forEach(button => { | |
| const value = button.getAttribute('value'); | |
| if (value === '-1') setTooltip(button, 'Decrease Indent'); | |
| else if (value === '+1') setTooltip(button, 'Increase Indent'); | |
| }); | |
| // Direction button | |
| toolbarElement.querySelectorAll('button.ql-direction').forEach(button => { | |
| if (button.getAttribute('value') === 'rtl') { | |
| setTooltip(button, 'Right to Left'); | |
| } | |
| }); | |
| // Dropdown pickers (size, header, color, background, font, align) | |
| const pickerTooltips = { | |
| 'ql-size': 'Text Size', | |
| 'ql-header': 'Heading', | |
| 'ql-color': 'Text Color', | |
| 'ql-background': 'Background Color', | |
| 'ql-font': 'Font Family', | |
| 'ql-align': 'Text Alignment' | |
| }; | |
| Object.keys(pickerTooltips).forEach(pickerClass => { | |
| const picker = toolbarElement.querySelector(`.ql-picker.${pickerClass}`); | |
| if (picker) { | |
| const label = picker.querySelector('.ql-picker-label'); | |
| if (label) setTooltip(label, pickerTooltips[pickerClass]); | |
| } | |
| }); | |
| // Fallback: add tooltips to any remaining buttons | |
| toolbarElement.querySelectorAll('button').forEach(button => { | |
| if (!button.title) { | |
| const classes = Array.from(button.classList); | |
| const qlClass = classes.find(cls => cls.startsWith('ql-') && cls !== 'ql-picker-label'); | |
| if (qlClass && buttonTooltips[qlClass]) { | |
| setTooltip(button, buttonTooltips[qlClass]); | |
| } | |
| } | |
| }); | |
| }, 100); | |
| } | |
| // Initialize tooltips after editor is ready | |
| addTooltipsToToolbar(); | |
| // Add format painter icon after toolbar is created | |
| setTimeout(() => { | |
| const formatPainterButton = document.querySelector('.ql-format-painter'); | |
| if (formatPainterButton && !formatPainterButton.querySelector('svg')) { | |
| // Add paintbrush SVG icon | |
| formatPainterButton.innerHTML = ` | |
| <svg viewBox="0 0 18 18"> | |
| <path d="M13.5 2.5L15.5 4.5L6.5 13.5L4.5 11.5L13.5 2.5Z" fill="currentColor" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> | |
| <path d="M3.5 15.5L4.5 14.5L5.5 15.5L4.5 16.5L3.5 15.5Z" fill="currentColor" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> | |
| <path d="M2.5 12.5L3.5 13.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> | |
| </svg> | |
| `; | |
| } | |
| }, 150); | |
| let delta = null; | |
| let html = ''; | |
| let messageId = 0; | |
| let lastMessageTime = 0; | |
| const MESSAGE_THROTTLE = 100; // milliseconds | |
| // Function to send message to Flutter | |
| function sendMessageToFlutter(message) { | |
| if (window.parent) { | |
| window.parent.postMessage(message, '*'); | |
| } | |
| } | |
| // Function to clean HTML by removing selection styles from images | |
| function cleanExportedHTML(html) { | |
| if (!html) return html; | |
| // Create a temporary div to parse and clean the HTML | |
| const tempDiv = document.createElement('div'); | |
| tempDiv.innerHTML = html; | |
| // Find all images and remove selection styles | |
| const images = tempDiv.querySelectorAll('img'); | |
| images.forEach(img => { | |
| // Remove the selection class | |
| img.classList.remove('ql-image-selected'); | |
| // Remove inline outline styles | |
| if (img.style.outline) { | |
| img.style.outline = ''; | |
| } | |
| if (img.style.outlineOffset) { | |
| img.style.outlineOffset = ''; | |
| } | |
| // If style attribute is now empty, remove it | |
| if (img.style.cssText.trim() === '') { | |
| img.removeAttribute('style'); | |
| } | |
| }); | |
| // Return the cleaned HTML | |
| return tempDiv.innerHTML; | |
| } | |
| // Function to send JSON data to Flutter | |
| function sendJsonToFlutter(data) { | |
| // Throttle messages to prevent spam | |
| const now = Date.now(); | |
| if (now - lastMessageTime < MESSAGE_THROTTLE) { | |
| return; | |
| } | |
| lastMessageTime = now; | |
| // Clean HTML if present before sending | |
| if (data.html) { | |
| data.html = cleanExportedHTML(data.html); | |
| } | |
| if (data.data && data.data.html) { | |
| data.data.html = cleanExportedHTML(data.data.html); | |
| } | |
| // Add unique ID to prevent loops | |
| data.messageId = ++messageId; | |
| data.source = 'iframe'; | |
| sendMessageToFlutter(JSON.stringify(data)); | |
| } | |
| // Listen for messages from Flutter | |
| window.addEventListener('message', function (event) { | |
| // Only process messages from the parent window (Flutter) | |
| if (event.source !== window.parent) { | |
| return; | |
| } | |
| try { | |
| const data = JSON.parse(event.data); | |
| // Only handle command messages, not response messages | |
| if (data.type === 'command') { | |
| handleFlutterCommand(data); | |
| } | |
| } catch (e) { | |
| // Handle plain string messages | |
| sendMessageToFlutter('Echo: ' + event.data); | |
| } | |
| }); | |
| // Handle commands from Flutter | |
| function handleFlutterCommand(data) { | |
| // Prevent processing the same command multiple times | |
| if (data._processed) { | |
| return; | |
| } | |
| data._processed = true; | |
| switch (data.action) { | |
| case 'insertTable': | |
| const rows = data.rows || 3; | |
| const cols = data.cols || 3; | |
| tableModule.insertTable(rows, cols); | |
| sendJsonToFlutter({ | |
| type: 'response', | |
| action: 'insertTable', | |
| success: true, | |
| message: `Table inserted: ${rows}x${cols}`, | |
| timestamp: Date.now() | |
| }); | |
| break; | |
| case 'getContents': | |
| const content = editor.getContents(); | |
| const htmlContent = editor.getSemanticHTML(); | |
| sendJsonToFlutter({ | |
| type: 'response', | |
| action: 'getContents', | |
| success: true, | |
| data: { | |
| delta: content, | |
| html: htmlContent, | |
| text: editor.getText() | |
| }, | |
| timestamp: Date.now() | |
| }); | |
| break; | |
| case 'setContents': | |
| if (data.delta) { | |
| editor.setContents(data.delta, Quill.sources.USER); | |
| sendJsonToFlutter({ | |
| type: 'response', | |
| action: 'setContents', | |
| success: true, | |
| message: 'Contents set successfully', | |
| timestamp: Date.now() | |
| }); | |
| } | |
| break; | |
| case 'setHTML': | |
| if (data.html) { | |
| // Use setTimeout to make this asynchronous and prevent blocking with large HTML | |
| setTimeout(function() { | |
| try { | |
| // Clear existing content first to avoid conflicts | |
| editor.setContents([{ insert: '\n' }], Quill.sources.API); | |
| // For very large HTML, we need to handle it carefully | |
| const htmlString = String(data.html); | |
| // Use dangerouslyPasteHTML with proper error handling | |
| // This method can handle any HTML input, including large content | |
| editor.clipboard.dangerouslyPasteHTML(0, htmlString, Quill.sources.USER); | |
| // Remove the initial newline we added | |
| const length = editor.getLength(); | |
| if (length > 1) { | |
| editor.deleteText(0, 1, Quill.sources.API); | |
| } | |
| sendJsonToFlutter({ | |
| type: 'response', | |
| action: 'setHTML', | |
| success: true, | |
| message: 'HTML content set successfully', | |
| timestamp: Date.now() | |
| }); | |
| } catch (e) { | |
| sendJsonToFlutter({ | |
| type: 'response', | |
| action: 'setHTML', | |
| success: false, | |
| message: 'Error setting HTML: ' + (e.message || String(e)), | |
| timestamp: Date.now() | |
| }); | |
| } | |
| }, 0); | |
| } else { | |
| sendJsonToFlutter({ | |
| type: 'response', | |
| action: 'setHTML', | |
| success: false, | |
| message: 'No HTML content provided', | |
| timestamp: Date.now() | |
| }); | |
| } | |
| break; | |
| case 'insertText': | |
| const text = data.text || ''; | |
| if (text) { | |
| const selection = editor.getSelection(true); | |
| if (selection) { | |
| // Insert text at the current cursor position | |
| editor.insertText(selection.index, text, Quill.sources.USER); | |
| // Move cursor to after the inserted text | |
| editor.setSelection(selection.index + text.length, Quill.sources.USER); | |
| } else { | |
| // No selection, insert at the end | |
| const length = editor.getLength(); | |
| editor.insertText(length - 1, text, Quill.sources.USER); | |
| editor.setSelection(length - 1 + text.length, Quill.sources.USER); | |
| } | |
| sendJsonToFlutter({ | |
| type: 'response', | |
| action: 'insertText', | |
| success: true, | |
| message: 'Text inserted successfully', | |
| timestamp: Date.now() | |
| }); | |
| } | |
| break; | |
| default: | |
| sendJsonToFlutter({ | |
| type: 'response', | |
| action: data.action || 'unknown', | |
| success: false, | |
| message: 'Unknown command', | |
| timestamp: Date.now() | |
| }); | |
| } | |
| } | |
| // Send initial message to Flutter when iframe loads | |
| window.addEventListener('load', function () { | |
| }); | |
| // Send content change notifications (throttled) | |
| let contentChangeTimeout; | |
| editor.on('text-change', function (delta, oldDelta, source) { | |
| if (source === 'user') { | |
| // Send immediate update with complete content | |
| const completeContent = editor.getContents(); | |
| const htmlContent = editor.getSemanticHTML(); | |
| const textContent = editor.getText(); | |
| sendJsonToFlutter({ | |
| type: 'contentChange', | |
| delta: completeContent, // Send complete content, not just the change | |
| html: htmlContent, | |
| text: textContent, | |
| timestamp: Date.now() | |
| }); | |
| // Also send a throttled summary for heavy operations | |
| clearTimeout(contentChangeTimeout); | |
| contentChangeTimeout = setTimeout(() => { | |
| }, 1000); // Wait 1 second after last change | |
| } | |
| }); | |
| // Handle selection changes to apply format when format painter is active | |
| editor.on('selection-change', function (range, oldRange, source) { | |
| if (FormatPainter.isActive && range && range.length > 0) { | |
| // Format painter is active and user has selected text | |
| // The format will be applied when they click the button again | |
| // Or we can auto-apply on selection change (uncomment below if preferred) | |
| // FormatPainter.applyFormat(); | |
| } else if (FormatPainter.isActive && (!range || range.length === 0)) { | |
| // Selection cleared, keep format painter active for next selection | |
| } | |
| }); | |
| // Image resize functionality | |
| function initImageResize() { | |
| const editorContainer = editor.container; | |
| const editorElement = editorContainer.querySelector('.ql-editor'); | |
| if (!editorElement) { | |
| console.warn('Editor element not found, retrying image resize initialization...'); | |
| setTimeout(initImageResize, 200); | |
| return; | |
| } | |
| // Track resize state | |
| let isResizing = false; | |
| let currentImage = null; | |
| let resizeHandle = null; | |
| let startX = 0; | |
| let startY = 0; | |
| let startWidth = 0; | |
| let startHeight = 0; | |
| let aspectRatio = 1; | |
| // Create resize handle | |
| function createResizeHandle() { | |
| if (resizeHandle && resizeHandle.parentNode) { | |
| return resizeHandle; | |
| } | |
| // Remove old handle if it exists but is orphaned | |
| if (resizeHandle && !resizeHandle.parentNode) { | |
| resizeHandle = null; | |
| } | |
| const handle = document.createElement('div'); | |
| handle.className = 'ql-image-resize-handle'; | |
| // Try appending to body first (for fixed positioning) | |
| // If that doesn't work well, we can fall back to editorElement | |
| try { | |
| document.body.appendChild(handle); | |
| } catch (e) { | |
| // Fallback to editor element if body append fails | |
| editorElement.appendChild(handle); | |
| } | |
| resizeHandle = handle; | |
| return handle; | |
| } | |
| // Position resize handle at bottom-right corner of image | |
| function positionResizeHandle(img, handle) { | |
| if (!img || !handle) return; | |
| const imgRect = img.getBoundingClientRect(); | |
| const editorRect = editorElement.getBoundingClientRect(); | |
| // Check if handle is in body or editorElement | |
| const isInBody = handle.parentNode === document.body; | |
| if (isInBody) { | |
| // Use fixed positioning relative to viewport | |
| handle.style.position = 'fixed'; | |
| handle.style.left = (imgRect.right - 7) + 'px'; // 7px to center the 14px handle | |
| handle.style.top = (imgRect.bottom - 7) + 'px'; | |
| } else { | |
| // Use absolute positioning relative to editor | |
| handle.style.position = 'absolute'; | |
| const scrollLeft = editorElement.scrollLeft; | |
| const scrollTop = editorElement.scrollTop; | |
| handle.style.left = (imgRect.right - editorRect.left - 7 + scrollLeft) + 'px'; | |
| handle.style.top = (imgRect.bottom - editorRect.top - 7 + scrollTop) + 'px'; | |
| } | |
| handle.style.right = 'auto'; | |
| handle.style.bottom = 'auto'; | |
| handle.style.display = 'block'; | |
| handle.style.visibility = 'visible'; | |
| handle.style.opacity = '1'; | |
| console.log('Positioning handle:', { | |
| imgRect: imgRect, | |
| editorRect: editorRect, | |
| isInBody: isInBody, | |
| left: handle.style.left, | |
| top: handle.style.top, | |
| position: handle.style.position, | |
| display: handle.style.display, | |
| visibility: handle.style.visibility | |
| }); | |
| } | |
| // Remove resize handle | |
| function removeResizeHandle() { | |
| if (resizeHandle) { | |
| resizeHandle.style.display = 'none'; | |
| } | |
| } | |
| // Update image size in Quill delta | |
| function updateImageSize(img, width, height) { | |
| // Update the DOM directly first for immediate visual feedback | |
| img.style.width = width + 'px'; | |
| img.style.height = height + 'px'; | |
| img.setAttribute('width', width); | |
| img.setAttribute('height', height); | |
| // Try to update through Quill's API | |
| try { | |
| const blot = Quill.find(img); | |
| if (blot) { | |
| const index = editor.getIndex(blot); | |
| if (index !== null) { | |
| // Quill stores images as embeds, so we need to update the embed's attributes | |
| // Try using formatText for width/height (may not work if not registered) | |
| // If that doesn't work, we'll rely on the DOM attributes which will be preserved in HTML output | |
| try { | |
| editor.formatText(index, 1, 'width', width + 'px', Quill.sources.USER); | |
| editor.formatText(index, 1, 'height', height + 'px', Quill.sources.USER); | |
| } catch (e) { | |
| // Width/height might not be registered formats, that's okay | |
| // The DOM attributes will be preserved in the HTML output | |
| console.log('Width/height format not registered, using DOM attributes'); | |
| } | |
| } | |
| } | |
| } catch (e) { | |
| // If Quill API fails, DOM updates will still work | |
| console.log('Error updating through Quill API:', e); | |
| } | |
| } | |
| // Handle mouse down on resize handle | |
| function handleResizeStart(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| if (!currentImage) return; | |
| isResizing = true; | |
| startX = e.clientX; | |
| startY = e.clientY; | |
| startWidth = currentImage.offsetWidth || currentImage.width; | |
| startHeight = currentImage.offsetHeight || currentImage.height; | |
| aspectRatio = startWidth / startHeight; | |
| // Prevent text selection during resize | |
| document.body.style.userSelect = 'none'; | |
| document.body.style.cursor = 'nwse-resize'; | |
| document.addEventListener('mousemove', handleResize); | |
| document.addEventListener('mouseup', handleResizeEnd); | |
| } | |
| // Handle mouse move during resize | |
| function handleResize(e) { | |
| if (!isResizing || !currentImage) return; | |
| const deltaX = e.clientX - startX; | |
| const deltaY = e.clientY - startY; | |
| // Calculate new dimensions maintaining aspect ratio | |
| let newWidth = startWidth + deltaX; | |
| let newHeight = startHeight + deltaY; | |
| // Maintain aspect ratio based on which direction has more movement | |
| if (Math.abs(deltaX) > Math.abs(deltaY)) { | |
| newHeight = newWidth / aspectRatio; | |
| } else { | |
| newWidth = newHeight * aspectRatio; | |
| } | |
| // Set minimum and maximum size constraints | |
| const minSize = 50; | |
| const maxSize = 2000; | |
| newWidth = Math.max(minSize, Math.min(maxSize, newWidth)); | |
| newHeight = Math.max(minSize, Math.min(maxSize, newHeight)); | |
| // Update image dimensions visually | |
| currentImage.style.width = newWidth + 'px'; | |
| currentImage.style.height = newHeight + 'px'; | |
| // Update handle position | |
| if (resizeHandle) { | |
| positionResizeHandle(currentImage, resizeHandle); | |
| } | |
| } | |
| // Handle mouse up - finalize resize | |
| function handleResizeEnd(e) { | |
| if (!isResizing || !currentImage) return; | |
| isResizing = false; | |
| // Restore text selection | |
| document.body.style.userSelect = ''; | |
| document.body.style.cursor = ''; | |
| // Update Quill delta with final dimensions | |
| const finalWidth = Math.round(parseInt(currentImage.style.width) || currentImage.offsetWidth); | |
| const finalHeight = Math.round(parseInt(currentImage.style.height) || currentImage.offsetHeight); | |
| updateImageSize(currentImage, finalWidth, finalHeight); | |
| // Clean up | |
| document.removeEventListener('mousemove', handleResize); | |
| document.removeEventListener('mouseup', handleResizeEnd); | |
| // Content change will be automatically triggered by formatText with Quill.sources.USER | |
| } | |
| // Handle image click - show resize handle | |
| editorElement.addEventListener('click', function (e) { | |
| // Don't interfere if we're resizing | |
| if (isResizing) return; | |
| if (e.target.tagName === 'IMG') { | |
| // Remove previous selection | |
| editorElement.querySelectorAll('img.ql-image-selected').forEach(img => { | |
| img.classList.remove('ql-image-selected'); | |
| img.style.outline = ''; | |
| img.style.outlineOffset = ''; | |
| }); | |
| // Select current image | |
| e.target.classList.add('ql-image-selected'); | |
| e.target.style.outline = '2px solid #007bff'; | |
| e.target.style.outlineOffset = '2px'; | |
| currentImage = e.target; | |
| // Create and position resize handle | |
| const handle = createResizeHandle(); | |
| // Small delay to ensure image is fully rendered | |
| setTimeout(function () { | |
| positionResizeHandle(e.target, handle); | |
| console.log('Image selected, handle positioned at:', handle.style.left, handle.style.top); | |
| console.log('Handle display:', handle.style.display); | |
| console.log('Handle element:', handle); | |
| }, 10); | |
| // Add event listener to handle if not already added | |
| handle.onmousedown = handleResizeStart; | |
| } else { | |
| // Click outside image - remove selection | |
| editorElement.querySelectorAll('img.ql-image-selected').forEach(img => { | |
| img.classList.remove('ql-image-selected'); | |
| img.style.outline = ''; | |
| img.style.outlineOffset = ''; | |
| }); | |
| currentImage = null; | |
| removeResizeHandle(); | |
| } | |
| }); | |
| // Handle selection change - remove image selection when text is selected | |
| editor.on('selection-change', function (range) { | |
| if (range && range.length > 0) { | |
| // Text is selected, remove image selection | |
| editorElement.querySelectorAll('img.ql-image-selected').forEach(img => { | |
| img.classList.remove('ql-image-selected'); | |
| img.style.outline = ''; | |
| img.style.outlineOffset = ''; | |
| }); | |
| currentImage = null; | |
| removeResizeHandle(); | |
| } | |
| }); | |
| // Update handle position on scroll | |
| editorElement.addEventListener('scroll', function () { | |
| if (currentImage && resizeHandle && resizeHandle.style.display !== 'none') { | |
| positionResizeHandle(currentImage, resizeHandle); | |
| } | |
| }); | |
| // Handle window resize | |
| window.addEventListener('resize', function () { | |
| if (currentImage && resizeHandle && resizeHandle.style.display !== 'none') { | |
| positionResizeHandle(currentImage, resizeHandle); | |
| } | |
| }); | |
| } | |
| // Initialize image resize after editor is ready | |
| setTimeout(initImageResize, 300); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment