This commit is contained in:
Umang G. Patel 2023-09-18 14:51:37 +00:00 committed by GitHub
commit 14ec0bfba5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 835 additions and 63 deletions

View file

@ -17,7 +17,7 @@
"build": "vite build",
"lint": "eslint src/ --ext .ts && yarn lint:tests",
"lint:errors": "eslint src/ --ext .ts --quiet",
"lint:fix": "eslint src/ --ext .ts --fix",
"lint:fix": "eslint src/ --ext .ts --fix && eslint test/ --ext .ts --fix",
"lint:tests": "eslint test/ --ext .ts",
"pull_tools": "git submodule update --init --recursive",
"_tools:checkout": "git submodule foreach \"git checkout master || git checkout main\"",

View file

@ -87,6 +87,15 @@ export enum BlockToolAPI {
ON_PASTE = 'onPaste',
}
/**
* Available block drop zones position w.r.t. focused block.
*/
export enum BlockDropZonePosition {
Top = 'top',
Bottom = 'bottom',
// @todo - Left, Right could be added in the future
}
/**
* Names of events used in Block
*/
@ -114,6 +123,8 @@ export default class Block extends EventsDispatcher<BlockEvents> {
focused: 'ce-block--focused',
selected: 'ce-block--selected',
dropTarget: 'ce-block--drop-target',
dropTargetTop: 'ce-block--drop-target-top',
dropTargetBottom: 'ce-block--drop-target-bottom',
};
}
@ -498,12 +509,41 @@ export default class Block extends EventsDispatcher<BlockEvents> {
}
/**
* Toggle drop target state
* Set the drop zone position and update the style to drop zone target style.
*
* @param {boolean} state - 'true' if block is drop target, false otherwise
* @param {boolean | BlockDropZonePosition} state - 'false' if block is not a drop zone or
* position of drop zone.
*/
public set dropTarget(state) {
this.holder.classList.toggle(Block.CSS.dropTarget, state);
public set dropZonePosition(state: boolean | BlockDropZonePosition) {
if (!state || this.selected) {
/**
* If state is undefined or block is selected for drag
* then remove the drop target style
*/
this.holder.classList.remove(Block.CSS.dropTarget, Block.CSS.dropTargetTop, Block.CSS.dropTargetBottom);
} else {
/**
* Otherwise, toggle the block's drop target and drop zone position.
*/
this.holder.classList.toggle(Block.CSS.dropTarget, !!state);
this.holder.classList.toggle(Block.CSS.dropTargetTop, state === BlockDropZonePosition.Top);
this.holder.classList.toggle(Block.CSS.dropTargetBottom, state === BlockDropZonePosition.Bottom);
}
}
/**
* Return Block's drop zone position or false if block is not a drop zone.
*
* @returns {BlockDropZonePosition | boolean}
*/
public get dropZonePosition(): boolean | BlockDropZonePosition {
if (this.holder.classList.contains(Block.CSS.dropTargetTop)) {
return BlockDropZonePosition.Top;
} else if (this.holder.classList.contains(Block.CSS.dropTargetBottom)) {
return BlockDropZonePosition.Bottom;
}
return false;
}
/**

View file

@ -5,6 +5,7 @@ import Module from '../__module';
import * as _ from '../utils';
import SelectionUtils from '../selection';
import Flipper from '../flipper';
import { BlockDropZonePosition } from '../block';
import type Block from '../block';
import { areBlocksMergeable } from '../utils/blocks';
@ -148,25 +149,42 @@ export default class BlockEvents extends Module {
}
/**
* Add drop target styles
* All drag enter on block
* - use to clear previous drop target zone style.
*
* @param {DragEvent} event - drag over event
*/
public dragEnter(event: DragEvent): void {
const { BlockManager } = this.Editor;
const block = BlockManager.getBlockByChildNode(event.target as Node);
/**
* Scroll to make element inside the viewport.
*/
_.scrollToView(block.holder);
/**
* Clear previous drop target zone for every block.
*/
BlockManager.clearDropZonePosition();
}
/**
* All drag over on block.
* - Check the position of drag and suggest drop zone accordingly.
*
* @param {DragEvent} event - drag over event
*/
public dragOver(event: DragEvent): void {
const block = this.Editor.BlockManager.getBlockByChildNode(event.target as Node);
const rect = block.holder.getBoundingClientRect();
block.dropTarget = true;
}
/**
* Remove drop target style
*
* @param {DragEvent} event - drag leave event
*/
public dragLeave(event: DragEvent): void {
const block = this.Editor.BlockManager.getBlockByChildNode(event.target as Node);
block.dropTarget = false;
/**
* Add style for target drop zone position.
*/
block.dropZonePosition = (rect.top + rect.height / 2 >= event.clientY) ?
BlockDropZonePosition.Top :
BlockDropZonePosition.Bottom;
}
/**
@ -511,7 +529,7 @@ export default class BlockEvents extends Module {
if (this.Editor.BlockManager.currentBlock) {
this.Editor.BlockManager.currentBlock.updateCurrentInput();
}
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
}, 20)();
}
@ -570,7 +588,7 @@ export default class BlockEvents extends Module {
if (this.Editor.BlockManager.currentBlock) {
this.Editor.BlockManager.currentBlock.updateCurrentInput();
}
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
}, 20)();
}

View file

@ -680,6 +680,15 @@ export default class BlockManager extends Module {
});
}
/**
* Remove drop zone positions from all Blocks.
*/
public clearDropZonePosition(): void {
this.blocks.forEach((block) => {
block.dropZonePosition = false;
});
}
/**
* 1) Find first-level Block from passed child Node
* 2) Mark it as current
@ -919,12 +928,12 @@ export default class BlockManager extends Module {
BlockEvents.keyup(event);
});
this.readOnlyMutableListeners.on(block.holder, 'dragover', (event: DragEvent) => {
BlockEvents.dragOver(event);
this.readOnlyMutableListeners.on(block.holder, 'dragenter', (event: DragEvent) => {
BlockEvents.dragEnter(event);
});
this.readOnlyMutableListeners.on(block.holder, 'dragleave', (event: DragEvent) => {
BlockEvents.dragLeave(event);
this.readOnlyMutableListeners.on(block.holder, 'dragover', (event: DragEvent) => {
BlockEvents.dragOver(event);
});
block.on('didMutated', (affectedBlock: Block) => {

View file

@ -225,7 +225,7 @@ export default class BlockSelection extends Module {
* @param {boolean} restoreSelection - if true, restore saved selection
*/
public clearSelection(reason?: Event, restoreSelection = false): void {
const { BlockManager, Caret, RectangleSelection } = this.Editor;
const { BlockManager, Caret, RectangleSelection, Toolbar } = this.Editor;
this.needToSelectAll = false;
this.nativeInputSelected = false;
@ -234,6 +234,16 @@ export default class BlockSelection extends Module {
const isKeyboard = reason && (reason instanceof KeyboardEvent);
const isPrintableKey = isKeyboard && _.isPrintableKey((reason as KeyboardEvent).keyCode);
/**
* Don't clear the selection during multiple element dragging.
*/
const isMouse = reason && (reason instanceof MouseEvent);
const isClickedOnSettingsToggler = isMouse && Toolbar.nodes.settingsToggler.contains(reason.target as HTMLElement);
if (isMouse && isClickedOnSettingsToggler) {
return;
}
/**
* If reason caused clear of the selection was printable key and any block is selected,
* remove selected blocks and insert pressed key

View file

@ -1,5 +1,7 @@
import SelectionUtils from '../selection';
import Block, { BlockDropZonePosition } from '../block';
import * as _ from '../utils';
import $ from '../dom';
import Module from '../__module';
/**
*
@ -13,6 +15,15 @@ export default class DragNDrop extends Module {
*/
private isStartedAtEditor = false;
/**
* Flag that identifies if the drag event is started at the editor.
*
* @returns {boolean}
*/
public get isDragStarted(): boolean {
return this.isStartedAtEditor;
}
/**
* Toggle read-only state
*
@ -36,14 +47,23 @@ export default class DragNDrop extends Module {
* Add drag events listeners to editor zone
*/
private enableModuleBindings(): void {
const { UI } = this.Editor;
const { UI, BlockManager } = this.Editor;
this.readOnlyMutableListeners.on(UI.nodes.holder, 'drop', async (dropEvent: DragEvent) => {
await this.processDrop(dropEvent);
}, true);
this.readOnlyMutableListeners.on(UI.nodes.holder, 'dragstart', () => {
this.processDragStart();
this.readOnlyMutableListeners.on(UI.nodes.holder, 'dragstart', (dragStartEvent: DragEvent) => {
this.processDragStart(dragStartEvent);
});
/**
* Clear drop targets if drop effect is none.
*/
this.readOnlyMutableListeners.on(UI.nodes.holder, 'dragend', (dragEndEvent: DragEvent) => {
if (dragEndEvent.dataTransfer.dropEffect === 'none') {
BlockManager.clearDropZonePosition();
}
});
/**
@ -71,13 +91,17 @@ export default class DragNDrop extends Module {
BlockManager,
Caret,
Paste,
BlockSelection,
} = this.Editor;
dropEvent.preventDefault();
BlockManager.blocks.forEach((block) => {
block.dropTarget = false;
});
/**
* If we are dropping a block, process it and return.
*/
if (this.isStartedAtEditor && BlockSelection.anyBlockSelected) {
this.processBlockDrop(dropEvent);
}
if (SelectionUtils.isAtEditor && !SelectionUtils.isCollapsed && this.isStartedAtEditor) {
document.execCommand('delete');
@ -89,28 +113,117 @@ export default class DragNDrop extends Module {
* Try to set current block by drop target.
* If drop target is not part of the Block, set last Block as current.
*/
const targetBlock = BlockManager.setCurrentBlockByChildNode(dropEvent.target as Node);
const firstLevelBlock = (dropEvent.target as HTMLElement).closest(`.${Block.CSS.wrapper}`);
let targetBlock = BlockManager.blocks.find((block) => block.holder === firstLevelBlock);
let shouldMoveToFirst = false;
if (targetBlock) {
this.Editor.Caret.setToBlock(targetBlock, Caret.positions.END);
if (targetBlock.dropZonePosition === BlockDropZonePosition.Top) {
const currentIndex = BlockManager.getBlockIndex(targetBlock);
let targetIndex;
if (currentIndex > 0) {
targetIndex = currentIndex - 1;
}
else {
// Paste the block at the end of first block.
targetIndex = 0;
// then swap the first block with second block.
shouldMoveToFirst = true;
}
targetBlock = BlockManager.getBlockByIndex(targetIndex);
}
Caret.setToBlock(targetBlock, Caret.positions.END);
} else {
const lastBlock = BlockManager.setCurrentBlockByChildNode(BlockManager.lastBlock.holder);
const firstLevelBlock = (BlockManager.lastBlock.holder as HTMLElement).closest(`.${Block.CSS.wrapper}`);
let lastBlock = BlockManager.blocks.find((block) => block.holder === firstLevelBlock);
this.Editor.Caret.setToBlock(lastBlock, Caret.positions.END);
Caret.setToBlock(lastBlock, Caret.positions.END);
}
// Clear drop zones.
BlockManager.clearDropZonePosition();
// Clear the selection.
BlockSelection.clearSelection();
await Paste.processDataTransfer(dropEvent.dataTransfer, true);
// swapping of the first block with second block.
if (shouldMoveToFirst) {
BlockManager.move(1, 0);
}
}
/**
* Handle drag start event
* Process block drop event.
*
* @param dropEvent {DragEvent} - drop event
*/
private processDragStart(): void {
if (SelectionUtils.isAtEditor && !SelectionUtils.isCollapsed) {
this.isStartedAtEditor = true;
}
private processBlockDrop(dropEvent: DragEvent): void {
const { BlockManager, BlockSelection } = this.Editor;
this.Editor.InlineToolbar.close();
/**
* Remove drag image from DOM.
*/
this.removeDragImage();
const selectedBlocks = BlockSelection.selectedBlocks;
const firstLevelBlock = (dropEvent.target as HTMLElement).closest(`.${Block.CSS.wrapper}`);
const targetBlock = BlockManager.blocks.find((block) => block.holder === firstLevelBlock);
if (!targetBlock) {
// This means that we are trying to drop a block without references.
return;
}
const targetIndex = BlockManager.getBlockIndex(targetBlock);
// we are dragging a set of blocks
const currentStartIndex = BlockManager.getBlockIndex(selectedBlocks[0]);
selectedBlocks.forEach((block, i) => {
const blockIndex = BlockManager.getBlockIndex(block);
let toIndex;
/**
* Calculate the index where the block should be moved to.
*/
if (targetBlock.dropZonePosition === BlockDropZonePosition.Top) {
if (targetIndex > currentStartIndex) {
toIndex = targetIndex - 1;
} else {
toIndex = targetIndex + i;
}
} else if (targetBlock.dropZonePosition === BlockDropZonePosition.Bottom) {
if (targetIndex > currentStartIndex) {
toIndex = targetIndex;
} else {
toIndex = targetIndex + 1 + i;
}
}
BlockManager.move(toIndex, blockIndex);
});
}
/**
* Handle drag start event by setting drag image.
*
* @param dragStartEvent - drag start event
*/
private processDragStart(dragStartEvent: DragEvent): void {
const { BlockSelection } = this.Editor;
/**
* If we are dragging a block, set the flag to true.
*/
this.isStartedAtEditor = true;
const selectedBlocks = BlockSelection.selectedBlocks;
const dragImage = this.createDragImage(selectedBlocks);
dragStartEvent.dataTransfer.setDragImage(dragImage, 0, 0);
}
/**
@ -119,4 +232,40 @@ export default class DragNDrop extends Module {
private processDragOver(dragEvent: DragEvent): void {
dragEvent.preventDefault();
}
/**
* Create drag image for drag-n-drop and add to Editor holder.
*
* @param blocks {Block[]} - blocks to create drag image for.
* @returns {HTMLElement} - drag image.
*/
private createDragImage(blocks: Block[]): HTMLElement {
const { UI } = this.Editor;
/**
* Create a drag image with all blocks content.
*/
const dragImage: HTMLElement = $.make('div');
dragImage.id = `drag-image-${_.generateId()}`;
dragImage.style.position = 'absolute';
dragImage.style.top = '-1000px';
const clones = blocks.map(block => block.holder.querySelector(`.${Block.CSS.content}`).cloneNode(true));
dragImage.append(...clones);
UI.nodes.holder.appendChild(dragImage);
return dragImage;
}
/**
* Remove drag image from Editor holder.
*/
private removeDragImage(): void {
const { UI } = this.Editor;
UI.nodes.holder.querySelector('[id^="drag-image-"]')?.remove();
}
}

View file

@ -161,7 +161,7 @@ export default class Toolbar extends Module<ToolbarNodes> {
open: () => void;
toggle: () => void;
hasFocus: () => boolean | undefined;
} {
} {
return {
opened: this.toolboxInstance?.opened,
close: () => {
@ -171,7 +171,7 @@ export default class Toolbar extends Module<ToolbarNodes> {
/**
* If Toolbox is not initialized yet, do nothing
*/
if (this.toolboxInstance === null) {
if (this.toolboxInstance === null) {
_.log('toolbox.open() called before initialization is finished', 'warn');
return;
@ -188,7 +188,7 @@ export default class Toolbar extends Module<ToolbarNodes> {
/**
* If Toolbox is not initialized yet, do nothing
*/
if (this.toolboxInstance === null) {
if (this.toolboxInstance === null) {
_.log('toolbox.toggle() called before initialization is finished', 'warn');
return;
@ -251,7 +251,7 @@ export default class Toolbar extends Module<ToolbarNodes> {
/**
* Some UI elements creates inside requestIdleCallback, so the can be not ready yet
*/
if (this.toolboxInstance === null) {
if (this.toolboxInstance === null) {
_.log('Can\'t open Toolbar since Editor initialization is not finished yet', 'warn');
return;
@ -345,7 +345,7 @@ export default class Toolbar extends Module<ToolbarNodes> {
} else {
this.blockActions.hide();
}
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
}, 50)();
}
@ -408,6 +408,7 @@ export default class Toolbar extends Module<ToolbarNodes> {
*/
this.nodes.settingsToggler = $.make('span', this.CSS.settingsToggler, {
innerHTML: IconMenu,
draggable: true,
});
$.append(this.nodes.actions, this.nodes.settingsToggler);
@ -496,9 +497,37 @@ export default class Toolbar extends Module<ToolbarNodes> {
/**
* Settings toggler
*
* mousedown is used because on click selection is lost in Safari and FF
* dargstart is used to select the current block/s to hide
* the tooltip and close Inilne toolbar for dragging.
*/
this.readOnlyMutableListeners.on(this.nodes.settingsToggler, 'mousedown', (e) => {
this.readOnlyMutableListeners.on(this.nodes.settingsToggler, 'dragstart', () => {
const { BlockManager, BlockSettings, BlockSelection } = this.Editor;
/** Close components */
this.tooltip.hide(true);
this.blockActions.hide();
this.toolboxInstance.close();
BlockSettings.close();
BlockManager.currentBlock = this.hoveredBlock;
BlockSelection.selectBlockByIndex(BlockManager.currentBlockIndex);
}, true);
/**
* Settings toggler
*
* dargend is used to move the select block toolbar setting to dropped position.
*/
this.readOnlyMutableListeners.on(this.nodes.settingsToggler, 'dragend', () => {
this.moveAndOpen(this.Editor.BlockManager.currentBlock);
}, true);
/**
* Settings toggler
*
* mouseup is used because on click selection is lost in Safari and FF
*/
this.readOnlyMutableListeners.on(this.nodes.settingsToggler, 'mouseup', (e) => {
/**
* Stop propagation to prevent block selection clearance
*
@ -526,9 +555,11 @@ export default class Toolbar extends Module<ToolbarNodes> {
*/
this.eventsDispatcher.on(BlockHovered, (data) => {
/**
* Do not move toolbar if Block Settings or Toolbox opened
* Do not move toolbar if Block Settings or Toolbox opened or Drag started.
*/
if (this.Editor.BlockSettings.opened || this.toolboxInstance?.opened) {
if (this.Editor.BlockSettings.opened ||
this.toolboxInstance?.opened ||
this.Editor.DragNDrop.isDragStarted) {
return;
}

View file

@ -355,13 +355,6 @@ export default class UI extends Module<UINodes> {
this.readOnlyMutableListeners.on(this.nodes.redactor, 'mousemove', _.throttle((event: MouseEvent | TouchEvent) => {
const hoveredBlock = (event.target as Element).closest('.ce-block');
/**
* Do not trigger 'block-hovered' for cross-block selection
*/
if (this.Editor.BlockSelection.anyBlockSelected) {
return;
}
if (!hoveredBlock) {
return;
}

View file

@ -146,7 +146,7 @@ function _log(
} else {
console[type](msg, ...argsToPass);
}
} catch (ignored) {}
} catch (ignored) { }
}
/**
@ -316,9 +316,9 @@ export function isPrintableKey(keyCode: number): boolean {
export async function sequence(
chains: ChainData[],
// eslint-disable-next-line @typescript-eslint/no-empty-function
success: (data: object) => void = (): void => {},
success: (data: object) => void = (): void => { },
// eslint-disable-next-line @typescript-eslint/no-empty-function
fallback: (data: object) => void = (): void => {}
fallback: (data: object) => void = (): void => { }
): Promise<void> {
/**
* Decorator
@ -450,7 +450,7 @@ export function debounce(func: (...args: unknown[]) => void, wait?: number, imme
* but if you'd like to disable the execution on the leading edge, pass
* `{leading: false}`. To disable execution on the trailing edge, ditto.
*/
export function throttle(func, wait, options: {leading?: boolean; trailing?: boolean} = undefined): () => void {
export function throttle(func, wait, options: { leading?: boolean; trailing?: boolean } = undefined): () => void {
let context, args, result;
let timeout = null;
let previous = 0;
@ -530,7 +530,7 @@ export function copyTextToClipboard(text): void {
/**
* Returns object with os name as key and boolean as value. Shows current user OS
*/
export function getUserOS(): {[key: string]: boolean} {
export function getUserOS(): { [key: string]: boolean } {
const OS = {
win: false,
mac: false,
@ -788,3 +788,35 @@ export function equals(var1: unknown, var2: unknown): boolean {
return var1 === var2;
}
/**
* Scrolls the viewport to bring the specified element into view.
*
* @param {HTMLElement} elem - The element to scroll to.
*/
export function scrollToView(elem: HTMLElement):void {
// Get the target element and its bounding rectangle
const targetRect = elem.getBoundingClientRect();
// Get the size of the viewport
const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
// Check if the target element is within the viewport
const isTargetInViewport = (
targetRect.top >= 0 &&
targetRect.left >= 0 &&
targetRect.bottom <= viewportHeight &&
targetRect.right <= viewportWidth
);
// Scroll the page if the target element is not within the viewport
if (!isTargetInViewport) {
window.scrollTo({
top: targetRect.top,
left: targetRect.left,
behavior: 'smooth',
});
}
}

View file

@ -57,6 +57,7 @@
border-width: 1px 1px 0 0;
transform-origin: right;
transform: rotate(45deg);
pointer-events: none;
}
&:after {
@ -73,6 +74,17 @@
#fff 1px,
#fff 6px
);
pointer-events: none;
}
}
&--drop-target-top &__content {
&:before {
top: 0%;
}
&:after {
top: 0%;
}
}

View file

@ -0,0 +1,478 @@
import Image from '@editorjs/simple-image';
import * as _ from '../../../src/components/utils';
import type EditorJS from '../../../../types/index';
describe('Drag and drop the block of Editor', function () {
beforeEach(function () {
cy.createEditor({
tools: {
image: Image,
},
data: {
blocks: [
{
type: 'paragraph',
data: {
text: 'Block 0',
},
},
{
type: 'paragraph',
data: {
text: 'Block 1',
},
},
{
type: 'paragraph',
data: {
text: 'Block 2',
},
},
],
},
}).as('editorInstance');
});
afterEach(function () {
if (this.editorInstance) {
this.editorInstance.destroy();
}
});
// Create URI
const base64Image = "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
const uri = 'data:image/png;base64,' + base64Image;
// Define the file to be dropped
const fileName = 'codex2x.png';
const fileType = 'image/png';
// Convert base64 to Blob
const blob = Cypress.Blob.base64StringToBlob(base64Image);
const file = new File([blob], fileName, { type: fileType });
const dataTransfer = new DataTransfer();
// add the file to the DataTransfer object
dataTransfer.items.add(file);
/**
* @todo check with dropping file other than the image.
*/
it('should drop image before the block', function () {
// Test by dropping the image.
cy.get('[data-cy=editorjs]')
.get('div.ce-block')
.eq(1)
.trigger('dragenter')
.then((blocks) => {
// Get the target block rect.
const targetBlockRect = blocks[0].getBoundingClientRect();
const yShiftFromMiddleLine = -20;
// Dragover on target block little bit above the middle line.
const dragOverYCoord =
targetBlockRect.y +
(targetBlockRect.height / 2 + yShiftFromMiddleLine);
cy.get('[data-cy=editorjs]')
.get('div.ce-block')
.eq(1)
.trigger('dragover', {
clientX: targetBlockRect.x,
clientY: dragOverYCoord,
})
.trigger('drop', { dataTransfer });
});
cy.get('[data-cy=editorjs]')
// In Edge test are performed slower, so we need to
// increase timeout to wait until image is loaded on the page
.get('div.ce-block')
.eq(1)
.find('img', { timeout: 10000 })
.should('have.attr', 'src', uri);
});
it('should drop image after the block', function () {
// Test by dropping the image.
cy.get('[data-cy=editorjs]')
.get('div.ce-block')
.eq(1)
.trigger('dragenter')
.then((blocks) => {
// Get the target block rect.
const targetBlockRect = blocks[0].getBoundingClientRect();
const yShiftFromMiddleLine = 20;
// Dragover on target block little bit below the middle line.
const dragOverYCoord =
targetBlockRect.y +
(targetBlockRect.height / 2 + yShiftFromMiddleLine);
cy.get('[data-cy=editorjs]')
.get('div.ce-block')
.eq(1)
.trigger('dragover', {
clientX: targetBlockRect.x,
clientY: dragOverYCoord,
})
.trigger('drop', { dataTransfer });
});
cy.get('[data-cy=editorjs]')
// In Edge test are performed slower, so we need to
// increase timeout to wait until image is loaded on the page
.get('div.ce-block')
.eq(2)
.find('img', { timeout: 10000 })
.should('have.attr', 'src', uri);
});
it('should drop image before the first block', function () {
// Test by dropping the image.
cy.get('[data-cy=editorjs]')
.get('div.ce-block')
.first()
.trigger('dragenter')
.then((blocks) => {
// Get the target block rect.
const targetBlockRect = blocks[0].getBoundingClientRect();
const yShiftFromMiddleLine = -20;
// Dragover on target block little bit above the middle line.
const dragOverYCoord =
targetBlockRect.y +
(targetBlockRect.height / 2 + yShiftFromMiddleLine);
cy.get('[data-cy=editorjs]')
.get('div.ce-block')
.eq(0)
.trigger('dragover', {
clientX: targetBlockRect.x,
clientY: dragOverYCoord,
})
.trigger('drop', { dataTransfer });
});
cy.get('[data-cy=editorjs]')
.get('div.ce-block')
// In Edge test are performed slower, so we need to
// increase timeout to wait until image is loaded on the page
.eq(0)
.find('img', { timeout: 10000 })
.should('have.attr', 'src', uri);
});
it('should have block dragover style on the top of target block', function () {
cy.get('[data-cy=editorjs]')
.get('div.ce-block')
.first()
.click()
.click();
const dataTransfer = new DataTransfer();
// eslint-disable-next-line cypress/require-data-selectors
cy.get('.ce-toolbar__settings-btn')
.trigger('dragstart', { dataTransfer });
cy.get('[data-cy=editorjs]')
.get('div.ce-block')
.eq(2)
.trigger('dragenter')
.then((blocks) => {
// Get the target block rect.
const targetBlockRect = blocks[0].getBoundingClientRect();
const yShiftFromMiddleLine = -20;
// Dragover on target block little bit above the middle line.
const dragOverYCoord =
targetBlockRect.y +
(targetBlockRect.height / 2 + yShiftFromMiddleLine);
cy.get('[data-cy=editorjs]')
.get('div.ce-block')
.eq(2)
.trigger('dragover', {
clientX: targetBlockRect.x,
clientY: dragOverYCoord,
})
.then(($element) => {
// check for dragover top style on target element.
const classes = $element.attr('class').split(' ');
expect(classes).to.include('ce-block--drop-target');
expect(classes).to.include('ce-block--drop-target-top');
});
});
});
it('should have block dragover style on the bottom of target block', function () {
cy.get('[data-cy=editorjs]')
.get('div.ce-block')
.first()
.click()
.click();
const dataTransfer = new DataTransfer();
// eslint-disable-next-line cypress/require-data-selectors
cy.get('.ce-toolbar__settings-btn')
.trigger('dragstart', { dataTransfer });
cy.get('[data-cy=editorjs]')
.get('div.ce-block')
.eq(2)
.trigger('dragenter')
.then((blocks) => {
// Get the target block rect.
const targetBlockRect = blocks[0].getBoundingClientRect();
const yShiftFromMiddleLine = 20;
// Dragover on target block little bit below the middle line.
const dragOverYCoord =
targetBlockRect.y +
(targetBlockRect.height / 2 + yShiftFromMiddleLine);
cy.get('[data-cy=editorjs]')
.get('div.ce-block')
.eq(2)
.trigger('dragover', {
clientX: targetBlockRect.x,
clientY: dragOverYCoord,
})
.then(($element) => {
// check for dragover top style on target element.
const classes = $element.attr('class').split(' ');
expect(classes).to.include('ce-block--drop-target');
expect(classes).to.include('ce-block--drop-target-bottom');
});
});
});
it('should drag the first block and drop after the last block.', function () {
cy.get('[data-cy=editorjs]')
.get('div.ce-block')
.first()
.click()
.click();
const dataTransfer = new DataTransfer();
// eslint-disable-next-line cypress/require-data-selectors
cy.get('.ce-toolbar__settings-btn')
.trigger('dragstart', { dataTransfer });
cy.get('[data-cy=editorjs]')
.get('div.ce-block')
.eq(2)
.trigger('dragenter')
.then((blocks) => {
// Get the target block rect.
const targetBlockRect = blocks[0].getBoundingClientRect();
const yShiftFromMiddleLine = 20;
// Dragover on target block little bit below the middle line.
const dragOverYCoord =
targetBlockRect.y +
(targetBlockRect.height / 2 + yShiftFromMiddleLine);
cy.get('[data-cy=editorjs]')
.get('div.ce-block')
.eq(2)
.trigger('dragover', {
clientX: targetBlockRect.x,
clientY: dragOverYCoord,
})
.trigger('drop', { dataTransfer });
});
// eslint-disable-next-line cypress/require-data-selectors
cy.get('.ce-toolbar__settings-btn')
.trigger('dragend', { dataTransfer });
cy.get<EditorJS>('@editorInstance')
.then(async (editor) => {
const { blocks } = await editor.save();
expect(blocks.length).to.eq(3); // 3 blocks are still here
expect(blocks[0].data.text).to.eq('Block 1');
expect(blocks[1].data.text).to.eq('Block 2');
expect(blocks[2].data.text).to.eq('Block 0');
});
});
it('should drag the last block and drop before the first block.', function () {
cy.get('[data-cy=editorjs]')
.get('div.ce-block')
.eq(2)
.click();
const dataTransfer = new DataTransfer();
// eslint-disable-next-line cypress/require-data-selectors
cy.get('.ce-toolbar__settings-btn')
.trigger('dragstart', { dataTransfer });
cy.get('[data-cy=editorjs]')
.get('div.ce-block')
.eq(0)
.trigger('dragenter')
.then((blocks) => {
// Get the target block rect.
const targetBlockRect = blocks[0].getBoundingClientRect();
const yShiftFromMiddleLine = -20;
// Dragover on target block little bit above the middle line.
const dragOverYCoord =
targetBlockRect.y +
(targetBlockRect.height / 2 + yShiftFromMiddleLine);
cy.get('[data-cy=editorjs]')
.get('div.ce-block')
.eq(0)
.trigger('dragover', {
clientX: targetBlockRect.x,
clientY: dragOverYCoord,
})
.trigger('drop', { dataTransfer });
});
// eslint-disable-next-line cypress/require-data-selectors
cy.get('.ce-toolbar__settings-btn')
.trigger('dragend', { dataTransfer });
cy.get<EditorJS>('@editorInstance')
.then(async (editor) => {
const { blocks } = await editor.save();
expect(blocks.length).to.eq(3); // 3 blocks are still here
expect(blocks[0].data.text).to.eq('Block 2');
expect(blocks[1].data.text).to.eq('Block 0');
expect(blocks[2].data.text).to.eq('Block 1');
});
});
it('should drag the first two block and drop after the last block.', function () {
cy.get('[data-cy=editorjs]')
.get('div.ce-block')
.eq(0)
.type('{selectall}')
.trigger('keydown', {
shiftKey: true,
keyCode: _.keyCodes.DOWN,
});
cy.get('[data-cy=editorjs]')
.get('div.ce-block')
.eq(1)
.trigger('mouseenter')
.trigger('mousemove')
.trigger('mouseleave');
const dataTransfer = new DataTransfer();
// eslint-disable-next-line cypress/require-data-selectors
cy.get('.ce-toolbar__settings-btn')
.trigger('dragstart', { dataTransfer });
cy.get('[data-cy=editorjs]')
.get('div.ce-block')
.eq(2)
.trigger('dragenter')
.then((blocks) => {
// Get the target block rect.
const targetBlockRect = blocks[0].getBoundingClientRect();
const yShiftFromMiddleLine = 20;
// Dragover on target block little bit below the middle line.
const dragOverYCoord =
targetBlockRect.y +
(targetBlockRect.height / 2 + yShiftFromMiddleLine);
cy.get('[data-cy=editorjs]')
.get('div.ce-block')
.eq(2)
.trigger('dragover', {
clientX: targetBlockRect.x,
clientY: dragOverYCoord,
})
.trigger('drop', { dataTransfer });
});
// eslint-disable-next-line cypress/require-data-selectors
cy.get('.ce-toolbar__settings-btn')
.trigger('dragend', { dataTransfer });
cy.get<EditorJS>('@editorInstance')
.then(async (editor) => {
const { blocks } = await editor.save();
expect(blocks.length).to.eq(3); // 3 blocks are still here
expect(blocks[0].data.text).to.eq('Block 2');
expect(blocks[1].data.text).to.eq('Block 0');
expect(blocks[2].data.text).to.eq('Block 1');
});
});
it('should drag the last two block and drop before the first block.', function () {
cy.get('[data-cy=editorjs]')
.get('div.ce-block')
.eq(1)
.type('{selectall}')
.trigger('keydown', {
shiftKey: true,
keyCode: _.keyCodes.DOWN,
});
cy.get('[data-cy=editorjs]')
.get('div.ce-block')
.eq(2)
.trigger('mouseenter')
.trigger('mousemove')
.trigger('mouseleave');
const dataTransfer = new DataTransfer();
// eslint-disable-next-line cypress/require-data-selectors
cy.get('.ce-toolbar__settings-btn')
.trigger('dragstart', { dataTransfer });
cy.get('[data-cy=editorjs]')
.get('div.ce-block')
.eq(0)
.trigger('dragenter')
.then((blocks) => {
// Get the target block rect.
const targetBlockRect = blocks[0].getBoundingClientRect();
const yShiftFromMiddleLine = -20;
// Dragover on target block little bit above the middle line.
const dragOverYCoord =
targetBlockRect.y +
(targetBlockRect.height / 2 + yShiftFromMiddleLine);
cy.get('[data-cy=editorjs]')
.get('div.ce-block')
.eq(0)
.trigger('dragover', {
clientX: targetBlockRect.x,
clientY: dragOverYCoord,
})
.trigger('drop', { dataTransfer });
});
// eslint-disable-next-line cypress/require-data-selectors
cy.get('.ce-toolbar__settings-btn')
.trigger('dragend', { dataTransfer });
cy.get<EditorJS>('@editorInstance')
.then(async (editor) => {
const { blocks } = await editor.save();
expect(blocks.length).to.eq(3); // 3 blocks are still here
expect(blocks[0].data.text).to.eq('Block 1');
expect(blocks[1].data.text).to.eq('Block 2');
expect(blocks[2].data.text).to.eq('Block 0');
});
});
});