Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

### Unreleased

- `Fix` - Place the caret after multi-block pasted content has been applied.

### 2.31.7

- `Fix` - Trigger `onChange` for native `<select>` changes
Expand Down
8 changes: 7 additions & 1 deletion src/components/modules/blockManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,11 +394,13 @@ export default class BlockManager extends Module {
* @param {string} toolName - name of Tool to insert
* @param {PasteEvent} pasteEvent - pasted data
* @param {boolean} replace - should replace current block
* @param {Function} onPasteComplete - called after the deferred onPaste handler
*/
public paste(
toolName: string,
pasteEvent: PasteEvent,
replace = false
replace = false,
onPasteComplete?: () => void
): Block {
const block = this.insert({
tool: toolName,
Expand All @@ -415,6 +417,10 @@ export default class BlockManager extends Module {
*/
window.requestIdleCallback(() => {
block.call(BlockToolAPI.ON_PASTE, pasteEvent);
if (onPasteComplete) {
/** Allow tools such as Paragraph to apply pasted content in the next frame. */
window.requestAnimationFrame(onPasteComplete);
}
});
} catch (e) {
_.log(`${toolName}: onPaste callback call is failed`, 'error', e);
Expand Down
29 changes: 10 additions & 19 deletions src/components/modules/paste.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import type {
SanitizerConfig,
SanitizerRule
} from '../../../types';
import type Block from '../block';
import type { SavedData } from '../../../types/data-formats';
import { clean, sanitizeBlocks } from '../utils/sanitizer';
import type BlockToolAdapter from '../tools/block';
Expand Down Expand Up @@ -251,9 +250,9 @@ export default class Paste extends Module {
const isCurrentBlockDefault = BlockManager.currentBlock && BlockManager.currentBlock.tool.isDefault;
const needToReplaceCurrentBlock = isCurrentBlockDefault && BlockManager.currentBlock.isEmpty;

dataToInsert.map(
async (content, i) => this.insertBlock(content, i === 0 && needToReplaceCurrentBlock)
);
await Promise.all(dataToInsert.map(
(content, i) => this.insertBlock(content, i === 0 && needToReplaceCurrentBlock)
));

if (BlockManager.currentBlock) {
Caret.setToBlock(BlockManager.currentBlock, Caret.positions.END);
Expand Down Expand Up @@ -842,23 +841,16 @@ export default class Paste extends Module {
*
* @param {PasteData} data - data to insert
* @param {boolean} canReplaceCurrentBlock - if true and is current Block is empty, will replace current Block
* @returns {void}
* @returns {Promise<void>} resolves after the pasted content has been applied
*/
private insertBlock(data: PasteData, canReplaceCurrentBlock = false): void {
const { BlockManager, Caret } = this.Editor;
private insertBlock(data: PasteData, canReplaceCurrentBlock = false): Promise<void> {
const { BlockManager } = this.Editor;
const { currentBlock } = BlockManager;
let block: Block;

if (canReplaceCurrentBlock && currentBlock && currentBlock.isEmpty) {
block = BlockManager.paste(data.tool, data.event, true);
Caret.setToBlock(block, Caret.positions.END);
const replace = canReplaceCurrentBlock && currentBlock && currentBlock.isEmpty;

return;
}

block = BlockManager.paste(data.tool, data.event);

Caret.setToBlock(block, Caret.positions.END);
return new Promise((resolve) => {
BlockManager.paste(data.tool, data.event, replace, resolve);
});
}

/**
Expand Down Expand Up @@ -1003,4 +995,3 @@ export default class Paste extends Module {
}) as PasteEvent;
}
}

37 changes: 37 additions & 0 deletions test/cypress/tests/copy-paste.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,43 @@ import type EditorJS from '../../../types/index';

describe('Copy pasting from Editor', function () {
context('pasting', function () {
it('should place the caret at the end after delayed multi-block paste', function () {
const callbacks: IdleRequestCallback[] = [];

cy.createEditor({});
cy.window().then((window) => {
cy.stub(window, 'requestIdleCallback').callsFake((callback: IdleRequestCallback) => {
callbacks.push(callback);

return callbacks.length;
});
});

cy.get('[data-cy=editorjs] div.ce-block')
.click()
.paste({
// eslint-disable-next-line @typescript-eslint/naming-convention
'text/plain': 'First block\n\nSecond block',
});

cy.then(() => {
callbacks.forEach(callback => callback({
didTimeout: false,
timeRemaining: () => 50,
}));
});

cy.get('[data-cy=editorjs] div.ce-paragraph')
.last()
.should('have.text', 'Second block');
cy.window().should((window) => {
const selection = window.getSelection();

expect(selection.anchorNode.textContent).to.eq('Second block');
expect(selection.anchorOffset).to.eq('Second block'.length);
});
});

it('should paste plain text', function () {
cy.createEditor({});

Expand Down