@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.
- package/.github/workflows/playwright.yml +33 -0
- package/LICENSE +20 -0
- package/README.md +162 -0
- package/demo/script.js +21 -0
- package/index.html +40 -0
- package/lib/DefaultOptions.ts +35 -0
- package/lib/ImageResize.ts +253 -0
- package/lib/modules/BaseModule.ts +49 -0
- package/lib/modules/DisplaySize.ts +64 -0
- package/lib/modules/Resize.ts +149 -0
- package/lib/types.ts +73 -0
- package/package.json +31 -0
- package/playwright.config.ts +79 -0
- package/preview/index.html +40 -0
- package/preview/script.js +21 -0
- package/tests/test_resize.spec.ts +39 -0
- package/tsconfig.json +26 -0
- package/vite.config.js +28 -0
- package/vite.preview.config.js +10 -0
|
@@ -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(" × ");
|
|
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
|
+
});
|