├── .editorconfig ├── .github └── funding.yml ├── .gitignore ├── .npmrc ├── .prettierrc.js ├── .travis.yml ├── index.ts ├── license.md ├── package.json ├── readme.md ├── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.yml] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.md] 16 | indent_style = space 17 | indent_size = 2 18 | -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | github: gtgalone 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | esm 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | tabWidth: 2, 3 | singleQuote: true, 4 | jsxSingleQuote: true, 5 | bracketSpacing: true, 6 | bracketSameLine: true, 7 | arrowParens: 'always', 8 | printWidth: 100, 9 | printWidth: 100, 10 | trailingComma: 'all', 11 | arrowParens: 'avoid', 12 | }; 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '10' 4 | - '8' 5 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import { useRef, useState, useEffect, RefObject } from 'react'; 2 | 3 | import Quill, { QuillOptions } from 'quill'; 4 | 5 | const theme = 'snow'; 6 | 7 | const modules = { 8 | toolbar: [ 9 | ['bold', 'italic', 'underline', 'strike'], 10 | [{ align: [] }], 11 | 12 | [{ list: 'ordered' }, { list: 'bullet' }], 13 | [{ indent: '-1' }, { indent: '+1' }], 14 | 15 | [{ size: ['small', false, 'large', 'huge'] }], 16 | [{ header: [1, 2, 3, 4, 5, 6, false] }], 17 | ['link', 'image', 'video'], 18 | [{ color: [] }, { background: [] }], 19 | 20 | ['clean'], 21 | ], 22 | clipboard: { 23 | matchVisual: false, 24 | }, 25 | }; 26 | 27 | const formats = [ 28 | 'bold', 29 | 'italic', 30 | 'underline', 31 | 'strike', 32 | 'align', 33 | 'list', 34 | 'indent', 35 | 'size', 36 | 'header', 37 | 'link', 38 | 'image', 39 | 'video', 40 | 'color', 41 | 'background', 42 | ]; 43 | 44 | function assign(target: any, _varArgs: any) { 45 | 'use strict'; 46 | if (target === null || target === undefined) { 47 | throw new TypeError('Cannot convert undefined or null to object'); 48 | } 49 | 50 | const to = Object(target); 51 | 52 | for (let index = 1; index < arguments.length; index++) { 53 | const nextSource = arguments[index]; 54 | 55 | if (nextSource !== null && nextSource !== undefined) { 56 | for (const nextKey in nextSource) { 57 | if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { 58 | to[nextKey] = nextSource[nextKey]; 59 | } 60 | } 61 | } 62 | } 63 | return to; 64 | } 65 | 66 | /** 67 | * 68 | * @param options Quill static options. https://github.com/gtgalone/react-quilljs#options 69 | * @returns Returns quill, quillRef, and Quill. https://github.com/gtgalone/react-quilljs#return 70 | */ 71 | export const useQuill = (options: QuillOptions | undefined = { theme, modules, formats }) => { 72 | const quillRef: RefObject = useRef(null); 73 | 74 | const [isLoaded, setIsLoaded] = useState(false); 75 | const [obj, setObj] = useState({ 76 | Quill: undefined as any | undefined, 77 | quillRef, 78 | quill: undefined as Quill | undefined, 79 | editorRef: quillRef, 80 | editor: undefined as Quill | undefined, 81 | }); 82 | 83 | useEffect(() => { 84 | if (!obj.Quill) { 85 | setObj(prev => assign(prev, { Quill: require('quill').default })); 86 | } 87 | if (obj.Quill && !obj.quill && quillRef && quillRef.current && isLoaded) { 88 | const opts = assign(options, { 89 | modules: assign(modules, options.modules), 90 | formats: options.formats || formats, 91 | theme: options.theme || theme, 92 | }); 93 | const quill = new obj.Quill(quillRef.current, opts); 94 | 95 | setObj(assign(assign({}, obj), { quill, editor: quill })); 96 | } 97 | setIsLoaded(true); 98 | }, [isLoaded, obj, options]); 99 | 100 | return obj; 101 | }; 102 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Jehun Seem (https//github.com/gtgalone) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-quilljs", 3 | "version": "2.0.5", 4 | "description": "React Hook Wrapper for Quill, powerful rich text editor", 5 | "license": "MIT", 6 | "repository": "gtgalone/react-quilljs", 7 | "author": { 8 | "name": "Jehun Seem", 9 | "email": "jehunseem@gmail.com", 10 | "url": "https://github.com/gtgalone" 11 | }, 12 | "scripts": { 13 | "build:cjs": "tsc", 14 | "build:esm": "tsc -m esNext --outDir esm", 15 | "build": "pnpm build:cjs && pnpm build:esm" 16 | }, 17 | "main": "lib/index.js", 18 | "module": "esm/index.js", 19 | "types": "lib/index.d.ts", 20 | "typings": "lib/index.d.ts", 21 | "files": [ 22 | "lib", 23 | "esm" 24 | ], 25 | "exports": { 26 | "require": "./lib/index.js", 27 | "import": "./esm/index.js", 28 | "default": "./lib/index.js" 29 | }, 30 | "keywords": [ 31 | "react quill", 32 | "react quilljs", 33 | "react-quill", 34 | "react-quilljs", 35 | "quill", 36 | "quilljs", 37 | "react-hook-quill", 38 | "react-hook-quilljs", 39 | "react-editor", 40 | "text-editor", 41 | "rich-text-editor" 42 | ], 43 | "peerDependencies": { 44 | "quill": "^2.0.3", 45 | "react": "17 - 19", 46 | "react-dom": "17 - 19" 47 | }, 48 | "devDependencies": { 49 | "@types/react": "^19.0.1", 50 | "@types/quill": "2.0.14", 51 | "@types/node": "22.10.2", 52 | "tslint": "6.1.3", 53 | "tslint-config-prettier": "1.18.0", 54 | "tslint-eslint-rules": "5.4.0", 55 | "tslint-plugin-prettier": "2.3.0", 56 | "tslint-react": "5.0.0", 57 | "typescript": "5.7.2" 58 | }, 59 | "packageManager": "pnpm@9.1.3+sha512.7c2ea089e1a6af306409c4fc8c4f0897bdac32b772016196c469d9428f1fe2d5a21daf8ad6512762654ac645b5d9136bb210ec9a00afa8dbc4677843ba362ecd" 60 | } 61 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # react-quilljs [![Build Status](https://travis-ci.org/gtgalone/react-quilljs.svg?branch=master)](https://travis-ci.org/gtgalone/react-quilljs) 2 | 3 |

4 | Quill Logo 5 |

6 |

7 | React Hook Wrapper for Quill. 8 |

9 | 10 |

11 | SSR Safe 12 | • 13 | Typescript Support 14 | • 15 | Unopinionated 16 | • 17 | No Dependencies 18 | • 19 | Tiny Package Size 20 |

21 | 22 | ## Install 23 | 24 | ``` 25 | // Install packages 26 | 27 | $ pnpm add react-quilljs quill 28 | or 29 | $ yarn add react-quilljs quill 30 | or 31 | $ npm install react-quilljs quill 32 | 33 | 34 | // If you are using Typescript 35 | 36 | $ pnpm add -D @types/quill 37 | ``` 38 | --- 39 | 40 | ## Usage 41 | 42 | [Code Sandbox Playground Example](https://codesandbox.io/s/react-quilljsbasic-wm0uk) 43 | 44 | ### Basic 45 | ```jsx 46 | import React from 'react'; 47 | 48 | import { useQuill } from 'react-quilljs'; 49 | // or const { useQuill } = require('react-quilljs'); 50 | 51 | import 'quill/dist/quill.snow.css'; // Add css for snow theme 52 | // or import 'quill/dist/quill.bubble.css'; // Add css for bubble theme 53 | 54 | export default () => { 55 | const { quill, quillRef } = useQuill(); 56 | 57 | console.log(quill); // undefined > Quill Object 58 | console.log(quillRef); // { current: undefined } > { current: Quill Editor Reference } 59 | 60 | return ( 61 |
62 |
63 |
64 | ); 65 | }; 66 | ``` 67 | --- 68 | ### With Initial Value 69 | ```jsx 70 | export default () => { 71 | const { quill, quillRef } = useQuill(); 72 | 73 | React.useEffect(() => { 74 | if (quill) { 75 | quill.clipboard.dangerouslyPasteHTML('

React Hook for Quill!

'); 76 | } 77 | }, [quill]); 78 | 79 | return ( 80 |
81 |
82 |
83 | ); 84 | }; 85 | ``` 86 | --- 87 | ### With onChange Handler 88 | * https://quilljs.com/docs/api/#text-change 89 | ```jsx 90 | export default () => { 91 | const { quill, quillRef } = useQuill(); 92 | 93 | React.useEffect(() => { 94 | if (quill) { 95 | quill.on('text-change', (delta, oldDelta, source) => { 96 | console.log('Text change!'); 97 | console.log(quill.getText()); // Get text only 98 | console.log(quill.getContents()); // Get delta contents 99 | console.log(quill.root.innerHTML); // Get innerHTML using quill 100 | console.log(quillRef.current.firstChild.innerHTML); // Get innerHTML using quillRef 101 | }); 102 | } 103 | }, [quill]); 104 | 105 | return ( 106 |
107 |
108 |
109 | ); 110 | }; 111 | ``` 112 | --- 113 | ### With Adding Plugins 114 | #### counter 115 | ```jsx 116 | export default () => { 117 | const counterRef = React.useRef(); 118 | const { quill, quillRef, Quill } = useQuill({ modules: { counter: true } }); 119 | 120 | if (Quill && !quill) { 121 | // For execute this line only once. 122 | Quill.register('modules/counter', function(quill, options) { 123 | quill.on('text-change', function() { 124 | const text = quill.getText(); 125 | // There are a couple issues with counting words 126 | // this way but we'll fix these later 127 | counterRef.current.innerText = text.split(/\s+/).length; 128 | }); 129 | }); 130 | } 131 | 132 | return ( 133 |
134 |
135 |
136 |
137 | ); 138 | }; 139 | ``` 140 | #### magic-url 141 | ```jsx 142 | export default () => { 143 | const { quill, quillRef, Quill } = useQuill({ modules: { magicUrl: true }}); 144 | 145 | if (Quill && !quill) { // For execute this line only once. 146 | const MagicUrl = require('quill-magic-url').default; // Install with 'yarn add quill-magic-url' 147 | Quill.register('modules/magicUrl', MagicUrl); 148 | } 149 | 150 | return ( 151 |
152 |
153 |
154 | ); 155 | }; 156 | ``` 157 | --- 158 | ### With Custom Options 159 | #### custom all options 160 | ```jsx 161 | import 'quill/dist/quill.snow.css'; // Add css for snow theme 162 | // import 'quill/dist/quill.bubble.css'; // Add css for bubble theme 163 | 164 | export default () => { 165 | 166 | const theme = 'snow'; 167 | // const theme = 'bubble'; 168 | 169 | const modules = { 170 | toolbar: [ 171 | ['bold', 'italic', 'underline', 'strike'], 172 | ], 173 | }; 174 | 175 | const placeholder = 'Compose an epic...'; 176 | 177 | const formats = ['bold', 'italic', 'underline', 'strike']; 178 | 179 | const { quillRef } = useQuill({ theme, modules, formats, placeholder }); 180 | 181 | return ( 182 |
183 |
184 |
185 | ); 186 | }; 187 | ``` 188 | #### custom toolbar with elements 189 | ```jsx 190 | export default () => { 191 | const { quillRef } = useQuill({ 192 | modules: { 193 | toolbar: '#toolbar' 194 | }, 195 | formats: ["size", "bold", "script"], // Important 196 | }); 197 | 198 | return ( 199 |
200 |
201 | 202 |
203 | 209 |
213 |
214 |
215 | ); 216 | }; 217 | ``` 218 | --- 219 | ### With Custom Attached Image Upload 220 | ```jsx 221 | import fetch from 'isomorphic-unfetch'; 222 | 223 | export default () => { 224 | const { quill, quillRef } = useQuill(); 225 | 226 | // Insert Image(selected by user) to quill 227 | const insertToEditor = (url) => { 228 | const range = quill.getSelection(); 229 | quill.insertEmbed(range.index, 'image', url); 230 | }; 231 | 232 | // Upload Image to Image Server such as AWS S3, Cloudinary, Cloud Storage, etc.. 233 | const saveToServer = async (file) => { 234 | const body = new FormData(); 235 | body.append('file', file); 236 | 237 | const res = await fetch('Your Image Server URL', { method: 'POST', body }); 238 | insertToEditor(res.uploadedImageUrl); 239 | }; 240 | 241 | // Open Dialog to select Image File 242 | const selectLocalImage = () => { 243 | const input = document.createElement('input'); 244 | input.setAttribute('type', 'file'); 245 | input.setAttribute('accept', 'image/*'); 246 | input.click(); 247 | 248 | input.onchange = () => { 249 | const file = input.files[0]; 250 | saveToServer(file); 251 | }; 252 | }; 253 | 254 | React.useEffect(() => { 255 | if (quill) { 256 | // Add custom handler for Image Upload 257 | quill.getModule('toolbar').addHandler('image', selectLocalImage); 258 | } 259 | }, [quill]); 260 | 261 | return ( 262 |
263 |
264 |
265 | ); 266 | }; 267 | ``` 268 | 269 | ## Parameters 270 | ### useQuill(options) 271 | ### options 272 | Options for [Quill Configuration](https://quilljs.com/docs/configuration/#configuration).\ 273 | Type: `Object` 274 | 275 | - `theme`\ 276 | [Quill Theme](https://quilljs.com/docs/themes/#themes).\ 277 | Type: `String`\ 278 | Default: `'snow'` 279 | 280 | - `modules`\ 281 | [Quill Modules](https://quilljs.com/docs/modules/toolbar/#container).\ 282 | Type: `Object`\ 283 | Default: 284 | ``` 285 | { 286 | toolbar: [ 287 | ['bold', 'italic', 'underline', 'strike'], 288 | [{ align: [] }], 289 | 290 | [{ list: 'ordered'}, { list: 'bullet' }], 291 | [{ indent: '-1'}, { indent: '+1' }], 292 | 293 | [{ size: ['small', false, 'large', 'huge'] }], 294 | [{ header: [1, 2, 3, 4, 5, 6, false] }], 295 | ['link', 'image', 'video'], 296 | [{ color: [] }, { background: [] }], 297 | ], 298 | clipboard: { 299 | matchVisual: false, 300 | }, 301 | } 302 | ``` 303 | 304 | - `formats`\ 305 | [Quill Formats](https://quilljs.com/docs/formats/#formats).\ 306 | Type: `Array`\ 307 | Default: 308 | ``` 309 | [ 310 | 'bold', 'italic', 'underline', 'strike', 311 | 'align', 'list', 'indent', 312 | 'size', 'header', 313 | 'link', 'image', 'video', 314 | 'color', 'background', 315 | ] 316 | ``` 317 | 318 | ## Return 319 | 320 | ### quill 321 | Quill object.\ 322 | You can use quill apis(https://quilljs.com/docs/api/) with this object.\ 323 | Type: `Object` 324 | 325 | ### quillRef 326 | Quill Editor reference.\ 327 | Type: `RefObject` 328 | 329 | ### Quill 330 | Quill class. You can use register, find, import, debug.\ 331 | Please refer example above.\ 332 | Type: `Class` 333 | 334 | --- 335 | ## Recommend Libraries 336 | 337 | - [React Checklist](https://github.com/gtgalone/react-checklist) - Make Checkbox List Easy and Simple with React Hooks. 338 | - [React Store](https://github.com/gtgalone/react-store) - React Hook Store with useContext and useReducer for State Management. 339 | - [Decode URI Component Charset](https://github.com/gtgalone/decode-uri-component-charset) - Decode URI Component with Charset such as 'euc-kr' without tears. 340 | 341 | ## Maintainer 342 | 343 | - [Jehun Seem](https://github.com/gtgalone) 344 | 345 | ## License 346 | 347 | MIT 348 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "declaration": true, 7 | "pretty": true, 8 | "sourceMap": false, 9 | "strict": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "noImplicitReturns": true, 15 | "noImplicitAny": false, 16 | "noFallthroughCasesInSwitch": true, 17 | "skipDefaultLibCheck": true, 18 | "outDir": "lib", 19 | "lib": [ 20 | "es2022", 21 | "dom" 22 | ], 23 | "importHelpers": true 24 | }, 25 | "exclude": [ 26 | "node_modules", 27 | "lib", 28 | "esm", 29 | "tests", 30 | "**/__tests__/**/*", 31 | "*.test.ts" 32 | ] 33 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended", 5 | "tslint-react", 6 | "tslint-eslint-rules", 7 | "tslint-config-prettier" 8 | ], 9 | "linterOptions": { 10 | "exclude": ["node_modules/**"] 11 | }, 12 | "jsRules": {}, 13 | "rules": { 14 | "ban-types": false, 15 | "interface-name": [true, "never-prefix"], 16 | "no-console": false, 17 | "max-classes-per-file": false, 18 | "no-any": false, 19 | "no-empty": false, 20 | "prefer-for-of": false, 21 | "jsx-no-lambda": [false], 22 | "no-empty-interface": [false], 23 | "object-literal-sort-keys": false, 24 | "no-unused-expression": false, 25 | "variable-name": [ 26 | true, 27 | "ban-keywords", 28 | "check-format", 29 | "allow-pascal-case", 30 | "allow-leading-underscore", 31 | "allow-trailing-underscore" 32 | ], 33 | "prettier": [ 34 | true, 35 | { 36 | "trailingComma": "es5", 37 | "singleQuote": true, 38 | "printWidth": 120, 39 | "tabWidth": 2 40 | } 41 | ], 42 | "ordered-imports": false 43 | }, 44 | "rulesDirectory": ["tslint-plugin-prettier"] 45 | } 46 | --------------------------------------------------------------------------------