Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to insert images by uploading to the server instead of Base64 encoding the images? #1089

Closed
guoyangqin opened this issue Oct 29, 2016 · 57 comments

Comments

@guoyangqin
Copy link

guoyangqin commented Oct 29, 2016

Quill works well, and inserting Base64-encoded images is ok but too larger images will exceed my database table limit.

if image insertion could be conducted by uploading to server folder (e.g. /img/test.jpg) and then include the url in the , that would make me not worry about limit, since texts don't occupy too much storage in database.

Is there any way to configure the quill to make this happen?

@benbro
Copy link
Contributor

benbro commented Oct 29, 2016

Related:
#90
#995

@guoyangqin
Copy link
Author

guoyangqin commented Oct 29, 2016

@benbro I read #995 and tried the imageHandler, but it's quite hard for me to revise my .js to allow addition of handler as @jackmu95's commits files do since I am new to js. Besides, the library that I include is quill.min.js rather than core.js that @jackmu95 altered. So, I am not sure how to deal with this issue, any other solution?

</style>
<link href="templates/texteditor/quill/quill.snow.css" rel="stylesheet">
<link href="templates/texteditor/quill/katex.min.css" rel="stylesheet">
<link href="templates/texteditor/quill/syntax-styles/googlecode.css" rel="stylesheet">

<!-- Include the Quill library -->
<script src="templates/texteditor/quill/katex.min.js"></script>
<script src="templates/texteditor/quill/highlight.pack.js"></script>
<script src="templates/texteditor/quill/quill.min.js"></script>

<script type="text/javascript">
hljs.initHighlightingOnLoad();

var quill = new Quill('#editor-container', {
modules: {
formula: true,
syntax: true,
toolbar: '#toolbar-container',
history:{
    delay:2000,
    maxStack:150,
    userOnly: true
}
},
placeholder: 'Compose an epic...',
theme: 'snow'
});
</script>

@jacobmllr95
Copy link
Contributor

@AbrahamChin It's not that easy to include the changes of my PR into the production builds so I made a Demo to showcase how it would work when my PR get's merged.

http://codepen.io/jackmu95/pen/EgBKZr

@guoyangqin
Copy link
Author

@jackmu95 thx, if I implemented it as you did, does it mean that images could be firstly posted to server and then display in the editor? thus base64 encoding is substituted by a URL directed to the server image upload directory?

@jacobmllr95
Copy link
Contributor

@AbrahamChin As you can see in my example the image is uploaded to a server (in my case Imgur) and the response from the server returns the image URL. This URL needs to be passed to the callback function.

@webbapps
Copy link

I tested the codepen script by dragging and dropping an image and after inspecting the image element, it appears to be a base64 image.

@beclass
Copy link

beclass commented Apr 27, 2017

请教下,选择图片后,出来的也是整个内容,图片路径是一串很长的字符串,如何单独把文件类型上传到服务器呢?

@gpyys
Copy link

gpyys commented Jul 22, 2017

@lpp288 你的问题解决了吗? 求指教

@beclass
Copy link

beclass commented Jul 24, 2017

@gpyys 没呢,那个就是base64,可以试下react-lz-editor

@magicdvd
Copy link

@gpyys @lpp288

Replace the default image handler with your own's

modules: {
    toolbar: {
      container: '#toolbar',
      handlers: {
        'image': imageHandler
      }
    }
  },

the offical image handler is here:

function () {
  let fileInput = this.container.querySelector('input.ql-image[type=file]');
  if (fileInput == null) {
    fileInput = document.createElement('input');
    fileInput.setAttribute('type', 'file');
    fileInput.setAttribute('accept', 'image/png, image/gif, image/jpeg, image/bmp, image/x-icon');
    fileInput.classList.add('ql-image');
    fileInput.addEventListener('change', () => {
      if (fileInput.files != null && fileInput.files[0] != null) {
        let reader = new FileReader();
        reader.onload = (e) => {
          let range = this.quill.getSelection(true);
          this.quill.updateContents(new Delta()
            .retain(range.index)
            .delete(range.length)
            .insert({ image: e.target.result })
          , Emitter.sources.USER);
          fileInput.value = "";
        }
        reader.readAsDataURL(fileInput.files[0]);
      }
    });
    this.container.appendChild(fileInput);
  }
  fileInput.click();
}

As the code , you may use any ajax lib to upload the file and create an image blot fill the src by url.

@magicdvd
Copy link

My code use axios.

        var formData = new FormData();
        formData.append("image", fileInput.files[0]);
        axios.post(UPLOAD_IMAGE_URI, formData, {
            headers: {
              'Content-Type': 'multipart/form-data'
            },
            responseType:'json'
        })
        .then(res => {
          if(res.data.error == 0){
            let range = quill.getSelection(true);
            this.quill.updateContents(new Delta()
              .retain(range.index)
              .delete(range.length)
              .insert({ image: res.data.url })
            , Quill.sources.USER);
          }else{
            console.error(res.data);
          }
        })
        .catch(e => {
          console.error(e);
        });

@beclass
Copy link

beclass commented Jul 27, 2017

@magicdvd 3Q

@zzkkui
Copy link

zzkkui commented Jul 27, 2017

@magicdvd
image
Why is there an error??

@shuckster
Copy link

@zzkkui I think you have to be slightly more specific than that. :D

If you're just running that little block of code as-is, then you'll get an undefined for fileInput, axios, and UPLOAD_IMAGE_URI at the very least.

@qufei1993
Copy link

@lpp288 @gpyys 你们这个问题 都解决没 有没有什么好的办法

@TaylorPzreal
Copy link

TaylorPzreal commented Aug 2, 2017

I solved the problem that upload image with url.
This is my code, hope help you:

   const editor = new Quill('#quill-editor', {
      bounds: '#quill-editor',
      modules: {
        toolbar: this.toolbarOptions
      },
      placeholder: 'Free Write...',
      theme: 'snow'
    });

      /**
       * Step1. select local image
       *
       */
    function selectLocalImage() {
      const input = document.createElement('input');
      input.setAttribute('type', 'file');
      input.click();

      // Listen upload local image and save to server
      input.onchange = () => {
        const file = input.files[0];

        // file type is only image.
        if (/^image\//.test(file.type)) {
          saveToServer(file);
        } else {
          console.warn('You could only upload images.');
        }
      };
    }

    /**
     * Step2. save to server
     *
     * @param {File} file
     */
    function saveToServer(file: File) {
      const fd = new FormData();
      fd.append('image', file);

      const xhr = new XMLHttpRequest();
      xhr.open('POST', 'http://localhost:3000/upload/image', true);
      xhr.onload = () => {
        if (xhr.status === 200) {
          // this is callback data: url
          const url = JSON.parse(xhr.responseText).data;
          insertToEditor(url);
        }
      };
      xhr.send(fd);
    }

    /**
     * Step3. insert image url to rich editor.
     *
     * @param {string} url
     */
    function insertToEditor(url: string) {
      // push image url to rich editor.
      const range = editor.getSelection();
      editor.insertEmbed(range.index, 'image', `http://localhost:9000${url}`);
    }

    // quill editor add image handler
    editor.getModule('toolbar').addHandler('image', () => {
      selectLocalImage();
    });

@gpyys
Copy link

gpyys commented Aug 2, 2017

@Q-Angelo https://segmentfault.com/a/1190000009877910 我是看的这个 解决的

@HarlemSquirrel
Copy link

I solved this for now with listener that looks for images added.

function quillFormImgListener (formSelector) { // eslint-disable-line no-unused-vars
  var $form = $(formSelector)

  $form.on('blur change keyup paste input', '[contenteditable]', function () {
    if (noUpdateInProgress) {
      var $images = $('.ql-editor img')
      $images.each(function () {
        var imageSrc = $(this).attr('src')
        if (imageSrc && imageSrc[0] === 'd') {
          console.log('Starting image upload...')
          noUpdateInProgress = false
          disableSubmit($form)
          uploadImageToImgurAndReplaceSrc($(this), enableSubmit)
        }
      })
    }
  })
}

function uploadImageToImgurAndReplaceSrc($image, callbackFunc) {
  var imageBase64 = $image.attr('src').split(',')[1];

  $.ajax({
    url: 'https://api.imgur.com/3/image',
    type: 'post',
    data: {
      image: imageBase64
    },
    headers: {
      Authorization: 'Client-ID ' + clientId
    },
    dataType: 'json',
    success: function (response) {
      $image.attr('src', response.data.link.replace(/^http(s?):/, ''));
      callbackFunc();
    }
  });
}

@riginoommen
Copy link

Can some one suggest some text editors(eg: ckeditors) which supports image upload by (base64 image conversion)

@Detachment
Copy link

@TaylorPzreal This works in my project, thanks bro~

@ajayshah1992
Copy link

Angular test editor

@smith153
Copy link

smith153 commented Feb 3, 2018

This is what I used for my project. Only complaint I have is I couldn't really figure out how to show some progress or notify the user that the img is uploading. Anyone got tips for that? For now I just disable the editor and then re-enable it once the upload is complete.

const editor_options = {
    theme: 'snow',
    modules: {
        toolbar: {
            container: [['bold', 'italic', 'underline', 'strike'], ['link', 'image', 'video']],
            handlers: { image: quill_img_handler },
        },
    },
};

function quill_img_handler() {
    let fileInput = this.container.querySelector('input.ql-image[type=file]');

    if (fileInput == null) {
        fileInput = document.createElement('input');
        fileInput.setAttribute('type', 'file');
        fileInput.setAttribute('accept', 'image/png, image/gif, image/jpeg, image/bmp, image/x-icon');
        fileInput.classList.add('ql-image');
        fileInput.addEventListener('change', () => {
            const files = fileInput.files;
            const range = this.quill.getSelection(true);

            if (!files || !files.length) {
                console.log('No files selected');
                return;
            }

            const formData = new FormData();
            formData.append('file', files[0]);

            this.quill.enable(false);

            axios
                .post('/api/image', formData)
                .then(response => {
                    this.quill.enable(true);
                    this.quill.editor.insertEmbed(range.index, 'image', response.data.url_path);
                    this.quill.setSelection(range.index + 1, Quill.sources.SILENT);
                    fileInput.value = '';
                })
                .catch(error => {
                    console.log('quill image upload failed');
                    console.log(error);
                    this.quill.enable(true);
                });
        });
        this.container.appendChild(fileInput);
    }
    fileInput.click();
}

@johnpuddephatt
Copy link

johnpuddephatt commented Mar 4, 2018

Not too familiar with Axios but with a regular XMLHttpRequest(), you can add an eventlistener to the upload, e.g.

var xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr.upload.addEventListener("progress", function(e) {
    var progress = Math.round((e.loaded * 100.0) / e.total);
    document.getElementById('progress').style.width = progress + "%";
  });

I have this working well, but quill.insertEmbed(range.index, 'image', url); is inserting images inline... anyone know how I can change this so that the current paragraph is split with the image inserted in between?

@ISNIT0
Copy link

ISNIT0 commented Apr 14, 2020

Hi all, this is how I solved this in the end:

quill.on("text-change", async function(delta, oldDelta, source) {
  const imgs = Array.from(
    container.querySelectorAll('img[src^="data:"]:not(.loading)')
  );
  for (const img of imgs) {
    img.classList.add("loading");
    img.setAttribute("src", await uploadBase64Img(img.getAttribute("src")));
    img.classList.remove("loading");
  }
});

async function uploadBase64Img(base64Str) {
    if (typeof base64Str !== 'string' || base64Str.length < 100) {
        return base64Str;
    }
    const url = await b64ToUrl(base64); // your upload logic
    return url;
}

It inserts the base64 then updates the document with a url when it's uploaded. If you're doing live-syncing of documents you'll probably want to not sync base64 images and wait for the url update

@HJassar
Copy link

HJassar commented Apr 14, 2020

@ISNIT0 Can you provide a full example, please? It will help a beginner like myself a lot!

@ISNIT0
Copy link

ISNIT0 commented Apr 14, 2020

@IJassar you mean the upload logic? This will vary completely based on your setup. I'm afraid I can't provide any more than this.

@HJassar
Copy link

HJassar commented Apr 14, 2020

@ISNIT0 I was thinking more like using imgur or some other API rather than a server. But thank you very much.

@webmatth
Copy link

Hi, on a angular project, I managed to get something working...
I created a Blot that extends a basic img tag... added data-filename attribute with the real file name...
I'm also overriding the toolbar add image with a new handler that open a file uploader that auto insert the image in the editor in the form of my custom image blot.

the image is displayed with a blob uri only while in edit mode...

On the server when I receive the Delta, I replace each custom blot entry url with real urls

Hope it helps!

@petergerard
Copy link

@IJassar I adapted the approach shared by @ISNIT0 (I like the simplicity of it and the fact that you can see the image immediately in the editor). I used my internal upload server. Here's the full code. You can probably replace path in the xhr.open with IMGUR API. You'd also need to update the const url = line to make it work with Imgur's json format.

// watch for images added:
quill.on("text-change", async function(delta, oldDelta, source) {
  const imgs = Array.from(
    quill.container.querySelectorAll('img[src^="data:"]:not(.loading)')
  );
  for (const img of imgs) {
    img.classList.add("loading");
    img.setAttribute("src", await uploadBase64Img(img.getAttribute("src")));
    img.classList.remove("loading");
  }
});

// wait for upload
async function uploadBase64Img(base64Str) {
    if (typeof base64Str !== 'string' || base64Str.length < 100) {
        return base64Str;
    }
    const url = await b64ToUrl(base64Str);
    return url;
}
		
/**
* Convert a base64 string in a Blob according to the data and contentType.
* 
* @param b64Data {String} Pure base64 string without contentType
* @param contentType {String} the content type of the file i.e (image/jpeg - image/png - text/plain)
* @param sliceSize {Int} SliceSize to process the byteCharacters
* @see http://stackoverflow.com/questions/16245767/creating-a-blob-from-a-base64-string-in-javascript
* @return Blob
*/
function b64toBlob(b64Data, contentType, sliceSize) {
   contentType = contentType || '';
   sliceSize = sliceSize || 512;

   var byteCharacters = atob(b64Data);
   var byteArrays = [];

   for (var offset = 0; offset < byteCharacters.length; offset += sliceSize) {
       var slice = byteCharacters.slice(offset, offset + sliceSize);

       var byteNumbers = new Array(slice.length);
       for (var i = 0; i < slice.length; i++) {
           byteNumbers[i] = slice.charCodeAt(i);
       }

       var byteArray = new Uint8Array(byteNumbers);

       byteArrays.push(byteArray);
   }

   var blob = new Blob(byteArrays, {type: contentType});
   return blob;
}

// this is my upload function. I'm converting the base64 to blob for more efficient file 
// upload and so it works with my existing file upload processing
// see here for more info on this approach https://ourcodeworld.com/articles/read/322/how-to-convert-a-base64-image-into-a-image-file-and-upload-it-with-an-asynchronous-form-using-jquery
function b64ToUrl(base64) { 
  return new Promise(resolve => {
    // Split the base64 string in data and contentType
    var block = base64.split(";");
    // Get the content type of the image
    var contentType = block[0].split(":")[1];
    // get the real base64 content of the file
    var realData = block[1].split(",")[1];
    // Convert it to a blob to upload
    var blob = b64toBlob(realData, contentType);
    // create form data
    const fd = new FormData();
    // replace "file_upload" with whatever form field you expect the file to be uploaded to
    fd.append('file_upload', blob);

    const xhr = new XMLHttpRequest();
    // replace "/upload" with whatever the path is to your upload handler
    xhr.open('POST', '/upload', true);
    xhr.onload = () => {
      if (xhr.status === 200) {
        // my upload handler responds with JSON of { "path": "/images/static_images/blob2.png" }
        const url = JSON.parse(xhr.responseText).path;
        resolve(url);
      }
    };
    xhr.send(fd);
  });
}

@HJassar
Copy link

HJassar commented Apr 16, 2020

@petergerard Thank you so very much! Oh man I have a lot of learning to do ^_^

@ponpondev
Copy link

ponpondev commented May 4, 2020

I made an attempt in Vue to get the File instance instead of transforming base64 into blob by rewriting the image handler.
The advantage of this is file name would also be sent to the server and the code is obviously more straight forward.

First add in the component a hidden file input

<template>
...
  <quill-editor
    id="content-editor"
    ref="quillEditor"
    v-model="content"
    :options="qOptions"
  />
...
  <input ref="imageInput" class="d-none" type="file" accept="image/*" @change="_doImageUpload">
...
</template>

Then set data, computed, and methods like this

data () {
  return {
    content: ''
    qOptions: {
      modules: {
        toolbar: {
          container: [['image']],
          handlers: {
            image: this.insertImage
          }
        }
      },
      theme: 'snow'
    },
    imageUpload: {
      url: 'path/to/image/upload/endpoint'
    }
  }
},
computed: {
  quillInstance () {
    return this.$refs.quillEditor.quill
  }
},
methods: {
    insertImage () {
      // manipulate the DOM to do a click on hidden input
      this.$refs.imageInput.click()
    },
    async _doImageUpload (event) {
      // for simplicity I only upload the first image
      const file = event.target.files[0]
      // create form data
      const fd = new FormData()
      // just add file instance to form data normally
      fd.append('image', file)
      // I use axios here, should be obvious enough
      const response = await this.$axios.post(this.imageUpload.url, fd)
      // clear input value to make selecting the same image work
      event.target.value = ''
      // get current index of the cursor
      const currentIndex = this.quillInstance.selection.lastRange.index
      // insert uploaded image url to 'image' embed (quill does this for you)
      // the embed looks like this: <img src="{url}" />
      this.quillInstance.insertEmbed(currentIndex, 'image', response.data.url)
      // set cursor position to after the image
      this.quillInstance.setSelection(currentIndex + 1, 0)
    }
}

Oh and I use vue-quill-editor with nuxt ssr for this.

@congatw-cg
Copy link

@fylzero my man! This was super helpful dude thank you!!! Just curious - how do you handle image deletions? i.e., if the user deletes the embedded image from Quill content, does the related image file somehow get deleted or just persist in the file storage?

@fylzero
Copy link

fylzero commented Jun 23, 2020

@congatw-cg It's funny you ask because I honestly never bothered tackling garbage clean up on this. I've thought about it but for the most part am assuming that most of the time if a user uploads an image they are actually using the image. I know it's not perfect but the only way i could think to clean up on this would be to delete the image when it is removed from the editor, but then you'd run into the problem of not allowing the user to ctrl/cmd+z and undo the image deletion. Kind of becomes a pain. So I just made the decision not to worry about it.

@muarachmann
Copy link

@fylzero my man! This was super helpful dude thank you!!! Just curious - how do you handle image deletions? i.e., if the user deletes the embedded image from Quill content, does the related image file somehow get deleted or just persist in the file storage?

You can use a cron for that. Maybe daily or so. What I do is i upload to some tmp folder and run the cron to delete after say 24hrs everyday. When you save the form (final upload), it just moves the file from the tmp storage to the permanent storage. Now the only limitation will be what if the cron is running when I have just uploaded an image. I think you can create back the image from the base64. You will just have to check if it exists or not to do this. Hope it helps.

@johnpuddephatt
Copy link

johnpuddephatt commented Oct 5, 2020 via email

@FrozenHearth
Copy link

I made an attempt in Vue to get the File instance instead of transforming base64 into blob by rewriting the image handler.
The advantage of this is file name would also be sent to the server and the code is obviously more straight forward.

First add in the component a hidden file input

<template>
...
  <quill-editor
    id="content-editor"
    ref="quillEditor"
    v-model="content"
    :options="qOptions"
  />
...
  <input ref="imageInput" class="d-none" type="file" accept="image/*" @change="_doImageUpload">
...
</template>

Then set data, computed, and methods like this

data () {
  return {
    content: ''
    qOptions: {
      modules: {
        toolbar: {
          container: [['image']],
          handlers: {
            image: this.insertImage
          }
        }
      },
      theme: 'snow'
    },
    imageUpload: {
      url: 'path/to/image/upload/endpoint'
    }
  }
},
computed: {
  quillInstance () {
    return this.$refs.quillEditor.quill
  }
},
methods: {
    insertImage () {
      // manipulate the DOM to do a click on hidden input
      this.$refs.imageInput.click()
    },
    async _doImageUpload (event) {
      // for simplicity I only upload the first image
      const file = event.target.files[0]
      // create form data
      const fd = new FormData()
      // just add file instance to form data normally
      fd.append('image', file)
      // I use axios here, should be obvious enough
      const response = await this.$axios.post(this.imageUpload.url, fd)
      // clear input value to make selecting the same image work
      event.target.value = ''
      // get current index of the cursor
      const currentIndex = this.quillInstance.selection.lastRange.index
      // insert uploaded image url to 'image' embed (quill does this for you)
      // the embed looks like this: <img src="{url}" />
      this.quillInstance.insertEmbed(currentIndex, 'image', response.data.url)
      // set cursor position to after the image
      this.quillInstance.setSelection(currentIndex + 1, 0)
    }
}

Oh and I use vue-quill-editor with nuxt ssr for this.

Was looking for a vue-only solution, and stumbled across this. You're a life-saver, man! Thanks a lot.

@cmd05
Copy link

cmd05 commented Mar 7, 2021

I'm currently uploading images from the quill editor to my server. Basically I had to make changes in 2 places. Here's what I did

Image upload button (edited quill.min,js): line 7045

if (fileInput.files != null && fileInput.files[0] != null) {
    var reader = new FileReader();
    reader.onload = function(e) {
        const exec = async _ => {

            // send the image as a file type to the server
            async function fetchUrl(file) {
                const formData = new FormData(form);
                formData.append("image", file);
                const response = await fetch('localhost/application/upload-image', { method: "POST", body: formData });
                const json = await response.text();
                return json;
            }

            var range = _this3.quill.getSelection(true);
            const target = e.target.result;
            const file = dataURLtoFile(target, 'file');
            const json = await fetchUrl(file);

            let obj = JSON.parse(json);
            // server returns the uploaded url of the image
            if (obj.url) {
                let url = obj.url;
                _this3.quill.updateContents(new _quillDelta2.default().retain(range.index).delete(range.length).insert({ image: url }), _emitter2.default.sources.USER);
            }   else {
                alert("Error Occured");
            }

            _this3.quill.setSelection(range.index + 1, _emitter2.default.sources.SILENT);
            fileInput.value = "";
        }
        exec();
    };
    reader.readAsDataURL(fileInput.files[0]);
}

Pasting on the quill editor:

let delay = ms => new Promise(res => setTimeout(res, ms));

// upload image by remote url
async function fetchUrl(src) {
	const formData = newFormdata();
	formData.append("src", src);
	
	const response = await fetch(`localhost/app/upload-image`, {
		method: "POST",
		body: formData
	});
	const json = await response.text();
	return json;
}

// On content paste with images
// Send a request containing source URL of the image, which the server downloads from
document.querySelector('.ql-editor').addEventListener('paste', e => {

    const clipboardData = e.clipboardData || window.clipboardData;
    let tmp = document.createElement('div');
    tmp.innerHTML = clipboardData.getData('text/html');

    const uploadCount = tmp.querySelectorAll("img").length;

    const main = async () => {
        // store the url format for your images so you upload only the images which have not been uploaded yet
	const validateUrl = document.querySelector("[name='img_valid_url']").value; 

        await delay(1000); // wait for paste to finish

        document.querySelectorAll('.ql-editor img').forEach(img => {
            if ((img.src).indexOf(validateUrl) !== 0) img.classList.add("loading-img"); // show a loading image animation
        })

        const els = document.querySelectorAll('.ql-editor img');

        async function loop() {
            for (let x = 0; x < els.length; x++) {
                let img = els[x]
                let src = img.src;
                img.classList.add("loading-img");

                if (src.indexOf(validateUrl) !== 0) {
                    await delay(1000)
                    const upload = async () => {
                        const json = await fetchUrl(src);
                        let obj = JSON.parse(json);
                        if (obj.url) {
                            img.src = obj.url;
                        } else {
                            img.src = `${URL}/assets/img-not-found.png`;;
                        }
                    }
                    upload().then(a => {  img.classList.remove("loading-img");   });
                } else {
                    img.classList.remove("loading-img");
                }
            }
        }
        loop();
    }
    main();
});

Server side:

$img = $_FILES['image'] ?? $_POST['src'] ?? false;

// create a function which saves the image either passed as file or saves it from a URL
if($img) {
    $url = $this->imageModel->uploadImage($img); 
    if($url !== false) echo $url;
}

I'm using cloudinary cloud for storing images so it can upload images either by their remote URL or the file itself

@sachinchatterG
Copy link

sachinchatterG commented May 15, 2021

To upload base64 image to a folder and url in database using quill editor via simple js and php

My Javascript

function up(){
          var delta = {};
          delta["question"]=quill1.root.innerHTML;
          
          var settings = {
            "url": "your url",
            "method": "POST",
            "timeout": 0,
            "headers": {
                "Authorization": "Basic base64auth",
                "Content-Type": "application/json"
            },
            "data": JSON.stringify(delta),
        };

        $.ajax(settings).done(function (response) {
         if(response=="success"){
             $("#success").show();
             $("#fail").hide();
         }
            else{
                 $("#success").hide();
                $("#fail").show();
            }
        });
      }

Now My PHP

$request_body = file_get_contents('php://input');
define('UPLOAD_DIR', 'qpics/');

            $data = json_decode($request_body,true);
            
            
            $b64_parts= explode('img src="',$data["question"]);
            if(count($b64_parts)>1){
                $data["question"]=$b64_parts[0];
                for($i=1;$i<count($b64_parts);$i++){
                    $b64_value=substr($b64_parts[$i],0,strpos($b64_parts[$i],'"'));
                    $image_parts = explode(";base64,",$b64_value);
                    $image_type_aux = explode("image/", $image_parts[0]);
                    $image_type = $image_type_aux[1];
                    $image_base64 = base64_decode($image_parts[1]);
                    $filepath=UPLOAD_DIR . uniqid() .".". $image_type;
                    $file = "../".$filepath;
                    file_put_contents($file, $image_base64);
                    
                    $data["question"].='img src="'.$filepath.substr($b64_parts[$i],strpos($b64_parts[$i],'"'));
                }
            }

and after uploading the image in qpics folder, just insert the html in db

@Aslam97
Copy link

Aslam97 commented May 19, 2021

My working solution only if user hit the submit button upload image to the server.

var tempImage = [];

// Check whether quill content is empty
function isQuillEmpty(quill) {
    if ((quill.getContents()['ops'] || []).length !== 1) {
        return false
    }

    return quill.getText().trim().length === 0
}

// Delta to HTML
function deltaToHTML(delta) {
    var tempQuill = new Quill(document.createElement('div'));
    tempQuill.setContents(delta);
    return tempQuill.root.innerHTML;
}

// Copy sanitize from link.js
function sanitize(url, protocols) {
    var anchor = document.createElement('a');
    anchor.href = url;

    var protocol = anchor.href.slice(0, anchor.href.indexOf(':'));
    return protocols.indexOf(protocol) > -1;
}

// do upload
async function uploadToServer(imageBlob) {
    var imageToUpload = tempImage.find(item => item.blob === imageBlob);

    var formData = new FormData();
    formData.append('image', imageToUpload.file);

    var res = await axios({
        url: '/s3',
        method: 'POST',
        data: formData,
        headers: {
            'Content-Type': 'multipart/form-data'
        }
    });

    return res.data;
}

// import existing image class
var ParchmentImage = Quill.import('formats/image');

// Overwrite static sanitize from image class
// data base64 too long, use blob instead (only for preview)
class KlsejournalImage extends ParchmentImage {
    static sanitize(url) {
        return sanitize(url, ['http', 'https', 'data', 'blob']) ? url : '//:0';
    }
}

// Append blob & save local file
function imageHandler() {
    var input = document.createElement('input');

    input.setAttribute('type', 'file');
    input.setAttribute('accept', 'image/*');
    input.click();

    input.onchange = () => {
        var [file] = input.files;

        if (/^image\//.test(file.type)) {
            var range = this.quill.getSelection();
            var blob = URL.createObjectURL(file);

            this.quill.insertEmbed(range.index, 'image', blob);
            this.quill.setSelection(range.index + 1);

            tempImage.push({ blob, file });
        } else {
            alert('You could only upload images.');
        }
    }
}

 // Changing the level to error.
  Quill.debug('error');
// Register the new image class
Quill.register(KlsejournalImage);

// Initialize Quill editor
var editor = new Quill('#editor', {
    placeholder: 'What do you want to talk about?',
    theme: "snow",
    modules: {
        toolbar: {
            container: [
                ["link", "image"],
            ],
            handlers: {
                image: imageHandler
            }
        },
    }
});

// submit post
var btnPost = document.getElementById('submit-post')
btnPost.addEventListener('click', async function(e) {
    if (isQuillEmpty(editor)) {
        alert('Cannot submit empty post!');
        return false;
    }

    var delta = editor.getContents();

    for (var i = 0; i < delta.ops.length; i++) {
        var insert = delta.ops[i].insert;

        var has = Object.prototype.hasOwnProperty;

        if (has.call(insert, 'image')) {
            var imageUrl = await uploadToServer(insert.image);
            insert.image = imageUrl;
        }
    }

    var html = deltaToHTML(delta);

    axios.post('/posts', { content: html })
        .then((res) => {
            window.location.reload();
        });
});

@ryowu
Copy link

ryowu commented Aug 15, 2022

@petergerard Thank you so much. Your sample code save me a lot of time! And the inline comments are really good.
I did some 'type' changes to make it work in TypeScript and it works perfect. I like your 'loading' switch, you have thought a lot.

@codecret
Copy link

codecret commented Oct 12, 2022

Hello everyone. In case someone wasn't still sure about any of this solutions, l leave this link here https://snyk.io/advisor/npm-package/react-quilljs which work perfectly for me. Best regards

It's great but how can we get the images after uploading them, in the same order we putted them

@abelardolg
Copy link

After you embed an image:
image

how could I get the entire text with the image? I would like to send to my server the entire html, from <p> to </p>, including img and text:
image

When I embed it, I try to get the content but the whole text is not available as html (as you can see the above image). It is held with a structure hard to pass to my server in order to be stored into my db:
image

@DitaGuerrero
Copy link

DitaGuerrero commented Nov 21, 2022

I leave this example that works perfectly for me. Good luck ;)

@lavaloop-official
Copy link

Is anybody aware of how to add headers to the request when quill wants to retrieve an image from the backend?

@quill-bot
Copy link

Quill 2.0 has been released (announcement post) with many changes and fixes. If this is still an issue please create a new issue after reviewing our updated Contributing guide 🙏

@ludejun
Copy link

ludejun commented May 2, 2024

You can use quill-react-commercial.

You can upload, resize, add remark, delete, align images.
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests