Quill: How to insert images by uploading to the server instead of Base64 encoding the images?

Created on 29 Oct 2016  ·  47Comments  ·  Source: quilljs/quill

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?

Most helpful comment

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();
    });

All 47 comments

@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>

@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

@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?

@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.

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

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

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

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

@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.

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);
        });

@magicdvd 3Q

@magicdvd
image
Why is there an error??

@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.

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

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();
    });

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

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();
    }
  });
}

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

@TaylorPzreal This works in my project, thanks bro~

Angular test editor

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();
}

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?

I wrote a plugin to upload image: quill-plugin-image-upload

With this plugin we can:

  • 🌟upload a image when it is inserted, and then replace the base64-url with a http-url
  • 🌟preview the image which is uploading with a loading animation
  • 🌟when the image is uploading, we can keep editing the content including changing the image's position or even delete the image.

And of course, it's easy to use! 😁

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();
    });

this work well, thanks~ and the cursor should be moved after the pic you inserted, like this

...
function insertToEditor(url: string) {
     // push image url to rich editor.
     const range = editor.getSelection();
     editor.insertEmbed(range.index, 'image', `http://localhost:9000${url}`);
     editor.setSelection(range.index + 1); 
   }
...

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();
    });

this work well, thanks~ and the cursor should be moved after the pic you inserted, like this

...
function insertToEditor(url: string) {
     // push image url to rich editor.
     const range = editor.getSelection();
     editor.insertEmbed(range.index, 'image', `http://localhost:9000${url}`);
     editor.setSelection(range.index + 1); 
   }
...

Hey @clarissahu still it's not setting index next to image. Any other solution?

A little late but what about uploading the delta content to the server as is and then finding "data:image/png;base64,...", decode the base64 to an image and then replace with the image URL?

@magicdvd I tried this and while I almost got it working, I got an error saying Delta is not defined, so I tried importing by doing

var Delta = Quill.import('delta');
and that fixed the delta undefined error, but then I got
Uncaught ReferenceError: Emitter is not defined
and I can't import emitter from quill and I can't seem to find a solution to this problem.

Any tips?

I want format image when insert image, like this:

class ImageBlot extends BlockEmbed {
  static create(src) {
    const node = super.create()

    node.setAttribute('src', src)
    return node
  }

  static value(node) {
    return node.getAttribute('src')
  }

  static formats(node) {
    // We still need to report unregistered embed formats
    let format = {}

    format.srcset = node.getAttribute('src') + ' 2x'
    return format
  }

  format(name, value) {
    // Handle unregistered embed formats
    if (name === 'srcset') {
      if (value) {
        this.domNode.setAttribute(name, value)
      } else {
        this.domNode.removeAttribute(name, value)
      }
    } else {
      super.format(name, value)
    }
  }

}
ImageBlot.blotName = 'imageBlot'
ImageBlot.tagName = 'img'


class ImageUpload {
  constructor(quill, options = {}) {
    // save the quill reference
    this.quill = quill;
    // save options
    this.options = options;
    // listen for drop and paste events
    this.quill.root.addEventListener('drop', ev => {
      ev.preventDefault()
      let native
      if (document.caretRangeFromPoint) {
        native = document.caretRangeFromPoint(ev.clientX, ev.clientY);
      } else if (document.caretPositionFromPoint) {
        const position = document.caretPositionFromPoint(ev.clientX, ev.clientY);
        native = document.createRange();
        native.setStart(position.offsetNode, position.offset);
        native.setEnd(position.offsetNode, position.offset);
      } else {
        return;
      }
      const normalized = quill.selection.normalizeNative(native);
      const range = quill.selection.normalizedToRange(normalized);
      if (ev.dataTransfer.files.length !== 0) this.upload(range, Array.from(ev.dataTransfer.files))
    })
  }



  upload(range, files) {
    files.forEach(file => {
      const observable = qiniu.upload(file, `md/${uuidv4()}.${file.name.split('.').pop()}`, token)
      observable.subscribe({ complete: ({ key, hash, domain }) => this.insertToEditor(range, `${domain}/${key}`), error: (e) => console.error(e) })
    })
  }

  insertToEditor(range, src) {
    this.quill.container.focus()
    this.quill.selection.update(Quill.sources.SILENT)
    setTimeout(() => {
      const update = new Delta().retain(range.index).delete(range.length).insert({imageBlot: src})
      this.quill.updateContents(update, Quill.sources.USER)
      this.quill.setSelection(
        range.index + 1,
        Quill.sources.SILENT
      )
    }, 1)

  }

}

what should I do

Iam using these configurations but cant add imagehandling

        this.editorForm = new FormGroup({
            'editor': new FormControl(null)
        })

    config = {
        toolbar: {
            container:
                [
                    [{ 'placeholder': ['[GuestName]', '[HotelName]'] }], // my custom dropdown
                    ['bold', 'italic', 'underline', 'strike'],        // toggled buttons
                    ['blockquote', 'code-block'],

                    [{ 'header': 1 }, { 'header': 2 }],               // custom button values
                    [{ 'list': 'ordered' }, { 'list': 'bullet' }],
                    [{ 'script': 'sub' }, { 'script': 'super' }],      // superscript/subscript
                    [{ 'indent': '-1' }, { 'indent': '+1' }],          // outdent/indent
                    [{ 'direction': 'rtl' }],                         // text direction

                    [{ 'size': ['small', false, 'large', 'huge'] }],  // custom dropdown
                    [{ 'header': [1, 2, 3, 4, 5, 6, false] }],

                    [{ 'color': [] }, { 'background': [] }],          // dropdown with defaults from theme
                    [{ 'font': [] }],
                    [{ 'align': [] }],

                    ['clean']                                    // remove formatting button

                ],
            handlers: {

            }
        }
    }

Please consider how images are to be deleted server-side as well, we need event handlers for both situations.

I like @HarlemSquirrel 's solution because it also works with drag and dropping images.
I adapted it a bit to my needs and added a progress bar for feedback.

<progress id='progressbar' style='display: none' value="0"></progress>

<div id="editor" ></div>

<script>
let editor = new Quill('#editor')
function uploadImageToImgurAndReplaceSrc($image) {
    var imageBase64 = $image.attr('src').split(',')[1];
    $('#progressbar').show()
    $.ajax({
        url: 'https://api.imgur.com/3/image',
        type: 'post',
        data: {
            image: imageBase64
        },
        headers: {
            Authorization: 'Client-ID ' + clientId
        },
        dataType: 'json',
        xhr: function() {
            var xhr = $.ajaxSettings.xhr();
            xhr.upload.onprogress = function(e) {
                console.log(e.loaded)
                if (e.lengthComputable) {  
                    $('#progressbar').attr({value:e.loaded, max:e.total});
                    // Upload finished
                    if (e.loaded == e.total) {
                        $('#progressbar').attr('value','0.0');
                    }
                }
            };
            return xhr;
        },
        success: (response) => {
            $image.attr('src', response.data.link.replace(/^http(s?):/, ''));
            $('#progressbar').hide()
        },
        error: (xhr, type, err) => {
            console.error(err)
            alert("Sorry we couldn't upload your image to Imgur.")
            $('#progressbar').hide()
        }
    })
}
let updateInProgress = false
editor.on('editor-change', () => {

    if (updateInProgress) return;

    var $images = $('#editor img')
    $images.each(function () {
        var imageSrc = $(this).attr('src')
        if (imageSrc && imageSrc[0] === 'd') {
            console.log('Starting image upload...')
            updateInProgress = true
            uploadImageToImgurAndReplaceSrc($(this))
        }
    })
})
</script>

If anyone needs a way to get here with Vue/Axios and I'm using the Vue Quill Editor with an import / component style. (https://github.com/surmon-china/vue-quill-editor)...

This took me a lot of research and combining answers to get to this. Hope it helps...

Call the "ref" whatever you want, just make sure to change it in all locations in the handler function as well. Also, I'm passing data via a prop object, this can be a data entry / whatever.

<template>
    <div>

        <quill-editor
            ref="quillio"
            v-model="postData.text"
            :options="editorOptions"
        ></quill-editor>

    </div>
</template>

I'm bringing in ImageDrop, this is a separate package that will handle drag and dropping images. I'll eventually write a handler for that as well.

<script>
import Quill from 'quill';
import { quillEditor } from 'vue-quill-editor';
import { ImageDrop } from 'quill-image-drop-module';
Quill.register('modules/imageDrop', ImageDrop);

export default {
    data() {
        return {
            editorOptions: {
                modules: {
                    toolbar: {
                        container: ['bold', 'italic', 'underline', { 'list': 'bullet' }, 'image'],
                        handlers: { 'image': this.imageButtonClickHandler }
                    },
                    imageDrop: true,
                }
            }
        }
    },
    components: {
        quillEditor,
        ImageDrop,
    },
    props: ['postData'],
    methods: {
        imageButtonClickHandler() {

            // Add a file input element to the document, then click it (open file).
            const input = document.createElement('input');
            input.setAttribute('type', 'file');
            input.click();

            // Once file is selected.
            input.onchange = () => {
                const file = input.files[0];

                // Validate file type is an image.
                if (/^image\//.test(file.type)) {

                    // Create form.
                    const formData = new FormData();
                    formData.append('image', file);

                    // Upload image to AWS via app route handler.
                    axios.post('/ajax/image', formData, {
                        headers: {
                            'Content-Type': 'multipart/form-data'
                        }
                    })
                    .then(function (response) {

                        // Set returned image URL to variable.
                        const url = response.data;

                        // Get the current cursor position.
                        const range = this.$refs.quillio.quill.getSelection();

                        // Insert the image via URL where the cursor is.
                        this.$refs.quillio.quill.insertEmbed(range.index, 'image', url);

                        // Move the cursor past the image.
                        this.$refs.quillio.quill.setSelection(range.index + 1);

                    })
                    .catch(function (error) {
                        console.log(error);
                    });
                }

                // Not an image.
                else {
                    console.warn('You could only upload images.');
                }
            };

        }
    }
}
</script>

Enjoy!

@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

I tried so hard but could not figure this out. Can you help me please?

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

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

@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.

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

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!

@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);
  });
}

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

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.

@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?

@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.

@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.

If setting up a cron job, you could use find to first get files older than 1 day, then pass these to rm:

find /path/to/files* -mtime +1 -exec rm {} \;

On 4 Oct 2020, at 01:24, Mua Rachmann notifications@github.com wrote:

@fylzero https://github.com/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.


You are receiving this because you commented.
Reply to this email directly, view it on GitHub https://github.com/quilljs/quill/issues/1089#issuecomment-703180938, or unsubscribe https://github.com/notifications/unsubscribe-auth/AB5IVPQ7TOEPS5QPKF6E643SI66FFANCNFSM4CUQGN3A.

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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Softvision-MariusComan picture Softvision-MariusComan  ·  3Comments

Kivylius picture Kivylius  ·  3Comments

GildedHonour picture GildedHonour  ·  3Comments

lustoykov picture lustoykov  ·  3Comments

lastmjs picture lastmjs  ·  3Comments