├── demo ├── react-quill.js ├── quill.snow.css ├── test.html ├── index.html └── index.js ├── .prettierrc ├── cypress.json ├── .gitignore ├── tsconfig.json ├── .travis.yml ├── test ├── polyfills │ ├── getSelection.js │ └── MutationObserver.js ├── setup.js ├── utils.js ├── index.spec.js └── index.js ├── LICENSE ├── webpack.config.js ├── .github └── ISSUE_TEMPLATE.md ├── package.json ├── CHANGELOG.md ├── src └── index.tsx └── README.md /demo/react-quill.js: -------------------------------------------------------------------------------- 1 | ../dist/react-quill.js -------------------------------------------------------------------------------- /demo/quill.snow.css: -------------------------------------------------------------------------------- 1 | ../node_modules/quill/dist/quill.snow.css -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5" 4 | } 5 | -------------------------------------------------------------------------------- /demo/test.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "integrationFolder": "test", 3 | "fileServerFolder": "demo", 4 | "testFiles": "*.spec.js", 5 | "videosFolder": ".cypress/videos", 6 | "screenshotsFolder": ".cypress/screenshots", 7 | "fixturesFolder": false, 8 | "pluginsFile": false, 9 | "supportFile": false 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Misc 2 | *~ 3 | ~* 4 | *.tmp 5 | *.orig 6 | *.bak 7 | *.log 8 | *.DS_STORE 9 | /tmp 10 | 11 | # Dependencies 12 | /node_modules 13 | 14 | # Compiled files 15 | /dist 16 | /lib 17 | 18 | # Packaging artifacts 19 | /react-quill-*.tgz 20 | /package 21 | 22 | # Test artifacts 23 | .cypress/screenshots 24 | .cypress/videos 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "./src/**/*.ts", 4 | "./src/**/*.tsx", 5 | ], 6 | "compilerOptions": { 7 | "strict": true, 8 | "target": "es5", 9 | "module": "commonjs", 10 | "moduleResolution": "node", 11 | "esModuleInterop": true, 12 | "declaration": true, 13 | "sourceMap": true, 14 | "inlineSources": true, 15 | "outDir": "lib", 16 | "lib": ["dom", "esnext"], 17 | "types": ["node"], 18 | "jsx": "react" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 10 4 | addons: 5 | apt: 6 | packages: 7 | # Ubuntu 16+ does not install this dependency by default, so we need to install it ourselves 8 | - libgconf-2-4 9 | cache: 10 | # Caches $HOME/.npm when npm ci is default script command 11 | # Caches node_modules in all other cases 12 | npm: true 13 | directories: 14 | # we also need to cache folder with Cypress binary 15 | - ~/.cache 16 | install: 17 | - npm install 18 | script: 19 | - npm run test 20 | -------------------------------------------------------------------------------- /test/polyfills/getSelection.js: -------------------------------------------------------------------------------- 1 | module.exports = function(global) { 2 | global.document = global.document || {}; 3 | global.window = global.window || {}; 4 | document.getSelection = getSelectionShim 5 | document.createRange = document.getSelection; 6 | } 7 | 8 | /** 9 | * DOM Traversal is not implemented in JSDOM 10 | * The best we can do is shim the functions 11 | */ 12 | function getSelectionShim() { 13 | return { 14 | getRangeAt: function() {}, 15 | removeAllRanges: function() {}, 16 | setStart: function() {}, 17 | setEnd: function() {}, 18 | addRange: function() {}, 19 | }; 20 | }; -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create mock DOM and browser globals 3 | * 4 | * See Enzyme docs: 5 | * https://github.com/airbnb/enzyme/blob/master/docs/guides/jsdom.md 6 | * https://github.com/airbnb/enzyme/blob/master/docs/api/mount.md 7 | */ 8 | 9 | require('jsdom-global')(); 10 | require('./polyfills/MutationObserver.js')(global); 11 | require('./polyfills/getSelection.js')(global); 12 | 13 | // Configure the Enzyme adapter 14 | const Enzyme = require('enzyme'); 15 | const EnzymeAdapter = require('enzyme-adapter-react-16'); 16 | Enzyme.configure({ adapter: new EnzymeAdapter() }); 17 | 18 | // Setup Chai to use Enzyme 19 | const chai = require('chai'); 20 | const chaiEnzyme = require('chai-enzyme'); 21 | chai.use(chaiEnzyme()); 22 | 23 | global.expect = chai.expect; 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020, zenoamaro${text}
` 56 | }); 57 | cy.getEditor().should("contain", text); 58 | }); 59 | 60 | it("can set value with Quill delta", function() { 61 | const text = "Hello, world"; 62 | const delta = { ops: [{ insert: text }] }; 63 | cy.renderWithProps({ 64 | value: delta 65 | }); 66 | cy.getEditor().should("contain", text); 67 | }); 68 | 69 | it("updates the content when the value prop changes", () => { 70 | const text = "Hello, world"; 71 | const change = "Goodbye, world"; 72 | cy.renderWithProps({value: text}) 73 | .getEditor() 74 | .should("contain", text) 75 | .setProps({value: change}) 76 | .getEditor() 77 | .should("contain", change) 78 | .should("not.contain", text); 79 | }); 80 | 81 | 82 | it("allows using Deltas as defaultValue", () => { 83 | const text = "Hello, world"; 84 | const delta = { ops: [{ insert: text }] }; 85 | cy.renderWithProps({ 86 | defaultValue: delta 87 | }); 88 | cy.getEditor().should("contain", text); 89 | }); 90 | 91 | it("allows using Deltas as defaultValue", () => { 92 | const text = "Hello, world"; 93 | const delta = { ops: [{ insert: text }] }; 94 | cy.renderWithProps({ 95 | defaultValue: delta 96 | }); 97 | cy.getEditor().should("contain", text); 98 | }); 99 | 100 | it("cannot type in read-only editor", function() { 101 | cy.renderWithProps({ readOnly: true }); 102 | cy.getEditor().should("have.attr", "contenteditable", "false"); 103 | }); 104 | 105 | it("preserves content when switching to read-only mode", function() { 106 | cy.renderWithProps({}); 107 | cy.getEditor().as("editor"); 108 | cy.get("@editor") 109 | .type("some text") 110 | .should("contain", "some text"); 111 | cy.setProps({ readOnly: true }); 112 | cy.get("@editor").should("contain", "some text"); 113 | }); 114 | 115 | it("calls onChange after typing", () => { 116 | const props = { 117 | onChange: (_, delta) => {}, 118 | value: "Hello, world
" 119 | }; 120 | cy.spy(props, "onChange"); 121 | cy.renderWithProps(props); 122 | cy.getEditor() 123 | .type("!!!") 124 | .then(() => { 125 | expect(props.onChange).to.be.called; 126 | }); 127 | }); 128 | 129 | it("can imperatively focus and blur using ref .blur() and .focus()", () => { 130 | cy.renderWithProps(); 131 | cy.getEditor() 132 | .type("hello") 133 | .then(editor => { 134 | cy.getEditorRef().then(reactquill => { 135 | reactquill.blur(); 136 | expect(editor).to.not.be.focused; 137 | reactquill.focus(); 138 | expect(editor).to.be.focused; 139 | }); 140 | }); 141 | }); 142 | 143 | it("can access underlying Quill instance using ref .getEditor()", () => { 144 | cy.renderWithProps(); 145 | cy.getEditorRef().then(reactquill => { 146 | expect(reactquill.getEditor()).to.be.instanceOf(Quill); 147 | }); 148 | }); 149 | 150 | it("passes options to Quill from props", () => { 151 | const enabledFormats = ["underline", "bold", "italic"]; 152 | const props = { 153 | placeholder: "foobar", 154 | readOnly: true, 155 | formats: enabledFormats, 156 | modules: { 157 | toolbar: enabledFormats 158 | } 159 | }; 160 | cy.renderWithProps(props); 161 | cy.getEditorRef().then(reactquill => { 162 | const quill = reactquill.getEditor(); 163 | expect(quill.options.placeholder).to.equal(props.placeholder); 164 | expect(quill.options.readOnly).to.equal(props.readOnly); 165 | expect(quill.options.modules).to.include.keys(Object.keys(props.modules)); 166 | expect(quill.options.formats).to.include.members(props.formats); 167 | }); 168 | }); 169 | 170 | it("prevents using Delta changesets from events as value", () => { 171 | const value = "Hello, world
"; 172 | const changedValue = "Adieu, world!"; 173 | let lastDelta; 174 | const onChange = (_, delta) => { 175 | lastDelta = delta; 176 | }; 177 | cy.renderWithProps({ value, onChange }); 178 | cy.getEditor() 179 | .type("?") 180 | .window(w => { 181 | expect(() => w.setProps({ value: lastDelta })).to.throw( 182 | "You are passing the `delta` object from the `onChange` event back" 183 | ); 184 | }); 185 | }); 186 | }); 187 | 188 | describe("old test suite", () => { 189 | // Instead of running the old test inside JSDOM, run 190 | // the tests inside the browser environment. 191 | // This works because we are using Chai and Mocha, 192 | // just like Cypress. 193 | 194 | const Enzyme = require("enzyme"); 195 | const EnzymeAdapter = require("enzyme-adapter-react-16"); 196 | Enzyme.configure({ adapter: new EnzymeAdapter() }); 197 | 198 | require("./index.js"); 199 | }); 200 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v2.0.0 4 | 5 | - Fully ported to TypeScript (#549) 6 | - Fully React16 compliant (#549) 7 | - Removed Mixin (#549) 8 | - Removed Toolbar (#549) 9 | - Support for React 18 (#793, #822) 10 | 11 | ## v1.3.4 12 | 13 | - Bump Quill to 1.3.7 to close a security vulnerability (#575) 14 | 15 | ## v1.3.3 16 | 17 | - Pin Quill types version (#420 @daggmano) 18 | 19 | ## v1.3.2 20 | 21 | - Add preserveWhitespace prop (#407 @royshouvik) 22 | 23 | ## v1.3.1 24 | 25 | - Add back default export (#374, #384 one19) 26 | 27 | ## v1.3.0 28 | 29 | - Add scrollingContainer prop 30 | - Fix Typescript exports 31 | - Fix tabindex prop 32 | 33 | ## v1.2.6 34 | 35 | Replaced React.DOM with react-dom-factories (#319 thienanle) 36 | 37 | ## v1.2.5 38 | 39 | - Fix issue with unnecessary editor focus on mount (#321 jetzhou) 40 | - Switch to Quill's clipboard.convert from the paste API that now grabs focus automatically 41 | 42 | ## v1.2.4 43 | 44 | - Only restore focus if editor had focus (#312 @MattKunze) 45 | 46 | ## v1.2.2 47 | 48 | - Add Typescript definitions (#277 @Charrondev) 49 | - Fixes for TS definitions (#294 @jdhungtington, #296 @ajaska) 50 | 51 | ## v1.1.0 52 | 53 | - Add support for React 16 and onwards by depending on `prop-types` and `create-react-class` (#181 @mikecousins) 54 | - Allow setting contents with a Quill Delta via the `value` prop (#101) 55 | - Add onFocus/onBlur props (#110) 56 | - Add tabindex support (#232) 57 | 58 | ## v1.0.0 59 | 60 | This release supports Quill v1.0.0+. ⚠️ There are many breaking changes, so refer to the documentation for information on how to migrate your application. 61 | 62 | - Updated to support Quill v1.0.0+ (@clemmy, @alexkrolick) 63 | - Bundling Quill with ReactQuill (@clemmy) 64 | - Deprecated `toolbar` property and component 65 | - Deprecated the `styles` property 66 | - Deprecated custom formats via the `formats` property 67 | - Deprecated the `pollInterval` property 68 | - Rerendering on `style` property change (@lavrton) 69 | - Improved docs for `bounds`, which now rerenders on change 70 | - Performing deep props comparison to avoid rerenders 71 | - Fixed the unprivileged editor not returning values 72 | - Restoring selection event after text change 73 | - Fixed the order of parameters in change events (@webcarrot) 74 | - Using 'core' instead of 'base' CSS (@alexkrolick) 75 | - Added support for the `placeholder` property (@alexkrolick) 76 | - Enable/disable editor using top-level Quill API (@alexkrolick) 77 | - Prevent whitespace issues when initializing the editor (@bobrafie) 78 | - Using buttons instead of spans for toolbar actions (@clemmy) 79 | - Removed getHtml from unprivileged editor (@clemmy) 80 | - Fixed calculations for range fields (@clemmy) 81 | - Removed deprecated destroy functionality (@clemmy) 82 | - Added return statement to proxy editor methods (@druti) 83 | - Inline styles support for Quill Toolbar (@e-jigsaw) 84 | - Fixed custom font size definitions (@clemmy) 85 | - Support for bullet and ordered lists in toolbar (@clemmy) 86 | - Updated the toolbar alignment section (@clemmy) 87 | - Updated rendering of toolbar actions (@clemmy) 88 | - Improved toolbar renderChoices implementation (@zhang-z) 89 | - Fixed use of `defaultValue` in Toolbar selects 90 | - Fixed bounds validation in setEditorSelection (@wouterh) 91 | - Exposed Quill in exports (@tdg5) 92 | - Added unhook function to clean up event listeners on unmount (@alexkrolick, @jrmmnr) 93 | - Fixed documentation typos (@l3kn) 94 | - Started testing with Enzyme (@alexkrolick) 95 | - Fixed issue where changing props caused re-render artifacts (#147) 96 | - Fixed bounds validation in setEditorSelection (@wouterh) 97 | - Updated README.md to reference core.css instead of base.css (@sandbochs) 98 | - Updated React peerDependency (@rpellerin) 99 | - Removed inline Parchment formats for font-size and font-family (#217) 100 | 101 | ## v0.4.1 102 | 103 | - Added contents of `dist` to NPM package. 104 | 105 | ## v0.4.0 106 | 107 | This release finally adds support for React 0.14. ⚠️ Shims to support older versions of React have been removed. 108 | 109 | - React 0.14 support (@jacktrades, #49) 110 | - Removed shims for React 0.12 and 0.13 111 | - Bumped Quill.js to v0.20.1 112 | - _Normal_ and _smaller_ sizes are not swapped anymore. (#63) 113 | - Various toolbar choice items are now correctly ordered. 114 | - Added image tooltips to the default set of modules (@kairxa, #54) 115 | - Fixed extra white-space in classnames (@asiniy, #67) 116 | - Published the Quill namespace on ReactQuill (@Sajam, #60) 117 | - Quill stylesheets are now linked to `dist/` for convenience. (#70) 118 | - Exposed editor accessor methods in change events. (#33) 119 | 120 | ## v0.3.0 121 | 122 | - Bumped Quill.js to v0.2.0 123 | - Exposed `focus` and `blur` public methods from component. 124 | - Exposed `getEditor` public method to retrieve the backing Quill instance from the component. 125 | - Added callbacks for listening to keyboard events. 126 | - Added tooltips for toolbar choice controls (@bird512). 127 | - Added support for child nodes in toolbar items (@1000hz). 128 | - Added support for custom formats in the configuration (@csk157). 129 | - Added an option to disable the toolbar entirely by passing `false` to `toolbar`. 130 | - Added an option to disable styles entirely by passing `false` to `style` (@kkerr1). 131 | - Fixed an issue where the Quill would duplicate React IDs inside the toolbar leading to errors. Fixes #15. 132 | - Fixes an issue where the editor could be used while null (@brucedlukens). 133 | - Fixes an issue where null would be set on the editor. Fixes #48. 134 | - Fixes an issue where the editor would be instantiated with the wrong value. Fixes #50. 135 | - Avoiding parsing Quill's `dist` directory with webpack. 136 | 137 | ## v0.2.2 138 | 139 | - Added missing `modules` propType and documentation. 140 | - Children are now cloned so ReactQuill can own their refs. Fixes #20. 141 | 142 | ## v0.2.1 143 | 144 | - Link toolbar button and module are now enabled by default. Fixes #19. 145 | 146 | ## v0.2.0 147 | 148 | - Fix React warnings about unique `key` props in toolbar (@Janekk). 149 | - Sending `delta` and `source` from editor change events. Fixes #17. 150 | - Rewritten uncontrolled and semi-controlled operation. Should fix #9, #10 and #14. 151 | - Editor props can now be changed after mounting. 152 | - Added callback for selection change event. Closes #12. 153 | 154 | ## v0.1.1 155 | 156 | - The pre-compiled distributable is not shipped with the NPM package anymore. Should fix #2. 157 | - Sourcemaps are now emitted for both distributables, as separate files. 158 | - Avoiding parsing Quill as it ships with a pre-built main. 159 | 160 | ## v0.1.0 161 | 162 | - Added support for toolbar separators. 163 | - Added support for font family selectors. 164 | - Updated the default toolbar to match Quill's. 165 | - Updated Quill to v0.19.12. 166 | 167 | ## v0.0.6 168 | 169 | - Added keywords for inclusion in [React.parts](https://react.parts). 170 | 171 | ## v0.0.5 172 | 173 | - Default empty content for components with no value. 174 | - Fixes wrong `QuillToolbar` propType. 175 | 176 | ## v0.0.4 177 | 178 | - Added color toggle to toolbar (@chrismcv) 179 | - Exporting default item sets on `QuillToolbar` 180 | - Fixed `QuillComponent` only accepting a single child. 181 | 182 | ## v0.0.3 183 | 184 | - Switched from `quilljs` package to `quill`. 185 | - Using the new `destroy()` from Quill. 186 | 187 | ## v0.0.2 188 | 189 | - Compatible with React 0.12. 190 | 191 | ## v0.0.1 192 | 193 | - Initial version. 194 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test suite uses mocha and enzyme to mock browser APIs 3 | * 4 | * See Enzyme docs: 5 | * https://github.com/airbnb/enzyme/blob/master/docs/guides/jsdom.md 6 | * https://github.com/airbnb/enzyme/blob/master/docs/api/mount.md 7 | */ 8 | 9 | const React = require('react'); 10 | const sinon = require('sinon'); 11 | const ReactQuill = require('../lib/index'); 12 | const { Quill } = require('../lib/index'); 13 | 14 | const { 15 | mountReactQuill, 16 | getQuillInstance, 17 | getQuillContentsAsHTML, 18 | setQuillContentsFromHTML, 19 | withMockedConsole, 20 | } = require('./utils'); 21 | 22 | console.log( 23 | '\n\ 24 | Note that some functionality cannot be tested outside of a full browser environment.\n\n\ 25 | To manually test the component:\n\ 26 | 1) Run "npm install" & "npm run build"\n\ 27 | 2) Open "demo/index.html" in a web browser.\ 28 | ' 29 | ); 30 | 31 | describe('Hello, world!
'; 73 | const wrapper = mountReactQuill({ value: html }); 74 | const quill = getQuillInstance(wrapper); 75 | expect(getQuillContentsAsHTML(wrapper)).to.equal(html); 76 | }); 77 | 78 | it('allows using HTML strings as defaultValue', () => { 79 | const html = 'Hello, world!
'; 80 | const wrapper = mountReactQuill({ defaultValue: html }); 81 | const quill = getQuillInstance(wrapper); 82 | expect(getQuillContentsAsHTML(wrapper)).to.equal(html); 83 | }); 84 | 85 | it('allows using Deltas as value', () => { 86 | const html = 'Hello, world!
'; 87 | const delta = { ops: [{ insert: 'Hello, world!' }] }; 88 | const wrapper = mountReactQuill({ value: delta }); 89 | const quill = getQuillInstance(wrapper); 90 | expect(getQuillContentsAsHTML(wrapper)).to.equal(html); 91 | }); 92 | 93 | it('prevents using Delta changesets from events as value', done => { 94 | const value = 'Hello, world!
'; 95 | const changedValue = 'Adieu, world!
'; 96 | let calledDone = false; 97 | let wrapper; 98 | 99 | const onChange = (value, delta) => { 100 | // Setting props unconditionally here will cause a change loop 101 | if (!calledDone) wrapper.setProps({ value: delta }); 102 | }; 103 | wrapper = mountReactQuill({ value, onChange }); 104 | 105 | const expectedErr = /You are passing the `delta` object from the `onChange` event back/; 106 | // this test knows a lot about the implementation, 107 | // but we need to wrap the right function with a catch 108 | // in order to prevent errors from it from propagating 109 | const originalValidateProps = wrapper.instance().validateProps; 110 | 111 | wrapper.instance().validateProps = function(props) { 112 | try { 113 | originalValidateProps.call(wrapper.instance(), props); 114 | } catch (err) { 115 | if (!calledDone && expectedErr.test(err)) { 116 | calledDone = true; 117 | done(); 118 | } 119 | } 120 | }.bind(wrapper.instance()); 121 | 122 | setQuillContentsFromHTML(wrapper, changedValue); 123 | }); 124 | 125 | it('allows using Deltas as defaultValue', () => { 126 | const html = 'Hello, world!
'; 127 | const delta = { ops: [{ insert: 'Hello, world!' }] }; 128 | const wrapper = mountReactQuill({ defaultValue: html }); 129 | const quill = getQuillInstance(wrapper); 130 | expect(getQuillContentsAsHTML(wrapper)).to.equal(html); 131 | }); 132 | 133 | it('calls onChange with the new value when Quill calls pasteHTML', () => { 134 | const onChangeSpy = sinon.spy(); 135 | const inHtml = 'Hello, world!
'; 136 | const onChange = value => { 137 | expect(inHtml).to.equal(value); 138 | onChangeSpy(); 139 | }; 140 | const wrapper = mountReactQuill({ onChange }); 141 | setQuillContentsFromHTML(wrapper, inHtml); 142 | expect(getQuillContentsAsHTML(wrapper)).to.equal(inHtml); 143 | expect(onChangeSpy).to.have.property('callCount', 1); 144 | }); 145 | 146 | it('calls onChange with the new value when Quill calls insertText', () => { 147 | const onChangeSpy = sinon.spy(); 148 | const inHtml = 'Hello, World!
'; 149 | const onChange = value => { 150 | expect(inHtml).to.equal(value); 151 | onChangeSpy(); 152 | }; 153 | const wrapper = mountReactQuill({ onChange }); 154 | const quill = getQuillInstance(wrapper); 155 | quill.insertText(0, 'Hello, World!', 'bold', true); 156 | expect(getQuillContentsAsHTML(wrapper)).to.equal(inHtml); 157 | expect(onChangeSpy).to.have.property('callCount', 1); 158 | }); 159 | 160 | it('shows defaultValue if value prop is undefined', () => { 161 | const defaultValue = 'Hello, world!
'; 162 | const wrapper = mountReactQuill({ defaultValue }); 163 | const quill = getQuillInstance(wrapper); 164 | // @ts-ignore untyped instance 165 | expect(wrapper.instance().getEditorContents()).to.equal(defaultValue); 166 | }); 167 | 168 | it('shows the value prop instead of defaultValue if both are defined', () => { 169 | const defaultValue = 'Hello, world!
'; 170 | const value = 'Good night, moon!
'; 171 | const wrapper = mountReactQuill({ 172 | defaultValue: defaultValue, 173 | value: value, 174 | }); 175 | const quill = getQuillInstance(wrapper); 176 | // @ts-ignore untyped instance 177 | expect(wrapper.instance().getEditorContents()).to.equal(value); 178 | }); 179 | 180 | it('uses a custom editing area if provided', () => { 181 | const div = React.createFactory('div'); 182 | const editingArea = div({ id: 'venus' }); 183 | const wrapper = mountReactQuill({}, editingArea); 184 | const quill = getQuillInstance(wrapper); 185 | expect(wrapper.getDOMNode().querySelector('div#venus')).not.to.be.null; 186 | }); 187 | 188 | /** 189 | * This can't be tested with the current state of JSDOM. 190 | * The selection functions have been shimmed in this test suite, 191 | * but they will not work until DOM traversal is implemented in 192 | * https://github.com/tmpvar/jsdom/issues/317. 193 | * Leaving this pending test as a reminder to follow up. 194 | */ 195 | it('focuses editor when calling focus()'); 196 | 197 | /** 198 | * A test for this may work if checking document.activeElement, 199 | * but chances are the focus was never removed from the body 200 | * after calling focus(). See JSDOM issue #317. 201 | */ 202 | it('removes focus from the editor when calling blur()'); 203 | 204 | /** 205 | * In a browser, querySelector('.ql-editor').textContent = 'hi' would 206 | * trigger a 'text-change' event, but here it doesn't. Is the polyfill 207 | * for MutationObserver not working? 208 | */ 209 | it('calls onChange after the textContent of the editor changes'); 210 | 211 | /** 212 | * This is hard to do without Selenium's 'type' function, but it is the 213 | * ultimate test of whether everything is working or not 214 | */ 215 | it('calls onChange after keypresses are sent to the editor'); 216 | }); 217 | -------------------------------------------------------------------------------- /test/polyfills/MutationObserver.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt 5 | * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt 6 | * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt 7 | * Code distributed by Google as part of the polymer project is also 8 | * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt 9 | */ 10 | // @version 0.7.24 11 | if (typeof WeakMap === "undefined") { 12 | (function() { 13 | var defineProperty = Object.defineProperty; 14 | var counter = Date.now() % 1e9; 15 | var WeakMap = function() { 16 | this.name = "__st" + (Math.random() * 1e9 >>> 0) + (counter++ + "__"); 17 | }; 18 | WeakMap.prototype = { 19 | set: function(key, value) { 20 | var entry = key[this.name]; 21 | if (entry && entry[0] === key) entry[1] = value; else defineProperty(key, this.name, { 22 | value: [ key, value ], 23 | writable: true 24 | }); 25 | return this; 26 | }, 27 | get: function(key) { 28 | var entry; 29 | return (entry = key[this.name]) && entry[0] === key ? entry[1] : undefined; 30 | }, 31 | "delete": function(key) { 32 | var entry = key[this.name]; 33 | if (!entry || entry[0] !== key) return false; 34 | entry[0] = entry[1] = undefined; 35 | return true; 36 | }, 37 | has: function(key) { 38 | var entry = key[this.name]; 39 | if (!entry) return false; 40 | return entry[0] === key; 41 | } 42 | }; 43 | window.WeakMap = WeakMap; 44 | })(); 45 | } 46 | 47 | module.exports = function(global) { 48 | if (global.JsMutationObserver) { 49 | return; 50 | } 51 | var registrationsTable = new WeakMap(); 52 | var setImmediate; 53 | if (/Trident|Edge/.test(navigator.userAgent)) { 54 | setImmediate = setTimeout; 55 | } else if (window.setImmediate) { 56 | setImmediate = window.setImmediate; 57 | } else { 58 | var setImmediateQueue = []; 59 | var sentinel = String(Math.random()); 60 | window.addEventListener("message", function(e) { 61 | if (e.data === sentinel) { 62 | var queue = setImmediateQueue; 63 | setImmediateQueue = []; 64 | queue.forEach(function(func) { 65 | func(); 66 | }); 67 | } 68 | }); 69 | setImmediate = function(func) { 70 | setImmediateQueue.push(func); 71 | window.postMessage(sentinel, "*"); 72 | }; 73 | } 74 | var isScheduled = false; 75 | var scheduledObservers = []; 76 | function scheduleCallback(observer) { 77 | scheduledObservers.push(observer); 78 | if (!isScheduled) { 79 | isScheduled = true; 80 | setImmediate(dispatchCallbacks); 81 | } 82 | } 83 | function wrapIfNeeded(node) { 84 | return window.ShadowDOMPolyfill && window.ShadowDOMPolyfill.wrapIfNeeded(node) || node; 85 | } 86 | function dispatchCallbacks() { 87 | isScheduled = false; 88 | var observers = scheduledObservers; 89 | scheduledObservers = []; 90 | observers.sort(function(o1, o2) { 91 | return o1.uid_ - o2.uid_; 92 | }); 93 | var anyNonEmpty = false; 94 | observers.forEach(function(observer) { 95 | var queue = observer.takeRecords(); 96 | removeTransientObserversFor(observer); 97 | if (queue.length) { 98 | observer.callback_(queue, observer); 99 | anyNonEmpty = true; 100 | } 101 | }); 102 | if (anyNonEmpty) dispatchCallbacks(); 103 | } 104 | function removeTransientObserversFor(observer) { 105 | observer.nodes_.forEach(function(node) { 106 | var registrations = registrationsTable.get(node); 107 | if (!registrations) return; 108 | registrations.forEach(function(registration) { 109 | if (registration.observer === observer) registration.removeTransientObservers(); 110 | }); 111 | }); 112 | } 113 | function forEachAncestorAndObserverEnqueueRecord(target, callback) { 114 | for (var node = target; node; node = node.parentNode) { 115 | var registrations = registrationsTable.get(node); 116 | if (registrations) { 117 | for (var j = 0; j < registrations.length; j++) { 118 | var registration = registrations[j]; 119 | var options = registration.options; 120 | if (node !== target && !options.subtree) continue; 121 | var record = callback(options); 122 | if (record) registration.enqueue(record); 123 | } 124 | } 125 | } 126 | } 127 | var uidCounter = 0; 128 | function JsMutationObserver(callback) { 129 | this.callback_ = callback; 130 | this.nodes_ = []; 131 | this.records_ = []; 132 | this.uid_ = ++uidCounter; 133 | } 134 | JsMutationObserver.prototype = { 135 | observe: function(target, options) { 136 | target = wrapIfNeeded(target); 137 | if (!options.childList && !options.attributes && !options.characterData || options.attributeOldValue && !options.attributes || options.attributeFilter && options.attributeFilter.length && !options.attributes || options.characterDataOldValue && !options.characterData) { 138 | throw new SyntaxError(); 139 | } 140 | var registrations = registrationsTable.get(target); 141 | if (!registrations) registrationsTable.set(target, registrations = []); 142 | var registration; 143 | for (var i = 0; i < registrations.length; i++) { 144 | if (registrations[i].observer === this) { 145 | registration = registrations[i]; 146 | registration.removeListeners(); 147 | registration.options = options; 148 | break; 149 | } 150 | } 151 | if (!registration) { 152 | registration = new Registration(this, target, options); 153 | registrations.push(registration); 154 | this.nodes_.push(target); 155 | } 156 | registration.addListeners(); 157 | }, 158 | disconnect: function() { 159 | this.nodes_.forEach(function(node) { 160 | var registrations = registrationsTable.get(node); 161 | for (var i = 0; i < registrations.length; i++) { 162 | var registration = registrations[i]; 163 | if (registration.observer === this) { 164 | registration.removeListeners(); 165 | registrations.splice(i, 1); 166 | break; 167 | } 168 | } 169 | }, this); 170 | this.records_ = []; 171 | }, 172 | takeRecords: function() { 173 | var copyOfRecords = this.records_; 174 | this.records_ = []; 175 | return copyOfRecords; 176 | } 177 | }; 178 | function MutationRecord(type, target) { 179 | this.type = type; 180 | this.target = target; 181 | this.addedNodes = []; 182 | this.removedNodes = []; 183 | this.previousSibling = null; 184 | this.nextSibling = null; 185 | this.attributeName = null; 186 | this.attributeNamespace = null; 187 | this.oldValue = null; 188 | } 189 | function copyMutationRecord(original) { 190 | var record = new MutationRecord(original.type, original.target); 191 | record.addedNodes = original.addedNodes.slice(); 192 | record.removedNodes = original.removedNodes.slice(); 193 | record.previousSibling = original.previousSibling; 194 | record.nextSibling = original.nextSibling; 195 | record.attributeName = original.attributeName; 196 | record.attributeNamespace = original.attributeNamespace; 197 | record.oldValue = original.oldValue; 198 | return record; 199 | } 200 | var currentRecord, recordWithOldValue; 201 | function getRecord(type, target) { 202 | return currentRecord = new MutationRecord(type, target); 203 | } 204 | function getRecordWithOldValue(oldValue) { 205 | if (recordWithOldValue) return recordWithOldValue; 206 | recordWithOldValue = copyMutationRecord(currentRecord); 207 | recordWithOldValue.oldValue = oldValue; 208 | return recordWithOldValue; 209 | } 210 | function clearRecords() { 211 | currentRecord = recordWithOldValue = undefined; 212 | } 213 | function recordRepresentsCurrentMutation(record) { 214 | return record === recordWithOldValue || record === currentRecord; 215 | } 216 | function selectRecord(lastRecord, newRecord) { 217 | if (lastRecord === newRecord) return lastRecord; 218 | if (recordWithOldValue && recordRepresentsCurrentMutation(lastRecord)) return recordWithOldValue; 219 | return null; 220 | } 221 | function Registration(observer, target, options) { 222 | this.observer = observer; 223 | this.target = target; 224 | this.options = options; 225 | this.transientObservedNodes = []; 226 | } 227 | Registration.prototype = { 228 | enqueue: function(record) { 229 | var records = this.observer.records_; 230 | var length = records.length; 231 | if (records.length > 0) { 232 | var lastRecord = records[length - 1]; 233 | var recordToReplaceLast = selectRecord(lastRecord, record); 234 | if (recordToReplaceLast) { 235 | records[length - 1] = recordToReplaceLast; 236 | return; 237 | } 238 | } else { 239 | scheduleCallback(this.observer); 240 | } 241 | records[length] = record; 242 | }, 243 | addListeners: function() { 244 | this.addListeners_(this.target); 245 | }, 246 | addListeners_: function(node) { 247 | var options = this.options; 248 | if (options.attributes) node.addEventListener("DOMAttrModified", this, true); 249 | if (options.characterData) node.addEventListener("DOMCharacterDataModified", this, true); 250 | if (options.childList) node.addEventListener("DOMNodeInserted", this, true); 251 | if (options.childList || options.subtree) node.addEventListener("DOMNodeRemoved", this, true); 252 | }, 253 | removeListeners: function() { 254 | this.removeListeners_(this.target); 255 | }, 256 | removeListeners_: function(node) { 257 | var options = this.options; 258 | if (options.attributes) node.removeEventListener("DOMAttrModified", this, true); 259 | if (options.characterData) node.removeEventListener("DOMCharacterDataModified", this, true); 260 | if (options.childList) node.removeEventListener("DOMNodeInserted", this, true); 261 | if (options.childList || options.subtree) node.removeEventListener("DOMNodeRemoved", this, true); 262 | }, 263 | addTransientObserver: function(node) { 264 | if (node === this.target) return; 265 | this.addListeners_(node); 266 | this.transientObservedNodes.push(node); 267 | var registrations = registrationsTable.get(node); 268 | if (!registrations) registrationsTable.set(node, registrations = []); 269 | registrations.push(this); 270 | }, 271 | removeTransientObservers: function() { 272 | var transientObservedNodes = this.transientObservedNodes; 273 | this.transientObservedNodes = []; 274 | transientObservedNodes.forEach(function(node) { 275 | this.removeListeners_(node); 276 | var registrations = registrationsTable.get(node); 277 | for (var i = 0; i < registrations.length; i++) { 278 | if (registrations[i] === this) { 279 | registrations.splice(i, 1); 280 | break; 281 | } 282 | } 283 | }, this); 284 | }, 285 | handleEvent: function(e) { 286 | e.stopImmediatePropagation(); 287 | switch (e.type) { 288 | case "DOMAttrModified": 289 | var name = e.attrName; 290 | var namespace = e.relatedNode.namespaceURI; 291 | var target = e.target; 292 | var record = new getRecord("attributes", target); 293 | record.attributeName = name; 294 | record.attributeNamespace = namespace; 295 | var oldValue = e.attrChange === MutationEvent.ADDITION ? null : e.prevValue; 296 | forEachAncestorAndObserverEnqueueRecord(target, function(options) { 297 | if (!options.attributes) return; 298 | if (options.attributeFilter && options.attributeFilter.length && options.attributeFilter.indexOf(name) === -1 && options.attributeFilter.indexOf(namespace) === -1) { 299 | return; 300 | } 301 | if (options.attributeOldValue) return getRecordWithOldValue(oldValue); 302 | return record; 303 | }); 304 | break; 305 | 306 | case "DOMCharacterDataModified": 307 | var target = e.target; 308 | var record = getRecord("characterData", target); 309 | var oldValue = e.prevValue; 310 | forEachAncestorAndObserverEnqueueRecord(target, function(options) { 311 | if (!options.characterData) return; 312 | if (options.characterDataOldValue) return getRecordWithOldValue(oldValue); 313 | return record; 314 | }); 315 | break; 316 | 317 | case "DOMNodeRemoved": 318 | this.addTransientObserver(e.target); 319 | 320 | case "DOMNodeInserted": 321 | var changedNode = e.target; 322 | var addedNodes, removedNodes; 323 | if (e.type === "DOMNodeInserted") { 324 | addedNodes = [ changedNode ]; 325 | removedNodes = []; 326 | } else { 327 | addedNodes = []; 328 | removedNodes = [ changedNode ]; 329 | } 330 | var previousSibling = changedNode.previousSibling; 331 | var nextSibling = changedNode.nextSibling; 332 | var record = getRecord("childList", e.target.parentNode); 333 | record.addedNodes = addedNodes; 334 | record.removedNodes = removedNodes; 335 | record.previousSibling = previousSibling; 336 | record.nextSibling = nextSibling; 337 | forEachAncestorAndObserverEnqueueRecord(e.relatedNode, function(options) { 338 | if (!options.childList) return; 339 | return record; 340 | }); 341 | } 342 | clearRecords(); 343 | } 344 | }; 345 | global.JsMutationObserver = JsMutationObserver; 346 | if (!global.MutationObserver) { 347 | global.MutationObserver = JsMutationObserver; 348 | JsMutationObserver._isPolyfilled = true; 349 | } 350 | } -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | React-Quill 3 | https://github.com/zenoamaro/react-quill 4 | */ 5 | 6 | import React from 'react'; 7 | import ReactDOM from 'react-dom'; 8 | import isEqual from 'lodash/isEqual'; 9 | 10 | import Quill, { 11 | QuillOptionsStatic, 12 | DeltaStatic, 13 | RangeStatic, 14 | BoundsStatic, 15 | StringMap, 16 | Sources, 17 | } from 'quill'; 18 | 19 | // Merged namespace hack to export types along with default object 20 | // See: https://github.com/Microsoft/TypeScript/issues/2719 21 | namespace ReactQuill { 22 | export type Value = string | DeltaStatic; 23 | export type Range = RangeStatic | null; 24 | 25 | export interface QuillOptions extends QuillOptionsStatic { 26 | tabIndex?: number, 27 | } 28 | 29 | export interface ReactQuillProps { 30 | bounds?: string | HTMLElement, 31 | children?: React.ReactElement