I have read many issues about uploading images, such as #633 #863.
When selecting an picture, the picture show immediately in the editor with base64 url, which was the default behaviour in snow/bubble theme.BUT I expect to replace the base64 url with the url getting from server.How can I solve the problem?
you can add custom toolbar handler. doc
Briefly speaking,
add your own image handler and upload your image to get the img url. then you can call insertEmbed to insert your Image blot into your editor.
Actually, it's not a problem regarding replacing the base64 url,
but you __get__ the file in the event you handle. (just register the handler, quill brings you the file)
so the codes might look like below:
modules: {
toolbar: {
handlers: {
image: this.imageHandler
}, ...
imageHandler() {
const input = document.createElement('input');
input.setAttribute('type', 'file');
input.setAttribute('accept', 'image/*');
input.click();
input.onchange = async function() {
const file = input.files[0];
console.log('User trying to uplaod this:', file);
const id = await uploadFile(file); // I'm using react, so whatever upload function
const range = this.quill.getSelection();
const link = `${ROOT_URL}/file/${id}`;
// this part the image is inserted
// by 'image' option below, you just have to put src(link) of img here.
this.quill.insertEmbed(range.index, 'image', link);
}.bind(this); // react thing
}
@lwyj123 @seongbin9786 Thanks. it's my fault to improperly describe the problem。
At first, I inserted image placeholder with the base64 url so that it could show immediately when picture was selected.And there were some other state such as the uploading state in the placeholder. Then when the picture finished uploading, inserting the image with right getting-from-server url to replace the placeholder that I inserted previously.
Hi, @zhatongning! I've been thinking about doing the exact same thing. Have you found a way to replace your base64 preview with the URL uploaded from server?
Adding to @seongbin9786 answer, this code adds a loading placeholder image and replaces it once the image has been successfully uploaded.
const imageHandler = () => {
const input = document.createElement('input');
input.setAttribute('type', 'file');
input.setAttribute('accept', 'image/*');
input.click();
input.onchange = async () => {
const file = input.files[0];
const formData = new FormData();
formData.append('image', file);
// Save current cursor state
const range = this.quill.getSelection(true);
// Insert temporary loading placeholder image
this.quill.insertEmbed(range.index, 'image', `${ window.location.origin }/images/loaders/placeholder.gif`);
// Move cursor to right side of image (easier to continue typing)
this.quill.setSelection(range.index + 1);
const res = await apiPostNewsImage(formData); // API post, returns image location as string e.g. 'http://www.example.com/images/foo.png'
// Remove placeholder image
this.quill.deleteText(range.index, 1);
// Insert uploaded image
this.quill.insertEmbed(range.index, 'image', res.body.image);
}
}
Thanks everyone, just one thing, it show be very nice to have this kind of examples in the official documentation. It avoids to search everywhere to find something like that ;)
@seongbin9786 @james-brndwgn What's this.quill here? Where did it come from? Can you please post any complete class code sample?
hey @seongbin9786 @james-brndwgn, adding custom image handler causing issues with editor typing. Editor can only type one character and then lose focus. Issue only comes up when editor is exported as functional component, and works fine as class component.
Here is the codesandbox example
Thank you @james-brndwgn, Your code was very helpful.
By default, ActionText(Rails6) use Trix, but I tried to use Quill on ActionText.
My code is below. I hope it will help for somebody.
onMountQuitEditor(){
let editor = this.querySelector('.editor')
let toolbar = this.querySelector('.toolbar')
this.quill = new Quill(editor, {
modules: { toolbar: {
container: toolbar,
handlers: { image: this.onImage }
}
},
theme: 'snow'
})
}
onImage(){
const input = document.createElement('input');
input.setAttribute('type', 'file');
input.setAttribute('accept', 'image/*');
input.click();
input.onchange = async () => {
const file = input.files[0];
const range = this.quill.getSelection(true);
const attachment = {
file: file,
range: range,
uploadDidCompleteCallback: (attachment, attributes) => this.onUploadComplete(attachment, attributes)
}
if (attachment.file) {
const upload = new AttachmentUpload(attachment, this.rootElement())
upload.start()
}
}
}
onUploadComplete(attachment, attributes){
const range = attachment.range //this.quill.getSelection(true);
// Insert temporary loading placeholder image
// this.quill.insertEmbed(range.index, 'image', `${ window.location.origin }/images/loaders/placeholder.gif`);
// Move cursor to right side of image (easier to continue typing)
this.quill.setSelection(range.index + 1);
// Remove placeholder image
// this.quill.deleteText(range.index, 1);
// Insert uploaded image
this.quill.insertEmbed(range.index, 'image', attachment.url);
}
// AttachmentUpload class is copied from @rails/actiontext/app/javascript/actiontext/attachment_upload.js
// and adjusted for Quill instead of Trix.
// https://github.com/rails/rails/blob/master/actiontext/app/javascript/actiontext/attachment_upload.js
import { DirectUpload } from "@rails/activestorage"
export class AttachmentUpload {
attachment: any;
element: any;
directUpload: DirectUpload;
constructor(attachment, element) {
this.attachment = attachment
this.element = element
this.directUpload = new DirectUpload(attachment.file, this.directUploadUrl, this)
}
start() {
this.directUpload.create(this.directUploadDidComplete.bind(this))
}
directUploadWillStoreFileWithXHR(xhr) {
xhr.upload.addEventListener("progress", event => {
// to show progress
// const progress = event.loaded / event.total * 100
// this.attachment.setUploadProgress(progress)
})
}
directUploadDidComplete(error, attributes) {
if (error) {
throw new Error(`Direct upload failed: ${error}`)
}
this.attachment['ssid'] = attributes.attachable_sgid;
this.attachment['url'] = this.createBlobUrl(attributes.signed_id, attributes.filename)
this.attachment.uploadDidCompleteCallback(this.attachment, attributes)
// this.attachment.setAttributes({
// sgid: attributes.attachable_sgid,
// url: this.createBlobUrl(attributes.signed_id, attributes.filename)
// })
}
createBlobUrl(signedId, filename) {
return this.blobUrlTemplate
.replace(":signed_id", signedId)
.replace(":filename", encodeURIComponent(filename))
}
get directUploadUrl() {
// html tag must have attribute 'data-direct-upload-url'
return this.element.dataset.directUploadUrl
}
get blobUrlTemplate() {
// html tag must have attribute 'data-blob-url-template'
return this.element.dataset.blobUrlTemplate
}
}
This is how I replaced placeholder image with my firebase storage image:
`// current cursor state
const range = this.quillEditorRef.getSelection(true);
// placeholder image
this.quillEditorRef.insertEmbed(range.index, 'image', e.target.result);
// method for saving in to storage
await this.saveToStorage(file);
const srcElem: HTMLImageElement = document.querySelector('[src^="data:image/"]');
srcElem.src = url; // url from your uploaded image`
Using an image handler like the following works when selecting an image from the toolbar:
new Quill(element, {
modules: {
toolbar: {
handlers: {
image: imageHandler
}
}
},
...
})
...but this doesn't handle replacing pasted images which are inserted as base64. What is the best way to replace the base64 url with an uploaded image url?
@sebastiansandqvist This may help you:
I am handling image upload(to server) using a Modal component.
imageHandler = () => {
this.setState({isUploadImageModalOpen: true});
};
//the Modal would contain a file selector as(ofcourse inside the render()).
//if isUploadImageModalOpen is true then open the modal with below code:
<input type="file" accept={"image/jpeg, image/png"} onChange={(e) => this.handleSelectedImage(range, e.target.files[0])}/>
When the modal opens the quill forgets the cursor position so, below is the code I store the position
let range = this.quillRef && this.quillRef.getSelection();//File selector force the editor to loose the focus and the file gets added at the zero index. This solves the problem by saving cursor position.
The rest of the code is self explanatory.
uploadToServer = (file) => {
if (file) {
this.setState({isUploading: true});
const data = new FormData();
data.append("file", file);
imageUploadCall = axios.CancelToken.source();
return axios({
url: "yourdomain.com",
method: "POST",
data,
header: {
'Content-Type': 'multipart/form-data'
},
cancelToken: imageUploadCall.token
})
} else {
//handle error
}
};
handleSelectedImage = (range, file) => {
this.uploadToServer(file).then(({data}) => {
this.setState({isUploading: false});
const position = range ? range.index : 0;
this.addImageToEditorContent(position, data);
}).catch(() => {
this.setState({isUploading: false});
//handle error
})
};
addImageToEditorContent = (position, link) => {
if (link) { //if user pressed cancel, no link returned by server & image shouldn't get added to the editor
this.quillRef.insertEmbed(position, 'image', link);
this.setState({isUploadImageModalOpen: false});
}
};
I am using React Quill as the text editor. This works fine until I add the custom image handler. If I add the image handler as below, I can't type into the editor. Typing lose focus on every single keypress.
https://codesandbox.io/s/dawn-violet-ezygm?file=/src/App.js
It. be great if anyone can help me. Thanks in advance
I have implemented react-quill to my application using hooks. So I tried this URL replace thing as you guys explained. but I still couldn't able to figure out how to get this.quill thing using hooks. any help would be greatly appreciated.
Most helpful comment
Actually, it's not a problem regarding replacing the base64 url,
but you __get__ the file in the event you handle. (just register the handler, quill brings you the file)
so the codes might look like below: