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

upload image: replace the base64 url with the url getting from sever ? #2034

Closed
zhatongning opened this issue Mar 22, 2018 · 29 comments
Closed

Comments

@zhatongning
Copy link

I have read many issues about uploading images, such as #633 #863.
When selecting an picture, the picture show immediately in the editor with base64 url, which was the default behaviour in snow/bubble theme.BUT I expect to replace the base64 url with the url getting from server.How can I solve the problem?

@lwyj123
Copy link

lwyj123 commented Mar 23, 2018

you can add custom toolbar handler. doc
Briefly speaking,
add your own image handler and upload your image to get the img url. then you can call insertEmbed to insert your Image blot into your editor.

@seongbin9786
Copy link

seongbin9786 commented Mar 23, 2018

Actually, it's not a problem regarding replacing the base64 url,
but you get the file in the event you handle. (just register the handler, quill brings you the file)

so the codes might look like below:

  1. Add handler config to your toolbar:
      modules: {
        toolbar: {
          handlers: {
            image: this.imageHandler
          }, ...
  1. Implement imageHandler function somewhere
  imageHandler() {
    const input = document.createElement('input');
    input.setAttribute('type', 'file');
    input.setAttribute('accept', 'image/*');
    input.click();
    input.onchange = async function() {
      const file = input.files[0];
      console.log('User trying to uplaod this:', file);

      const id = await uploadFile(file); // I'm using react, so whatever upload function
      const range = this.quill.getSelection();
      const link = `${ROOT_URL}/file/${id}`;

      // this part the image is inserted
      // by 'image' option below, you just have to put src(link) of img here. 
      this.quill.insertEmbed(range.index, 'image', link); 
    }.bind(this); // react thing
  }

@zhatongning
Copy link
Author

@lwyj123 @seongbin9786 Thanks. it's my fault to improperly describe the problem。
At first, I inserted image placeholder with the base64 url so that it could show immediately when picture was selected.And there were some other state such as the uploading state in the placeholder. Then when the picture finished uploading, inserting the image with right getting-from-server url to replace the placeholder that I inserted previously.

@diegocouto
Copy link

Hi, @zhatongning! I've been thinking about doing the exact same thing. Have you found a way to replace your base64 preview with the URL uploaded from server?

@james-brndwgn
Copy link

james-brndwgn commented Jul 19, 2018

Adding to @seongbin9786 answer, this code adds a loading placeholder image and replaces it once the image has been successfully uploaded.

const imageHandler = () => {
  const input = document.createElement('input');

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

  input.onchange = async () => {
    const file = input.files[0];
    const formData = new FormData();

    formData.append('image', file);

    // Save current cursor state
    const range = this.quill.getSelection(true);

    // Insert temporary loading placeholder image
    this.quill.insertEmbed(range.index, 'image', `${ window.location.origin }/images/loaders/placeholder.gif`); 

    // Move cursor to right side of image (easier to continue typing)
    this.quill.setSelection(range.index + 1);

    const res = await apiPostNewsImage(formData); // API post, returns image location as string e.g. 'http://www.example.com/images/foo.png'
    
    // Remove placeholder image
    this.quill.deleteText(range.index, 1);

    // Insert uploaded image
    this.quill.insertEmbed(range.index, 'image', res.body.image); 
  }
}

@emulienfou
Copy link

Thanks everyone, just one thing, it show be very nice to have this kind of examples in the official documentation. It avoids to search everywhere to find something like that ;)

@Puspendert
Copy link

@seongbin9786 @james-brndwgn What's this.quill here? Where did it come from? Can you please post any complete class code sample?

@Puspendert
Copy link

Puspendert commented Jan 20, 2020

hey @seongbin9786 @james-brndwgn, adding custom image handler causing issues with editor typing. Editor can only type one character and then lose focus. Issue only comes up when editor is exported as functional component, and works fine as class component.
Here is the codesandbox example

@ghost
Copy link

ghost commented Apr 5, 2020

Thank you @james-brndwgn, Your code was very helpful.

By default, ActionText(Rails6) use Trix, but I tried to use Quill on ActionText.
My code is below. I hope it will help for somebody.

  onMountQuitEditor(){
    let editor = this.querySelector('.editor')
    let toolbar = this.querySelector('.toolbar')
    this.quill = new Quill(editor, {
      modules: { toolbar: {
                   container: toolbar,
                   handlers: { image: this.onImage }
                 }
               },
      theme: 'snow'
    })
  }
  onImage(){
    const input = document.createElement('input');

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

    input.onchange = async () => {
      const file = input.files[0];
      const range = this.quill.getSelection(true);
      const attachment = {
        file: file,
        range: range,
        uploadDidCompleteCallback: (attachment, attributes) => this.onUploadComplete(attachment, attributes)
      }
      if (attachment.file) {
        const upload = new AttachmentUpload(attachment, this.rootElement())
        upload.start()
      }
    }
  }
  onUploadComplete(attachment, attributes){
    const range = attachment.range //this.quill.getSelection(true);

    // Insert temporary loading placeholder image
    // this.quill.insertEmbed(range.index, 'image', `${ window.location.origin }/images/loaders/placeholder.gif`);

    // Move cursor to right side of image (easier to continue typing)
    this.quill.setSelection(range.index + 1);

    // Remove placeholder image
    // this.quill.deleteText(range.index, 1);

    // Insert uploaded image
    this.quill.insertEmbed(range.index, 'image', attachment.url);
  }
//  AttachmentUpload class is copied from @rails/actiontext/app/javascript/actiontext/attachment_upload.js
//  and adjusted for Quill instead of Trix.
// https://github.com/rails/rails/blob/master/actiontext/app/javascript/actiontext/attachment_upload.js
import { DirectUpload } from "@rails/activestorage"

export class AttachmentUpload {
  attachment: any;
  element: any;
  directUpload: DirectUpload;
  constructor(attachment, element) {
    this.attachment = attachment
    this.element = element
    this.directUpload = new DirectUpload(attachment.file, this.directUploadUrl, this)
  }

  start() {
    this.directUpload.create(this.directUploadDidComplete.bind(this))
  }

  directUploadWillStoreFileWithXHR(xhr) {
    xhr.upload.addEventListener("progress", event => {
      // to show progress 
      // const progress = event.loaded / event.total * 100
      // this.attachment.setUploadProgress(progress)
    })
  }

  directUploadDidComplete(error, attributes) {
    if (error) {
      throw new Error(`Direct upload failed: ${error}`)
    }

    this.attachment['ssid'] = attributes.attachable_sgid;
    this.attachment['url'] = this.createBlobUrl(attributes.signed_id, attributes.filename)

    this.attachment.uploadDidCompleteCallback(this.attachment, attributes)

    // this.attachment.setAttributes({
    //   sgid: attributes.attachable_sgid,
    //   url: this.createBlobUrl(attributes.signed_id, attributes.filename)
    // })

  }

  createBlobUrl(signedId, filename) {
    return this.blobUrlTemplate
      .replace(":signed_id", signedId)
      .replace(":filename", encodeURIComponent(filename))
  }

  get directUploadUrl() {
    // html tag must have attribute 'data-direct-upload-url' 
    return this.element.dataset.directUploadUrl
  }

  get blobUrlTemplate() {
    // html tag must have attribute 'data-blob-url-template' 
    return this.element.dataset.blobUrlTemplate
  }
}

@WUKS87
Copy link

WUKS87 commented Apr 10, 2020

This is how I replaced placeholder image with my firebase storage image:

`// current cursor state
const range = this.quillEditorRef.getSelection(true);

// placeholder image
this.quillEditorRef.insertEmbed(range.index, 'image', e.target.result);

// method for saving in to storage
await this.saveToStorage(file);

const srcElem: HTMLImageElement = document.querySelector('[src^="data:image/"]');
srcElem.src = url; // url from your uploaded image`

@sebastiansandqvist
Copy link

sebastiansandqvist commented Jun 2, 2020

Using an image handler like the following works when selecting an image from the toolbar:

new Quill(element, {
  modules: {
    toolbar: {
      handlers: {
        image: imageHandler
      }
    }
  },
  ...
})

...but this doesn't handle replacing pasted images which are inserted as base64. What is the best way to replace the base64 url with an uploaded image url?

@Puspendert
Copy link

Puspendert commented Jun 11, 2020

@sebastiansandqvist This may help you:

I am handling image upload(to server) using a Modal component.

imageHandler = () => {
        this.setState({isUploadImageModalOpen: true});
    };

//the Modal would contain a file selector as(ofcourse inside the render()).
//if isUploadImageModalOpen is true then open the modal with below code:

<input type="file" accept={"image/jpeg, image/png"} onChange={(e) => this.handleSelectedImage(range, e.target.files[0])}/>

When the modal opens the quill forgets the cursor position so, below is the code I store the position

let range = this.quillRef && this.quillRef.getSelection();//File selector force the editor to loose the focus and the file gets added at the zero index. This solves the problem by saving cursor position.

The rest of the code is self explanatory.

uploadToServer = (file) => {
    if (file) {
        this.setState({isUploading: true});
        const data = new FormData();
        data.append("file", file);
        imageUploadCall = axios.CancelToken.source();
        return axios({
            url: "yourdomain.com",
            method: "POST",
            data,
            header: {
                'Content-Type': 'multipart/form-data'
            },
            cancelToken: imageUploadCall.token
        })
    } else {
          //handle error
    }
};

handleSelectedImage = (range, file) => {
    this.uploadToServer(file).then(({data}) => {
        this.setState({isUploading: false});
        const position = range ? range.index : 0;
        this.addImageToEditorContent(position, data);
    }).catch(() => {
        this.setState({isUploading: false});
              //handle error
    })
};


addImageToEditorContent = (position, link) => {
    if (link) { //if user pressed cancel, no link returned by server & image shouldn't get added to the editor
        this.quillRef.insertEmbed(position, 'image', link);
        this.setState({isUploadImageModalOpen: false});
    }
};

@anmol5varma
Copy link

anmol5varma commented Sep 2, 2020

I am using React Quill as the text editor. This works fine until I add the custom image handler. If I add the image handler as below, I can't type into the editor. Typing lose focus on every single keypress.
https://codesandbox.io/s/dawn-violet-ezygm?file=/src/App.js

It. be great if anyone can help me. Thanks in advance

@tmKamal
Copy link

tmKamal commented Nov 10, 2020

I have implemented react-quill to my application using hooks. So I tried this URL replace thing as you guys explained. but I still couldn't able to figure out how to get this.quill thing using hooks. any help would be greatly appreciated.

@PrognosticatorR
Copy link

same issue here

@tomislav-arambasic
Copy link

tomislav-arambasic commented Jan 15, 2021

Here's my full(y) working code with custom image handler and few other tweaks. Thanks @seongbin9786


import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import ReactQuill, { Quill } from 'react-quill';
import 'react-quill/dist/quill.snow.css';
import './style.scss';

/* 
 * Use inline styling instead of react-quill classes for align
 */
const AlignStyle = Quill.import('attributors/style/align');
Quill.register(AlignStyle, true);

/* 
 * Fixes link input. 
 * If user didn't prefix the link with custom protocol, link wouldn't open in app.
 * We force prepend 'http:' if no protocol.
 */
const Link = Quill.import('formats/link');
const builtInFunc = Link.sanitize;
Link.sanitize = function customSanitizeLinkInput(linkValueInput) {
    let val = linkValueInput;

    // Do nothing, since this implies user's already using a custom protocol
    if (/^\w+:/.test(val));
    else if (!/^https?:/.test(val))
        val = "http://" + val;

    return builtInFunc.call(this, val); // retain the built-in logic
};

/* 
 * Quill/React-Quill only offers image uploads from local machine. This
 * lets user input image URL
 */
function imageHandler() {
  const range = this.quill.getSelection();
  const value = prompt('Insert image URL');
  if (value){
      this.quill.insertEmbed(range.index, 'image', value, Quill.sources.USER);
  }
}

export default class RichTextEditor extends Component {
  componentDidMount() {
    // We're changing hyperlink tooltip because default one is https://quilljs.com/
    const { hyperlinkTooltip } = this.props;

    const input = document.querySelector(
      "input[data-link]"
      );
      input.dataset.link = hyperlinkTooltip;
      input.placeholder = hyperlinkTooltip;
  }

  render() {
    const {
      value,
      onChange,
      className,
      ...otherProps,
    } = this.props;

    const classes = classNames('rich-text-editor', className);

    return (
        <ReactQuill
          className={classes}
          theme="snow" 
          value={value} 
          modules={RichTextEditor.modules} 
          formats={RichTextEditor.formats}
          onChange={onChange} 
          {...otherProps}
        />
    );
  }
}

/*
 * Quill modules to attach to editor
 * See http://quilljs.com/docs/modules/ for complete options
 */
RichTextEditor.modules = {
  toolbar: {
    container: [
      [{ 'header': [1, 2, false] }],
      ["bold", "italic", "underline", "strike", { color: [] }, { background: [] }],
      [
        { list: "ordered" },
        { list: "bullet" },
        { align: [] }
      ],
      ["link", "image"],
      ["clean"]
    ],
    handlers: {
      image: imageHandler
  },
},
  clipboard: {
    matchVisual: false,
  }
};

/*
 * Quill editor formats
 * See http://quilljs.com/docs/formats/
 */
RichTextEditor.formats = [
  "header",
  "bold",
  "italic",
  "underline",
  "strike",
  "color",
  "background",
  "list",
  "bullet",
  "align",
  "link",
  "image",
];

RichTextEditor.propTypes = {
  value: PropTypes.string.isRequired,
  onChange: PropTypes.func.isRequired,
  className: PropTypes.string,
  hyperlinkTooltip: PropTypes.string,
};

@hvma411
Copy link

hvma411 commented Feb 14, 2021

So finally what is this.quill? I had a problem with quill loosing focus on typing when I wanted to use my own img handler, so I decided to make it class component because then it works like @Puspendert said but now my const range = this.quill.getSelection(); stoped working and it says that is undefined. Any idea?

@tomislav-arambasic
Copy link

So finally what is this.quill? I had a problem with quill loosing focus on typing when I wanted to use my own img handler, so I decided to make it class component because then it works like @Puspendert said but now my const range = this.quill.getSelection(); stoped working and it says that is undefined. Any idea?

Put the function calling this.quill outside your class and it'll work.
If called inside your class, this refers to your class, but Quill is instantiated in the upper class.

function getRange {
return this.qiull.getSelection();
}

class YourClass extends Component {
constructor...

function myImageHandler() {
const range = getRange();
...

render() {}
...
}

@hvma411
Copy link

hvma411 commented Feb 20, 2021

So finally what is this.quill? I had a problem with quill loosing focus on typing when I wanted to use my own img handler, so I decided to make it class component because then it works like @Puspendert said but now my const range = this.quill.getSelection(); stoped working and it says that is undefined. Any idea?

Put the function calling this.quill outside your class and it'll work.
If called inside your class, this refers to your class, but Quill is instantiated in the upper class.

function getRange {
return this.qiull.getSelection();
}

class YourClass extends Component {
constructor...

function myImageHandler() {
const range = getRange();
...

render() {}
...
}

It still don't work for me and I really don't know what should I do to repair it. Maybe I just miss something in my code. @tomislav-arambasic can you look at this and tell me what am I doing wrong? My code below:

function getRange() {
    return this.quill.getSelection()
}

class TextEditor extends React.Component {

    uploadImageCallBack = async (file) => {
        const fileName = uuidv4()
        let downloadURL = ""
        await storageRef.ref().child("articlesImages/" + fileName).put(file)
            .then(async snapshot => {
                downloadURL = await storageRef.ref().child("articlesImages/" + fileName).getDownloadURL();
            }
        )
        return downloadURL
    }

    imageHandler = () => {
        const input = document.createElement('input');
        input.setAttribute('type', 'file');
        input.setAttribute('accept', 'image/*');
        input.click();
        input.onchange = async function() {
          const file = input.files[0];
          console.log('User trying to uplaod this:', file);
    
          const link = await this.uploadImageCallBack(file);
          const range = getRange()

          this.quill.insertEmbed(range.index, 'image', link); 

        }.bind(this)
    }


    render() {
        const format = [
            'header',
            'font',
            'size',
            'bold',
            'italic',
            'underline',
            'strike',
            'blockquote',
            'list',
            'bullet',
            'indent',
            'link',
            'image',
            'video',
            'code-block',
        ]

        const modules = {
            toolbar: {
                container: [
                    [{'header': '1'}, {'header': '2'}, {'font': []}],
                    [{size: []}],
                    ['bold', 'italic', 'underline', 'strike', 'blockquote'],
                    [{'list': 'ordered'}, {'list': 'bullet'},
                        {'indent': '-1'}, {'indent': '+1'}],
                    ['link', 'image'],
                    ['clean'], ['code-block']
                ],
                handlers: {
                    image: this.imageHandler
                }
            },
            clipboard: {
                natchVisual: false,
            },
        }

        const { onChange, content, } = this.props;
        return (
            <ReactQuill
            // ref={ this.quill }
            value={ content }
            onChange={ onChange }
            theme="snow"
            modules={ modules }
            formats={ format }
            readOnly={ false }
        />
        )
    }
}

TextEditor.propTypes = {
    onChange: PropTypes.func.isRequired,
    content: PropTypes.string.isRequired
}

export default TextEditor

@HoangLong08
Copy link

Here is my solution. It was running on my project. Note: "Index" is name file

[const [editorState, setEditorState] = useState({
    valueEditor: "",
  });
<ReactQuill
      
        value={editorState.valueEditor || ""}
        onChange={(e) => handleChangeEditor(e)}
        theme="snow"
        modules={Index.modules}
        formats={Index.formats}
      />
Index.modules = {
  toolbar: [
    [{ header: [1, 2, 3, 4, 5, 6, false] }],
    [{ font: [] }],
    [{ size: [] }],
    [{ align: [] }],
    ["bold", "italic", "underline", "strike", "blockquote"],
    [{ list: "ordered" }, { list: "bullet" }],
    [{ color: [] }, { background: [] }],
    ["link", "image", "video"],
  ],
  ImageResize: {
    modules: ["Resize", "DisplaySize"],
  },
  imageCompress: {
    quality: 0.7, // default
    maxWidth: 1000, // default
    maxHeight: 1000, // default
    imageType: "image/jpeg", // default
    debug: true, // default
  },

  imageUploader: {
    upload: async (file) => {
      const bodyFormData = new FormData();
      bodyFormData.append("file", file);
      const response = await axios({
        method: "POST",
        url: "http://127.0.0.1:5000/upload-image",
        headers: authHeaderAdmin(),
        data: bodyFormData,
      });
      return response.data.image;
    },
  },
};

Index.formats = [
  "header",
  "font",
  "size",
  "align",
  "bold",
  "italic",
  "underline",
  "strike",
  "blockquote",
  "list",
  "bullet",
  "indent",
  "color",
  "background",
  "link",
  "image",
  "video",
]

@AjayDhimanELS
Copy link

@Puspendert Hi Puspender, I would love to see the whole approach(code) of yours, as I am also trying to do something similar(Opening a modal to drag and drop an image and then show it in the editor). I am able to upload the image with my handler but am unable to access the image to show it in the editor.

@Puspendert
Copy link

Puspendert commented Aug 29, 2021

@AjayDhimanELS #2034 (comment) Did you try everything explained here?
For showing the image, your backend should return the public URL of the image.

@AjayDhimanELS
Copy link

@Puspendert Thanks for your prompt reply. Yes, I was receiving the src URL for the image but was having problems with quill editor instance. Never mind, solved now. Thanks again.

@charIeszhao
Copy link

@hvma411 Hey I just ran into the same issue in React, but then I realize I shouldn't have defined the image handler in the modules, but instead adding it after initilizing the quill editor. Here's my code (btw I'm using react hooks)

const imageHandler = () => { /* write your code here  */};

useEffect(() => {
    editorRef.current
      ?.getEditor()
      .getModule('toolbar')
      .addHandler('image', imageHandler);
  }, [imageHandler]);

Hope you have solved it already, and this might help someone else.

@charIeszhao
Copy link

charIeszhao commented Oct 19, 2021

Does anyone have any idea of replacing the base64 urls on pasting content?

Currently I'm stuck on this one as there might be multiple images in the clipboard and text contents as well, so I can't find a proper way to replace them, because I don't know where to insert the images. It's not like select and insert one image to the cursor point.

Any help will be appreciated!

@haobarry
Copy link

Does anyone have any idea of replacing the base64 urls on pasting content?

有没有人想过把 base64的网址替换成粘贴内容?

Currently I'm stuck on this one as there might be multiple images in the clipboard and text contents as well, so I can't find a proper way to replace them, because I don't know where to insert the images. It's not like select and insert one image to the cursor point.

目前我被这一个卡住了,因为剪贴板中可能有多个图像和文本内容,所以我不能找到一个合适的方式来替换它们,因为我不知道在哪里插入图像。这不像选择和插入一个图像到光标点。

Any help will be appreciated!

任何帮助都会被感激!

是否可以用正则替换

@win9s
Copy link

win9s commented May 11, 2024

Does anyone have any idea of replacing the base64 urls on pasting content?

Currently I'm stuck on this one as there might be multiple images in the clipboard and text contents as well, so I can't find a proper way to replace them, because I don't know where to insert the images. It's not like select and insert one image to the cursor point.

Any help will be appreciated!

editor.clipboard.addMatcher('img', (node: HTMLImageElement, delta) => {
			if (node.src.startsWith('data:image')) {
				const base64 = node.src.split(',')[1];
				const buffer = Buffer.from(base64, 'base64');
				const name = `${Date.now()}.png`;
				const resFilePath = path.join(mailFileFolder, name);
				fs.writeFileSync(resFilePath, buffer);
				const ops = delta.ops;
				ops.forEach((op) => {
					if (op.insert && op.insert.image === node.src) {
						op.insert.image = resFilePath;
					}
				});
				return delta;
			}
		});

it works for me, which could replace base64 url when paste img

@krishnagkmit
Copy link

hey @seongbin9786 @james-brndwgn, adding custom image handler causing issues with editor typing. Editor can only type one character and then lose focus. Issue only comes up when editor is exported as functional component, and works fine as class component. Here is the codesandbox example

@Puspendert, I am facing the same issue, did you find any solution for this

@krishnagkmit
Copy link

I am using React Quill as the text editor. This works fine until I add the custom image handler. If I add the image handler as below, I can't type into the editor. Typing lose focus on every single keypress. https://codesandbox.io/s/dawn-violet-ezygm?file=/src/App.js

It. be great if anyone can help me. Thanks in advance

@anmol5varma, I am facing the same issue, did you find any solution for this

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