resize-quill-image 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 littlenines
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,193 @@
1
+ <p align="center"> <h1 align="center">resize-quill-image</h1> </p>
2
+
3
+ *`resize-quill-image` is a lightweight Quill module that enables image resizing inside the editor.
4
+
5
+ This module was originally created because we needed a working image resize solution for our own project.
6
+ Most existing Quill modules were either deprecated or not updated for the latest Quill versions.
7
+ While there are a few small issues, they’re usually project-specific and not caused by the module itself.
8
+ You can find these cases and their solutions in the [Problems](#problems) section.*
9
+
10
+ ---
11
+ - [Installation](#installation)
12
+ - [Usage](#usage)
13
+ - [Import the module](#1-import-the-module)
14
+ - [Register the module with Quill](#2-register-the-module-with-quill)
15
+ - [Configure the Quill editor](#3-configure-the-quill-editor)
16
+ - [Options](#options)
17
+ - [Option Descriptions](#option-descriptions)
18
+ - [Cleanup / Destroy](#cleanup--destroy)
19
+ - [Usage](#usage-1)
20
+ - [When to use](#when-to-use)
21
+ - [Example with unmount](#example-with-unmount)
22
+ - [Problems](#problems)
23
+ - [Resize overlay goes outside the editor](#problem-resize-overlay-goes-outside-the-editor)
24
+ - [Image resize handles and border do not appear](#problem-image-resize-handles-and-border-do-not-appear)
25
+ - [License](#license)
26
+
27
+ ---
28
+ ## Demo GIF
29
+ ![](images/demo.gif)
30
+
31
+ ## 📦 [Installation](#installation)
32
+
33
+ ```bash
34
+ npm install resize-quill-image
35
+ ```
36
+
37
+ ---
38
+
39
+ ## [Usage](#usage)
40
+
41
+ ### 1. Import the module
42
+
43
+ ```js
44
+ import ImageResize from 'resize-quill-image';
45
+ ```
46
+
47
+ ### 2. Register the module with Quill
48
+
49
+ ```js
50
+ Quill.register('modules/imageResize', ImageResize);
51
+ ```
52
+
53
+ ### 3. Configure the Quill editor
54
+
55
+ ```js
56
+ const quill = new Quill(editorContainer, {
57
+ modules: {
58
+ syntax: true,
59
+ toolbar: toolbarOptions,
60
+ imageResize: true
61
+ },
62
+ placeholder: 'Compose an epic...',
63
+ theme: 'snow'
64
+ });
65
+ ```
66
+
67
+ ---
68
+
69
+ ## [Options](#options)
70
+
71
+ You can configure the behavior of `resize-quill-image` by passing options inside your Quill config:
72
+
73
+ ```js
74
+ imageResize: {
75
+ helpIcon: true,
76
+ displaySize: true,
77
+ styleSelection: true
78
+ }
79
+ ```
80
+
81
+ ### [Option Descriptions](#option-descriptions)
82
+
83
+ | Option | Default | Description |
84
+ |------------------|---------|------------------------------------------------------------------------------------------------------------------|
85
+ | `helpIcon` | `true` | Shows a `?` icon on the image overlay. Describes keyboard shortcuts:<br>• `Shift` → vertical<br>• `Alt` → horizontal<br>• `Ctrl` → custom/free resize <br> ![](https://github.com/littlenines/resize-quill-image/blob/ec361ecea1d93ca7b343f367e4d0956a5dc56432/images/controls.png) |
86
+ | `displaySize` | `true` | Displays current image width and height as a badge while resizing. <br> ![](https://github.com/littlenines/resize-quill-image/blob/ec361ecea1d93ca7b343f367e4d0956a5dc56432/images/badge.png) |
87
+ | `styleSelection` | `true` | Disables the blue selection overlay. To keep default behavior: `imageResize: { styleSelection: false }` <br> ![](images/style-selection.png) |
88
+
89
+ ---
90
+
91
+ ## [Cleanup / Destroy](#cleanup--destroy)
92
+
93
+ If you're dynamically mounting and unmounting the Quill editor (for example in a Single Page Application or during route changes), it's important to properly **clean up** the `resize-quill-image` module to avoid memory leaks or event duplication.
94
+
95
+ This module provides a `destroy()` method that you can call when tearing down your Quill instance.
96
+
97
+ ### [Usage](#usage-1)
98
+
99
+ Call the `destroy()` method on the `imageResize` module instance:
100
+
101
+ ```js
102
+ const imageResizeModule = quill.getModule('imageResize');
103
+
104
+ if (imageResizeModule?.destroy) {
105
+ imageResizeModule.destroy();
106
+ }
107
+ ```
108
+
109
+ This will:
110
+
111
+ - Remove all event listeners (`click`, `selection-change`, `text-change`)
112
+ - Remove any visible resize overlays or handles
113
+ - Clean up drag and tooltip controllers
114
+ - Reset internal references for garbage collection
115
+
116
+ ### [When to use](#when-to-use)
117
+
118
+ - If you're unmounting your editor component
119
+ - If you're switching pages in an SPA
120
+ - If you're reinitializing Quill manually
121
+
122
+ ### [Example with unmount](#example-with-unmount)
123
+
124
+ ```js
125
+ useEffect(() => {
126
+ const quill = new Quill(editorRef.current, { ... });
127
+
128
+ return () => {
129
+ const module = quill.getModule('imageResize');
130
+ if (module?.destroy) module.destroy();
131
+ };
132
+ }, []);
133
+ ```
134
+
135
+ This helps ensure your editor stays clean and efficient across mounts.
136
+
137
+ ---
138
+
139
+ ## [Problems](#problems)
140
+
141
+ ### <ins>Problem: Resize overlay goes outside the editor</ins>
142
+
143
+ If your `.ql-container` has a fixed height like this:
144
+
145
+ ```css
146
+ .ql-container {
147
+ height: 500px;
148
+ }
149
+ ```
150
+
151
+ Then the resize overlay may appear cut off or outside the bounds of the editor when the image goes beyond the height.
152
+
153
+ **Solution:**
154
+
155
+ Instead of a fixed height, use `min-height` and `max-height`:
156
+
157
+ ```css
158
+ .ql-container {
159
+ min-height: 500px;
160
+ max-height: 500px;
161
+ }
162
+ ```
163
+
164
+ This keeps the resize functionality working correctly and fully visible.
165
+
166
+ ### <ins>Problem: Image resize handles and border do not appear</ins>
167
+
168
+ If you don't see the image resize handles or overlay border, make sure you’ve included `highlight.js`.
169
+ Quill’s `syntax` module depends on it, and without it, modules like `imageResize` may silently fail to render overlays.
170
+
171
+ Add this to your HTML:
172
+
173
+ ```html
174
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
175
+ ```
176
+
177
+ More info in the Quill docs for newer highlight.js cdn version:
178
+ https://quilljs.com/docs/modules/syntax#syntax-highlighter-module
179
+
180
+ ## [License](#license)
181
+
182
+ MIT License
183
+ Free for personal and commercial use.
184
+
185
+ ---
186
+ > [!NOTE]
187
+ > If you encounter any bugs, memory leaks, or unexpected behavior, feel free to open an issue on the [GitHub repository](https://github.com/littlenines/resize-quill-image/issues).
188
+ > Your feedback helps make the module better for everyone.
189
+ > If you want to contribute to the project — whether it's fixing a bug or improving performance — your contributions are welcome and appreciated.
190
+
191
+ > [!TIP]
192
+ > If you just want the code and prefer to build your own module on top of it, you're free to do that.
193
+ > Everything is located in the `/src` directory for full access and customization.
@@ -0,0 +1,3 @@
1
+ "use strict";const y=require("quill"),r="#AED2FF",f="#687EFF",c="#fff",h={minWidth:16,minHeight:16,noSelectionClass:"no-selection",handleStyles:{position:"absolute",width:"15px",height:"15px",backgroundColor:r,border:`1px solid ${f}`},overlayStyles:{position:"absolute",boxSizing:"border-box",border:"1px dashed #777",zIndex:8},positions:[{top:0,left:0,clipPath:"polygon(0% 0%, 100% 0%, 0% 100%)"},{top:0,right:0,clipPath:"polygon(100% 0%, 0% 0%, 100% 100%)"},{bottom:0,left:0,clipPath:"polygon(0% 100%, 100% 100%, 0% 0%)"},{bottom:0,right:0,clipPath:"polygon(100% 100%, 0% 100%, 100% 0%)"}],displaySizeStyles:{position:"absolute",fontSize:"12px",backgroundColor:r,color:c,padding:"2px 4px",borderRadius:"4px",userSelect:"none",pointerEvents:"none"},displaySizePositionStyles:{right:"20px",bottom:"5px",left:"auto"},tooltip:{iconStyles:{position:"absolute",top:"8px",right:"8px",width:"20px",height:"20px",background:r,color:c,borderRadius:"50%",textAlign:"center",lineHeight:"20px",fontSize:"14px",cursor:"pointer",userSelect:"none"},textStyles:{position:"absolute",background:r,color:c,padding:"6px 8px",borderRadius:"4px",display:"none",pointerEvents:"none",fontSize:"12px",whiteSpace:"pre"}}};class v{constructor(t,e){this.parent=t,this.styles=e,this.overlay=null}create(){this.overlay=document.createElement("div"),Object.assign(this.overlay.style,this.styles),this.parent.appendChild(this.overlay)}remove(){this.overlay&&(this.overlay.remove(),this.overlay=null)}reposition(t){if(!this.overlay)return;const e=t.getBoundingClientRect(),i=this.parent.getBoundingClientRect();Object.assign(this.overlay.style,{left:`${e.left-i.left-1+this.parent.scrollLeft}px`,top:`${e.top-i.top+this.parent.scrollTop}px`,width:`${e.width}px`,height:`${e.height}px`})}}class M{constructor(t,e,i,n){this.overlay=t,this.positions=e,this.handleStyles=i,this.mousedownCallback=n,this.boxes=[]}createHandles(){this.positions.forEach((t,e)=>{const i=document.createElement("div");Object.assign(i.style,this.handleStyles,t),i.style.clipPath=t.clipPath,e===0||e===3?i.style.cursor="nwse-resize":(e===1||e===2)&&(i.style.cursor="nesw-resize"),i.addEventListener("mousedown",this.mousedownCallback,!1),i.addEventListener("touchstart",this.mousedownCallback,{passive:!1}),this.overlay.appendChild(i),this.boxes.push(i)})}removeHandles(){this.boxes.forEach(t=>{t.removeEventListener("mousedown",this.mousedownCallback),t.removeEventListener("touchstart",this.mousedownCallback),t.onmousedown=null,t.ontouchstart=null,t.remove()}),this.boxes=[],this.overlay=null,this.positions=null,this.handleStyles=null,this.mousedownCallback=null}}class S{constructor(t,e,i,n,a){this.minWidth=t,this.minHeight=e,this.overlayManager=i,this.displaySizeManager=n,this.tooltipInfoManager=a,this.img=null,this.dragBox=null,this.startX=0,this.startY=0,this.startWidth=0,this.startHeight=0,this.handleDrag=this.handleDrag.bind(this),this.handleMouseup=this.handleMouseup.bind(this)}setDisplaySizeManager(t){this.displaySizeManager=t}setTooltipInfoManager(t){this.tooltipInfoManager=t}addEventListeners(){document.addEventListener("mousemove",this.handleDrag),document.addEventListener("touchmove",this.handleDrag,{passive:!1}),document.addEventListener("mouseup",this.handleMouseup,!0),document.addEventListener("touchend",this.handleMouseup,!0),document.addEventListener("touchcancel",this.handleMouseup,!0)}removeEventListeners(){document.removeEventListener("mousemove",this.handleDrag),document.removeEventListener("touchmove",this.handleDrag),document.removeEventListener("mouseup",this.handleMouseup,!0),document.removeEventListener("touchend",this.handleMouseup,!0),document.removeEventListener("touchcancel",this.handleMouseup,!0)}startDragging(t,e,i){t.preventDefault(),this.img=e,this.dragBox=i,t.type==="touchstart"?(this.startX=t.changedTouches[0].clientX,this.startY=t.changedTouches[0].clientY):(this.startX=t.clientX,this.startY=t.clientY),this.startWidth=e.width||e.naturalWidth,this.startHeight=e.height||e.naturalHeight,this.addEventListeners()}handleDrag(t){if(!this.img)return;let e,i;t.type==="touchmove"?(e=t.changedTouches[0].clientX,i=t.changedTouches[0].clientY):(e=t.clientX,i=t.clientY);let n=e-this.startX,a=i-this.startY,p=this.startWidth/this.startHeight;const u=t.ctrlKey,g=t.shiftKey,m=t.altKey;if(u){let s=this.startWidth+n,o=this.startHeight+a;s=Math.max(s,this.minWidth),o=Math.max(o,this.minHeight),this.img.style.width=`${s}px`,this.img.style.height=`${o}px`}else if(g){let s=this.startHeight+a;this.img.style.height=`${Math.max(s,this.minHeight)}px`}else if(m){let s=this.startWidth+n;this.img.style.width=`${Math.max(s,this.minWidth)}px`}else{const s=Math.abs(n)>Math.abs(a)?n:a;let o=this.startWidth+s,d=o/p;o=Math.max(o,this.minWidth),d=Math.max(d,this.minHeight),this.img.style.width=`${o}px`,this.img.style.height=`${d}px`}this.overlayManager&&this.overlayManager.reposition(this.img),this.displaySizeManager&&this.displaySizeManager.update(),this.tooltipInfoManager&&this.tooltipInfoManager.update(),this.startX=e,this.startY=i,this.startWidth=this.img.offsetWidth,this.startHeight=this.img.offsetHeight}handleMouseup(){this.removeEventListeners(),this.img=null,this.dragBox=null}destroy(){this.removeEventListeners(),this.img=null,this.dragBox=null,this.overlayManager=null,this.displaySizeManager=null,this.tooltipInfoManager=null}}class x{constructor(t,e){this.overlay=t,this.img=e,this.display=null}create(){this.display=document.createElement("div"),Object.assign(this.display.style,h.displaySizeStyles),this.overlay.appendChild(this.display),this.update()}update(){if(!this.display||!this.img)return;const t=this.img.offsetWidth,e=this.img.offsetHeight;this.display.innerHTML=`${t} × ${e}`;const i=this.display.getBoundingClientRect();t>120&&e>30?Object.assign(this.display.style,h.displaySizePositionStyles):Object.assign(this.display.style,{right:`-${i.width+4}px`,bottom:"0",left:"auto"})}remove(){this.display&&(this.display.remove(),this.display=null,this.overlay=null,this.img=null)}}class b{constructor(t){this.overlay=t,this.icon=null,this.tooltip=null,this.handleMouseEnter=this.handleMouseEnter.bind(this),this.handleMouseLeave=this.handleMouseLeave.bind(this)}create(){this.icon=document.createElement("div"),this.icon.textContent="?",Object.assign(this.icon.style,h.tooltip.iconStyles),this.tooltip=document.createElement("div"),this.tooltip.textContent=`Shift for vertical
2
+ Ctrl for custom
3
+ Alt for horizontal`,Object.assign(this.tooltip.style,{...h.tooltip.textStyles}),this.icon.addEventListener("mouseenter",this.handleMouseEnter),this.icon.addEventListener("mouseleave",this.handleMouseLeave),this.overlay.appendChild(this.icon),this.overlay.appendChild(this.tooltip),this.update()}handleMouseEnter(){this.tooltip&&(this.tooltip.style.display="block")}handleMouseLeave(){this.tooltip&&(this.tooltip.style.display="none")}update(){if(!this.icon||!this.tooltip)return;const t=this.icon.getBoundingClientRect(),e=this.icon.offsetTop+25,i=t.left-100;Object.assign(this.tooltip.style,{top:`${e}px`,left:`${i}px`})}remove(){!this.icon&&!this.tooltip||(this.icon&&(this.icon.removeEventListener("mouseenter",this.handleMouseEnter),this.icon.removeEventListener("mouseleave",this.handleMouseLeave),this.icon.remove(),this.icon=null),this.tooltip&&(this.tooltip.remove(),this.tooltip=null),this.overlay=null)}}const C=".no-selection::selection{background:transparent!important}",E=l=>{if(typeof document>"u")return;const t=document.createElement("style");t.textContent=l,document.head.appendChild(t)};E(C);class L extends y.Module{constructor(t,e={}){super(t,e),this.quill=t,this.options={helpIcon:!0,displaySize:!0,styleSelection:!0,...h,...e},this.img=null,this.overlayManager=new v(this.quill.root.parentNode,this.options.overlayStyles),this.dragController=new S(this.options.minWidth,this.options.minHeight,this.overlayManager,null,null),this.imageFormat=this.quill.constructor.import("formats/image"),this.bindHandlers(),this.addEventListeners()}bindHandlers(){this.handleClick=this.handleClick.bind(this),this.handleSelectionChange=this.handleSelectionChange.bind(this),this.handleTextChange=this.handleTextChange.bind(this),this.handleMousedown=this.handleMousedown.bind(this)}addEventListeners(){this.quill.root.addEventListener("click",this.handleClick),this.quill.on("selection-change",this.handleSelectionChange),this.quill.on("text-change",this.handleTextChange)}removeEventListeners(){this.quill.root.removeEventListener("click",this.handleClick),this.quill.off("selection-change",this.handleSelectionChange),this.quill.off("text-change",this.handleTextChange)}handleClick(t){if(t.target.tagName==="IMG"){const e=this.quill.constructor.find(t.target);e&&this.quill.setSelection(e.offset(this.quill.scroll),e.length())}}handleSelectionChange(t){if(!t)return this.hide();const[e]=this.quill.scroll.descendant(this.quill.constructor.import("formats/image"),t.index);e&&e.domNode instanceof HTMLImageElement?(this.disableTextSelection(),this.show(e.domNode)):(this.enableTextSelection(),this.hide())}disableTextSelection(){this.options.styleSelection&&this.quill.root.classList.add(this.options.noSelectionClass)}enableTextSelection(){this.options.styleSelection&&this.quill.root.classList.remove(this.options.noSelectionClass)}handleTextChange(){this.img&&(this.quill.root.contains(this.img)?(this.overlayManager.reposition(this.img),this.displaySizeManager&&this.displaySizeManager.update(),this.tooltipInfoManager&&this.tooltipInfoManager.update()):(this.hide(),this.enableTextSelection()))}show(t){this.img!==t&&(!t||!(t instanceof HTMLImageElement)||(this.hide(),this.img=t,this.overlayManager.overlay||this.overlayManager.create(),this.handleManager=new M(this.overlayManager.overlay,this.options.positions,this.options.handleStyles,this.handleMousedown),this.handleManager.createHandles(),this.overlayManager.reposition(this.img),this.options.displaySize&&(this.displaySizeManager=new x(this.overlayManager.overlay,this.img),this.displaySizeManager.create(),this.dragController.setDisplaySizeManager(this.displaySizeManager)),this.options.helpIcon&&(this.tooltipInfoManager=new b(this.overlayManager.overlay),this.tooltipInfoManager.create(),this.dragController.setTooltipInfoManager(this.tooltipInfoManager))))}hide(){this.dragController.setDisplaySizeManager(null),this.dragController.setTooltipInfoManager(null),this.handleManager&&this.handleManager.removeHandles(),this.overlayManager.remove(),this.displaySizeManager&&this.displaySizeManager.remove(),this.displaySizeManager=null,this.tooltipInfoManager&&(this.tooltipInfoManager.remove(),this.tooltipInfoManager=null),this.img=null}handleMousedown(t){t.target instanceof HTMLElement&&this.dragController.startDragging(t,this.img,t.target)}destroy(){var t;this.removeEventListeners(),this.hide(),(t=this.dragController)==null||t.destroy(),this.dragController=null}}module.exports=L;
@@ -0,0 +1,26 @@
1
+ declare module 'resize-quill-image' {
2
+ import { Module } from 'quill';
3
+
4
+ interface ImageResizeOptions {
5
+ helpIcon?: boolean;
6
+ displaySize?: boolean;
7
+ styleSelection?: boolean;
8
+ noSelectionClass?: string;
9
+ minWidth?: number;
10
+ minHeight?: number;
11
+ overlayStyles?: Record<string, any>;
12
+ handleStyles?: Record<string, any>;
13
+ positions?: Array<{
14
+ top?: number;
15
+ left?: number;
16
+ right?: number;
17
+ bottom?: number;
18
+ clipPath?: string;
19
+ }>;
20
+ }
21
+
22
+ export default class ImageResize extends Module {
23
+ constructor(quill: any, options?: ImageResizeOptions);
24
+ destroy(): void;
25
+ }
26
+ }
@@ -0,0 +1,258 @@
1
+ import { Module as y } from "quill";
2
+ const r = "#AED2FF", f = "#687EFF", c = "#fff", h = {
3
+ minWidth: 16,
4
+ minHeight: 16,
5
+ noSelectionClass: "no-selection",
6
+ handleStyles: {
7
+ position: "absolute",
8
+ width: "15px",
9
+ height: "15px",
10
+ backgroundColor: r,
11
+ border: `1px solid ${f}`
12
+ },
13
+ overlayStyles: {
14
+ position: "absolute",
15
+ boxSizing: "border-box",
16
+ border: "1px dashed #777",
17
+ zIndex: 8
18
+ },
19
+ positions: [
20
+ { top: 0, left: 0, clipPath: "polygon(0% 0%, 100% 0%, 0% 100%)" },
21
+ { top: 0, right: 0, clipPath: "polygon(100% 0%, 0% 0%, 100% 100%)" },
22
+ { bottom: 0, left: 0, clipPath: "polygon(0% 100%, 100% 100%, 0% 0%)" },
23
+ { bottom: 0, right: 0, clipPath: "polygon(100% 100%, 0% 100%, 100% 0%)" }
24
+ ],
25
+ displaySizeStyles: {
26
+ position: "absolute",
27
+ fontSize: "12px",
28
+ backgroundColor: r,
29
+ color: c,
30
+ padding: "2px 4px",
31
+ borderRadius: "4px",
32
+ userSelect: "none",
33
+ pointerEvents: "none"
34
+ },
35
+ displaySizePositionStyles: {
36
+ right: "20px",
37
+ bottom: "5px",
38
+ left: "auto"
39
+ },
40
+ tooltip: {
41
+ iconStyles: {
42
+ position: "absolute",
43
+ top: "8px",
44
+ right: "8px",
45
+ width: "20px",
46
+ height: "20px",
47
+ background: r,
48
+ color: c,
49
+ borderRadius: "50%",
50
+ textAlign: "center",
51
+ lineHeight: "20px",
52
+ fontSize: "14px",
53
+ cursor: "pointer",
54
+ userSelect: "none"
55
+ },
56
+ textStyles: {
57
+ position: "absolute",
58
+ background: r,
59
+ color: c,
60
+ padding: "6px 8px",
61
+ borderRadius: "4px",
62
+ display: "none",
63
+ pointerEvents: "none",
64
+ fontSize: "12px",
65
+ whiteSpace: "pre"
66
+ }
67
+ }
68
+ };
69
+ class v {
70
+ constructor(t, e) {
71
+ this.parent = t, this.styles = e, this.overlay = null;
72
+ }
73
+ create() {
74
+ this.overlay = document.createElement("div"), Object.assign(this.overlay.style, this.styles), this.parent.appendChild(this.overlay);
75
+ }
76
+ remove() {
77
+ this.overlay && (this.overlay.remove(), this.overlay = null);
78
+ }
79
+ reposition(t) {
80
+ if (!this.overlay) return;
81
+ const e = t.getBoundingClientRect(), i = this.parent.getBoundingClientRect();
82
+ Object.assign(this.overlay.style, {
83
+ left: `${e.left - i.left - 1 + this.parent.scrollLeft}px`,
84
+ top: `${e.top - i.top + this.parent.scrollTop}px`,
85
+ width: `${e.width}px`,
86
+ height: `${e.height}px`
87
+ });
88
+ }
89
+ }
90
+ class M {
91
+ constructor(t, e, i, n) {
92
+ this.overlay = t, this.positions = e, this.handleStyles = i, this.mousedownCallback = n, this.boxes = [];
93
+ }
94
+ createHandles() {
95
+ this.positions.forEach((t, e) => {
96
+ const i = document.createElement("div");
97
+ Object.assign(i.style, this.handleStyles, t), i.style.clipPath = t.clipPath, e === 0 || e === 3 ? i.style.cursor = "nwse-resize" : (e === 1 || e === 2) && (i.style.cursor = "nesw-resize"), i.addEventListener("mousedown", this.mousedownCallback, !1), i.addEventListener("touchstart", this.mousedownCallback, { passive: !1 }), this.overlay.appendChild(i), this.boxes.push(i);
98
+ });
99
+ }
100
+ removeHandles() {
101
+ this.boxes.forEach((t) => {
102
+ t.removeEventListener("mousedown", this.mousedownCallback), t.removeEventListener("touchstart", this.mousedownCallback), t.onmousedown = null, t.ontouchstart = null, t.remove();
103
+ }), this.boxes = [], this.overlay = null, this.positions = null, this.handleStyles = null, this.mousedownCallback = null;
104
+ }
105
+ }
106
+ class S {
107
+ constructor(t, e, i, n, a) {
108
+ this.minWidth = t, this.minHeight = e, this.overlayManager = i, this.displaySizeManager = n, this.tooltipInfoManager = a, this.img = null, this.dragBox = null, this.startX = 0, this.startY = 0, this.startWidth = 0, this.startHeight = 0, this.handleDrag = this.handleDrag.bind(this), this.handleMouseup = this.handleMouseup.bind(this);
109
+ }
110
+ setDisplaySizeManager(t) {
111
+ this.displaySizeManager = t;
112
+ }
113
+ setTooltipInfoManager(t) {
114
+ this.tooltipInfoManager = t;
115
+ }
116
+ addEventListeners() {
117
+ document.addEventListener("mousemove", this.handleDrag), document.addEventListener("touchmove", this.handleDrag, { passive: !1 }), document.addEventListener("mouseup", this.handleMouseup, !0), document.addEventListener("touchend", this.handleMouseup, !0), document.addEventListener("touchcancel", this.handleMouseup, !0);
118
+ }
119
+ removeEventListeners() {
120
+ document.removeEventListener("mousemove", this.handleDrag), document.removeEventListener("touchmove", this.handleDrag), document.removeEventListener("mouseup", this.handleMouseup, !0), document.removeEventListener("touchend", this.handleMouseup, !0), document.removeEventListener("touchcancel", this.handleMouseup, !0);
121
+ }
122
+ startDragging(t, e, i) {
123
+ t.preventDefault(), this.img = e, this.dragBox = i, t.type === "touchstart" ? (this.startX = t.changedTouches[0].clientX, this.startY = t.changedTouches[0].clientY) : (this.startX = t.clientX, this.startY = t.clientY), this.startWidth = e.width || e.naturalWidth, this.startHeight = e.height || e.naturalHeight, this.addEventListeners();
124
+ }
125
+ handleDrag(t) {
126
+ if (!this.img) return;
127
+ let e, i;
128
+ t.type === "touchmove" ? (e = t.changedTouches[0].clientX, i = t.changedTouches[0].clientY) : (e = t.clientX, i = t.clientY);
129
+ let n = e - this.startX, a = i - this.startY, p = this.startWidth / this.startHeight;
130
+ const u = t.ctrlKey, g = t.shiftKey, m = t.altKey;
131
+ if (u) {
132
+ let s = this.startWidth + n, o = this.startHeight + a;
133
+ s = Math.max(s, this.minWidth), o = Math.max(o, this.minHeight), this.img.style.width = `${s}px`, this.img.style.height = `${o}px`;
134
+ } else if (g) {
135
+ let s = this.startHeight + a;
136
+ this.img.style.height = `${Math.max(s, this.minHeight)}px`;
137
+ } else if (m) {
138
+ let s = this.startWidth + n;
139
+ this.img.style.width = `${Math.max(s, this.minWidth)}px`;
140
+ } else {
141
+ const s = Math.abs(n) > Math.abs(a) ? n : a;
142
+ let o = this.startWidth + s, d = o / p;
143
+ o = Math.max(o, this.minWidth), d = Math.max(d, this.minHeight), this.img.style.width = `${o}px`, this.img.style.height = `${d}px`;
144
+ }
145
+ this.overlayManager && this.overlayManager.reposition(this.img), this.displaySizeManager && this.displaySizeManager.update(), this.tooltipInfoManager && this.tooltipInfoManager.update(), this.startX = e, this.startY = i, this.startWidth = this.img.offsetWidth, this.startHeight = this.img.offsetHeight;
146
+ }
147
+ handleMouseup() {
148
+ this.removeEventListeners(), this.img = null, this.dragBox = null;
149
+ }
150
+ destroy() {
151
+ this.removeEventListeners(), this.img = null, this.dragBox = null, this.overlayManager = null, this.displaySizeManager = null, this.tooltipInfoManager = null;
152
+ }
153
+ }
154
+ class x {
155
+ constructor(t, e) {
156
+ this.overlay = t, this.img = e, this.display = null;
157
+ }
158
+ create() {
159
+ this.display = document.createElement("div"), Object.assign(this.display.style, h.displaySizeStyles), this.overlay.appendChild(this.display), this.update();
160
+ }
161
+ update() {
162
+ if (!this.display || !this.img) return;
163
+ const t = this.img.offsetWidth, e = this.img.offsetHeight;
164
+ this.display.innerHTML = `${t} × ${e}`;
165
+ const i = this.display.getBoundingClientRect();
166
+ t > 120 && e > 30 ? Object.assign(this.display.style, h.displaySizePositionStyles) : Object.assign(this.display.style, {
167
+ right: `-${i.width + 4}px`,
168
+ bottom: "0",
169
+ left: "auto"
170
+ });
171
+ }
172
+ remove() {
173
+ this.display && (this.display.remove(), this.display = null, this.overlay = null, this.img = null);
174
+ }
175
+ }
176
+ class b {
177
+ constructor(t) {
178
+ this.overlay = t, this.icon = null, this.tooltip = null, this.handleMouseEnter = this.handleMouseEnter.bind(this), this.handleMouseLeave = this.handleMouseLeave.bind(this);
179
+ }
180
+ create() {
181
+ this.icon = document.createElement("div"), this.icon.textContent = "?", Object.assign(this.icon.style, h.tooltip.iconStyles), this.tooltip = document.createElement("div"), this.tooltip.textContent = `Shift for vertical
182
+ Ctrl for custom
183
+ Alt for horizontal`, Object.assign(this.tooltip.style, { ...h.tooltip.textStyles }), this.icon.addEventListener("mouseenter", this.handleMouseEnter), this.icon.addEventListener("mouseleave", this.handleMouseLeave), this.overlay.appendChild(this.icon), this.overlay.appendChild(this.tooltip), this.update();
184
+ }
185
+ handleMouseEnter() {
186
+ this.tooltip && (this.tooltip.style.display = "block");
187
+ }
188
+ handleMouseLeave() {
189
+ this.tooltip && (this.tooltip.style.display = "none");
190
+ }
191
+ update() {
192
+ if (!this.icon || !this.tooltip) return;
193
+ const t = this.icon.getBoundingClientRect(), e = this.icon.offsetTop + 25, i = t.left - 100;
194
+ Object.assign(this.tooltip.style, {
195
+ top: `${e}px`,
196
+ left: `${i}px`
197
+ });
198
+ }
199
+ remove() {
200
+ !this.icon && !this.tooltip || (this.icon && (this.icon.removeEventListener("mouseenter", this.handleMouseEnter), this.icon.removeEventListener("mouseleave", this.handleMouseLeave), this.icon.remove(), this.icon = null), this.tooltip && (this.tooltip.remove(), this.tooltip = null), this.overlay = null);
201
+ }
202
+ }
203
+ const C = ".no-selection::selection{background:transparent!important}", E = (l) => {
204
+ if (typeof document > "u") return;
205
+ const t = document.createElement("style");
206
+ t.textContent = l, document.head.appendChild(t);
207
+ };
208
+ E(C);
209
+ class w extends y {
210
+ constructor(t, e = {}) {
211
+ super(t, e), this.quill = t, this.options = { helpIcon: !0, displaySize: !0, styleSelection: !0, ...h, ...e }, this.img = null, this.overlayManager = new v(this.quill.root.parentNode, this.options.overlayStyles), this.dragController = new S(this.options.minWidth, this.options.minHeight, this.overlayManager, null, null), this.imageFormat = this.quill.constructor.import("formats/image"), this.bindHandlers(), this.addEventListeners();
212
+ }
213
+ bindHandlers() {
214
+ this.handleClick = this.handleClick.bind(this), this.handleSelectionChange = this.handleSelectionChange.bind(this), this.handleTextChange = this.handleTextChange.bind(this), this.handleMousedown = this.handleMousedown.bind(this);
215
+ }
216
+ addEventListeners() {
217
+ this.quill.root.addEventListener("click", this.handleClick), this.quill.on("selection-change", this.handleSelectionChange), this.quill.on("text-change", this.handleTextChange);
218
+ }
219
+ removeEventListeners() {
220
+ this.quill.root.removeEventListener("click", this.handleClick), this.quill.off("selection-change", this.handleSelectionChange), this.quill.off("text-change", this.handleTextChange);
221
+ }
222
+ handleClick(t) {
223
+ if (t.target.tagName === "IMG") {
224
+ const e = this.quill.constructor.find(t.target);
225
+ e && this.quill.setSelection(e.offset(this.quill.scroll), e.length());
226
+ }
227
+ }
228
+ handleSelectionChange(t) {
229
+ if (!t) return this.hide();
230
+ const [e] = this.quill.scroll.descendant(this.quill.constructor.import("formats/image"), t.index);
231
+ e && e.domNode instanceof HTMLImageElement ? (this.disableTextSelection(), this.show(e.domNode)) : (this.enableTextSelection(), this.hide());
232
+ }
233
+ disableTextSelection() {
234
+ this.options.styleSelection && this.quill.root.classList.add(this.options.noSelectionClass);
235
+ }
236
+ enableTextSelection() {
237
+ this.options.styleSelection && this.quill.root.classList.remove(this.options.noSelectionClass);
238
+ }
239
+ handleTextChange() {
240
+ this.img && (this.quill.root.contains(this.img) ? (this.overlayManager.reposition(this.img), this.displaySizeManager && this.displaySizeManager.update(), this.tooltipInfoManager && this.tooltipInfoManager.update()) : (this.hide(), this.enableTextSelection()));
241
+ }
242
+ show(t) {
243
+ this.img !== t && (!t || !(t instanceof HTMLImageElement) || (this.hide(), this.img = t, this.overlayManager.overlay || this.overlayManager.create(), this.handleManager = new M(this.overlayManager.overlay, this.options.positions, this.options.handleStyles, this.handleMousedown), this.handleManager.createHandles(), this.overlayManager.reposition(this.img), this.options.displaySize && (this.displaySizeManager = new x(this.overlayManager.overlay, this.img), this.displaySizeManager.create(), this.dragController.setDisplaySizeManager(this.displaySizeManager)), this.options.helpIcon && (this.tooltipInfoManager = new b(this.overlayManager.overlay), this.tooltipInfoManager.create(), this.dragController.setTooltipInfoManager(this.tooltipInfoManager))));
244
+ }
245
+ hide() {
246
+ this.dragController.setDisplaySizeManager(null), this.dragController.setTooltipInfoManager(null), this.handleManager && this.handleManager.removeHandles(), this.overlayManager.remove(), this.displaySizeManager && this.displaySizeManager.remove(), this.displaySizeManager = null, this.tooltipInfoManager && (this.tooltipInfoManager.remove(), this.tooltipInfoManager = null), this.img = null;
247
+ }
248
+ handleMousedown(t) {
249
+ t.target instanceof HTMLElement && this.dragController.startDragging(t, this.img, t.target);
250
+ }
251
+ destroy() {
252
+ var t;
253
+ this.removeEventListeners(), this.hide(), (t = this.dragController) == null || t.destroy(), this.dragController = null;
254
+ }
255
+ }
256
+ export {
257
+ w as default
258
+ };
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "resize-quill-image",
3
+ "version": "1.0.0",
4
+ "description": "A lightweight Quill module to resize images with handles and overlays",
5
+ "main": "dist/index.cjs.js",
6
+ "module": "dist/index.es.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.es.js",
12
+ "require": "./dist/index.cjs.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "LICENSE",
18
+ "README.md"
19
+ ],
20
+ "keywords": [
21
+ "quill",
22
+ "quilljs",
23
+ "quill-module",
24
+ "editor",
25
+ "quill-resize-image",
26
+ "image",
27
+ "resize",
28
+ "quill image resize",
29
+ "resize quill image"
30
+ ],
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/littlenines/resize-quill-image.git"
34
+ },
35
+ "homepage": "https://github.com/littlenines/resize-quill-image#readme",
36
+ "bugs": {
37
+ "url": "https://github.com/littlenines/resize-quill-image/issues"
38
+ },
39
+ "author": "littlenines",
40
+ "license": "MIT",
41
+ "scripts": {
42
+ "build": "vite build && node -e \"require('fs').cpSync('src/index.d.ts', 'dist/index.d.ts')\""
43
+ },
44
+ "peerDependencies": {
45
+ "quill": ">=1.3.7 <=2.0.3"
46
+ },
47
+ "devDependencies": {
48
+ "vite": "^6.3.5"
49
+ }
50
+ }