mirror of
https://github.com/codex-team/editor.js
synced 2024-06-10 09:52:36 +02:00
Fix copy in FireFox (#1632)
* Fix copy in FireFox * Add test cases * Eslint fix * Improve readability * fix eslint
This commit is contained in:
parent
d5aaa56ca0
commit
14acef136c
|
@ -4,6 +4,7 @@
|
|||
|
||||
- `Fix` - Create a new block when clicked at the bottom [#1588](https://github.com/codex-team/editor.js/issues/1588).
|
||||
- `Fix` — Fix sanitisation problem with Inline Tools [#1631](https://github.com/codex-team/editor.js/issues/1631)
|
||||
- `Fix` — Fix copy in FireFox [1625](https://github.com/codex-team/editor.js/issues/1625)
|
||||
- `Refactoring` - The Sanitizer module is util now.
|
||||
- `Refactoring` - Tooltip module is util now.
|
||||
- `Refactoring` — Refactoring based on LGTM [#1577](https://github.com/codex-team/editor.js/issues/1577).
|
||||
|
|
|
@ -504,7 +504,7 @@ export default class Block {
|
|||
/**
|
||||
* call Tool's method with the instance context
|
||||
*/
|
||||
if (this.toolInstance[methodName] && this.toolInstance[methodName] instanceof Function) {
|
||||
if (_.isFunction(this.toolInstance[methodName])) {
|
||||
if (methodName === BlockToolAPI.APPEND_CALLBACK) {
|
||||
_.log(
|
||||
'`appendCallback` hook is deprecated and will be removed in the next major release. ' +
|
||||
|
|
|
@ -167,7 +167,7 @@ export default class BlockEvents extends Module {
|
|||
*
|
||||
* @param {ClipboardEvent} event - clipboard event
|
||||
*/
|
||||
public handleCommandC(event: ClipboardEvent): Promise<void> {
|
||||
public handleCommandC(event: ClipboardEvent): void {
|
||||
const { BlockSelection } = this.Editor;
|
||||
|
||||
if (!BlockSelection.anyBlockSelected) {
|
||||
|
@ -175,7 +175,7 @@ export default class BlockEvents extends Module {
|
|||
}
|
||||
|
||||
// Copy Selected Blocks
|
||||
return BlockSelection.copySelectedBlocks(event);
|
||||
BlockSelection.copySelectedBlocks(event);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -183,21 +183,26 @@ export default class BlockEvents extends Module {
|
|||
*
|
||||
* @param {ClipboardEvent} event - clipboard event
|
||||
*/
|
||||
public async handleCommandX(event: ClipboardEvent): Promise<void> {
|
||||
public handleCommandX(event: ClipboardEvent): void {
|
||||
const { BlockSelection, BlockManager, Caret } = this.Editor;
|
||||
|
||||
if (!BlockSelection.anyBlockSelected) {
|
||||
return;
|
||||
}
|
||||
|
||||
await BlockSelection.copySelectedBlocks(event);
|
||||
BlockSelection.copySelectedBlocks(event).then(() => {
|
||||
const selectionPositionIndex = BlockManager.removeSelectedBlocks();
|
||||
|
||||
const selectionPositionIndex = BlockManager.removeSelectedBlocks();
|
||||
/**
|
||||
* Insert default block in place of removed ones
|
||||
*/
|
||||
const insertedBlock = BlockManager.insertDefaultBlockAtIndex(selectionPositionIndex, true);
|
||||
|
||||
Caret.setToBlock(BlockManager.insertDefaultBlockAtIndex(selectionPositionIndex, true), Caret.positions.START);
|
||||
Caret.setToBlock(insertedBlock, Caret.positions.START);
|
||||
|
||||
/** Clear selection */
|
||||
BlockSelection.clearSelection(event);
|
||||
/** Clear selection */
|
||||
BlockSelection.clearSelection(event);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -286,7 +286,7 @@ export default class BlockSelection extends Module {
|
|||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async copySelectedBlocks(e: ClipboardEvent): Promise<void> {
|
||||
public copySelectedBlocks(e: ClipboardEvent): Promise<void> {
|
||||
/**
|
||||
* Prevent default copy
|
||||
*/
|
||||
|
@ -305,15 +305,22 @@ export default class BlockSelection extends Module {
|
|||
fakeClipboard.appendChild(fragment);
|
||||
});
|
||||
|
||||
const savedData = await Promise.all(this.selectedBlocks.map((block) => block.save()));
|
||||
|
||||
const textPlain = Array.from(fakeClipboard.childNodes).map((node) => node.textContent)
|
||||
.join('\n\n');
|
||||
const textHTML = fakeClipboard.innerHTML;
|
||||
|
||||
e.clipboardData.setData('text/plain', textPlain);
|
||||
e.clipboardData.setData('text/html', textHTML);
|
||||
e.clipboardData.setData(this.Editor.Paste.MIME_TYPE, JSON.stringify(savedData));
|
||||
|
||||
return Promise
|
||||
.all(this.selectedBlocks.map((block) => block.save()))
|
||||
.then(savedData => {
|
||||
try {
|
||||
e.clipboardData.setData(this.Editor.Paste.MIME_TYPE, JSON.stringify(savedData));
|
||||
} catch (err) {
|
||||
// In Firefox we can't set data in async function
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -62,3 +62,55 @@ Cypress.Commands.add('paste', {
|
|||
|
||||
return subject;
|
||||
});
|
||||
|
||||
/**
|
||||
* Copy command to dispatch copy event on subject
|
||||
*
|
||||
* @usage
|
||||
* cy.get('div').copy().then(data => {})
|
||||
*/
|
||||
Cypress.Commands.add('copy', { prevSubject: true }, async (subject) => {
|
||||
const clipboardData: {[type: string]: any} = {};
|
||||
|
||||
const copyEvent = Object.assign(new Event('copy', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
}), {
|
||||
clipboardData: {
|
||||
setData: (type: string, data: any): void => {
|
||||
console.log(type, data);
|
||||
clipboardData[type] = data;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
subject[0].dispatchEvent(copyEvent);
|
||||
|
||||
return clipboardData;
|
||||
});
|
||||
|
||||
/**
|
||||
* Cut command to dispatch cut event on subject
|
||||
*
|
||||
* @usage
|
||||
* cy.get('div').cut().then(data => {})
|
||||
*/
|
||||
Cypress.Commands.add('cut', { prevSubject: true }, async (subject) => {
|
||||
const clipboardData: {[type: string]: any} = {};
|
||||
|
||||
const copyEvent = Object.assign(new Event('cut', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
}), {
|
||||
clipboardData: {
|
||||
setData: (type: string, data: any): void => {
|
||||
console.log(type, data);
|
||||
clipboardData[type] = data;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
subject[0].dispatchEvent(copyEvent);
|
||||
|
||||
return clipboardData;
|
||||
});
|
||||
|
|
16
test/cypress/support/index.d.ts
vendored
16
test/cypress/support/index.d.ts
vendored
|
@ -24,6 +24,22 @@ declare global {
|
|||
* @param data - map with MIME type as a key and data as value
|
||||
*/
|
||||
paste(data: {[type: string]: string}): Chainable<Subject>
|
||||
|
||||
/**
|
||||
* Copy command to dispatch copy event on subject
|
||||
*
|
||||
* @usage
|
||||
* cy.get('div').copy().then(data => {})
|
||||
*/
|
||||
copy(): Chainable<{ [type: string]: any }>;
|
||||
|
||||
/**
|
||||
* Cut command to dispatch cut event on subject
|
||||
*
|
||||
* @usage
|
||||
* cy.get('div').cut().then(data => {})
|
||||
*/
|
||||
cut(): Chainable<{ [type: string]: any }>;
|
||||
}
|
||||
|
||||
interface ApplicationWindow {
|
||||
|
|
274
test/cypress/tests/copy-paste.spec.ts
Normal file
274
test/cypress/tests/copy-paste.spec.ts
Normal file
|
@ -0,0 +1,274 @@
|
|||
import Header from '../../../example/tools/header';
|
||||
import Image from '../../../example/tools/simple-image';
|
||||
import * as _ from '../../../src/components/utils';
|
||||
|
||||
describe('Copy pasting from Editor', () => {
|
||||
beforeEach(() => {
|
||||
if (this && this.editorInstance) {
|
||||
this.editorInstance.destroy();
|
||||
} else {
|
||||
cy.createEditor({
|
||||
tools: {
|
||||
header: Header,
|
||||
image: Image,
|
||||
},
|
||||
}).as('editorInstance');
|
||||
}
|
||||
});
|
||||
|
||||
context('pasting', () => {
|
||||
it('should paste plain text', () => {
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
.paste({
|
||||
'text/plain': 'Some plain text',
|
||||
})
|
||||
.should('contain', 'Some plain text');
|
||||
});
|
||||
|
||||
it('should paste inline html data', () => {
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
.paste({
|
||||
'text/html': '<p><b>Some text</b></p>',
|
||||
})
|
||||
.should('contain.html', '<b>Some text</b>');
|
||||
});
|
||||
|
||||
it('should paste several blocks if plain text contains new lines', () => {
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
.paste({
|
||||
'text/plain': 'First block\n\nSecond block',
|
||||
});
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.then(blocks => {
|
||||
expect(blocks[0].textContent).to.eq('First block');
|
||||
expect(blocks[1].textContent).to.eq('Second block');
|
||||
});
|
||||
});
|
||||
|
||||
it('should paste several blocks if html contains several paragraphs', () => {
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
.paste({
|
||||
'text/html': '<p>First block</p><p>Second block</p>',
|
||||
});
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.then(blocks => {
|
||||
expect(blocks[0].textContent).to.eq('First block');
|
||||
expect(blocks[1].textContent).to.eq('Second block');
|
||||
});
|
||||
});
|
||||
|
||||
it('should paste using custom data type', () => {
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
.paste({
|
||||
'application/x-editor-js': JSON.stringify([
|
||||
{
|
||||
tool: 'paragraph',
|
||||
data: {
|
||||
text: 'First block',
|
||||
},
|
||||
},
|
||||
{
|
||||
tool: 'paragraph',
|
||||
data: {
|
||||
text: 'Second block',
|
||||
},
|
||||
},
|
||||
]),
|
||||
});
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.then(blocks => {
|
||||
expect(blocks[0].textContent).to.eq('First block');
|
||||
expect(blocks[1].textContent).to.eq('Second block');
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse block tags', () => {
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
.paste({
|
||||
'text/html': '<h2>First block</h2><p>Second block</p>',
|
||||
});
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('h2.ce-header')
|
||||
.should('contain', 'First block');
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-paragraph')
|
||||
.should('contain', 'Second block');
|
||||
});
|
||||
|
||||
it('should parse pattern', () => {
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
.paste({
|
||||
'text/plain': 'https://codex.so/public/app/img/external/codex2x.png',
|
||||
});
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('img')
|
||||
.should('have.attr', 'src', 'https://codex.so/public/app/img/external/codex2x.png');
|
||||
});
|
||||
});
|
||||
|
||||
context('copying', () => {
|
||||
it('should copy inline fragment', () => {
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
.type('Some text{selectall}')
|
||||
.copy()
|
||||
.then(clipboardData => {
|
||||
/**
|
||||
* As no blocks selected, clipboard data will be empty as will be handled by browser
|
||||
*/
|
||||
expect(clipboardData).to.be.empty;
|
||||
});
|
||||
});
|
||||
|
||||
it('should copy several blocks', () => {
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
.type('First block{enter}');
|
||||
|
||||
cy.get('[data-cy=editorjs')
|
||||
.get('div.ce-block')
|
||||
.next()
|
||||
.type('Second block')
|
||||
.type('{movetostart}')
|
||||
.trigger('keydown', {
|
||||
shiftKey: true,
|
||||
keyCode: _.keyCodes.UP,
|
||||
})
|
||||
.copy()
|
||||
.then(clipboardData => {
|
||||
expect(clipboardData['text/html']).to.eq('<p>First block</p><p>Second block</p>');
|
||||
expect(clipboardData['text/plain']).to.eq(`First block\n\nSecond block`);
|
||||
|
||||
/**
|
||||
* Need to wait for custom data as it is set asynchronously
|
||||
*/
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(0).then(() => {
|
||||
expect(clipboardData['application/x-editor-js']).not.to.be.undefined;
|
||||
|
||||
const data = JSON.parse(clipboardData['application/x-editor-js']);
|
||||
|
||||
expect(data[0].tool).to.eq('paragraph');
|
||||
expect(data[0].data).to.deep.eq({ text: 'First block' });
|
||||
expect(data[1].tool).to.eq('paragraph');
|
||||
expect(data[1].data).to.deep.eq({ text: 'Second block' });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('cutting', () => {
|
||||
it('should cut inline fragment', () => {
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
.type('Some text{selectall}')
|
||||
.cut()
|
||||
.then(clipboardData => {
|
||||
/**
|
||||
* As no blocks selected, clipboard data will be empty as will be handled by browser
|
||||
*/
|
||||
expect(clipboardData).to.be.empty;
|
||||
});
|
||||
});
|
||||
|
||||
it('should cut several blocks', () => {
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
.type('First block{enter}');
|
||||
|
||||
cy.get('[data-cy=editorjs')
|
||||
.get('div.ce-block')
|
||||
.next()
|
||||
.type('Second block')
|
||||
.type('{movetostart}')
|
||||
.trigger('keydown', {
|
||||
shiftKey: true,
|
||||
keyCode: _.keyCodes.UP,
|
||||
})
|
||||
.cut()
|
||||
.then(clipboardData => {
|
||||
expect(clipboardData['text/html']).to.eq('<p>First block</p><p>Second block</p>');
|
||||
expect(clipboardData['text/plain']).to.eq(`First block\n\nSecond block`);
|
||||
|
||||
/**
|
||||
* Need to wait for custom data as it is set asynchronously
|
||||
*/
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(0).then(() => {
|
||||
expect(clipboardData['application/x-editor-js']).not.to.be.undefined;
|
||||
|
||||
const data = JSON.parse(clipboardData['application/x-editor-js']);
|
||||
|
||||
expect(data[0].tool).to.eq('paragraph');
|
||||
expect(data[0].data).to.deep.eq({ text: 'First block' });
|
||||
expect(data[1].tool).to.eq('paragraph');
|
||||
expect(data[1].data).to.deep.eq({ text: 'Second block' });
|
||||
});
|
||||
});
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.should('not.contain', 'First block')
|
||||
.should('not.contain', 'Second block');
|
||||
});
|
||||
|
||||
it('should cut lots of blocks', () => {
|
||||
const numberOfBlocks = 50;
|
||||
|
||||
for (let i = 0; i < numberOfBlocks; i++) {
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.last()
|
||||
.click()
|
||||
.type(`Block ${i}{enter}`);
|
||||
}
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.first()
|
||||
.click()
|
||||
.type('{ctrl+A}')
|
||||
.type('{ctrl+A}')
|
||||
.cut()
|
||||
.then((clipboardData) => {
|
||||
/**
|
||||
* Need to wait for custom data as it is set asynchronously
|
||||
*/
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(0).then(() => {
|
||||
expect(clipboardData['application/x-editor-js']).not.to.be.undefined;
|
||||
|
||||
const data = JSON.parse(clipboardData['application/x-editor-js']);
|
||||
|
||||
expect(data.length).to.eq(numberOfBlocks + 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue