diff --git a/app/assets/javascripts/activeadmin/quill.imageUploader.js b/app/assets/javascripts/activeadmin/quill.imageUploader.js new file mode 100644 index 0000000..fdc32c9 --- /dev/null +++ b/app/assets/javascripts/activeadmin/quill.imageUploader.js @@ -0,0 +1,198 @@ +import LoadingImage from "./blots/image.js"; + +class ImageUploader { + constructor(quill, options) { + this.quill = quill; + this.options = options; + this.range = null; + this.placeholderDelta = null; + + if (typeof this.options.upload !== "function") + console.warn( + "[Missing config] upload function that returns a promise is required" + ); + + var toolbar = this.quill.getModule("toolbar"); + if (toolbar) { + toolbar.addHandler("image", this.selectLocalImage.bind(this)); + } + + this.handleDrop = this.handleDrop.bind(this); + this.handlePaste = this.handlePaste.bind(this); + + this.quill.root.addEventListener("drop", this.handleDrop, false); + this.quill.root.addEventListener("paste", this.handlePaste, false); + } + + selectLocalImage() { + this.quill.focus(); + this.range = this.quill.getSelection(); + this.fileHolder = document.createElement("input"); + this.fileHolder.setAttribute("type", "file"); + this.fileHolder.setAttribute("accept", "image/*"); + this.fileHolder.setAttribute("style", "visibility:hidden"); + + this.fileHolder.onchange = this.fileChanged.bind(this); + + document.body.appendChild(this.fileHolder); + + this.fileHolder.click(); + + window.requestAnimationFrame(() => { + document.body.removeChild(this.fileHolder); + }); + } + + handleDrop(evt) { + if ( + evt.dataTransfer && + evt.dataTransfer.files && + evt.dataTransfer.files.length + ) { + evt.stopPropagation(); + evt.preventDefault(); + if (document.caretRangeFromPoint) { + const selection = document.getSelection(); + const range = document.caretRangeFromPoint(evt.clientX, evt.clientY); + if (selection && range) { + selection.setBaseAndExtent( + range.startContainer, + range.startOffset, + range.startContainer, + range.startOffset + ); + } + } else { + const selection = document.getSelection(); + const range = document.caretPositionFromPoint(evt.clientX, evt.clientY); + if (selection && range) { + selection.setBaseAndExtent( + range.offsetNode, + range.offset, + range.offsetNode, + range.offset + ); + } + } + + this.quill.focus(); + this.range = this.quill.getSelection(); + let file = evt.dataTransfer.files[0]; + + setTimeout(() => { + this.quill.focus(); + this.range = this.quill.getSelection(); + this.readAndUploadFile(file); + }, 0); + } + } + + handlePaste(evt) { + let clipboard = evt.clipboardData || window.clipboardData; + + // IE 11 is .files other browsers are .items + if (clipboard && (clipboard.items || clipboard.files)) { + let items = clipboard.items || clipboard.files; + const IMAGE_MIME_REGEX = /^image\/(jpe?g|gif|png|svg|webp)$/i; + + for (let i = 0; i < items.length; i++) { + if (IMAGE_MIME_REGEX.test(items[i].type)) { + let file = items[i].getAsFile ? items[i].getAsFile() : items[i]; + + if (file) { + this.quill.focus(); + this.range = this.quill.getSelection(); + evt.preventDefault(); + setTimeout(() => { + this.quill.focus(); + this.range = this.quill.getSelection(); + this.readAndUploadFile(file); + }, 0); + } + } + } + } + } + + readAndUploadFile(file) { + let isUploadReject = false; + + const fileReader = new FileReader(); + + fileReader.addEventListener( + "load", + () => { + if (!isUploadReject) { + let base64ImageSrc = fileReader.result; + this.insertBase64Image(base64ImageSrc); + } + }, + false + ); + + if (file) { + fileReader.readAsDataURL(file); + } + + this.options.upload(file).then( + (imageUrl) => { + this.insertToEditor(imageUrl); + }, + (error) => { + isUploadReject = true; + this.removeBase64Image(); + console.warn(error); + } + ); + } + + fileChanged() { + const file = this.fileHolder.files[0]; + this.readAndUploadFile(file); + } + + insertBase64Image(url) { + const range = this.range; + + this.placeholderDelta = this.quill.insertEmbed( + range.index, + LoadingImage.blotName, + `${url}`, + "user" + ); + } + + insertToEditor(url) { + const range = this.range; + + const lengthToDelete = this.calculatePlaceholderInsertLength(); + + // Delete the placeholder image + this.quill.deleteText(range.index, lengthToDelete, "user"); + // Insert the server saved image + this.quill.insertEmbed(range.index, "image", `${url}`, "user"); + + range.index++; + this.quill.setSelection(range, "user"); + } + + // The length of the insert delta from insertBase64Image can vary depending on what part of the line the insert occurs + calculatePlaceholderInsertLength() { + return this.placeholderDelta.ops.reduce((accumulator, deltaOperation) => { + if (deltaOperation.hasOwnProperty('insert')) + accumulator++; + + return accumulator; + }, 0); + } + + removeBase64Image() { + const range = this.range; + const lengthToDelete = this.calculatePlaceholderInsertLength(); + + this.quill.deleteText(range.index, lengthToDelete, "user"); + } +} + +window.ImageUploader = ImageUploader; +export default ImageUploader; \ No newline at end of file diff --git a/app/assets/javascripts/activeadmin/quill.imageUploader.min.js b/app/assets/javascripts/activeadmin/quill.imageUploader.min.js index 64fe6e6..480a34e 100644 --- a/app/assets/javascripts/activeadmin/quill.imageUploader.min.js +++ b/app/assets/javascripts/activeadmin/quill.imageUploader.min.js @@ -1 +1 @@ -!function(e){var t={};function n(i){if(t[i])return t[i].exports;var r=t[i]={i:i,l:!1,exports:{}};return e[i].call(r.exports,r,r.exports,n),r.l=!0,r.exports}n.m=e,n.c=t,n.d=function(e,t,i){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:i})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var i=Object.create(null);if(n.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var r in e)n.d(i,r,function(t){return e[t]}.bind(null,r));return i},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=1)}([function(e,t){e.exports=Quill},function(e,t,n){"use strict";n.r(t);var i=n(0),r=n.n(i),o=function(){function e(e,t){for(var n=0;n