@mgreminger/quill-image-resize-module 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.
@@ -0,0 +1,33 @@
1
+ name: Playwright Tests
2
+ on:
3
+ push:
4
+ branches: [main, master]
5
+ pull_request:
6
+ branches: [main, master]
7
+ jobs:
8
+ test:
9
+ timeout-minutes: 60
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: actions/setup-node@v4
14
+ with:
15
+ node-version: lts/*
16
+ - name: Install dependencies
17
+ run: npm ci
18
+ - name: Install Playwright Browsers
19
+ run: npx playwright install --with-deps
20
+ - name: Build library
21
+ run: npm run build
22
+ - name: Build preview site
23
+ run: npm run build-preview
24
+ - name: Start Preview Server
25
+ run: npm run preview &
26
+ - name: Run Playwright tests
27
+ run: npm run test
28
+ - uses: actions/upload-artifact@v4
29
+ if: ${{ !cancelled() }}
30
+ with:
31
+ name: playwright-report
32
+ path: playwright-report/
33
+ retention-days: 30
package/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License
2
+ Copyright © 2018
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the “Software”), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in
12
+ all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,162 @@
1
+ # Quill ImageResize Module
2
+
3
+ A module for Quill rich text editor to allow images to be resized.
4
+
5
+ A fork of [kensnyder/quill-image-resize-module](https://github.com/kensnyder/quill-image-resize-module) with the following changes:
6
+ * Updated to work with Quill 2
7
+ * Modernized toolchain using vite and TypeScript
8
+ * Toolbar buttons removed since allignment settings were not preserved in the document Delta
9
+ * The presence of resize handles no longer impacts underlying selection range so keyboard actions such as copy to clipboard and type to replace still work as expected
10
+ * Keyboard shortcuts added to increase image size (+ key) and decrease image size (- key)
11
+ * Resize handles now appear when image is selected with the keyboard (using shift with the arrow keys)
12
+ * Works with touch events in addition to mouse events
13
+
14
+ ## Demo
15
+
16
+ [Preview Site](https://mgreminger.github.io/quill-image-resize-module/)
17
+
18
+ ## Usage
19
+
20
+ ### Webpack/ES6
21
+
22
+ ```javascript
23
+ import Quill from "quill";
24
+ import { ImageResize } from "quill-image-resize-module";
25
+
26
+ Quill.register("modules/imageResize", ImageResize);
27
+
28
+ const quill = new Quill(editor, {
29
+ // ...
30
+ modules: {
31
+ // ...
32
+ imageResize: {
33
+ // See optional "config" below
34
+ },
35
+ },
36
+ });
37
+ ```
38
+
39
+ ### Script Tag
40
+
41
+ Copy image-resize.min.js into your web root or include from node_modules
42
+
43
+ ```html
44
+ <script src="/node_modules/quill-image-resize-module/image-resize.min.js"></script>
45
+ ```
46
+
47
+ ```javascript
48
+ var quill = new Quill(editor, {
49
+ // ...
50
+ modules: {
51
+ // ...
52
+ ImageResize: {
53
+ // See optional "config" below
54
+ },
55
+ },
56
+ });
57
+ ```
58
+
59
+ ### Config
60
+
61
+ For the default experience, pass an empty object, like so:
62
+
63
+ ```javascript
64
+ var quill = new Quill(editor, {
65
+ // ...
66
+ modules: {
67
+ // ...
68
+ ImageResize: {},
69
+ },
70
+ });
71
+ ```
72
+
73
+ Functionality is broken down into modules, which can be mixed and matched as you like. For example,
74
+ the default is to include all modules:
75
+
76
+ ```javascript
77
+ const quill = new Quill(editor, {
78
+ // ...
79
+ modules: {
80
+ // ...
81
+ ImageResize: {
82
+ modules: ["Resize", "DisplaySize"],
83
+ },
84
+ },
85
+ });
86
+ ```
87
+
88
+ Each module is described below.
89
+
90
+ #### `Resize` - Resize the image
91
+
92
+ Adds handles to the image's corners which can be dragged with the mouse to resize the image.
93
+
94
+ The look and feel can be controlled with options:
95
+
96
+ ```javascript
97
+ var quill = new Quill(editor, {
98
+ // ...
99
+ modules: {
100
+ // ...
101
+ ImageResize: {
102
+ // ...
103
+ handleStyles: {
104
+ backgroundColor: "black",
105
+ border: "none",
106
+ color: white,
107
+ // other camelCase styles for size display
108
+ },
109
+ },
110
+ },
111
+ });
112
+ ```
113
+
114
+ #### `DisplaySize` - Display pixel size
115
+
116
+ Shows the size of the image in pixels near the bottom right of the image.
117
+
118
+ The look and feel can be controlled with options:
119
+
120
+ ```javascript
121
+ var quill = new Quill(editor, {
122
+ // ...
123
+ modules: {
124
+ // ...
125
+ ImageResize: {
126
+ // ...
127
+ displayStyles: {
128
+ backgroundColor: "black",
129
+ border: "none",
130
+ color: white,
131
+ // other camelCase styles for size display
132
+ },
133
+ },
134
+ },
135
+ });
136
+ ```
137
+
138
+ #### `BaseModule` - Include your own custom module
139
+
140
+ You can write your own module by extending the `BaseModule` class, and then including it in
141
+ the module setup.
142
+
143
+ For example,
144
+
145
+ ```javascript
146
+ import { Resize, BaseModule } from "quill-image-resize-module";
147
+
148
+ class MyModule extends BaseModule {
149
+ // See src/modules/BaseModule.js for documentation on the various lifecycle callbacks
150
+ }
151
+
152
+ var quill = new Quill(editor, {
153
+ // ...
154
+ modules: {
155
+ // ...
156
+ ImageResize: {
157
+ modules: [MyModule, Resize],
158
+ // ...
159
+ },
160
+ },
161
+ });
162
+ ```
package/demo/script.js ADDED
@@ -0,0 +1,21 @@
1
+ import "quill/dist/quill.snow.css";
2
+ import Quill from "quill";
3
+ import ImageResize from "../lib/ImageResize";
4
+
5
+ Quill.register("modules/imageResize", ImageResize);
6
+
7
+ console.log("Using dev environment");
8
+
9
+ let quill = new Quill("#editor", {
10
+ theme: "snow",
11
+ modules: {
12
+ imageResize: {},
13
+ },
14
+ });
15
+
16
+ let quill2 = new Quill("#editor2", {
17
+ theme: "snow",
18
+ modules: {
19
+ imageResize: {},
20
+ },
21
+ });
package/index.html ADDED
@@ -0,0 +1,40 @@
1
+ <!doctype html>
2
+ <html>
3
+
4
+ <head>
5
+ <meta charset="utf-8" />
6
+ <title>Quill Image Resize Module Demo</title>
7
+ </head>
8
+
9
+ <body>
10
+ <h1>Quill Image Resize Module Demo</h1>
11
+ <div id="editor" style="max-height: 500px; overflow: auto">
12
+ <p>Click on the Image Below to resize</p>
13
+ <p>
14
+ <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/f/f5/Typescript.svg/64px-Typescript.svg.png" />
15
+ </p>
16
+ <p>Some initial <strong>bold</strong> text</p>
17
+ <p>
18
+ <img src="https://upload.wikimedia.org/wikipedia/commons/a/a4/JavaScript_code.png" />
19
+ </p>
20
+ </div>
21
+
22
+ <h1>Editor 2</h1>
23
+ <div id="editor2" style="max-height: 500px; overflow: auto">
24
+ <p>Click on the Image Below to resize</p>
25
+ <p>
26
+ <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/f/f5/Typescript.svg/64px-Typescript.svg.png" />
27
+ </p>
28
+ <p>Some initial <strong>bold</strong> text</p>
29
+ <p>
30
+ <img src="https://upload.wikimedia.org/wikipedia/commons/a/a4/JavaScript_code.png" />
31
+ </p>
32
+ </div>
33
+ <p>
34
+ <label>Text Input:</label>
35
+ <input></input>
36
+ </p>
37
+ <script type="module" src="./demo/script.js"></script>
38
+ </body>
39
+
40
+ </html>
@@ -0,0 +1,35 @@
1
+ import type { Options } from "./types";
2
+
3
+ const DefaultOptions: Options = {
4
+ modules: ["DisplaySize", "Resize"],
5
+ minWidth: 13,
6
+ keyboardSizeDelta: 10,
7
+ overlayStyles: {
8
+ position: "absolute",
9
+ boxSizing: "border-box",
10
+ border: "1px dashed #444",
11
+ },
12
+ handleStyles: {
13
+ position: "absolute",
14
+ height: "12px",
15
+ width: "12px",
16
+ backgroundColor: "white",
17
+ border: "1px solid #777",
18
+ boxSizing: "border-box",
19
+ opacity: "0.80",
20
+ },
21
+ displayStyles: {
22
+ position: "absolute",
23
+ font: "12px/1.0 Arial, Helvetica, sans-serif",
24
+ padding: "4px 8px",
25
+ textAlign: "center",
26
+ backgroundColor: "white",
27
+ color: "#333",
28
+ border: "1px solid #777",
29
+ boxSizing: "border-box",
30
+ opacity: "0.80",
31
+ cursor: "default",
32
+ },
33
+ };
34
+
35
+ export default DefaultOptions;
@@ -0,0 +1,253 @@
1
+ import defaultsDeep from "lodash/defaultsDeep";
2
+ import type Quill from "quill";
3
+ import type { Range } from "quill";
4
+ import type { Options, ImageResizeOptions, Modules } from "./types";
5
+ import { Parchment } from "quill";
6
+ import DefaultOptions from "./DefaultOptions";
7
+ import { DisplaySize } from "./modules/DisplaySize";
8
+ import { Resize } from "./modules/Resize";
9
+
10
+ const knownModules = { DisplaySize: DisplaySize, Resize: Resize };
11
+
12
+ /**
13
+ * Custom module for quilljs to allow user to resize <img> elements
14
+ * (Works on Chrome, Edge, Safari and replaces Firefox's native resize behavior)
15
+ * @see https://quilljs.com/blog/building-a-custom-module/
16
+ */
17
+ export default class ImageResize {
18
+ quill: Quill;
19
+ options: Options;
20
+ moduleClasses: Modules;
21
+ modules: (DisplaySize | Resize)[];
22
+ img: HTMLImageElement | null = null;
23
+ overlay: HTMLDivElement | null = null;
24
+
25
+ constructor(quill: Quill, options: ImageResizeOptions = {}) {
26
+ // save the quill reference and options
27
+ this.quill = quill;
28
+
29
+ // Apply the options to our defaults, and stash them for later
30
+ // defaultsDeep doesn't do arrays as you'd expect, so we'll need to apply the classes array from options separately
31
+ let moduleClasses: Modules | false = false;
32
+ if (options.modules) {
33
+ moduleClasses = options.modules.slice();
34
+ }
35
+
36
+ // Apply options to default options
37
+ this.options = defaultsDeep({}, options, DefaultOptions) as Options;
38
+
39
+ // (see above about moduleClasses)
40
+ if (moduleClasses !== false) {
41
+ this.options.modules = moduleClasses;
42
+ }
43
+
44
+ // respond to image being selected
45
+ this.quill.root.addEventListener("click", this.handleClick);
46
+ this.quill.on("selection-change", this.handleSelectionChange);
47
+ this.quill.on("text-change", this.handleTextChange);
48
+
49
+ if (this.quill.root.parentNode instanceof HTMLElement) {
50
+ this.quill.root.parentNode.style.position =
51
+ this.quill.root.parentNode.style.position || "relative";
52
+ } else {
53
+ console.warn("parentNode is not an HTMLElement");
54
+ }
55
+
56
+ // setup modules
57
+ this.moduleClasses = this.options.modules;
58
+
59
+ this.modules = [];
60
+ }
61
+
62
+ initializeModules = () => {
63
+ this.removeModules();
64
+
65
+ this.modules = this.moduleClasses.map((ModuleClass) => {
66
+ if (typeof ModuleClass === "string") {
67
+ return new knownModules[ModuleClass](this);
68
+ } else {
69
+ return new ModuleClass(this);
70
+ }
71
+ });
72
+
73
+ this.modules.forEach((module) => {
74
+ module.onCreate();
75
+ });
76
+
77
+ this.onUpdate();
78
+ };
79
+
80
+ onUpdate = () => {
81
+ this.repositionElements();
82
+ this.modules.forEach((module) => {
83
+ module.onUpdate();
84
+ });
85
+ };
86
+
87
+ removeModules = () => {
88
+ this.modules.forEach((module) => {
89
+ module.onDestroy();
90
+ });
91
+
92
+ this.modules = [];
93
+ };
94
+
95
+ handleClick = (event: MouseEvent) => {
96
+ // chrome and webkit don't automatically select an image when it's clicked so need to do this manually
97
+ if (event.target instanceof HTMLImageElement) {
98
+ const blot = (this.quill.constructor as typeof Quill).find(event.target);
99
+ if (blot instanceof Parchment.EmbedBlot) {
100
+ this.quill.setSelection(blot.offset(this.quill.scroll), blot.length());
101
+ }
102
+ }
103
+ };
104
+
105
+ handleSelectionChange = (range: Range | null) => {
106
+ let firstImage: HTMLImageElement | null = null;
107
+
108
+ if (range) {
109
+ const blots = this.quill.scroll.descendants(
110
+ Parchment.EmbedBlot,
111
+ range.index,
112
+ range.length,
113
+ );
114
+ for (const blot of blots) {
115
+ if (blot.domNode instanceof HTMLImageElement) {
116
+ firstImage = blot.domNode;
117
+ }
118
+ }
119
+ }
120
+
121
+ if (firstImage) {
122
+ this.show(firstImage);
123
+ } else if (this.img) {
124
+ // clicked on a non image
125
+ this.hide();
126
+ }
127
+ };
128
+
129
+ handleTextChange = () => {
130
+ if (this.img) {
131
+ if (!(this.quill.constructor as typeof Quill).find(this.img)) {
132
+ this.hide();
133
+ }
134
+ }
135
+ };
136
+
137
+ show = (img: HTMLImageElement) => {
138
+ // keep track of this img element
139
+ this.img = img;
140
+
141
+ this.showOverlay();
142
+
143
+ this.initializeModules();
144
+ };
145
+
146
+ showOverlay = () => {
147
+ if (this.overlay) {
148
+ this.hideOverlay();
149
+ }
150
+
151
+ // prevent spurious text selection
152
+ this.setUserSelect("none");
153
+
154
+ this.quill.root.addEventListener("keydown", this.handleKeyboardShortcuts);
155
+
156
+ // Create and add the overlay
157
+ this.overlay = document.createElement("div");
158
+ Object.assign(this.overlay.style, this.options.overlayStyles);
159
+
160
+ this.quill.root.parentNode?.appendChild(this.overlay);
161
+
162
+ this.repositionElements();
163
+ };
164
+
165
+ hideOverlay = () => {
166
+ if (!this.overlay) {
167
+ return;
168
+ }
169
+
170
+ // Remove the overlay
171
+ this.quill.root.parentNode?.removeChild(this.overlay);
172
+ this.overlay = null;
173
+
174
+ // reset user-select
175
+ this.setUserSelect("");
176
+
177
+ this.quill.root.removeEventListener(
178
+ "keydown",
179
+ this.handleKeyboardShortcuts,
180
+ );
181
+ };
182
+
183
+ repositionElements = () => {
184
+ if (!this.overlay || !this.img) {
185
+ return;
186
+ }
187
+
188
+ // position the overlay over the image
189
+ if (this.quill.root.parentNode instanceof HTMLElement) {
190
+ const parent = this.quill.root.parentNode;
191
+ const imgRect = this.img.getBoundingClientRect();
192
+ if (imgRect.width === 0 || imgRect.height === 0) {
193
+ // Actual image is not in the DOM yet (just image tag)
194
+ // This occurs after undoing a delete, best to remove overlay
195
+ this.hide();
196
+ return;
197
+ }
198
+ const containerRect = parent.getBoundingClientRect();
199
+
200
+ Object.assign(this.overlay.style, {
201
+ left: `${imgRect.left - containerRect.left - 1 + parent.scrollLeft}px`,
202
+ top: `${imgRect.top - containerRect.top + parent.scrollTop}px`,
203
+ width: `${imgRect.width}px`,
204
+ height: `${imgRect.height}px`,
205
+ });
206
+ } else {
207
+ console.warn("parentNode is not an HTMLElement");
208
+ }
209
+ };
210
+
211
+ hide = () => {
212
+ this.hideOverlay();
213
+ this.removeModules();
214
+ this.img = null;
215
+ };
216
+
217
+ setUserSelect = (value: string) => {
218
+ // set on contenteditable element and <html>
219
+ this.quill.root.style.setProperty("user-select", value);
220
+ document.documentElement.style.setProperty("user-select", value);
221
+ };
222
+
223
+ handleKeyboardShortcuts = (event: KeyboardEvent) => {
224
+ if (event.defaultPrevented) {
225
+ return;
226
+ }
227
+
228
+ switch (event.key) {
229
+ case "+":
230
+ if (this.img) {
231
+ this.img.width = Math.max(
232
+ this.img.width + this.options.keyboardSizeDelta,
233
+ this.options.minWidth,
234
+ );
235
+ this.onUpdate();
236
+ }
237
+ break;
238
+ case "-":
239
+ if (this.img) {
240
+ this.img.width = Math.max(
241
+ this.img.width - this.options.keyboardSizeDelta,
242
+ this.options.minWidth,
243
+ );
244
+ this.onUpdate();
245
+ }
246
+ break;
247
+ default:
248
+ return;
249
+ }
250
+
251
+ event.preventDefault();
252
+ };
253
+ }
@@ -0,0 +1,49 @@
1
+ import type ImageResize from "../ImageResize";
2
+ import type { Options } from "../types";
3
+
4
+ export class BaseModule {
5
+ overlay: HTMLDivElement | null;
6
+ img: HTMLImageElement | null;
7
+ options: Options;
8
+ requestUpdate: () => void;
9
+
10
+ constructor(resizer: ImageResize) {
11
+ this.overlay = resizer.overlay;
12
+ this.img = resizer.img;
13
+ this.options = resizer.options;
14
+ this.requestUpdate = resizer.onUpdate;
15
+ }
16
+ /*
17
+ requestUpdate (passed in by the library during construction, above) can be used to let the library know that
18
+ you've changed something about the image that would require re-calculating the overlay (and all of its child
19
+ elements)
20
+
21
+ For example, if you add a margin to the element, you'll want to call this or else all the controls will be
22
+ misaligned on-screen.
23
+ */
24
+
25
+ /*
26
+ onCreate will be called when the element is clicked on
27
+
28
+ If the module has any user controls, it should create any containers that it'll need here.
29
+ The overlay has absolute positioning, and will be automatically repositioned and resized as needed, so you can
30
+ use your own absolute positioning and the 'top', 'right', etc. styles to be positioned relative to the element
31
+ on-screen.
32
+ */
33
+ onCreate = () => {};
34
+
35
+ /*
36
+ onDestroy will be called when the element is de-selected, or when this module otherwise needs to tidy up.
37
+
38
+ If you created any DOM elements in onCreate, please remove them from the DOM and destroy them here.
39
+ */
40
+ onDestroy = () => {};
41
+
42
+ /*
43
+ onUpdate will be called any time that the element is changed (e.g. resized, aligned, etc.)
44
+
45
+ This frequently happens during resize dragging, so keep computations light while here to ensure a smooth
46
+ user experience.
47
+ */
48
+ onUpdate = () => {};
49
+ }
@@ -0,0 +1,64 @@
1
+ import { BaseModule } from "./BaseModule";
2
+
3
+ export class DisplaySize extends BaseModule {
4
+ display: HTMLDivElement | null = null;
5
+
6
+ onCreate = () => {
7
+ // Create the container to hold the size display
8
+ this.display = document.createElement("div");
9
+
10
+ // Apply styles
11
+ Object.assign(this.display.style, this.options.displayStyles);
12
+
13
+ // Attach it
14
+ this.overlay?.appendChild(this.display);
15
+ };
16
+
17
+ onDestroy = () => {};
18
+
19
+ onUpdate = () => {
20
+ if (!this.display || !this.img) {
21
+ return;
22
+ }
23
+
24
+ const size = this.getCurrentSize();
25
+ this.display.innerHTML = size.join(" &times; ");
26
+ if (size[0] > 120 && size[1] > 30) {
27
+ // position on top of image
28
+ Object.assign(this.display.style, {
29
+ right: "4px",
30
+ bottom: "4px",
31
+ left: "auto",
32
+ });
33
+ } else if (this.img.style.float == "right") {
34
+ // position off bottom left
35
+ const dispRect = this.display.getBoundingClientRect();
36
+ Object.assign(this.display.style, {
37
+ right: "auto",
38
+ bottom: `-${dispRect.height + 4}px`,
39
+ left: `-${dispRect.width + 4}px`,
40
+ });
41
+ } else {
42
+ // position off bottom right
43
+ const dispRect = this.display.getBoundingClientRect();
44
+ Object.assign(this.display.style, {
45
+ right: `-${dispRect.width + 4}px`,
46
+ bottom: `-${dispRect.height + 4}px`,
47
+ left: "auto",
48
+ });
49
+ }
50
+ };
51
+
52
+ getCurrentSize = () => {
53
+ if (this.img) {
54
+ return [
55
+ this.img.width,
56
+ Math.round(
57
+ (this.img.width / this.img.naturalWidth) * this.img.naturalHeight,
58
+ ),
59
+ ];
60
+ } else {
61
+ return [0, 0];
62
+ }
63
+ };
64
+ }
@@ -0,0 +1,149 @@
1
+ import { BaseModule } from "./BaseModule";
2
+
3
+ export class Resize extends BaseModule {
4
+ boxes: HTMLElement[] = [];
5
+ dragBox: HTMLElement | null = null;
6
+ dragStartX: number = 0;
7
+ preDragWidth: number = 0;
8
+
9
+ onCreate = () => {
10
+ // track resize handles
11
+ this.boxes = [];
12
+
13
+ // add 4 resize handles
14
+ this.addBox("nwse-resize"); // top left
15
+ this.addBox("nesw-resize"); // top right
16
+ this.addBox("nwse-resize"); // bottom right
17
+ this.addBox("nesw-resize"); // bottom left
18
+
19
+ this.positionBoxes();
20
+ };
21
+
22
+ onDestroy = () => {
23
+ for (const box of this.boxes) {
24
+ box.removeEventListener("mousedown", this.handleMousedown, false);
25
+ }
26
+
27
+ // reset drag handle cursors
28
+ this.setCursor("");
29
+ };
30
+
31
+ positionBoxes = () => {
32
+ const handleXOffset = `${-parseFloat(this.options.handleStyles.width) / 2}px`;
33
+ const handleYOffset = `${-parseFloat(this.options.handleStyles.height) / 2}px`;
34
+
35
+ // set the top and left for each drag handle
36
+ [
37
+ { left: handleXOffset, top: handleYOffset }, // top left
38
+ { right: handleXOffset, top: handleYOffset }, // top right
39
+ { right: handleXOffset, bottom: handleYOffset }, // bottom right
40
+ { left: handleXOffset, bottom: handleYOffset }, // bottom left
41
+ ].forEach((pos, idx) => {
42
+ Object.assign(this.boxes[idx].style, pos);
43
+ });
44
+ };
45
+
46
+ addBox = (cursor: string) => {
47
+ // create div element for resize handle
48
+ const box = document.createElement("div");
49
+ box.classList.add(cursor);
50
+
51
+ // Star with the specified styles
52
+ Object.assign(box.style, this.options.handleStyles);
53
+ box.style.cursor = cursor;
54
+
55
+ // Set the width/height to use 'px'
56
+ box.style.width = this.options.handleStyles.width;
57
+ box.style.height = this.options.handleStyles.height;
58
+
59
+ // listen for mousedown on each box
60
+ box.addEventListener("mousedown", this.handleMousedown, false);
61
+ box.addEventListener("touchstart", this.handleMousedown, {
62
+ passive: false,
63
+ });
64
+ // add drag handle to document
65
+ this.overlay?.appendChild(box);
66
+ // keep track of drag handle
67
+ this.boxes.push(box);
68
+ };
69
+
70
+ handleMousedown = (evt: MouseEvent | TouchEvent) => {
71
+ if (evt.target instanceof HTMLElement) {
72
+ // note which box
73
+ this.dragBox = evt.target;
74
+ // note starting mousedown position
75
+ if (evt.type === "touchstart") {
76
+ this.dragStartX = (evt as TouchEvent).changedTouches[0].clientX;
77
+ } else {
78
+ this.dragStartX = (evt as MouseEvent).clientX;
79
+ }
80
+ // store the width before the drag
81
+ if (this.img) {
82
+ this.preDragWidth = this.img.width || this.img.naturalWidth;
83
+ }
84
+ // set the proper cursor everywhere
85
+ this.setCursor(this.dragBox.style.cursor);
86
+ // listen for movement and mouseup
87
+ document.addEventListener("mousemove", this.handleDrag);
88
+ document.addEventListener("touchmove", this.handleDrag, {
89
+ passive: false,
90
+ });
91
+ document.addEventListener("mouseup", this.handleMouseup, true);
92
+ document.addEventListener("touchend", this.handleMouseup, true);
93
+ document.addEventListener("touchcancel", this.handleMouseup, true);
94
+ } else {
95
+ console.warn("mousedown target is not an HTMLElement");
96
+ }
97
+ };
98
+
99
+ handleMouseup = (evt: MouseEvent | TouchEvent) => {
100
+ evt.stopPropagation();
101
+
102
+ // reset cursor everywhere
103
+ this.setCursor("");
104
+ // stop listening for movement and mouseup
105
+ document.removeEventListener("mousemove", this.handleDrag);
106
+ document.removeEventListener("touchmove", this.handleDrag);
107
+ document.removeEventListener("mouseup", this.handleMouseup, true);
108
+ document.removeEventListener("touchend", this.handleMouseup, true);
109
+ document.removeEventListener("touchcancel", this.handleMouseup, true);
110
+ };
111
+
112
+ handleDrag = (evt: MouseEvent | TouchEvent) => {
113
+ if (!this.img) {
114
+ // image not set yet
115
+ return;
116
+ }
117
+ // update image size
118
+ let clientX: number;
119
+ if (evt.type === "touchmove") {
120
+ clientX = (evt as TouchEvent).changedTouches[0].clientX;
121
+ } else {
122
+ clientX = (evt as MouseEvent).clientX;
123
+ }
124
+
125
+ const deltaX = clientX - this.dragStartX;
126
+ if (this.dragBox === this.boxes[0] || this.dragBox === this.boxes[3]) {
127
+ // left-side resize handler; dragging right shrinks image
128
+ this.img.width = Math.max(
129
+ Math.round(this.preDragWidth - deltaX),
130
+ this.options.minWidth,
131
+ );
132
+ } else {
133
+ // right-side resize handler; dragging right enlarges image
134
+ this.img.width = Math.max(
135
+ Math.round(this.preDragWidth + deltaX),
136
+ this.options.minWidth,
137
+ );
138
+ }
139
+ this.requestUpdate();
140
+ };
141
+
142
+ setCursor = (value: string) => {
143
+ [document.body, this.img].forEach((el) => {
144
+ if (el) {
145
+ el.style.cursor = value; // eslint-disable-line no-param-reassign
146
+ }
147
+ });
148
+ };
149
+ }
package/lib/types.ts ADDED
@@ -0,0 +1,73 @@
1
+ import type { DisplaySize } from "./modules/DisplaySize";
2
+ import type { Resize } from "./modules/Resize";
3
+
4
+ export type Modules = (
5
+ | "DisplaySize"
6
+ | "Resize"
7
+ | typeof DisplaySize
8
+ | typeof Resize
9
+ )[];
10
+
11
+ export type Options = {
12
+ modules: Modules;
13
+ minWidth: number;
14
+ keyboardSizeDelta: number;
15
+ overlayStyles: {
16
+ position: string;
17
+ boxSizing: string;
18
+ border: string;
19
+ };
20
+ handleStyles: {
21
+ position: string;
22
+ height: string;
23
+ width: string;
24
+ backgroundColor: string;
25
+ border: string;
26
+ boxSizing: string;
27
+ opacity: string;
28
+ };
29
+ displayStyles: {
30
+ position: string;
31
+ font: string;
32
+ padding: string;
33
+ textAlign: string;
34
+ backgroundColor: string;
35
+ color: string;
36
+ border: string;
37
+ boxSizing: string;
38
+ opacity: string;
39
+ cursor: string;
40
+ };
41
+ };
42
+
43
+ export type ImageResizeOptions = {
44
+ modules?: Modules;
45
+ minWidth?: number;
46
+ keyboardSizeDelta?: number;
47
+ overlayStyles?: {
48
+ position?: string;
49
+ boxSizing?: string;
50
+ border?: string;
51
+ };
52
+ handleStyles?: {
53
+ position?: string;
54
+ height?: string;
55
+ width?: string;
56
+ backgroundColor?: string;
57
+ border?: string;
58
+ boxSizing?: string;
59
+ opacity?: string;
60
+ };
61
+ displayStyles?: {
62
+ position?: string;
63
+ font?: string;
64
+ padding?: string;
65
+ textAlign?: string;
66
+ backgroundColor?: string;
67
+ colo?: string;
68
+ border?: string;
69
+ boxSizing?: string;
70
+ opacity?: string;
71
+ cursor?: string;
72
+ };
73
+ };
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@mgreminger/quill-image-resize-module",
3
+ "private": false,
4
+ "version": "1.0.0",
5
+ "description": "A module for Quill rich text editor to allow images to be resized.",
6
+ "type": "module",
7
+ "scripts": {
8
+ "dev": "vite --port 5173",
9
+ "build": "vite build",
10
+ "test": "playwright test",
11
+ "lint": "prettier --write .",
12
+ "build-preview": "vite build --config vite.preview.config.js",
13
+ "preview": "vite preview --port 5173 --config vite.preview.config.js",
14
+ "deploy-gh-pages": "gh-pages -d dist-preview"
15
+ },
16
+ "devDependencies": {
17
+ "@playwright/test": "^1.50.1",
18
+ "@rollup/plugin-typescript": "^12.1.2",
19
+ "@types/lodash": "^4.17.15",
20
+ "@types/node": "^22.13.0",
21
+ "gh-pages": "^6.3.0",
22
+ "prettier": "^3.4.2",
23
+ "quill": "^2.0.3",
24
+ "tslib": "^2.8.1",
25
+ "typescript": "^5.7.3",
26
+ "vite": "^6.0.11"
27
+ },
28
+ "dependencies": {
29
+ "lodash": "^4.17.4"
30
+ }
31
+ }
@@ -0,0 +1,79 @@
1
+ import { defineConfig, devices } from "@playwright/test";
2
+
3
+ /**
4
+ * Read environment variables from file.
5
+ * https://github.com/motdotla/dotenv
6
+ */
7
+ // import dotenv from 'dotenv';
8
+ // import path from 'path';
9
+ // dotenv.config({ path: path.resolve(__dirname, '.env') });
10
+
11
+ /**
12
+ * See https://playwright.dev/docs/test-configuration.
13
+ */
14
+ export default defineConfig({
15
+ testDir: "./tests",
16
+ /* Run tests in files in parallel */
17
+ fullyParallel: true,
18
+ /* Fail the build on CI if you accidentally left test.only in the source code. */
19
+ forbidOnly: !!process.env.CI,
20
+ /* Retry on CI only */
21
+ retries: process.env.CI ? 2 : 0,
22
+ /* Opt out of parallel tests on CI. */
23
+ workers: process.env.CI ? 1 : undefined,
24
+ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
25
+ reporter: "html",
26
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
27
+ use: {
28
+ /* Base URL to use in actions like `await page.goto('/')`. */
29
+ // baseURL: 'http://127.0.0.1:3000',
30
+
31
+ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
32
+ trace: "on-first-retry",
33
+ },
34
+
35
+ /* Configure projects for major browsers */
36
+ projects: [
37
+ {
38
+ name: "chromium",
39
+ use: { ...devices["Desktop Chrome"] },
40
+ },
41
+
42
+ {
43
+ name: "firefox",
44
+ use: { ...devices["Desktop Firefox"] },
45
+ },
46
+
47
+ {
48
+ name: "webkit",
49
+ use: { ...devices["Desktop Safari"] },
50
+ },
51
+
52
+ /* Test against mobile viewports. */
53
+ // {
54
+ // name: 'Mobile Chrome',
55
+ // use: { ...devices['Pixel 5'] },
56
+ // },
57
+ // {
58
+ // name: 'Mobile Safari',
59
+ // use: { ...devices['iPhone 12'] },
60
+ // },
61
+
62
+ /* Test against branded browsers. */
63
+ // {
64
+ // name: 'Microsoft Edge',
65
+ // use: { ...devices['Desktop Edge'], channel: 'msedge' },
66
+ // },
67
+ // {
68
+ // name: 'Google Chrome',
69
+ // use: { ...devices['Desktop Chrome'], channel: 'chrome' },
70
+ // },
71
+ ],
72
+
73
+ /* Run your local dev server before starting the tests */
74
+ // webServer: {
75
+ // command: 'npm run start',
76
+ // url: 'http://127.0.0.1:3000',
77
+ // reuseExistingServer: !process.env.CI,
78
+ // },
79
+ });
@@ -0,0 +1,40 @@
1
+ <!doctype html>
2
+ <html>
3
+
4
+ <head>
5
+ <meta charset="utf-8" />
6
+ <title>Quill Image Resize Module Demo</title>
7
+ </head>
8
+
9
+ <body>
10
+ <h1>Quill Image Resize Module Demo</h1>
11
+ <div id="editor" style="max-height: 500px; overflow: auto">
12
+ <p>Click on the Image Below to resize</p>
13
+ <p>
14
+ <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/f/f5/Typescript.svg/64px-Typescript.svg.png" />
15
+ </p>
16
+ <p>Some initial <strong>bold</strong> text</p>
17
+ <p>
18
+ <img src="https://upload.wikimedia.org/wikipedia/commons/a/a4/JavaScript_code.png" />
19
+ </p>
20
+ </div>
21
+
22
+ <h1>Editor 2</h1>
23
+ <div id="editor2" style="max-height: 500px; overflow: auto">
24
+ <p>Click on the Image Below to resize</p>
25
+ <p>
26
+ <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/f/f5/Typescript.svg/64px-Typescript.svg.png" />
27
+ </p>
28
+ <p>Some initial <strong>bold</strong> text</p>
29
+ <p>
30
+ <img src="https://upload.wikimedia.org/wikipedia/commons/a/a4/JavaScript_code.png" />
31
+ </p>
32
+ </div>
33
+ <p>
34
+ <label>Text Input:</label>
35
+ <input></input>
36
+ </p>
37
+ <script type="module" src="./script.js"></script>
38
+ </body>
39
+
40
+ </html>
@@ -0,0 +1,21 @@
1
+ import "quill/dist/quill.snow.css";
2
+ import Quill from "quill";
3
+ import ImageResize from "../dist/quill-image-resize-module";
4
+
5
+ Quill.register("modules/imageResize", ImageResize);
6
+
7
+ console.log("Using production environment");
8
+
9
+ let quill = new Quill("#editor", {
10
+ theme: "snow",
11
+ modules: {
12
+ imageResize: {},
13
+ },
14
+ });
15
+
16
+ let quill2 = new Quill("#editor2", {
17
+ theme: "snow",
18
+ modules: {
19
+ imageResize: {},
20
+ },
21
+ });
@@ -0,0 +1,39 @@
1
+ import { test, expect } from "@playwright/test";
2
+
3
+ test("resize using mouse drag", async ({ page, browserName }) => {
4
+ await page.goto("http://localhost:5173/");
5
+
6
+ await page.locator("img").first().click();
7
+
8
+ await expect(page.locator("text=64 × 64")).toBeVisible();
9
+
10
+ const bounds = await page.locator("div.nwse-resize").nth(1).boundingBox();
11
+
12
+ expect(bounds).toBeTruthy();
13
+
14
+ const mouseX = bounds!.x + bounds!.width / 2;
15
+ const mouseY = bounds!.y + bounds!.height / 2;
16
+
17
+ await page.locator("div.nwse-resize").nth(1).hover();
18
+ await page.mouse.down();
19
+ await page.mouse.move(mouseX + 30, mouseY);
20
+ await page.mouse.up();
21
+
22
+ await expect(page.locator("text=94 × 94")).toBeVisible();
23
+
24
+ expect(await page.locator("img").first().getAttribute("width")).toBe("94");
25
+ });
26
+
27
+ test("resize using keyboard shortcuts", async ({ page, browserName }) => {
28
+ await page.goto("http://localhost:5173/");
29
+
30
+ await page.locator("img").first().click();
31
+
32
+ await expect(page.locator("text=64 × 64")).toBeVisible();
33
+
34
+ for (let i = 0; i < 3; i++) await page.keyboard.press("+");
35
+
36
+ await expect(page.locator("text=94 × 94")).toBeVisible();
37
+
38
+ expect(await page.locator("img").first().getAttribute("width")).toBe("94");
39
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "module": "ESNext",
6
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
7
+ "skipLibCheck": true,
8
+
9
+ /* Bundler mode */
10
+ "moduleResolution": "bundler",
11
+ "emitDeclarationOnly": true,
12
+ "declarationDir": "./dist",
13
+ "allowImportingTsExtensions": true,
14
+ "isolatedModules": true,
15
+ "moduleDetection": "force",
16
+ "declaration": true,
17
+
18
+ /* Linting */
19
+ "strict": true,
20
+ "noUnusedLocals": true,
21
+ "noUnusedParameters": true,
22
+ "noFallthroughCasesInSwitch": true,
23
+ "noUncheckedSideEffectImports": true
24
+ },
25
+ "include": ["lib"]
26
+ }
package/vite.config.js ADDED
@@ -0,0 +1,28 @@
1
+ import path, { dirname, resolve } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { defineConfig } from "vite";
4
+ import typescript from "@rollup/plugin-typescript";
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+
8
+ export default defineConfig({
9
+ build: {
10
+ lib: {
11
+ entry: resolve(__dirname, "lib/ImageResize.ts"),
12
+ name: "ImageResize",
13
+ fileName: "quill-image-resize-module",
14
+ },
15
+ rollupOptions: {
16
+ plugins: [
17
+ typescript({
18
+ noForceEmit: true,
19
+ }),
20
+ ],
21
+ external: ["quill"],
22
+ output: {
23
+ globals: { quill: "Quill" },
24
+ },
25
+ },
26
+ minify: false,
27
+ },
28
+ });
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from "vite";
2
+
3
+ export default defineConfig({
4
+ root: "./preview",
5
+ build: {
6
+ outDir: "../dist-preview",
7
+ emptyOutDir: true
8
+ },
9
+ base: './'
10
+ });