diff --git a/package.json b/package.json index c77cfb452..b180e5fc8 100644 --- a/package.json +++ b/package.json @@ -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\"", diff --git a/src/components/block/index.ts b/src/components/block/index.ts index b47fe7811..688c5c375 100644 --- a/src/components/block/index.ts +++ b/src/components/block/index.ts @@ -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 { 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 { } /** - * 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; } /** diff --git a/src/components/modules/blockEvents.ts b/src/components/modules/blockEvents.ts index ee2d00c32..cf8fffe90 100644 --- a/src/components/modules/blockEvents.ts +++ b/src/components/modules/blockEvents.ts @@ -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 dragOver(event: DragEvent): void { - const block = this.Editor.BlockManager.getBlockByChildNode(event.target as Node); + 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); - block.dropTarget = true; + /** + * Clear previous drop target zone for every block. + */ + BlockManager.clearDropZonePosition(); } /** - * Remove drop target style + * All drag over on block. + * - Check the position of drag and suggest drop zone accordingly. * - * @param {DragEvent} event - drag leave event + * @param {DragEvent} event - drag over event */ - public dragLeave(event: DragEvent): void { + public dragOver(event: DragEvent): void { const block = this.Editor.BlockManager.getBlockByChildNode(event.target as Node); + const rect = block.holder.getBoundingClientRect(); - 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)(); } diff --git a/src/components/modules/blockManager.ts b/src/components/modules/blockManager.ts index 0f5a9de80..5bf8efed9 100644 --- a/src/components/modules/blockManager.ts +++ b/src/components/modules/blockManager.ts @@ -665,6 +665,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 @@ -904,12 +913,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) => { diff --git a/src/components/modules/blockSelection.ts b/src/components/modules/blockSelection.ts index c0e552a7e..25d8610df 100644 --- a/src/components/modules/blockSelection.ts +++ b/src/components/modules/blockSelection.ts @@ -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 diff --git a/src/components/modules/dragNDrop.ts b/src/components/modules/dragNDrop.ts index f1b3d3936..b219731cc 100644 --- a/src/components/modules/dragNDrop.ts +++ b/src/components/modules/dragNDrop.ts @@ -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; + + /** + * 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; - this.Editor.InlineToolbar.close(); + 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(); + } } diff --git a/src/components/modules/toolbar/index.ts b/src/components/modules/toolbar/index.ts index 6f822380a..284d3a858 100644 --- a/src/components/modules/toolbar/index.ts +++ b/src/components/modules/toolbar/index.ts @@ -161,7 +161,7 @@ export default class Toolbar extends Module { open: () => void; toggle: () => void; hasFocus: () => boolean | undefined; - } { + } { return { opened: this.toolboxInstance?.opened, close: () => { @@ -171,7 +171,7 @@ export default class Toolbar extends Module { /** * 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 { /** * 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 { /** * 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 { } 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 { */ 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 { /** * 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 { */ 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; } diff --git a/src/components/modules/ui.ts b/src/components/modules/ui.ts index 1378bd0cb..fb5e682b1 100644 --- a/src/components/modules/ui.ts +++ b/src/components/modules/ui.ts @@ -355,13 +355,6 @@ export default class UI extends Module { 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; } diff --git a/src/components/utils.ts b/src/components/utils.ts index 6e30817b1..f4a78f285 100644 --- a/src/components/utils.ts +++ b/src/components/utils.ts @@ -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 { /** * 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', + }); + } +} \ No newline at end of file diff --git a/src/styles/block.css b/src/styles/block.css index fb68133e4..ff24aae75 100644 --- a/src/styles/block.css +++ b/src/styles/block.css @@ -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%; } } diff --git a/test/cypress/tests/dragnDrop.cy.ts b/test/cypress/tests/dragnDrop.cy.ts new file mode 100644 index 000000000..f6a87cfc5 --- /dev/null +++ b/test/cypress/tests/dragnDrop.cy.ts @@ -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('@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('@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('@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('@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'); + }); + }); +});