diff --git a/blots/cursor.js b/blots/cursor.js index 320dd1920b..feca853adf 100644 --- a/blots/cursor.js +++ b/blots/cursor.js @@ -59,7 +59,8 @@ class Cursor extends EmbedBlot { restore() { if (this.selection.composing || this.parent == null) return null; const range = this.selection.getNativeRange(); - // Link format will insert text outside of anchor tag + // Browser may push down styles/nodes inside the cursor blot. + // https://dvcs.w3.org/hg/editing/raw-file/tip/editing.html#push-down-values while ( this.domNode.lastChild != null && this.domNode.lastChild !== this.textNode @@ -144,6 +145,31 @@ class Cursor extends EmbedBlot { } } + // Avoid .ql-cursor being a descendant of ``. + // The reason is Safari pushes down `` on text insertion. + // That will make DOM nodes not sync with the model. + // + // For example ({I} is the caret), given the markup: + // \uFEFF{I} + // When typing a char "x", `` will be pushed down inside the `` first: + // \uFEFF{I} + // And then "x" will be inserted after ``: + // \uFEFFd{I} + optimize(context) { + super.optimize(context); + + let { parent } = this; + while (parent) { + if (parent.domNode.tagName === 'A') { + this.savedLength = Cursor.CONTENTS.length; + parent.isolate(this.offset(parent), this.length()).unwrap(); + this.savedLength = 0; + break; + } + parent = parent.parent; + } + } + value() { return ''; } diff --git a/modules/clipboard.js b/modules/clipboard.js index 8eadd406a6..fc5a81d088 100644 --- a/modules/clipboard.js +++ b/modules/clipboard.js @@ -155,7 +155,8 @@ class Clipboard extends Module { if (!html && files.length > 0) { this.quill.uploader.upload(range, files); return; - } else if (html && files.length > 0) { + } + if (html && files.length > 0) { const doc = new DOMParser().parseFromString(html, 'text/html'); if ( doc.body.childElementCount === 1 && diff --git a/test/unit/core/selection.js b/test/unit/core/selection.js index 164f5f8bed..1419c8d161 100644 --- a/test/unit/core/selection.js +++ b/test/unit/core/selection.js @@ -441,6 +441,40 @@ describe('Selection', function() {

01${Cursor.CONTENTS}23

`); }); + + describe('unlink cursor', function() { + const cursorHTML = `${Cursor.CONTENTS}`; + + it('one level', function() { + this.setup( + '

link


', + 4, + ); + this.selection.format('bold', false); + expect(this.container).toEqualHTML(` +

link${cursorHTML}


+ `); + }); + + it('nested formats', function() { + this.setup( + '

bold


', + 4, + ); + this.selection.format('italic', false); + expect(this.container).toEqualHTML(` +

bold${cursorHTML}


+ `); + }); + + it('ignore link format', function() { + this.setup('

bold


', 4); + this.selection.format('link', 'https://example.com'); + expect(this.container).toEqualHTML(` +

bold${cursorHTML}


+ `); + }); + }); }); describe('getBounds()', function() {