diff --git a/blots/scroll.js b/blots/scroll.js index cabcbfb6a0..e9accc9aa7 100644 --- a/blots/scroll.js +++ b/blots/scroll.js @@ -22,6 +22,7 @@ class Scroll extends Parchment.Scroll { }, {}); } this.optimize(); + this.enable(); } deleteAt(index, length) { @@ -40,6 +41,10 @@ class Scroll extends Parchment.Scroll { this.optimize(); } + enable(enabled = true) { + this.domNode.setAttribute('contenteditable', enabled); + } + formatAt(index, length, format, value) { if (this.whitelist != null && !this.whitelist[format]) return; super.formatAt(index, length, format, value); diff --git a/core/editor.js b/core/editor.js index b771e789ae..ea598b6f17 100644 --- a/core/editor.js +++ b/core/editor.js @@ -1,6 +1,5 @@ import Delta from 'quill-delta'; import DeltaOp from 'quill-delta/lib/op'; -import Emitter from './emitter'; import Parchment from 'parchment'; import CodeBlock from '../formats/code'; import CursorBlot from '../blots/cursor'; @@ -11,16 +10,12 @@ import extend from 'extend'; class Editor { - constructor(scroll, emitter, selection) { + constructor(scroll) { this.scroll = scroll; - this.selection = selection; - this.emitter = emitter; - this.emitter.on(Emitter.events.SCROLL_UPDATE, this.update.bind(this, null)); this.delta = this.getDelta(); - this.enable(); } - applyDelta(delta, source = Emitter.sources.API) { + applyDelta(delta) { let consumeNextNewline = false; this.scroll.update(); let scrollLength = this.scroll.length(); @@ -68,19 +63,15 @@ class Editor { }, 0); this.scroll.batch = false; this.scroll.optimize(); - return this.update(delta, source); + return this.update(delta); } - deleteText(index, length, source = Emitter.sources.API) { + deleteText(index, length) { this.scroll.deleteAt(index, length); - return this.update(new Delta().retain(index).delete(length), source); + return this.update(new Delta().retain(index).delete(length)); } - enable(enabled = true) { - this.scroll.domNode.setAttribute('contenteditable', enabled); - } - - formatLine(index, length, formats = {}, source = Emitter.sources.API) { + formatLine(index, length, formats = {}) { this.scroll.update(); Object.keys(formats).forEach((format) => { let lines = this.scroll.lines(index, Math.max(length, 1)); @@ -98,14 +89,14 @@ class Editor { }); }); this.scroll.optimize(); - return this.update(new Delta().retain(index).retain(length, clone(formats)), source); + return this.update(new Delta().retain(index).retain(length, clone(formats))); } - formatText(index, length, formats = {}, source = Emitter.sources.API) { + formatText(index, length, formats = {}) { Object.keys(formats).forEach((format) => { this.scroll.formatAt(index, length, format, formats[format]); }); - return this.update(new Delta().retain(index).retain(length, clone(formats)), source); + return this.update(new Delta().retain(index).retain(length, clone(formats))); } getContents(index, length) { @@ -154,18 +145,18 @@ class Editor { }).join(''); } - insertEmbed(index, embed, value, source = Emitter.sources.API) { + insertEmbed(index, embed, value) { this.scroll.insertAt(index, embed, value); - return this.update(new Delta().retain(index).insert({ [embed]: value }), source); + return this.update(new Delta().retain(index).insert({ [embed]: value })); } - insertText(index, text, formats = {}, source = Emitter.sources.API) { + insertText(index, text, formats = {}) { text = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); this.scroll.insertAt(index, text); Object.keys(formats).forEach((format) => { this.scroll.formatAt(index, text.length, format, formats[format]); }); - return this.update(new Delta().retain(index).insert(text, clone(formats)), source) + return this.update(new Delta().retain(index).insert(text, clone(formats))); } isBlank() { @@ -175,7 +166,7 @@ class Editor { return child.length() <= 1 && Object.keys(child.formats()).length == 0; } - removeFormat(index, length, source) { + removeFormat(index, length) { let text = this.getText(index, length); let [line, offset] = this.scroll.line(index + length); let suffixLength = 0, suffix = new Delta(); @@ -190,13 +181,11 @@ class Editor { let contents = this.getContents(index, length + suffixLength); let diff = contents.diff(new Delta().insert(text).concat(suffix)); let delta = new Delta().retain(index).concat(diff); - return this.applyDelta(delta, source); + return this.applyDelta(delta); } - update(change, source = Emitter.sources.USER, mutations = []) { + update(change, mutations = [], cursorIndex = undefined) { let oldDelta = this.delta; - let range = this.selection.lastRange; - let index = range && range.length === 0 ? range.index : undefined; if (mutations.length === 1 && mutations[0].type === 'characterData' && Parchment.find(mutations[0].target)) { @@ -207,7 +196,7 @@ class Editor { let oldValue = mutations[0].oldValue.replace(CursorBlot.CONTENTS, ''); let oldText = new Delta().insert(oldValue); let newText = new Delta().insert(textBlot.value()); - let diffDelta = new Delta().retain(index).concat(oldText.diff(newText, index)); + let diffDelta = new Delta().retain(index).concat(oldText.diff(newText, cursorIndex)); change = diffDelta.reduce(function(delta, op) { if (op.insert) { return delta.insert(op.insert, formats); @@ -219,14 +208,7 @@ class Editor { } else { this.delta = this.getDelta(); if (!change || !equal(oldDelta.compose(change), this.delta)) { - change = oldDelta.diff(this.delta, index); - } - } - if (change.length() > 0) { - let args = [Emitter.events.TEXT_CHANGE, change, oldDelta, source]; - this.emitter.emit(Emitter.events.EDITOR_CHANGE, ...args); - if (source !== Emitter.sources.SILENT) { - this.emitter.emit(...args); + change = oldDelta.diff(this.delta, cursorIndex); } } return change; diff --git a/core/quill.js b/core/quill.js index 67ba84cc03..50a97c701e 100644 --- a/core/quill.js +++ b/core/quill.js @@ -65,8 +65,8 @@ class Quill { emitter: this.emitter, whitelist: this.options.formats }); + this.editor = new Editor(this.scroll); this.selection = new Selection(this.scroll, this.emitter); - this.editor = new Editor(this.scroll, this.emitter, this.selection); this.theme = new this.options.theme(this, this.options); this.keyboard = this.theme.addModule('keyboard'); this.clipboard = this.theme.addModule('clipboard'); @@ -77,6 +77,13 @@ class Quill { this.root.classList.toggle('ql-blank', this.editor.isBlank()); } }); + this.emitter.on(Emitter.events.SCROLL_UPDATE, (source, mutations) => { + let range = this.selection.lastRange; + let index = range && range.length === 0 ? range.index : undefined; + modify.call(this, () => { + return this.editor.update(null, mutations, index); + }, source); + }); let contents = this.clipboard.convert(`
${html}


`); this.setContents(contents); this.history.clear(); @@ -104,9 +111,9 @@ class Quill { deleteText(index, length, source) { [index, length, , source] = overload(index, length, source); - return modify.call(this, source, index, -1*length, () => { - return this.editor.deleteText(index, length, source); - }); + return modify.call(this, () => { + return this.editor.deleteText(index, length); + }, source, index, -1*length); } disable() { @@ -114,7 +121,7 @@ class Quill { } enable(enabled = true) { - this.editor.enable(enabled); + this.scroll.enable(enabled); this.container.classList.toggle('ql-disabled', !enabled); if (!enabled) { this.blur(); @@ -127,38 +134,38 @@ class Quill { } format(name, value, source = Emitter.sources.API) { - if (!this.options.strict && !this.isEnabled() && source === Emitter.sources.USER) { - return new Delta(); - } - let range = this.getSelection(true); - let change = new Delta(); - if (range == null) return change; - if (Parchment.query(name, Parchment.Scope.BLOCK)) { - change = this.formatLine(range, name, value, source); - } else if (range.length === 0) { - this.selection.format(name, value); + return modify.call(this, () => { + let range = this.getSelection(true); + let change = new Delta(); + if (range == null) { + return change; + } else if (Parchment.query(name, Parchment.Scope.BLOCK)) { + change = this.editor.formatLine(range.index, range.length, { [name]: value }); + } else if (range.length === 0) { + this.selection.format(name, value); + return change; + } else { + change = this.editor.formatText(range.index, range.length, { [name]: value }); + } + this.setSelection(range, Emitter.sources.SILENT); return change; - } else { - change = this.formatText(range, name, value, source); - } - this.setSelection(range, Emitter.sources.SILENT); - return change; + }, source); } formatLine(index, length, name, value, source) { let formats; [index, length, formats, source] = overload(index, length, name, value, source); - return modify.call(this, source, index, 0, () => { - return this.editor.formatLine(index, length, formats, source); - }); + return modify.call(this, () => { + return this.editor.formatLine(index, length, formats); + }, source, index, 0); } formatText(index, length, name, value, source) { let formats; [index, length, formats, source] = overload(index, length, name, value, source); - return modify.call(this, source, index, 0, () => { - return this.editor.formatText(index, length, formats, source); - }); + return modify.call(this, () => { + return this.editor.formatText(index, length, formats); + }, source, index, 0); } getBounds(index, length = 0) { @@ -206,17 +213,17 @@ class Quill { } insertEmbed(index, embed, value, source = Quill.sources.API) { - return modify.call(this, source, index, null, () => { - return this.editor.insertEmbed(index, embed, value, source); - }); + return modify.call(this, () => { + return this.editor.insertEmbed(index, embed, value); + }, source, index); } insertText(index, text, name, value, source) { let formats; [index, , formats, source] = overload(index, 0, name, value, source); - return modify.call(this, source, index, text.length, () => { - return this.editor.insertText(index, text, formats, source); - }); + return modify.call(this, () => { + return this.editor.insertText(index, text, formats); + }, source, index, text.length); } isEnabled() { @@ -241,23 +248,22 @@ class Quill { removeFormat(index, length, source) { [index, length, , source] = overload(index, length, source); - return modify.call(this, source, index, null, () => { - return this.editor.removeFormat(index, length, source); - }); + return modify.call(this, () => { + return this.editor.removeFormat(index, length); + }, source, index); } setContents(delta, source = Emitter.sources.API) { - if (!this.options.strict && !this.isEnabled() && source === Emitter.sources.USER) { - return new Delta(); - } - delta = new Delta(delta).slice(); - let lastOp = delta.ops[delta.ops.length - 1]; - // Quill contents must always end with newline - if (lastOp == null || lastOp.insert[lastOp.insert.length-1] !== '\n') { - delta.insert('\n'); - } - delta.delete(this.getLength()); - return this.editor.applyDelta(delta, source); + return modify.call(this, () => { + delta = new Delta(delta).slice(); + let lastOp = delta.ops[delta.ops.length - 1]; + // Quill contents must always end with newline + if (lastOp == null || lastOp.insert[lastOp.insert.length-1] !== '\n') { + delta.insert('\n'); + } + delta.delete(this.getLength()); + return this.editor.applyDelta(delta); + }, source); } setSelection(index, length, source) { @@ -282,19 +288,12 @@ class Quill { } updateContents(delta, source = Emitter.sources.API) { - if (!this.options.strict && !this.isEnabled() && source === Emitter.sources.USER) { - return new Delta(); - } - let range = this.getSelection(); - if (Array.isArray(delta)) { - delta = new Delta(delta.slice()); - } - let change = this.editor.applyDelta(delta, source); - if (range != null) { - range = shiftRange(range, change, source); - this.setSelection(range, Emitter.sources.SILENT); - } - return change; + return modify.call(this, () => { + if (Array.isArray(delta)) { + delta = new Delta(delta.slice()); + } + return this.editor.applyDelta(delta, source); + }, source, true); } } Quill.DEFAULTS = { @@ -377,21 +376,31 @@ function expandConfig(container, userConfig) { return userConfig; } -function modify(source, index, shift, modifier) { - let change = new Delta(); +// Handle selection preservation and TEXT_CHANGE emission +// common to modification APIs +function modify(modifier, source, index, shift) { if (!this.options.strict && !this.isEnabled() && source === Emitter.sources.USER) { return new Delta(); } - let range = this.getSelection(); - change = modifier(); + let range = index == null ? null : this.getSelection(); + let oldDelta = this.editor.delta; + let change = modifier(); if (range != null) { - if (shift === null) { - range = shiftRange(range, index, change, source); + if (index === true) index = range.index; + if (shift == null) { + range = shiftRange(range, change, source); } else if (shift !== 0) { range = shiftRange(range, index, shift, source); } this.setSelection(range, Emitter.sources.SILENT); } + if (change.length() > 0) { + let args = [Emitter.events.TEXT_CHANGE, change, oldDelta, source]; + this.emitter.emit(Emitter.events.EDITOR_CHANGE, ...args); + if (source !== Emitter.sources.SILENT) { + this.emitter.emit(...args); + } + } return change; } diff --git a/test/helpers/unit.js b/test/helpers/unit.js index 41cd27272e..01b2dbbd48 100644 --- a/test/helpers/unit.js +++ b/test/helpers/unit.js @@ -110,9 +110,9 @@ function initialize(klass, html, container = this.container) { let emitter = new Emitter(); let scroll = new Scroll(container, { emitter: emitter }); if (klass === Scroll) return scroll; - let selection = new Selection(scroll, emitter); - let editor = new Editor(scroll, emitter, selection); - if (klass === Selection) return selection; - if (klass === Editor) return editor; - if (klass[0] === Editor && klass[1] === Selection) return [editor, selection]; + if (klass === Editor) return new Editor(scroll); + if (klass === Selection) return new Selection(scroll, emitter); + if (klass[0] === Editor && klass[1] === Selection) { + return [new Editor(scroll), new Selection(scroll, emitter)]; + } } diff --git a/test/unit/core/editor.js b/test/unit/core/editor.js index 824776431c..3518e8e2b5 100644 --- a/test/unit/core/editor.js +++ b/test/unit/core/editor.js @@ -1,6 +1,5 @@ import Delta from 'quill-delta'; import Editor from '../../../core/editor'; -import Emitter from '../../../core/emitter'; import Selection, { Range } from '../../../core/selection'; @@ -442,46 +441,4 @@ describe('Editor', function() { expect(editor.getFormat(1, 3)).toEqual({ italic: true, header: 1, align: ['right', 'center'] }); }); }); - - describe('events', function() { - it('api text insert', function() { - let editor = this.initialize(Editor, '

0123

'); - editor.update(); - spyOn(editor.emitter, 'emit').and.callThrough(); - let old = editor.getDelta(); - editor.insertText(2, '!'); - let delta = new Delta().retain(2).insert('!'); - expect(editor.emitter.emit).toHaveBeenCalledWith(Emitter.events.TEXT_CHANGE, delta, old, Emitter.sources.API); - }); - - it('user text insert', function(done) { - let editor = this.initialize(Editor, '

0123

'); - editor.update(); - spyOn(editor.emitter, 'emit').and.callThrough(); - let old = editor.getDelta(); - this.container.firstChild.firstChild.data = '01!23'; - let delta = new Delta().retain(2).insert('!'); - setTimeout(() => { - expect(editor.emitter.emit).toHaveBeenCalledWith(Emitter.events.TEXT_CHANGE, delta, old, Emitter.sources.USER); - done(); - }, 1); - }); - - it('insert same character', function(done) { - let [editor, selection] = this.initialize([Editor, Selection], '

aaaa

'); - selection.setRange(new Range(2, 0)); - editor.update(); - spyOn(editor.emitter, 'emit').and.callThrough(); - let old = editor.getDelta(); - let textNode = this.container.firstChild.firstChild - textNode.data = 'aaaaa'; - selection.setNativeRange(textNode.data, 3); - let delta = new Delta().retain(2).insert('a'); - setTimeout(() => { - let args = editor.emitter.emit.calls.mostRecent().args; - expect(args).toEqual([Emitter.events.TEXT_CHANGE, delta, old, Emitter.sources.USER]); - done(); - }, 1); - }); - }); }); diff --git a/test/unit/core/quill.js b/test/unit/core/quill.js index e3f395ffb8..b5dd2d021b 100644 --- a/test/unit/core/quill.js +++ b/test/unit/core/quill.js @@ -154,6 +154,49 @@ describe('Quill', function() { }); }); + describe('events', function() { + beforeEach(function() { + this.quill = this.initialize(Quill, '

0123

'); + this.quill.update(); + spyOn(this.quill.emitter, 'emit').and.callThrough(); + this.oldDelta = this.quill.getContents(); + }); + + it('api text insert', function() { + this.quill.insertText(2, '!'); + let delta = new Delta().retain(2).insert('!'); + expect(this.quill.emitter.emit) + .toHaveBeenCalledWith(Emitter.events.TEXT_CHANGE, delta, this.oldDelta, Emitter.sources.API); + }); + + it('user text insert', function(done) { + this.container.firstChild.firstChild.firstChild.data = '01!23'; + let delta = new Delta().retain(2).insert('!'); + setTimeout(() => { + expect(this.quill.emitter.emit) + .toHaveBeenCalledWith(Emitter.events.TEXT_CHANGE, delta, this.oldDelta, Emitter.sources.USER); + done(); + }, 1); + }); + + it('insert same character', function(done) { + this.quill.setText('aaaa\n'); + this.quill.setSelection(2); + this.quill.update(); + let old = this.quill.getContents(); + let textNode = this.container.firstChild.firstChild.firstChild; + textNode.data = 'aaaaa'; + this.quill.selection.setNativeRange(textNode.data, 3); + // this.quill.selection.update(Emitter.sources.SILENT); + let delta = new Delta().retain(2).insert('a'); + setTimeout(() => { + let args = this.quill.emitter.emit.calls.mostRecent().args; + expect(args).toEqual([Emitter.events.TEXT_CHANGE, delta, old, Emitter.sources.USER]); + done(); + }, 1); + }); + }); + describe('setContents()', function() { it('empty', function() { let quill = this.initialize(Quill, '');