zenoamaro / react-quill

A Quill component for React.
https://zenoamaro.github.io/react-quill
MIT License
6.76k stars 921 forks source link

How to get the deleted image info inside react-quill editor #708

Open khayrullaev opened 3 years ago

khayrullaev commented 3 years ago

I am using react-quill with quill-image-uploader. I want to get the url when an image is deleted to also delete it from my storage. Is there any way to achieve this?

react-quill version 1.3.5

import React, { useState, useEffect } from 'react';
import ReactQuill, { Quill } from 'react-quill';
import ImageUploader from 'quill-image-uploader';

Quill.register('modules/imageUploader', ImageUploader);

const QuillEditor = ({ className, ...rest }) => 

const [modules, setModules] = useState({});

  useEffect(() => {
    setModules({
      toolbar: [
        [{ header: [1, 2, 3, false] }],
        ['bold', 'italic', 'underline', 'strike', 'blockquote'],
        [{ list: 'ordered' }, { list: 'bullet' }],
        ['link', 'image'],
        ['clean'],
      ],

      imageResize: {},

      imageUploader: {
        upload: (file) => {
          return new Promise((resolve, reject) => {
            const formData = new FormData();
            formData.append('image', file);

            fetch(
              'https://api.imgbb.com/1/upload?key=d36eb6591370ae7f9089d85875e56b22',
              {
                method: 'POST',
                body: formData,
              }
            )
              .then((response) => response.json())
              .then((result) => {
                console.log(result);
                resolve(result.data.url);
              })
              .catch((error) => {
                reject('Upload failed');
                console.error('Error:', error);
              });
          });
        },
      },
    });
  }, []);

  return (
    <ReactQuill
      modules={modules}
    />
  );
};
rajpurohityogesh commented 2 years ago

Hey I am facing the same issue can you please tell me how did you handle this.

ziayamin commented 2 years ago

Hi! I have the same issue. any solution?

Jhoancanchila commented 2 years ago

hello ! what was the solution for this?

rajpurohityogesh commented 2 years ago

Hey, guys actually I resolved this approach using another approach. I used a different editor it's called Tiny Editor.

In the below code you can see in the setup attribute I have implemented a logic using which we can do image delete from serve. But it works when we select the image by clicking on it and then press either the delete key or the backspace key.

import React , {Component, createRef} from 'react';
import { withRouter } from "react-router";
import axios from 'axios';
import FormData from "form-data";
import { v4 as uuidv4 } from "uuid";
import Aux from '../../hoc/_Aux';

import { Editor } from '@tinymce/tinymce-react';

import { EditorState } from 'draft-js';
import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css';

import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';

class FormTiny extends Component {

  constructor(props){
    super(props)
    this.state = {
      title: '',
      category:'Coal',
      content: EditorState.createEmpty(),
      imageFolder: uuidv4(),
      image: null,
      imageUrl:null,
      files:[]
    }
    this.imageRef = createRef();
    this.editorRef = createRef();
  }

  addBlog=()=>{

    if(this.state.title==="" || this.editorRef.current.getContent()==="" || this.state.image==null){
      toast.warn("Please Fill All The Details");
    }
    else {
      const formData = new FormData();
      formData.append("blogImageId",uuidv4());
      formData.append('category', this.state.category);
      formData.append('thumbnail', this.state.image);
      formData.append('thumbnailName', this.state.image.name);
      formData.append('title', this.state.title );
      formData.append("imageFolder", this.state.imageFolder);
      formData.append('content', this.editorRef.current.getContent());

      axios.post('/api/postBlog',formData)
      .then(response =>{
          toast.success("Your Blog Has Been Posted Successfully !!");
          setTimeout(()=>{
            this.props.history.push('/allBlog');
          },2000);
      })
      .catch(error => {
        console.log(error.response)
        toast.error("Some Error Occured On Server Side");
      });
    }
  }

  handleQuillEditorChange = (content)=>{
    this.setState({content: content})
  }

  thumbnailUploadHandeler = (event) =>{
    this.setState({ image:event.target.files[0] })
    this.setState({ imageUrl:URL.createObjectURL(event.target.files[0]) })
  }

  render(){

      return(
        <Aux>

            <div style={{padding:"0"}} className="form-group input-group mb-3 col-md-3 col-sm-10">
                <div className="input-group-prepend">
                <label className="input-group-text" htmlFor="inputCategory">Category</label>
                </div>
                <select className="custom-select" id="inputCategory" value={this.state.category} onChange={(e)=>this.setState({category:e.target.value})}>
                <option value="Coal">Coal</option>
                <option value="Timber">Timber</option>
                <option value="Salt">Salt</option>
                </select>
            </div>

            <div className="form-group">
                <label htmlFor="blogInputFile">Select Thumbnail</label>
                <input ref={this.imageRef} type="file" className="form-control-file" onChange={this.thumbnailUploadHandeler} id="blogInputFile" aria-describedby="fileHelp" accept="image/gif, image/jpeg, image/png" required />
                {this.state.imageUrl!=null?<img style={{margin:"1em 0"}} height="20%" width="20%" src={this.state.imageUrl} alt="Preview" />:""}
            </div>

            <div className="form-group">
                <label htmlFor="inputTitle">Title</label>
                <input type="text" value={this.state.title} onChange={(event)=>this.setState({title:(event.target.value)})} className="form-control" id="inputTitle" aria-describedby="postTitle" required/>
            </div>

            <label htmlFor="exampleInputPassword1">Content</label>
            <Editor
                apiKey={process.env.REACT_APP_TinyMCE_API_KEY}
                onInit={(evt, editor) => this.editorRef.current = editor}
                initialValue="<p>Start writing about port.</p>"
                init={{
                    height: 500,
                    menubar: false,
                    image_dimensions: true,
                    plugins: [
                        'advlist autolink lists link image charmap print preview anchor',
                        'searchreplace visualblocks code fullscreen',
                        'insertdatetime media table paste code help wordcount'
                    ],

                    toolbar: 'undo redo | p h1 h2 blockquote | ' +
                    'bold italic underline strikethrough backcolor forecolor | image media table | alignleft aligncenter ' +
                    'alignright alignjustify | bullist numlist outdent indent | ' +
                    'removeformat | help',

                    setup: (ed) => {
                        ed.on('KeyDown', (e) => {
                            if ((e.keyCode === 8 || e.keyCode === 46) && ed.selection) { // delete & backspace keys
                                var selectedNode = ed.selection.getNode(); 
                                if (selectedNode && selectedNode.nodeName === 'IMG') {
                                    var a = document.createElement('a');
                                    a.href = selectedNode.src;
                                    if(a.hostname===process.env.REACT_APP_IMAGE_HOSTNAME){
                                        var imageName = selectedNode.src.split('/').slice(-1)[0];
                                        const formData = {
                                            imageFolder: this.state.imageFolder,
                                            imageName: imageName
                                        }
                                        axios.post("/api/blogImageDelete",formData)
                                        .then(response =>{
                                            toast.success(response.data.msg);
                                        })
                                        .catch(error => {
                                            console.log(error.response)
                                            toast.error("Some Error Occured On Server Side");
                                        });
                                    }
                                }
                            }
                        });
                    },

                    relative_urls : false,
                    remove_script_host : false,
                    convert_urls : false,
                    images_upload_handler: (blobInfo, success, failure) => {
                        setTimeout(() => {
                            const formData = new FormData();
                            formData.append('imageFolder', this.state.imageFolder);
                            formData.append("imageId",uuidv4());
                            formData.append('blogAddedImage', blobInfo.blob());

                            axios.post("/api/blogImageUpload",formData)
                            .then(response =>{
                                success(process.env.REACT_APP_IMAGE_PRE_URL+"/blogContentImages/"+this.state.imageFolder+"/"+response.data.customFileName);
                            })
                            .catch(error => {
                                console.log(error.response)
                                failure('HTTP Error: ' + error.status, { remove: true });
                                toast.error("Some Error Occured On Server Side");
                            });
                        }, 1000);
                    },
                    content_style: 'body { font-family:Helvetica,Arial,sans-serif; font-size:14px }',

                }}
            /> 

            <button style={{margin:"1em 0"}} type="submit" onClick={this.addBlog} className="btn btn-primary">Submit</button>
            <button style={{margin:"1em 0 1em 1em"}} type="button" onClick={()=>{this.props.history.push('/allBlog')}} className="btn btn-danger">Cancel</button>
            <ToastContainer toastClassName="colored-toast" autoClose={2000} hideProgressBar />
        </Aux>

      )
  }

}

export default withRouter(FormTiny)
Joji6666 commented 1 year ago

Maybe my method can help you.

im using custom imageHandler this is my code

///////

import { $_lib_fetchData } from '@/lib/commonUtils'
import React, { useEffect, useMemo, useRef } from 'react'
import ReactQuill from 'react-quill'
import 'react-quill/dist/quill.snow.css'

interface MyEditorPropsInterface {
  setValue: React.Dispatch<React.SetStateAction<string>>
  setImageFiles: React.Dispatch<React.SetStateAction<any>>
  imageFiles: any[]
  taskName: string
  value: string
}

let groupCode = ''

const MyEditor = ({
  setValue,
  setImageFiles,
  imageFiles,
  taskName,
  value
}: MyEditorPropsInterface) => {
  const quillRef = useRef<any>()

  const imageHandler = () => {
    const input: any = document.createElement('input')
    input.setAttribute('type', 'file')
    input.setAttribute('accept', 'image/*')
    input.setAttribute('multiple', '')
    input.click()

    input.addEventListener('change', async (e: any) => {
      const files: File[] = [...input.files]
      const formData: FormData = new FormData()

      if (groupCode !== '') {
        formData.append('fileGroupCode', groupCode)
      }

      if (files.length > 0) {
        files.forEach((file: any) => formData.append('files', file))
      }

      formData.append('taskName', taskName)

      const res = await $_lib_fetchData({
        url: '/files',
        method: 'post',
        params: formData
      })
      if (res.data) {
        groupCode = res.data.fileGroupCode
        res.data.fileIds.forEach((fileId: any) => {
          const editor = quillRef.current.getEditor()
          const range = editor.getSelection()
          const src = `http://yourUrl../${fileId}`
          editor.insertEmbed(range.index, 'image', src)
          setImageFiles((prev: any) => [...prev, { path: src, id: fileId }])
        })
      }
    })
  }

  const deleteImage = async (fileId: string) => {
    const res = await $_lib_fetchData({
      url: `/files/${fileId}`,
      method: 'delete',
      params: {}
    })
  }

  const modules = useMemo(
    () => ({
      toolbar: {
        container: [
          [{ font: [] }],
          [{ header: [1, 2, false] }],

          ['bold', 'italic', 'underline', 'strike', 'blockquote'],
          [
            { list: 'ordered' },
            { list: 'bullet' },
            { indent: '-1' },
            { indent: '+1' }
          ],
          [{ align: [] }, { color: [] }, { background: [] }],

          ['image', 'link', 'video']
        ],
        handlers: { image: imageHandler }
      },
      clipboard: {
        matchVisual: false
      }
    }),
    []
  )

  const formats = [
    'font',
    'header',
    'bold',
    'italic',
    'underline',
    'strike',
    'blockquote',
    'list',
    'bullet',
    'indent',
    'link',
    'image',
    'video',
    'align',
    'color',
    'background'
  ]

  useEffect(() => {
    if (
      quillRef.current?.lastDeltaChangeSet?.ops[1]?.delete === 1 &&
      imageFiles.length > 0
    ) {
      for (let index = 0; index < imageFiles.length; index++) {
        if (!quillRef.current?.value.includes(imageFiles[index].path)) {
          const tempImageFiles = structuredClone(imageFiles)
          const filteredIamgeFiles = tempImageFiles.filter(
            (image: any) => image.id !== imageFiles[index].id
          )
          deleteImage(imageFiles[index].id)

          setImageFiles(filteredIamgeFiles)
        }
      }
    }
  }, [quillRef.current?.lastDeltaChangeSet?.ops[1]?.delete])

  return (
    <>
      <ReactQuill
        ref={quillRef}
        style={{ height: '400px' }}
        modules={modules}
        formats={formats}
        theme='snow'
        value={value}
        onChange={setValue}
      />
    </>
  )
}

export default MyEditor

///////

As you can see from the code, I uploaded a file to the database every time I uploaded an image.

After that, I updated the src value and id value received as a response to the arbitrary state I created.

Then, we sent a request to delete the image in the DB by detecting the deletion using the lastDeltaChangeSet property of react-quill and useEffect.

Try modifying my code to suit your own code.

Developers who have additional, better methods, please reply.

lishenyu16 commented 1 year ago

great solution

lishenyu16 commented 1 year ago

hi @Joji6666 , i used ur code above, but I got error when using 'setImageFiles' and the editor disappears, Error: 'addRange(): The given range isn't in document.' it seems that it doesn't allow me to set value in the image handler.

Joji6666 commented 1 year ago

Could it be that your development environment is different from mine? I am using the latest version of NextJS and react-quill is using version 2.0.0. Sorry for the late reply.