quill icon indicating copy to clipboard operation
quill copied to clipboard

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

Open guoyangqin opened this issue 8 years ago • 51 comments

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?

guoyangqin avatar Oct 29 '16 07:10 guoyangqin

Related: https://github.com/quilljs/quill/issues/90 https://github.com/quilljs/quill/pull/995

benbro avatar Oct 29 '16 07:10 benbro

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

guoyangqin avatar Oct 29 '16 12:10 guoyangqin

@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

jacobmllr95 avatar Oct 29 '16 17:10 jacobmllr95

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

guoyangqin avatar Oct 29 '16 18:10 guoyangqin

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

jacobmllr95 avatar Oct 31 '16 07:10 jacobmllr95

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

webbapps avatar Nov 12 '16 05:11 webbapps

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

beclass avatar Apr 27 '17 08:04 beclass

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

gpyys avatar Jul 22 '17 14:07 gpyys

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

beclass avatar Jul 24 '17 12:07 beclass

@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 avatar Jul 26 '17 14:07 magicdvd

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 avatar Jul 26 '17 15:07 magicdvd

@magicdvd 3Q

beclass avatar Jul 27 '17 09:07 beclass

@magicdvd image Why is there an error??

zzkkui avatar Jul 27 '17 12:07 zzkkui

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

shuckster avatar Jul 27 '17 12:07 shuckster

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

qufei1993 avatar Jul 29 '17 14:07 qufei1993

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

TaylorPzreal avatar Aug 02 '17 05:08 TaylorPzreal

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

gpyys avatar Aug 02 '17 05:08 gpyys

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

HarlemSquirrel avatar Sep 10 '17 19:09 HarlemSquirrel

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

riginoommen avatar Oct 03 '17 11:10 riginoommen

@TaylorPzreal This works in my project, thanks bro~

Detachment avatar Oct 12 '17 01:10 Detachment

Angular test editor

ajayshah1992 avatar Nov 17 '17 18:11 ajayshah1992

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

smith153 avatar Feb 03 '18 19:02 smith153

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?

johnpuddephatt avatar Mar 04 '18 14:03 johnpuddephatt

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! 😁

dragonwong avatar Nov 20 '18 13:11 dragonwong

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

clarissahu avatar Dec 18 '18 07:12 clarissahu

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?

madhavan-sundararaj avatar Feb 02 '19 14:02 madhavan-sundararaj

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?

thelmn avatar Feb 19 '19 21:02 thelmn

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

FM1337 avatar Mar 29 '19 18:03 FM1337

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

tboevil avatar Apr 10 '19 07:04 tboevil

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: {
                
            }
        }
    }

ErdoganAlper avatar May 23 '19 08:05 ErdoganAlper