Skip to content

Commit

Permalink
Fetch upstream (#22)
Browse files Browse the repository at this point in the history
* Merge pull request slab#2882 from tzyl/fix/insert-inline-embed-before-block-embed-with-delete

Fix insert inline embed with delete before block embed

(cherry picked from commit 58b1747)

* use Op.length

(cherry picked from commit 738a196)

* add failing tests

(cherry picked from commit a02978f)

* track all implicit newline indexes and shift for delete

(cherry picked from commit cf101f6)

* add test for tracking indexes between insert/delete

(cherry picked from commit 50dbbed)

* add fix and failing test for implicit newline insertion

(cherry picked from commit 99bfdcd)

* fix linter

(cherry picked from commit 7e98bc2)

* prefer file over html when uploading

- file should include the image data
- copying image from slack will include both, but image src is
  inaccessible without login

(cherry picked from commit e164f12)

* fix mixed html/file test and handle image only case

(cherry picked from commit ead3355)

* fix formatting

* tiny refactoring

Co-authored-by: Jason Chen <[email protected]>
  • Loading branch information
DokaRus and jhchen authored Oct 29, 2021
1 parent 67ed456 commit f6e556b
Show file tree
Hide file tree
Showing 4 changed files with 269 additions and 28 deletions.
42 changes: 23 additions & 19 deletions core/editor.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import cloneDeep from 'lodash.clonedeep';
import isEqual from 'lodash.isequal';
import merge from 'lodash.merge';
import Delta, { AttributeMap } from 'quill-delta';
import { LeafBlot } from 'parchment';
import Delta, { AttributeMap, Op } from 'quill-delta';
import { LeafBlot, Scope } from 'parchment';
import { Range } from './selection';
import CursorBlot from '../blots/cursor';
import Block, { BlockEmbed, bubbleFormats } from '../blots/block';
Expand All @@ -24,28 +24,23 @@ class Editor {
}

applyDelta(delta) {
let consumeNextNewline = false;
this.scroll.update();
let scrollLength = this.scroll.length();
this.scroll.batchStart();
const normalizedDelta = normalizeDelta(delta);
const deleteDelta = new Delta();
normalizedDelta.reduce((index, op) => {
const length = op.retain || op.delete || op.insert.length || 1;
const length = Op.length(op);
let attributes = op.attributes || {};
let addedNewline = false;
if (op.insert != null) {
deleteDelta.retain(length);
if (typeof op.insert === 'string') {
let text = op.insert;
if (text.endsWith('\n') && consumeNextNewline) {
consumeNextNewline = false;
text = text.slice(0, -1);
}
if (
(index >= scrollLength ||
this.scroll.descendant(BlockEmbed, index)[0]) &&
!text.endsWith('\n')
) {
consumeNextNewline = true;
}
const text = op.insert;
addedNewline =
!text.endsWith('\n') &&
(scrollLength <= index ||
this.scroll.descendant(BlockEmbed, index)[0]);
this.scroll.insertAt(index, text);
const [line, offset] = this.scroll.line(index);
let formats = merge({}, bubbleFormats(line));
Expand All @@ -57,9 +52,15 @@ class Editor {
} else if (typeof op.insert === 'object') {
const key = Object.keys(op.insert)[0]; // There should only be one key
if (key == null) return index;
addedNewline =
this.scroll.query(key, Scope.INLINE) != null &&
(scrollLength <= index ||
this.scroll.descendant(BlockEmbed, index)[0]);
this.scroll.insertAt(index, key, op.insert[key]);
}
scrollLength += length;
} else {
deleteDelta.push(op);
}
const keys = Object.keys(attributes);
this.immediateFormats.forEach(format => {
Expand All @@ -71,14 +72,17 @@ class Editor {
Object.keys(attributes).forEach(name => {
this.scroll.formatAt(index, length, name, attributes[name]);
});
return index + length;
const addedLength = addedNewline ? 1 : 0;
scrollLength += addedLength;
deleteDelta.delete(addedLength);
return index + length + addedLength;
}, 0);
normalizedDelta.reduce((index, op) => {
deleteDelta.reduce((index, op) => {
if (typeof op.delete === 'number') {
this.scroll.deleteAt(index, op.delete);
return index;
}
return index + (op.retain || op.insert.length || 1);
return index + Op.length(op);
}, 0);
this.scroll.batchEnd();
this.scroll.optimize();
Expand Down
28 changes: 23 additions & 5 deletions modules/clipboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,9 @@ class Clipboard extends Module {
}

onCapturePaste(e) {
if (e.defaultPrevented || !this.quill.isEnabled()) return;
if (e.defaultPrevented || !this.quill.isEnabled()) {
return;
}

this.raiseCallback('onPaste', e);

Expand All @@ -189,15 +191,31 @@ class Clipboard extends Module {

const range = this.quill.getSelection(true);

if (range == null) return;
if (range == null) {
return;
}

const html = e.clipboardData.getData('text/html');
const text = e.clipboardData.getData('text/plain');
const files = Array.from(e.clipboardData.files || []);
if (!html && files.length > 0) {
this.quill.uploader.upload(range, files);
} else {
this.onPaste(range, { html, text });
return;
}

if (html && files.length > 0) {
const { body } = new DOMParser().parseFromString(html, 'text/html');
const documentContainsImage =
body.childElementCount === 1 &&
body.firstElementChild.tagName === 'IMG';

if (documentContainsImage) {
this.quill.uploader.upload(range, files);
return;
}
}

const text = e.clipboardData.getData('text/plain');
this.onPaste(range, { html, text });
}

raiseCallback(name, event) {
Expand Down
197 changes: 197 additions & 0 deletions test/unit/core/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,203 @@ describe('Editor', function() {
);
});

it('multiple inserts and deletes', function() {
const editor = this.initialize(Editor, '<p>0123</p>');
editor.applyDelta(
new Delta()
.retain(1)
.insert('a')
.delete(2)
.insert('cd')
.delete(1)
.insert('efg'),
);
expect(this.container).toEqualHTML('<p>0acdefg</p>');
});

it('insert text with delete in existing block', function() {
const editor = this.initialize(
Editor,
'<p>0123</p><iframe src="#" class="ql-video" frameborder="0" allowfullscreen="true"></iframe>',
);
editor.applyDelta(
new Delta()
.retain(4)
.insert('abc')
// Retain newline at end of block being inserted into.
.retain(1)
.delete(1),
);
expect(this.container).toEqualHTML('<p>0123abc</p>');
});

it('insert text with delete before block embed', function() {
const editor = this.initialize(
Editor,
'<p>0123</p><iframe src="#" class="ql-video" frameborder="0" allowfullscreen="true"></iframe>',
);
editor.applyDelta(
new Delta()
.retain(5)
// Explicit newline required to maintain correct index calculation for the delete.
.insert('abc\n')
.delete(1),
);
expect(this.container).toEqualHTML('<p>0123</p><p>abc</p>');
});

it('insert inline embed with delete in existing block', function() {
const editor = this.initialize(
Editor,
'<p>0123</p><iframe src="#" class="ql-video" frameborder="0" allowfullscreen="true"></iframe>',
);
editor.applyDelta(
new Delta()
.retain(4)
.insert({ image: '/assets/favicon.png' })
// Retain newline at end of block being inserted into.
.retain(1)
.delete(1),
);
expect(this.container).toEqualHTML(
'<p>0123<img src="/assets/favicon.png"></p>',
);
});

it('insert inline embed with delete before block embed', function() {
const editor = this.initialize(
Editor,
'<p>0123</p><iframe src="#" class="ql-video" frameborder="0" allowfullscreen="true"></iframe>',
);
editor.applyDelta(
new Delta()
.retain(5)
.insert({ image: '/assets/favicon.png' })
// Explicit newline required to maintain correct index calculation for the delete.
.insert('\n')
.delete(1),
);
expect(this.container).toEqualHTML(
'<p>0123</p><p><img src="/assets/favicon.png"></p>',
);
});

it('insert inline embed with delete before block embed using delete op first', function() {
const editor = this.initialize(
Editor,
'<p>0123</p><iframe src="#" class="ql-video" frameborder="0" allowfullscreen="true"></iframe>',
);
editor.applyDelta(
new Delta()
.retain(5)
.delete(1)
.insert({ image: '/assets/favicon.png' })
// Explicit newline required to maintain correct index calculation for the delete.
.insert('\n'),
);
expect(this.container).toEqualHTML(
'<p>0123</p><p><img src="/assets/favicon.png"></p>',
);
});

it('insert inline embed and text with delete before block embed', function() {
const editor = this.initialize(
Editor,
'<p>0123</p><iframe src="#" class="ql-video" frameborder="0" allowfullscreen="true"></iframe>',
);
editor.applyDelta(
new Delta()
.retain(5)
.insert({ image: '/assets/favicon.png' })
// Explicit newline required to maintain correct index calculation for the delete.
.insert('abc\n')
.delete(1),
);
expect(this.container).toEqualHTML(
'<p>0123</p><p><img src="/assets/favicon.png">abc</p>',
);
});

it('insert block embed with delete before block embed', function() {
const editor = this.initialize(
Editor,
'<p>0123</p><iframe src="#" class="ql-video" frameborder="0" allowfullscreen="true"></iframe>',
);
editor.applyDelta(
new Delta()
.retain(5)
.insert({ video: '#changed' })
.delete(1),
);
expect(this.container).toEqualHTML(
'<p>0123</p><iframe src="#changed" class="ql-video" frameborder="0" allowfullscreen="true"></iframe>',
);
});

it('deletes block embed and appends text', function() {
const editor = this.initialize(
Editor,
`<p><br></p><iframe class="ql-video" frameborder="0" allowfullscreen="true" src="#"></iframe><p>b</p>`,
);
editor.applyDelta(
new Delta()
.retain(1)
.insert('a')
.delete(1),
);
expect(this.container).toEqualHTML('<p><br></p><p>ab</p>');
});

it('multiple delete block embed and append texts', function() {
const editor = this.initialize(
Editor,
`<p><br></p><iframe class="ql-video" frameborder="0" allowfullscreen="true" src="#"></iframe><iframe class="ql-video" frameborder="0" allowfullscreen="true" src="#"></iframe><p>b</p>`,
);
editor.applyDelta(
new Delta()
.retain(1)
.insert('a')
.delete(1)
.insert('!')
.delete(1),
);
expect(this.container).toEqualHTML('<p><br></p><p>a!b</p>');
});

it('multiple nonconsecutive delete block embed and append texts', function() {
const editor = this.initialize(
Editor,
`<p><br></p>
<iframe class="ql-video" frameborder="0" allowfullscreen="true" src="#"></iframe>
<p>a</p>
<iframe class="ql-video" frameborder="0" allowfullscreen="true" src="#"></iframe>
<p>bb</p>
<iframe class="ql-video" frameborder="0" allowfullscreen="true" src="#"></iframe>
<p>ccc</p>
<iframe class="ql-video" frameborder="0" allowfullscreen="true" src="#"></iframe>
<p>dddd</p>`,
);
const old = editor.getDelta();
const delta = new Delta()
.retain(1)
.insert('1')
.delete(1)
.retain(2)
.insert('2')
.delete(1)
.retain(3)
.insert('3')
.delete(1)
.retain(4)
.insert('4')
.delete(1);
editor.applyDelta(delta);
expect(editor.getDelta()).toEqual(old.compose(delta));
expect(this.container).toEqualHTML(
'<p><br></p><p>1a</p><p>2bb</p><p>3ccc</p><p>4dddd</p>',
);
});

it('improper block embed insert', function() {
const editor = this.initialize(Editor, '<p>0123</p>');
editor.applyDelta(new Delta().retain(2).insert({ video: '#' }));
Expand Down
30 changes: 26 additions & 4 deletions test/unit/modules/clipboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,16 @@ describe('Clipboard', function() {
}, 2);
});

// Copying from Word includes both html and files
it('pastes html data if present with file', function(done) {
const upload = spyOn(this.quill.uploader, 'upload');
this.quill.clipboard.onCapturePaste(
// eslint-disable-next-line prefer-object-spread
Object.assign({}, this.clipboardEvent, { files: ['file '] }),
);
this.quill.clipboard.onCapturePaste({
...this.clipboardEvent,
clipboardData: {
...this.clipboardEvent.clipboardData,
files: ['file'],
},
});
setTimeout(() => {
expect(upload).not.toHaveBeenCalled();
expect(this.quill.root).toEqualHTML(
Expand All @@ -48,6 +52,24 @@ describe('Clipboard', function() {
}, 2);
});

it('pastes image file if present with image only html', function(done) {
const upload = spyOn(this.quill.uploader, 'upload');
this.quill.clipboard.onCapturePaste({
...this.clipboardEvent,
clipboardData: {
getData: type =>
type === 'text/html'
? `<meta charset='utf-8'><img src="/assets/favicon.png"/>`
: '|',
files: ['file'],
},
});
setTimeout(() => {
expect(upload).toHaveBeenCalled();
done();
}, 2);
});

it('does not fire selection-change', function(done) {
const change = jasmine.createSpy('change');
this.quill.on('selection-change', change);
Expand Down

0 comments on commit f6e556b

Please sign in to comment.