Skip to content

Commit 5196a88

Browse files
committed
build: Update quill.imageUploader plugin
1 parent 5e9a3b6 commit 5196a88

4 files changed

Lines changed: 238 additions & 1 deletion

File tree

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import LoadingImage from "./blots/image.js";
2+
3+
class ImageUploader {
4+
constructor(quill, options) {
5+
this.quill = quill;
6+
this.options = options;
7+
this.range = null;
8+
this.placeholderDelta = null;
9+
10+
if (typeof this.options.upload !== "function")
11+
console.warn(
12+
"[Missing config] upload function that returns a promise is required"
13+
);
14+
15+
var toolbar = this.quill.getModule("toolbar");
16+
if (toolbar) {
17+
toolbar.addHandler("image", this.selectLocalImage.bind(this));
18+
}
19+
20+
this.handleDrop = this.handleDrop.bind(this);
21+
this.handlePaste = this.handlePaste.bind(this);
22+
23+
this.quill.root.addEventListener("drop", this.handleDrop, false);
24+
this.quill.root.addEventListener("paste", this.handlePaste, false);
25+
}
26+
27+
selectLocalImage() {
28+
this.quill.focus();
29+
this.range = this.quill.getSelection();
30+
this.fileHolder = document.createElement("input");
31+
this.fileHolder.setAttribute("type", "file");
32+
this.fileHolder.setAttribute("accept", "image/*");
33+
this.fileHolder.setAttribute("style", "visibility:hidden");
34+
35+
this.fileHolder.onchange = this.fileChanged.bind(this);
36+
37+
document.body.appendChild(this.fileHolder);
38+
39+
this.fileHolder.click();
40+
41+
window.requestAnimationFrame(() => {
42+
document.body.removeChild(this.fileHolder);
43+
});
44+
}
45+
46+
handleDrop(evt) {
47+
if (
48+
evt.dataTransfer &&
49+
evt.dataTransfer.files &&
50+
evt.dataTransfer.files.length
51+
) {
52+
evt.stopPropagation();
53+
evt.preventDefault();
54+
if (document.caretRangeFromPoint) {
55+
const selection = document.getSelection();
56+
const range = document.caretRangeFromPoint(evt.clientX, evt.clientY);
57+
if (selection && range) {
58+
selection.setBaseAndExtent(
59+
range.startContainer,
60+
range.startOffset,
61+
range.startContainer,
62+
range.startOffset
63+
);
64+
}
65+
} else {
66+
const selection = document.getSelection();
67+
const range = document.caretPositionFromPoint(evt.clientX, evt.clientY);
68+
if (selection && range) {
69+
selection.setBaseAndExtent(
70+
range.offsetNode,
71+
range.offset,
72+
range.offsetNode,
73+
range.offset
74+
);
75+
}
76+
}
77+
78+
this.quill.focus();
79+
this.range = this.quill.getSelection();
80+
let file = evt.dataTransfer.files[0];
81+
82+
setTimeout(() => {
83+
this.quill.focus();
84+
this.range = this.quill.getSelection();
85+
this.readAndUploadFile(file);
86+
}, 0);
87+
}
88+
}
89+
90+
handlePaste(evt) {
91+
let clipboard = evt.clipboardData || window.clipboardData;
92+
93+
// IE 11 is .files other browsers are .items
94+
if (clipboard && (clipboard.items || clipboard.files)) {
95+
let items = clipboard.items || clipboard.files;
96+
const IMAGE_MIME_REGEX = /^image\/(jpe?g|gif|png|svg|webp)$/i;
97+
98+
for (let i = 0; i < items.length; i++) {
99+
if (IMAGE_MIME_REGEX.test(items[i].type)) {
100+
let file = items[i].getAsFile ? items[i].getAsFile() : items[i];
101+
102+
if (file) {
103+
this.quill.focus();
104+
this.range = this.quill.getSelection();
105+
evt.preventDefault();
106+
setTimeout(() => {
107+
this.quill.focus();
108+
this.range = this.quill.getSelection();
109+
this.readAndUploadFile(file);
110+
}, 0);
111+
}
112+
}
113+
}
114+
}
115+
}
116+
117+
readAndUploadFile(file) {
118+
let isUploadReject = false;
119+
120+
const fileReader = new FileReader();
121+
122+
fileReader.addEventListener(
123+
"load",
124+
() => {
125+
if (!isUploadReject) {
126+
let base64ImageSrc = fileReader.result;
127+
this.insertBase64Image(base64ImageSrc);
128+
}
129+
},
130+
false
131+
);
132+
133+
if (file) {
134+
fileReader.readAsDataURL(file);
135+
}
136+
137+
this.options.upload(file).then(
138+
(imageUrl) => {
139+
this.insertToEditor(imageUrl);
140+
},
141+
(error) => {
142+
isUploadReject = true;
143+
this.removeBase64Image();
144+
console.warn(error);
145+
}
146+
);
147+
}
148+
149+
fileChanged() {
150+
const file = this.fileHolder.files[0];
151+
this.readAndUploadFile(file);
152+
}
153+
154+
insertBase64Image(url) {
155+
const range = this.range;
156+
157+
this.placeholderDelta = this.quill.insertEmbed(
158+
range.index,
159+
LoadingImage.blotName,
160+
`${url}`,
161+
"user"
162+
);
163+
}
164+
165+
insertToEditor(url) {
166+
const range = this.range;
167+
168+
const lengthToDelete = this.calculatePlaceholderInsertLength();
169+
170+
// Delete the placeholder image
171+
this.quill.deleteText(range.index, lengthToDelete, "user");
172+
// Insert the server saved image
173+
this.quill.insertEmbed(range.index, "image", `${url}`, "user");
174+
175+
range.index++;
176+
this.quill.setSelection(range, "user");
177+
}
178+
179+
// The length of the insert delta from insertBase64Image can vary depending on what part of the line the insert occurs
180+
calculatePlaceholderInsertLength() {
181+
return this.placeholderDelta.ops.reduce((accumulator, deltaOperation) => {
182+
if (deltaOperation.hasOwnProperty('insert'))
183+
accumulator++;
184+
185+
return accumulator;
186+
}, 0);
187+
}
188+
189+
removeBase64Image() {
190+
const range = this.range;
191+
const lengthToDelete = this.calculatePlaceholderInsertLength();
192+
193+
this.quill.deleteText(range.index, lengthToDelete, "user");
194+
}
195+
}
196+
197+
window.ImageUploader = ImageUploader;
198+
export default ImageUploader;

app/assets/javascripts/activeadmin/quill.imageUploader.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
.image-uploading {
2+
position: relative;
3+
display: inline-block;
4+
}
5+
6+
.image-uploading img {
7+
max-width: 98% !important;
8+
filter: blur(5px);
9+
opacity: 0.3;
10+
}
11+
12+
.image-uploading::before {
13+
content: "";
14+
box-sizing: border-box;
15+
position: absolute;
16+
top: 50%;
17+
left: 50%;
18+
width: 30px;
19+
height: 30px;
20+
margin-top: -15px;
21+
margin-left: -15px;
22+
border-radius: 50%;
23+
border: 3px solid #ccc;
24+
border-top-color: #1e986c;
25+
z-index: 1;
26+
animation: spinner 0.6s linear infinite;
27+
}
28+
29+
@keyframes spinner {
30+
to {
31+
transform: rotate(360deg);
32+
}
33+
}

0 commit comments

Comments
 (0)