From b466afd7ff07e4ba1bfa73c6169b37fa3e46fc2f Mon Sep 17 00:00:00 2001 From: Jason Chen Date: Sat, 10 Jun 2017 16:57:41 -0700 Subject: [PATCH 1/9] dont think this is needed --- modules/formula.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/modules/formula.js b/modules/formula.js index 18a157677d..e5c79c0f60 100644 --- a/modules/formula.js +++ b/modules/formula.js @@ -17,10 +17,6 @@ class FormulaBlot extends Embed { static value(domNode) { return domNode.getAttribute('data-value'); } - - index() { - return 1; - } } FormulaBlot.blotName = 'formula'; FormulaBlot.className = 'ql-formula'; From 365a4cfcd1069577fca513a038e79b44ef9e2bea Mon Sep 17 00:00:00 2001 From: Jason Chen Date: Sat, 10 Jun 2017 16:58:03 -0700 Subject: [PATCH 2/9] initial inline embed implementation --- blots/embed.js | 27 ++++++++++++++++++++++++++- modules/formula.js | 5 ++--- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/blots/embed.js b/blots/embed.js index ec738f28f8..a3c8fe4a42 100644 --- a/blots/embed.js +++ b/blots/embed.js @@ -1,5 +1,30 @@ import Parchment from 'parchment'; + class Embed extends Parchment.Embed { } -export default Embed; + +class InlineEmbed extends Embed { + constructor(node) { + super(node); + const wrapper = document.createElement('span'); + wrapper.setAttribute('contenteditable', false); + [].slice.call(this.domNode.childNodes).forEach(function(childNode) { + wrapper.appendChild(childNode); + }); + this.leftGuard = document.createTextNode("\uFEFF"); + this.rightGuard = document.createTextNode("\uFEFF"); + this.domNode.appendChild(this.leftGuard); + this.domNode.appendChild(wrapper); + this.domNode.appendChild(this.rightGuard); + } + + index(node, offset) { + if (node === this.leftGuard) return 0; + if (node === this.rightGuard) return 1; + return super.index(node, offset); + } +} + + +export { Embed as default, InlineEmbed }; diff --git a/modules/formula.js b/modules/formula.js index e5c79c0f60..c3dcf7383f 100644 --- a/modules/formula.js +++ b/modules/formula.js @@ -1,16 +1,15 @@ -import Embed from '../blots/embed'; +import { InlineEmbed } from '../blots/embed'; import Quill from '../core/quill'; import Module from '../core/module'; -class FormulaBlot extends Embed { +class FormulaBlot extends InlineEmbed { static create(value) { let node = super.create(value); if (typeof value === 'string') { window.katex.render(value, node); node.setAttribute('data-value', value); } - node.setAttribute('contenteditable', false); return node; } From ca693318581961ee49ca8a4c89f16b989bc90c9b Mon Sep 17 00:00:00 2001 From: Jason Chen Date: Tue, 13 Jun 2017 18:21:27 -0700 Subject: [PATCH 3/9] select embed on click and add class --- assets/core.styl | 4 ++++ core/selection.js | 14 ++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/assets/core.styl b/assets/core.styl index 4e8f8bb071..564b59223f 100644 --- a/assets/core.styl +++ b/assets/core.styl @@ -172,6 +172,10 @@ resets(arr) .ql-align-right text-align: right + .ql-embed-selected + border: 1px solid #777 + user-select: none + .ql-editor.ql-blank::before color: rgba(0,0,0,0.6) content: attr(data-placeholder) diff --git a/core/selection.js b/core/selection.js index 1a0a547d08..803bb91af2 100644 --- a/core/selection.js +++ b/core/selection.js @@ -37,6 +37,20 @@ class Selection { setTimeout(this.update.bind(this, Emitter.sources.USER), 100); }); }); + this.root.addEventListener('click', (e) => { + const blot = Parchment.find(e.target, true); + if (blot instanceof Parchment.Embed) { + blot.domNode.classList.add('ql-embed-selected'); + const range = new Range(blot.offset(scroll), blot.length()); + this.setRange(range, Emitter.sources.USER); + e.stopPropagation(); + } else { + const selectedNode = document.querySelector('.ql-embed-selected'); + if (selectedNode) { + selectedNode.classList.remove('ql-embed-selected'); + } + } + }); this.emitter.on(Emitter.events.EDITOR_CHANGE, (type, delta) => { if (type === Emitter.events.TEXT_CHANGE && delta.length() > 0) { this.update(Emitter.sources.SILENT); From c98a6a68044f0a5e334ccc67c2a5276dcbc4864b Mon Sep 17 00:00:00 2001 From: Jason Chen Date: Tue, 13 Jun 2017 19:10:05 -0700 Subject: [PATCH 4/9] handle arrow keying across inline embed --- core/selection.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/core/selection.js b/core/selection.js index 803bb91af2..99dedc74c4 100644 --- a/core/selection.js +++ b/core/selection.js @@ -1,4 +1,5 @@ import Parchment from 'parchment'; +import { InlineEmbed } from '../blots/embed'; import clone from 'clone'; import equal from 'deep-equal'; import Emitter from './emitter'; @@ -71,6 +72,27 @@ class Selection { this.update(Emitter.sources.SILENT); } + fixInlineEmbed(native) { + if (native == null) return; + const [start, end] = [native.start, native.end].map(function(pos) { + const blot = Parchment.find(pos.node, true); + if (blot instanceof InlineEmbed) { + let node, offset; + if (pos.node === blot.leftGuard && pos.offset === 1) { + [node, offset] = blot.position(blot.length()); + return { node, offset }; + } else if (pos.node === blot.rightGuard && pos.offset === 0) { + [node, offset] = blot.position(0); + return { node, offset }; + } + } + return pos; + }); + if (native.start !== start || native.end !== end) { + this.setNativeRange(start.node, start.offset, end.node, end.offset); + } + } + focus() { if (this.hasFocus()) return; this.root.focus(); @@ -305,6 +327,7 @@ class Selection { update(source = Emitter.sources.USER) { let oldRange = this.lastRange; let [lastRange, nativeRange] = this.getRange(); + this.fixInlineEmbed(nativeRange); this.lastRange = lastRange; if (this.lastRange != null) { this.savedRange = this.lastRange; From 4e874c48d7e7b7652cdbd3294d486d71a166fb37 Mon Sep 17 00:00:00 2001 From: Jason Chen Date: Tue, 20 Jun 2017 10:41:03 -0700 Subject: [PATCH 5/9] handle typing around inline embed --- blots/embed.js | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/blots/embed.js b/blots/embed.js index a3c8fe4a42..e9dfac9f05 100644 --- a/blots/embed.js +++ b/blots/embed.js @@ -1,4 +1,7 @@ import Parchment from 'parchment'; +import TextBlot from './text'; + +const GUARD_TEXT = "\uFEFF"; class Embed extends Parchment.Embed { } @@ -12,8 +15,8 @@ class InlineEmbed extends Embed { [].slice.call(this.domNode.childNodes).forEach(function(childNode) { wrapper.appendChild(childNode); }); - this.leftGuard = document.createTextNode("\uFEFF"); - this.rightGuard = document.createTextNode("\uFEFF"); + this.leftGuard = document.createTextNode(GUARD_TEXT); + this.rightGuard = document.createTextNode(GUARD_TEXT); this.domNode.appendChild(this.leftGuard); this.domNode.appendChild(wrapper); this.domNode.appendChild(this.rightGuard); @@ -24,6 +27,38 @@ class InlineEmbed extends Embed { if (node === this.rightGuard) return 1; return super.index(node, offset); } + + restore(node) { + let text, textNode; + if (node === this.leftGuard) { + text = this.leftGuard.data.split(GUARD_TEXT).join(''); + if (this.prev instanceof TextBlot) { + this.prev.insertAt(this.prev.length(), text); + } else { + textNode = document.createTextNode(text); + this.parent.insertBefore(Parchment.create(textNode), this); + } + this.leftGuard.data = GUARD_TEXT; + } else if (node === this.rightGuard) { + text = this.rightGuard.data.split(GUARD_TEXT).join(''); + if (this.next instanceof TextBlot) { + this.next.insertAt(0, text); + } else { + textNode = document.createTextNode(text); + this.parent.insertBefore(Parchment.create(textNode), this.next); + } + this.rightGuard.data = GUARD_TEXT; + } + } + + update(mutations) { + mutations.forEach((mutation) => { + if (mutation.type === 'characterData' && + (mutation.target === this.leftGuard || mutation.target === this.rightGuard)) { + this.restore(mutation.target); + } + }); + } } From ee869f40c10af82822a5001a4f0e5c371b7a9b80 Mon Sep 17 00:00:00 2001 From: Jason Chen Date: Tue, 20 Jun 2017 12:52:30 -0700 Subject: [PATCH 6/9] pass through parchment context --- blots/block.js | 4 ++-- blots/inline.js | 4 ++-- formats/bold.js | 4 ++-- formats/code.js | 6 +++--- formats/list.js | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/blots/block.js b/blots/block.js index 239dc4823e..457f46f970 100644 --- a/blots/block.js +++ b/blots/block.js @@ -123,8 +123,8 @@ class Block extends Parchment.Block { this.cache = {}; } - optimize() { - super.optimize(); + optimize(context) { + super.optimize(context); this.cache = {}; } diff --git a/blots/inline.js b/blots/inline.js index 24afe626af..e0c81196b2 100644 --- a/blots/inline.js +++ b/blots/inline.js @@ -29,8 +29,8 @@ class Inline extends Parchment.Inline { } } - optimize() { - super.optimize(); + optimize(context) { + super.optimize(context); if (this.parent instanceof Inline && Inline.compare(this.statics.blotName, this.parent.statics.blotName) > 0) { let parent = this.parent.isolate(this.offset(), this.length()); diff --git a/formats/bold.js b/formats/bold.js index 7d51d0b106..8ddad6de39 100644 --- a/formats/bold.js +++ b/formats/bold.js @@ -9,8 +9,8 @@ class Bold extends Inline { return true; } - optimize() { - super.optimize(); + optimize(context) { + super.optimize(context); if (this.domNode.tagName !== this.statics.tagName[0]) { this.replaceWith(this.statics.blotName); } diff --git a/formats/code.js b/formats/code.js index c1630fc8e5..fff023589f 100644 --- a/formats/code.js +++ b/formats/code.js @@ -81,16 +81,16 @@ class CodeBlock extends Block { } } - optimize() { + optimize(context) { if (!this.domNode.textContent.endsWith('\n')) { this.appendChild(Parchment.create('text', '\n')); } - super.optimize(); + super.optimize(context); let next = this.next; if (next != null && next.prev === this && next.statics.blotName === this.statics.blotName && this.statics.formats(this.domNode) === next.statics.formats(next.domNode)) { - next.optimize(); + next.optimize(context); next.moveChildren(this); next.remove(); } diff --git a/formats/list.js b/formats/list.js index 12b1b51850..8b168e5d8c 100644 --- a/formats/list.js +++ b/formats/list.js @@ -96,8 +96,8 @@ class List extends Container { } } - optimize() { - super.optimize(); + optimize(context) { + super.optimize(context); let next = this.next; if (next != null && next.prev === this && next.statics.blotName === this.statics.blotName && From a18110da48274f150532df4029536abfb5f0e0b1 Mon Sep 17 00:00:00 2001 From: Jason Chen Date: Tue, 20 Jun 2017 13:55:09 -0700 Subject: [PATCH 7/9] use function interface for batch --- blots/scroll.js | 9 +++++++++ core/editor.js | 5 ++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/blots/scroll.js b/blots/scroll.js index 7f43f600f9..59bd54b791 100644 --- a/blots/scroll.js +++ b/blots/scroll.js @@ -27,6 +27,15 @@ class Scroll extends Parchment.Scroll { this.enable(); } + batchStart() { + this.batch = true; + } + + batchEnd() { + this.batch = false; + this.optimize(); + } + deleteAt(index, length) { let [first, offset] = this.line(index); let [last, ] = this.line(index + length); diff --git a/core/editor.js b/core/editor.js index 515add0f4b..469e74bfba 100644 --- a/core/editor.js +++ b/core/editor.js @@ -22,7 +22,7 @@ class Editor { let consumeNextNewline = false; this.scroll.update(); let scrollLength = this.scroll.length(); - this.scroll.batch = true; + this.scroll.batchStart(); delta = normalizeDelta(delta); delta.reduce((index, op) => { let length = op.retain || op.delete || op.insert.length || 1; @@ -64,8 +64,7 @@ class Editor { } return index + (op.retain || op.insert.length || 1); }, 0); - this.scroll.batch = false; - this.scroll.optimize(); + this.scroll.batchEnd(); return this.update(delta); } From 56ce0ee542ff7acc663a5a8fbc71dcbb3cffe156 Mon Sep 17 00:00:00 2001 From: Jason Chen Date: Tue, 20 Jun 2017 15:13:49 -0700 Subject: [PATCH 8/9] use context to restore selection --- blots/cursor.js | 29 ++++++++++++++++------------- blots/embed.js | 24 +++++++++++++++++++++--- blots/scroll.js | 6 +++--- core/quill.js | 1 - core/selection.js | 6 ++++++ 5 files changed, 46 insertions(+), 20 deletions(-) diff --git a/blots/cursor.js b/blots/cursor.js index e6f3be10d4..2b947558b5 100644 --- a/blots/cursor.js +++ b/blots/cursor.js @@ -1,7 +1,6 @@ import Parchment from 'parchment'; import Embed from './embed'; import TextBlot from './text'; -import Emitter from '../core/emitter'; class Cursor extends Embed { @@ -58,8 +57,7 @@ class Cursor extends Embed { } restore() { - if (this.selection.composing) return; - if (this.parent == null) return; + if (this.selection.composing || this.parent == null) return; let textNode = this.textNode; let range = this.selection.getNativeRange(); let restoreText, start, end; @@ -84,21 +82,26 @@ class Cursor extends Embed { } } this.remove(); - if (start == null) return; - this.selection.emitter.once(Emitter.events.SCROLL_OPTIMIZE, () => { + if (start != null) { [start, end] = [start, end].map(function(offset) { return Math.max(0, Math.min(restoreText.data.length, offset - 1)); }); - this.selection.setNativeRange(restoreText, start, restoreText, end); - }); + return { + startNode: restoreText, + startOffset: start, + endNode: restoreText, + endOffset: end + }; + } } - update(mutations) { - mutations.forEach((mutation) => { - if (mutation.type === 'characterData' && mutation.target === this.textNode) { - this.restore(); - } - }); + update(mutations, context) { + if (mutations.some((mutation) => { + return mutation.type === 'characterData' && mutation.target === this.textNode; + })) { + let range = this.restore(); + if (range) context.range = range; + } } value() { diff --git a/blots/embed.js b/blots/embed.js index e9dfac9f05..da530701f9 100644 --- a/blots/embed.js +++ b/blots/embed.js @@ -29,33 +29,51 @@ class InlineEmbed extends Embed { } restore(node) { - let text, textNode; + let range, text, textNode; if (node === this.leftGuard) { text = this.leftGuard.data.split(GUARD_TEXT).join(''); if (this.prev instanceof TextBlot) { this.prev.insertAt(this.prev.length(), text); + range = { + startNode: this.prev.domNode, + startOffset: this.prev.domNode.data.length + }; } else { textNode = document.createTextNode(text); this.parent.insertBefore(Parchment.create(textNode), this); + range = { + startNode: textNode, + startOffset: text.length + }; } this.leftGuard.data = GUARD_TEXT; } else if (node === this.rightGuard) { text = this.rightGuard.data.split(GUARD_TEXT).join(''); if (this.next instanceof TextBlot) { this.next.insertAt(0, text); + range = { + startNode: this.next.domNode, + startOffset: text.length + } } else { textNode = document.createTextNode(text); this.parent.insertBefore(Parchment.create(textNode), this.next); + range = { + startNode: textNode, + startOffset: text.length + }; } this.rightGuard.data = GUARD_TEXT; } + return range; } - update(mutations) { + update(mutations, context) { mutations.forEach((mutation) => { if (mutation.type === 'characterData' && (mutation.target === this.leftGuard || mutation.target === this.rightGuard)) { - this.restore(mutation.target); + let range = this.restore(mutation.target); + if (range) context.range = range; } }); } diff --git a/blots/scroll.js b/blots/scroll.js index 59bd54b791..f279cf0a2f 100644 --- a/blots/scroll.js +++ b/blots/scroll.js @@ -118,11 +118,11 @@ class Scroll extends Parchment.Scroll { return getLines(this, index, length); } - optimize(mutations = []) { + optimize(mutations = [], context = {}) { if (this.batch === true) return; - super.optimize(mutations); + super.optimize(mutations, context); if (mutations.length > 0) { - this.emitter.emit(Emitter.events.SCROLL_OPTIMIZE, mutations); + this.emitter.emit(Emitter.events.SCROLL_OPTIMIZE, mutations, context); } } diff --git a/core/quill.js b/core/quill.js index a2b272ed33..37a1fd8769 100644 --- a/core/quill.js +++ b/core/quill.js @@ -76,7 +76,6 @@ class Quill { this.emitter = new Emitter(); this.scroll = Parchment.create(this.root, { emitter: this.emitter, - scrollingContainer: this.scrollingContainer, whitelist: this.options.formats }); this.editor = new Editor(this.scroll); diff --git a/core/selection.js b/core/selection.js index 99dedc74c4..ee63d6cdb4 100644 --- a/core/selection.js +++ b/core/selection.js @@ -69,6 +69,12 @@ class Selection { } catch (ignored) {} }); }); + this.emitter.on(Emitter.events.SCROLL_OPTIMIZE, (mutations, context) => { + if (context.range) { + const { startNode, startOffset, endNode, endOffset } = context.range; + this.setNativeRange(startNode, startOffset, endNode, endOffset); + } + }); this.update(Emitter.sources.SILENT); } From 2fac8f4363507c67512c2d9e2385259a7669d4f2 Mon Sep 17 00:00:00 2001 From: Jason Chen Date: Tue, 20 Jun 2017 15:35:50 -0700 Subject: [PATCH 9/9] fix tests --- test/unit/blots/scroll.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/blots/scroll.js b/test/unit/blots/scroll.js index 7ad14e40d1..f4bda62de1 100644 --- a/test/unit/blots/scroll.js +++ b/test/unit/blots/scroll.js @@ -15,7 +15,7 @@ describe('Scroll', function() { let scroll = this.initialize(Scroll, '

Hello World!

'); spyOn(scroll.emitter, 'emit').and.callThrough(); scroll.insertAt(5, '!'); - expect(scroll.emitter.emit).toHaveBeenCalledWith(Emitter.events.SCROLL_OPTIMIZE, jasmine.any(Array)); + expect(scroll.emitter.emit).toHaveBeenCalledWith(Emitter.events.SCROLL_OPTIMIZE, jasmine.any(Array), jasmine.any(Object)); }); it('user change', function(done) { @@ -23,7 +23,7 @@ describe('Scroll', function() { spyOn(scroll.emitter, 'emit').and.callThrough(); scroll.domNode.firstChild.appendChild(document.createTextNode('!')); setTimeout(function() { - expect(scroll.emitter.emit).toHaveBeenCalledWith(Emitter.events.SCROLL_OPTIMIZE, jasmine.any(Array)); + expect(scroll.emitter.emit).toHaveBeenCalledWith(Emitter.events.SCROLL_OPTIMIZE, jasmine.any(Array), jasmine.any(Object)); expect(scroll.emitter.emit).toHaveBeenCalledWith(Emitter.events.SCROLL_UPDATE, Emitter.sources.USER, jasmine.any(Array)); done(); }, 1);