├── .editorconfig ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── README.md ├── demo ├── .eslintignore ├── .eslintrc ├── package.json ├── src │ ├── index.html │ └── index.js └── webpack.config.js ├── package.json └── src ├── ImageUploadPlaceholder.js ├── constant.js ├── imageIdManger.js ├── index.js └── style.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | indent_size = 4 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # system ignore 2 | .DS_Store 3 | Thumbs.db 4 | 5 | # npm ignore 6 | node_modules/ 7 | npm-debug.log 8 | 9 | # webpack ignore 10 | dist 11 | 12 | # eslint-cache 13 | .eslintcache 14 | .vscode 15 | package-lock.json -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | 7 | demo/ 8 | node_modules/ 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 0.0.5 4 | 5 | - rename className 6 | 7 | ## 0.0.4 8 | 9 | - update the way to import styles 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # quill-plugin-image-upload 2 | 3 | A plugin for uploading image in Quill 🌇 4 | 5 | - 🌟 upload a image when it is inserted, and then replace the base64-url with a http-url 6 | - 🌟 preview the image which is uploading with a loading animation 7 | - 🌟 when the image is uploading, we can keep editing the content including changing the image's position or even delete the image. 8 | 9 | ![](https://user-images.githubusercontent.com/2622602/49206584-73c6b080-f3ed-11e8-8164-aad28508d4c4.gif) 10 | 11 | ## Install 12 | 13 | ```bash 14 | npm install quill-plugin-image-upload --save 15 | ``` 16 | 17 | ## Start 18 | 19 | ```js 20 | import Quill from 'quill'; 21 | import 'quill/dist/quill.snow.css'; 22 | import imageUpload from 'quill-plugin-image-upload'; 23 | 24 | // register quill-plugin-image-upload 25 | Quill.register('modules/imageUpload', imageUpload); 26 | 27 | new Quill('#editor', { 28 | theme: 'snow', 29 | modules: { 30 | toolbar: [ 31 | 'image' 32 | ], 33 | imageUpload: { 34 | upload: file => { 35 | // return a Promise that resolves in a link to the uploaded image 36 | return new Promise((resolve, reject) => { 37 | ajax().then(data => resolve(data.imageUrl)); 38 | }); 39 | } 40 | }, 41 | }, 42 | }); 43 | ``` 44 | 45 | ## Demo 46 | 47 | ```bash 48 | cd demo 49 | npm install 50 | npm start 51 | ``` 52 | -------------------------------------------------------------------------------- /demo/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | webpack.config.js -------------------------------------------------------------------------------- /demo/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "browser": true 5 | }, 6 | "parserOptions": { 7 | "ecmaVersion": 7, 8 | "sourceType": "module" 9 | }, 10 | "extends": "eslint:recommended", 11 | "globals": { 12 | "require": false, 13 | "module": false 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quill-plugin-image-upload-demo", 3 | "version": "0.0.1", 4 | "description": "", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "build": "webpack", 8 | "watch": "webpack --progress --watch", 9 | "start": "webpack-dev-server --open --host 0.0.0.0", 10 | "eslint": "echo \"Checking code style, please wait ...\" && eslint ./src *.js --cache", 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "pre-commit": [ 17 | "eslint" 18 | ], 19 | "dependencies": { 20 | "babel-core": "^6.26.0", 21 | "babel-loader": "^7.1.2", 22 | "babel-preset-es2015": "^6.24.1", 23 | "clean-webpack-plugin": "^0.1.17", 24 | "css-loader": "^0.28.7", 25 | "eslint": "^4.10.0", 26 | "eslint-loader": "^1.9.0", 27 | "html-webpack-plugin": "^2.30.1", 28 | "pre-commit": "^1.2.2", 29 | "quill": "^1.3.6", 30 | "quill-plugin-image-upload": "0.0.6", 31 | "style-loader": "^0.19.0", 32 | "uglifyjs-webpack-plugin": "^1.0.1", 33 | "webpack": "^3.8.1", 34 | "webpack-dev-server": "^2.9.3" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /demo/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | demo | quill-plugin-image-upload 7 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /demo/src/index.js: -------------------------------------------------------------------------------- 1 | const Quill = require('quill'); 2 | require('quill/dist/quill.snow.css'); 3 | const imageUpload = require('quill-plugin-image-upload'); 4 | 5 | Quill.register('modules/imageUpload', imageUpload); 6 | 7 | const MOCK_IMG_SRC = 'http://tva1.sinaimg.cn/crop.0.0.217.217.180/4c8b519djw8fa45br0vpxj2062062q33.jpg'; 8 | const quill = new Quill('#editor', { 9 | theme: 'snow', 10 | modules: { 11 | toolbar: [ 12 | [{ 13 | 'header': [1, 2, 3, 4, 5, false] 14 | }, { 15 | 'size': ['small', false, 'large', 'huge'] 16 | }], 17 | [{ 18 | 'color': [] 19 | }, { 20 | 'background': [] 21 | }, 'bold', 'italic', 'underline', 'strike'], 22 | ['link', 'blockquote', 'code-block', 'image'], 23 | [{ 24 | 'align': [] 25 | }, { 26 | 'indent': '-1' 27 | }, { 28 | 'indent': '+1' 29 | }, { 30 | list: 'ordered' 31 | }, { 32 | list: 'bullet' 33 | }], 34 | ['clean'] // outdent/indent 35 | ], 36 | imageUpload: { 37 | upload: file => { 38 | // return a Promise that resolves in a link to the uploaded image 39 | return new Promise((resolve, reject) => { 40 | setTimeout(() => { 41 | resolve(MOCK_IMG_SRC); // Must resolve as a link to the image 42 | }, 1000); 43 | // const fd = new FormData(); 44 | // fd.append("upload_file", file); 45 | 46 | // const xhr = new XMLHttpRequest(); 47 | // xhr.open("POST", `${window.location.pathname}/api/files/add`, true); 48 | // xhr.onload = () => { 49 | // if (xhr.status === 200) { 50 | // const response = JSON.parse(xhr.responseText); 51 | // resolve(response.file_path); // Must resolve as a link to the image 52 | // } 53 | // }; 54 | // xhr.send(fd); 55 | }); 56 | } 57 | }, 58 | }, 59 | placeholder: 'please write something...', 60 | }); 61 | 62 | document.getElementById('output').onclick = function() { 63 | console.log(quill.root.innerHTML); 64 | } 65 | -------------------------------------------------------------------------------- /demo/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const CleanWebpackPlugin = require('clean-webpack-plugin'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); 5 | 6 | module.exports = { 7 | entry: { 8 | workboard: './src/index.js' 9 | }, 10 | output: { 11 | filename: 'index.[chunkhash].js', 12 | path: path.resolve(__dirname, 'dist') 13 | }, 14 | module: { 15 | rules: [ 16 | // { 17 | // enforce: "pre", 18 | // test: /\.js$/, 19 | // exclude: /node_modules/, 20 | // loader: "eslint-loader", 21 | // }, 22 | { 23 | test: /\.css$/, 24 | use: [ 25 | 'style-loader', 26 | 'css-loader' 27 | ] 28 | }, 29 | { 30 | test: /\.js$/, 31 | loader: 'babel-loader', 32 | query: { 33 | presets: ['es2015'], 34 | }, 35 | }, 36 | ] 37 | }, 38 | devServer: { 39 | contentBase: './dist' 40 | }, 41 | plugins: [ 42 | new CleanWebpackPlugin(['dist']), 43 | new HtmlWebpackPlugin({ 44 | template: './src/index.html', 45 | // chunks: ['workboard'], 46 | filename: 'index.html' 47 | }), 48 | new UglifyJSPlugin() 49 | ] 50 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quill-plugin-image-upload", 3 | "version": "0.0.6", 4 | "description": "a plugin for uploading image in Quill", 5 | "main": "./src/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/dragonwong/quill-plugin-image-upload.git" 12 | }, 13 | "keywords": [ 14 | "quill", 15 | "image", 16 | "upload", 17 | "uploader", 18 | "src" 19 | ], 20 | "author": "", 21 | "license": "ISC", 22 | "bugs": { 23 | "url": "https://github.com/dragonwong/quill-plugin-image-upload/issues" 24 | }, 25 | "homepage": "https://github.com/dragonwong/quill-plugin-image-upload#readme" 26 | } 27 | -------------------------------------------------------------------------------- /src/ImageUploadPlaceholder.js: -------------------------------------------------------------------------------- 1 | const Quill = require('quill'); 2 | const constant = require('./constant'); 3 | 4 | const Image = Quill.import('formats/image'); 5 | 6 | class ImageUploadPlaceholder extends Image { 7 | static create(value) { 8 | let id; 9 | let src; 10 | 11 | const arr = value.split(constant.ID_SPLIT_FLAG); 12 | if (arr.length > 1) { 13 | id = arr[0]; 14 | src = arr[1]; 15 | } else { 16 | src = value; 17 | } 18 | 19 | let node = super.create(src); 20 | if (typeof src === 'string') { 21 | node.setAttribute('src', this.sanitize(src)); 22 | } 23 | 24 | if (id) { 25 | node.setAttribute('id', id); 26 | } 27 | return node; 28 | } 29 | } 30 | 31 | ImageUploadPlaceholder.blotName = 'imageUpload'; 32 | ImageUploadPlaceholder.className = constant.IMAGE_UPLOAD_PLACEHOLDER_CLASS_NAME; 33 | 34 | Quill.register({ 35 | 'formats/imageUploadPlaceholder': ImageUploadPlaceholder 36 | }); 37 | -------------------------------------------------------------------------------- /src/constant.js: -------------------------------------------------------------------------------- 1 | const constant = { 2 | ID_SPLIT_FLAG: '__ID_SPLIT__', 3 | IMAGE_UPLOAD_PLACEHOLDER_CLASS_NAME: 'quill-plugin-image-upload-placeholder', 4 | }; 5 | 6 | module.exports = constant; 7 | -------------------------------------------------------------------------------- /src/imageIdManger.js: -------------------------------------------------------------------------------- 1 | const imageIdManger = { 2 | id: 0, 3 | name: 'QUILL_IMAGE_PLUS', 4 | generate() { 5 | const id = this.id; 6 | this.id = id + 1; 7 | return `${this.name}_${id}`; 8 | }, 9 | } 10 | 11 | module.exports = imageIdManger; 12 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | require('./ImageUploadPlaceholder.js'); 2 | require('./style.js'); 3 | const imageIdManger = require('./imageIdManger'); 4 | const constant = require('./constant'); 5 | 6 | class ImageUpload { 7 | constructor(quill, options) { 8 | this.quill = quill; 9 | this.options = options; 10 | this.range = null; 11 | 12 | if (typeof (this.options.upload) !== "function") 13 | console.warn('[Missing config] upload function that returns a promise is required'); 14 | 15 | var toolbar = this.quill.getModule("toolbar"); 16 | toolbar.addHandler("image", this.selectLocalImage.bind(this)); 17 | } 18 | 19 | selectLocalImage() { 20 | this.range = this.quill.getSelection(); 21 | this.fileHolder = document.createElement("input"); 22 | this.fileHolder.setAttribute("type", "file"); 23 | this.fileHolder.setAttribute('accept', 'image/*'); 24 | this.fileHolder.onchange = this.fileChanged.bind(this); 25 | this.fileHolder.click(); 26 | } 27 | 28 | fileChanged() { 29 | const file = this.fileHolder.files[0]; 30 | const imageId = imageIdManger.generate(); 31 | 32 | const fileReader = new FileReader(); 33 | fileReader.addEventListener("load", () => { 34 | let base64ImageSrc = fileReader.result; 35 | this.insertBase64Image(base64ImageSrc, imageId); 36 | }, false); 37 | if (file) { 38 | fileReader.readAsDataURL(file); 39 | } 40 | 41 | this.options.upload(file) 42 | .then((imageUrl) => { 43 | this.insertToEditor(imageUrl, imageId); 44 | }, 45 | (error) => { 46 | console.warn(error.message); 47 | } 48 | ) 49 | } 50 | 51 | insertBase64Image(url, imageId) { 52 | const range = this.range; 53 | this.quill.insertEmbed(range.index, "imageUpload", `${imageId}${constant.ID_SPLIT_FLAG}${url}`); 54 | } 55 | 56 | insertToEditor(url, imageId) { 57 | const imageElement = document.getElementById(imageId); 58 | if (imageElement) { 59 | imageElement.setAttribute('src', url); 60 | imageElement.removeAttribute('id'); 61 | imageElement.classList.remove(constant.IMAGE_UPLOAD_PLACEHOLDER_CLASS_NAME); 62 | } 63 | } 64 | } 65 | 66 | module.exports = ImageUpload; 67 | -------------------------------------------------------------------------------- /src/style.js: -------------------------------------------------------------------------------- 1 | const constant = require('./constant'); 2 | 3 | const ANIMATION_NAME = 'quill-plugin-image-upload-spinner'; 4 | 5 | const styleElement = document.createElement('style'); 6 | styleElement.type = 'text/css'; 7 | document.getElementsByTagName('head')[0].appendChild(styleElement); 8 | 9 | styleElement.appendChild(document.createTextNode(` 10 | .${constant.IMAGE_UPLOAD_PLACEHOLDER_CLASS_NAME} { 11 | display: inline-block; 12 | width: 30px; 13 | height: 30px; 14 | border-radius: 50%; 15 | border: 3px solid #ccc; 16 | border-top-color: #1e986c; 17 | animation: ${ANIMATION_NAME} 0.6s linear infinite; 18 | } 19 | @keyframes ${ANIMATION_NAME} { 20 | to { 21 | transform: rotate(360deg); 22 | } 23 | } 24 | `)); 25 | --------------------------------------------------------------------------------