From e2e57dcc0ac66ecc952a87141af063cfd04f52d1 Mon Sep 17 00:00:00 2001 From: Illia Sheremetov Date: Mon, 18 Sep 2023 11:01:42 +0200 Subject: [PATCH 01/12] Enable block drag and drop by default --- packages/ckeditor5-clipboard/src/clipboard.ts | 4 ++-- packages/ckeditor5-clipboard/tests/clipboard.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/ckeditor5-clipboard/src/clipboard.ts b/packages/ckeditor5-clipboard/src/clipboard.ts index 9a1ceb5f980..053e0ebdff4 100644 --- a/packages/ckeditor5-clipboard/src/clipboard.ts +++ b/packages/ckeditor5-clipboard/src/clipboard.ts @@ -10,7 +10,7 @@ import { Plugin } from '@ckeditor/ckeditor5-core'; import ClipboardPipeline from './clipboardpipeline'; -import DragDrop from './dragdrop'; +import DragDropExperimental from './dragdropexperimental'; import PastePlainText from './pasteplaintext'; /** @@ -35,6 +35,6 @@ export default class Clipboard extends Plugin { * @inheritDoc */ public static get requires() { - return [ ClipboardPipeline, DragDrop, PastePlainText ] as const; + return [ ClipboardPipeline, DragDropExperimental, PastePlainText ] as const; } } diff --git a/packages/ckeditor5-clipboard/tests/clipboard.js b/packages/ckeditor5-clipboard/tests/clipboard.js index ca4cb495601..7f9b888badc 100644 --- a/packages/ckeditor5-clipboard/tests/clipboard.js +++ b/packages/ckeditor5-clipboard/tests/clipboard.js @@ -5,12 +5,12 @@ import Clipboard from '../src/clipboard'; import ClipboardPipeline from '../src/clipboardpipeline'; -import DragDrop from '../src/dragdrop'; +import DragDropExperimental from '../src/dragdropexperimental'; import PastePlainText from '../src/pasteplaintext'; describe( 'Clipboard Feature', () => { it( 'requires ClipboardPipeline, DragDrop and PastePlainText', () => { - expect( Clipboard.requires ).to.deep.equal( [ ClipboardPipeline, DragDrop, PastePlainText ] ); + expect( Clipboard.requires ).to.deep.equal( [ ClipboardPipeline, DragDropExperimental, PastePlainText ] ); } ); it( 'has proper name', () => { From 0e310e87b526f3a91d1298e77e5c54ac5fe1d60b Mon Sep 17 00:00:00 2001 From: Illia Sheremetov Date: Mon, 18 Sep 2023 12:37:14 +0200 Subject: [PATCH 02/12] Provide 100% test coverage --- packages/ckeditor5-clipboard/tests/dragdrop.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/ckeditor5-clipboard/tests/dragdrop.js b/packages/ckeditor5-clipboard/tests/dragdrop.js index 33c84cd4fa3..b415202fd6e 100644 --- a/packages/ckeditor5-clipboard/tests/dragdrop.js +++ b/packages/ckeditor5-clipboard/tests/dragdrop.js @@ -1795,6 +1795,24 @@ describe( 'Drag and Drop', () => { expect( data.targetRanges[ 0 ].isEqual( view.createRangeOn( viewDocument.getRoot().getChild( 1 ) ) ) ).to.be.true; } ); } ); + + it( 'should not fire overrided clipboardInput event in drag and drop', done => { + const dataTransferMock = createDataTransfer( { 'text/html': '

x

', 'text/plain': 'y' } ); + const preventDefaultSpy = sinon.spy(); + + viewDocument.on( 'clipboardInput', ( evt, data ) => { + expect( preventDefaultSpy.calledOnce ).to.be.true; + expect( data.dataTransfer ).to.equal( dataTransferMock ); + expect( data.method ).to.equal( 'paste' ); + + done(); + } ); + + viewDocument.fire( 'paste', { + dataTransfer: dataTransferMock, + preventDefault: preventDefaultSpy + } ); + } ); } ); describe( 'integration with the WidgetToolbarRepository plugin', () => { From ff47d1dcc0962457e43fd624fe6a7c042a213832 Mon Sep 17 00:00:00 2001 From: Illia Sheremetov Date: Mon, 18 Sep 2023 12:51:48 +0200 Subject: [PATCH 03/12] Change name of test --- packages/ckeditor5-clipboard/tests/dragdrop.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-clipboard/tests/dragdrop.js b/packages/ckeditor5-clipboard/tests/dragdrop.js index b415202fd6e..4d4246b6141 100644 --- a/packages/ckeditor5-clipboard/tests/dragdrop.js +++ b/packages/ckeditor5-clipboard/tests/dragdrop.js @@ -1796,7 +1796,7 @@ describe( 'Drag and Drop', () => { } ); } ); - it( 'should not fire overrided clipboardInput event in drag and drop', done => { + it( 'should not use overrided clipboardInput event in drag and drop', done => { const dataTransferMock = createDataTransfer( { 'text/html': '

x

', 'text/plain': 'y' } ); const preventDefaultSpy = sinon.spy(); From 96895e7926c47223c6b454adb411e60c9f691516 Mon Sep 17 00:00:00 2001 From: Illia Sheremetov Date: Mon, 18 Sep 2023 13:15:06 +0200 Subject: [PATCH 04/12] Fix tests in image --- packages/ckeditor5-image/tests/image/imageblockediting.js | 3 ++- packages/ckeditor5-image/tests/image/imageinlineediting.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-image/tests/image/imageblockediting.js b/packages/ckeditor5-image/tests/image/imageblockediting.js index ddb0d4e66e1..75874459107 100644 --- a/packages/ckeditor5-image/tests/image/imageblockediting.js +++ b/packages/ckeditor5-image/tests/image/imageblockediting.js @@ -672,7 +672,8 @@ describe( 'ImageBlockEditing', () => { domTarget: domNode, target: viewElement, dataTransfer, - targetRanges: [ targetViewRange ] + targetRanges: [ targetViewRange ], + domEvent: sinon.spy() } ); expect( getModelData( model ) ).to.equal( diff --git a/packages/ckeditor5-image/tests/image/imageinlineediting.js b/packages/ckeditor5-image/tests/image/imageinlineediting.js index 9fdc7cb4c57..7f6334a302c 100644 --- a/packages/ckeditor5-image/tests/image/imageinlineediting.js +++ b/packages/ckeditor5-image/tests/image/imageinlineediting.js @@ -714,7 +714,8 @@ describe( 'ImageInlineEditing', () => { domTarget: domNode, target: viewElement, dataTransfer, - targetRanges: [ targetViewRange ] + targetRanges: [ targetViewRange ], + domEvent: sinon.spy() } ); expect( getModelData( model ) ).to.equal( From 03a3b81e36391da971f7703fb0e3a2f29b1ee5b8 Mon Sep 17 00:00:00 2001 From: Illia Sheremetov Date: Tue, 19 Sep 2023 16:25:49 +0200 Subject: [PATCH 05/12] rename dragdropexperimantal to dragdrop --- .../features/build-drag-drop-source.js | 6 +- .../docs/features/drag-drop.md | 12 +- packages/ckeditor5-clipboard/package.json | 1 - .../ckeditor5-clipboard/src/augmentation.ts | 2 - packages/ckeditor5-clipboard/src/clipboard.ts | 4 +- packages/ckeditor5-clipboard/src/dragdrop.ts | 495 ++-- .../src/dragdropexperimental.ts | 754 ----- packages/ckeditor5-clipboard/src/index.ts | 1 - .../ckeditor5-clipboard/tests/clipboard.js | 4 +- .../ckeditor5-clipboard/tests/dragdrop.js | 786 ++++-- .../tests/dragdropexperimental.js | 2507 ----------------- .../tests/dragdroptarget.js | 4 +- .../tests/manual/dragdrop.html | 409 ++- .../tests/manual/dragdrop.js | 513 ++-- .../tests/manual/dragdrop.md | 7 - .../tests/manual/dragdropexperimental.html | 287 -- .../tests/manual/dragdropexperimental.js | 343 --- .../tests/manual/dragdropexperimental.md | 0 18 files changed, 1380 insertions(+), 4755 deletions(-) delete mode 100644 packages/ckeditor5-clipboard/src/dragdropexperimental.ts delete mode 100644 packages/ckeditor5-clipboard/tests/dragdropexperimental.js delete mode 100644 packages/ckeditor5-clipboard/tests/manual/dragdropexperimental.html delete mode 100644 packages/ckeditor5-clipboard/tests/manual/dragdropexperimental.js delete mode 100644 packages/ckeditor5-clipboard/tests/manual/dragdropexperimental.md diff --git a/packages/ckeditor5-clipboard/docs/_snippets/features/build-drag-drop-source.js b/packages/ckeditor5-clipboard/docs/_snippets/features/build-drag-drop-source.js index 858b3f47fbc..1e085bbbb52 100644 --- a/packages/ckeditor5-clipboard/docs/_snippets/features/build-drag-drop-source.js +++ b/packages/ckeditor5-clipboard/docs/_snippets/features/build-drag-drop-source.js @@ -44,7 +44,7 @@ import { HorizontalLine } from '@ckeditor/ckeditor5-horizontal-line'; // import { - DragDropExperimental, + DragDrop, DragDropBlockToolbar } from '@ckeditor/ckeditor5-clipboard'; import { BlockToolbar } from '@ckeditor/ckeditor5-ui'; @@ -141,14 +141,14 @@ ClassicEditor.defaultConfig = defaultConfig; class ClassicEditorExperimental extends ClassicEditorBase {} ClassicEditorExperimental.builtinPlugins = [ ...defaultPlugins, - DragDropExperimental + DragDrop ]; ClassicEditorExperimental.defaultConfig = defaultConfig; class BalloonEditorExperimental extends BalloonEditorBase {} BalloonEditorExperimental.builtinPlugins = [ ...defaultPlugins, - DragDropExperimental, + DragDrop, DragDropBlockToolbar, BlockToolbar ]; diff --git a/packages/ckeditor5-clipboard/docs/features/drag-drop.md b/packages/ckeditor5-clipboard/docs/features/drag-drop.md index 0ff9722cf2c..a61218e99af 100644 --- a/packages/ckeditor5-clipboard/docs/features/drag-drop.md +++ b/packages/ckeditor5-clipboard/docs/features/drag-drop.md @@ -79,14 +79,14 @@ Select a block or blocks, and drag them across the document. You can place block #### Installation -To enable the block drag and drop in a classic editor, you need to add the {@link module:clipboard/dragdropexperimental~DragDropExperimental `DragDropExperimental`} module to your editor configuration: +To enable the block drag and drop in a classic editor, you need to add the {@link module:clipboard/dragdrop~DragDrop `DragDrop`} module to your editor configuration: ```js import { ClassicEditor } from '@ckeditor/ckeditor5-editor-classic'; -import { Clipboard, DragDropExperimental } from '@ckeditor/ckeditor5-clipboard'; +import { Clipboard, DragDrop } from '@ckeditor/ckeditor5-clipboard'; ClassicEditor.create( document.querySelector( '#editor' ), { - plugins: [ Clipboard, DragDropExperimental, /* ... */ ], + plugins: [ Clipboard, DragDrop, /* ... */ ], }) .then( /* ... */ ) .catch( /* ... */ ); @@ -100,12 +100,12 @@ In the balloon block editor, you can also drag content blocks using the drag han #### Installation -To enable the block drag and drop in a balloon block editor, you need to add the {@link module:clipboard/dragdropexperimental~DragDropExperimental `DragDropExperimental`} and the {@link module:clipboard/dragdropblocktoolbar~DragDropBlockToolbar `DragDropBlockToolbar`} modules to your editor configuration:: +To enable the block drag and drop in a balloon block editor, you need to add the {@link module:clipboard/dragdrop~DragDrop `DragDrop`} and the {@link module:clipboard/dragdropblocktoolbar~DragDropBlockToolbar `DragDropBlockToolbar`} modules to your editor configuration:: ```js import { BalloonEditor } from '@ckeditor/ckeditor5-editor-balloon'; import { - DragDropExperimental, + DragDrop, DragDropBlockToolbar, } from '@ckeditor/ckeditor5-clipboard'; import { BlockToolbar } from '@ckeditor/ckeditor5-ui'; @@ -113,7 +113,7 @@ import { BlockToolbar } from '@ckeditor/ckeditor5-ui'; BalloonEditor.create(document.querySelector( '#editor' ), { plugins: [ Clipboard, - DragDropExperimental, + DragDrop, DragDropBlockToolbar, BlockToolbar, /* ... */ diff --git a/packages/ckeditor5-clipboard/package.json b/packages/ckeditor5-clipboard/package.json index a3593152f1b..36979208c0d 100644 --- a/packages/ckeditor5-clipboard/package.json +++ b/packages/ckeditor5-clipboard/package.json @@ -47,7 +47,6 @@ "@ckeditor/ckeditor5-mention": "39.0.2", "@ckeditor/ckeditor5-page-break": "39.0.2", "@ckeditor/ckeditor5-paragraph": "39.0.2", - "@ckeditor/ckeditor5-paste-from-office": "39.0.2", "@ckeditor/ckeditor5-remove-format": "39.0.2", "@ckeditor/ckeditor5-source-editing": "39.0.2", "@ckeditor/ckeditor5-style": "39.0.2", diff --git a/packages/ckeditor5-clipboard/src/augmentation.ts b/packages/ckeditor5-clipboard/src/augmentation.ts index 4da8164cb9f..d3750fa9e90 100644 --- a/packages/ckeditor5-clipboard/src/augmentation.ts +++ b/packages/ckeditor5-clipboard/src/augmentation.ts @@ -8,7 +8,6 @@ import type { ClipboardPipeline, PastePlainText, DragDrop, - DragDropExperimental, DragDropTarget, DragDropBlockToolbar } from './index'; @@ -19,7 +18,6 @@ declare module '@ckeditor/ckeditor5-core' { [ ClipboardPipeline.pluginName ]: ClipboardPipeline; [ PastePlainText.pluginName ]: PastePlainText; [ DragDrop.pluginName ]: DragDrop; - [ DragDropExperimental.pluginName ]: DragDropExperimental; [ DragDropTarget.pluginName ]: DragDropTarget; [ DragDropBlockToolbar.pluginName ]: DragDropBlockToolbar; } diff --git a/packages/ckeditor5-clipboard/src/clipboard.ts b/packages/ckeditor5-clipboard/src/clipboard.ts index 053e0ebdff4..9a1ceb5f980 100644 --- a/packages/ckeditor5-clipboard/src/clipboard.ts +++ b/packages/ckeditor5-clipboard/src/clipboard.ts @@ -10,7 +10,7 @@ import { Plugin } from '@ckeditor/ckeditor5-core'; import ClipboardPipeline from './clipboardpipeline'; -import DragDropExperimental from './dragdropexperimental'; +import DragDrop from './dragdrop'; import PastePlainText from './pasteplaintext'; /** @@ -35,6 +35,6 @@ export default class Clipboard extends Plugin { * @inheritDoc */ public static get requires() { - return [ ClipboardPipeline, DragDropExperimental, PastePlainText ] as const; + return [ ClipboardPipeline, DragDrop, PastePlainText ] as const; } } diff --git a/packages/ckeditor5-clipboard/src/dragdrop.ts b/packages/ckeditor5-clipboard/src/dragdrop.ts index 32599d73cf8..08fefe04b47 100644 --- a/packages/ckeditor5-clipboard/src/dragdrop.ts +++ b/packages/ckeditor5-clipboard/src/dragdrop.ts @@ -7,32 +7,46 @@ * @module clipboard/dragdrop */ -import { Plugin, type Editor } from '@ckeditor/ckeditor5-core'; +import { Plugin } from '@ckeditor/ckeditor5-core'; import { LiveRange, MouseObserver, type DataTransfer, type Element, - type Position, + type Model, type Range, + type Position, type ViewDocumentMouseDownEvent, type ViewDocumentMouseUpEvent, type ViewElement, - type ViewRange + type DomEventData } from '@ckeditor/ckeditor5-engine'; -import { Widget, isWidget, type WidgetToolbarRepository } from '@ckeditor/ckeditor5-widget'; +import { + Widget, + isWidget, + type WidgetToolbarRepository +} from '@ckeditor/ckeditor5-widget'; import { env, uid, + global, + createElement, + DomEmitterMixin, delay, + Rect, type DelayedFunc, - type ObservableChangeEvent + type ObservableChangeEvent, + type DomEmitter } from '@ckeditor/ckeditor5-utils'; -import ClipboardPipeline, { type ClipboardContentInsertionEvent, type ViewDocumentClipboardOutputEvent } from './clipboardpipeline'; +import ClipboardPipeline, { + type ClipboardContentInsertionEvent, + type ViewDocumentClipboardOutputEvent +} from './clipboardpipeline'; + import ClipboardObserver, { type ViewDocumentDragEndEvent, type ViewDocumentDragEnterEvent, @@ -42,7 +56,7 @@ import ClipboardObserver, { type ViewDocumentClipboardInputEvent } from './clipboardobserver'; -import { throttle, type DebouncedFunc } from 'lodash-es'; +import DragDropTarget from './dragdroptarget'; import '../theme/clipboard.css'; @@ -128,6 +142,8 @@ import '../theme/clipboard.css'; * The drag and drop feature. It works on top of the {@link module:clipboard/clipboardpipeline~ClipboardPipeline}. * * Read more about the clipboard integration in the {@glink framework/deep-dive/clipboard clipboard deep-dive} guide. + * + * @internal */ export default class DragDrop extends Plugin { /** @@ -148,19 +164,26 @@ export default class DragDrop extends Plugin { private _draggableElement!: Element | null; /** - * A throttled callback updating the drop marker. + * A delayed callback removing draggable attributes. */ - private _updateDropMarkerThrottled!: DebouncedFunc<( targetRange: Range ) => void>; + private _clearDraggableAttributesDelayed: DelayedFunc<() => void> = delay( () => this._clearDraggableAttributes(), 40 ); /** - * A delayed callback removing the drop marker. + * Whether the dragged content can be dropped only in block context. */ - private _removeDropMarkerDelayed!: DelayedFunc<() => void>; + // TODO handle drag from other editor instance + // TODO configure to use block, inline or both + private _blockMode: boolean = false; /** - * A delayed callback removing draggable attributes. + * DOM Emitter. + */ + private _domEmitter: DomEmitter = new ( DomEmitterMixin() )(); + + /** + * The DOM element used to generate dragged preview image. */ - private _clearDraggableAttributesDelayed!: DelayedFunc<() => void>; + private _previewContainer?: HTMLElement; /** * @inheritDoc @@ -173,7 +196,7 @@ export default class DragDrop extends Plugin { * @inheritDoc */ public static get requires() { - return [ ClipboardPipeline, Widget ] as const; + return [ ClipboardPipeline, Widget, DragDropTarget ] as const; } /** @@ -186,15 +209,6 @@ export default class DragDrop extends Plugin { this._draggedRange = null; this._draggingUid = ''; this._draggableElement = null; - this._updateDropMarkerThrottled = throttle( targetRange => this._updateDropMarker( targetRange ), 40 ); - this._removeDropMarkerDelayed = delay( () => this._removeDropMarker(), 40 ); - this._clearDraggableAttributesDelayed = delay( () => this._clearDraggableAttributes(), 40 ); - - if ( editor.plugins.has( 'DragDropExperimental' ) ) { - this.forceDisabled( 'DragDropExperimental' ); - - return; - } view.addObserver( ClipboardObserver ); view.addObserver( MouseObserver ); @@ -202,7 +216,6 @@ export default class DragDrop extends Plugin { this._setupDragging(); this._setupContentInsertionIntegration(); this._setupClipboardInputIntegration(); - this._setupDropMarker(); this._setupDraggableAttributeHandling(); this.listenTo>( editor, 'change:isReadOnly', ( evt, name, isReadOnly ) => { @@ -233,8 +246,11 @@ export default class DragDrop extends Plugin { this._draggedRange = null; } - this._updateDropMarkerThrottled.cancel(); - this._removeDropMarkerDelayed.cancel(); + if ( this._previewContainer ) { + this._previewContainer.remove(); + } + + this._domEmitter.stopListening(); this._clearDraggableAttributesDelayed.cancel(); return super.destroy(); @@ -246,14 +262,12 @@ export default class DragDrop extends Plugin { private _setupDragging(): void { const editor = this.editor; const model = editor.model; - const modelDocument = model.document; const view = editor.editing.view; const viewDocument = view.document; + const dragDropTarget = editor.plugins.get( DragDropTarget ); // The handler for the drag start; it is responsible for setting data transfer object. this.listenTo( viewDocument, 'dragstart', ( evt, data ) => { - const selection = modelDocument.selection; - // Don't drag the editable element itself. if ( data.target && data.target.is( 'editableElement' ) ) { data.preventDefault(); @@ -261,34 +275,7 @@ export default class DragDrop extends Plugin { return; } - // TODO we could clone this node somewhere and style it to match editing view but without handles, - // selection outline, WTA buttons, etc. - // data.dataTransfer._native.setDragImage( data.domTarget, 0, 0 ); - - // Check if this is dragstart over the widget (but not a nested editable). - const draggableWidget = data.target ? findDraggableWidget( data.target ) : null; - - if ( draggableWidget ) { - const modelElement = editor.editing.mapper.toModelElement( draggableWidget )!; - - this._draggedRange = LiveRange.fromRange( model.createRangeOn( modelElement ) ); - - // Disable toolbars so they won't obscure the drop area. - if ( editor.plugins.has( 'WidgetToolbarRepository' ) ) { - const widgetToolbarRepository: WidgetToolbarRepository = editor.plugins.get( 'WidgetToolbarRepository' ); - - widgetToolbarRepository.forceDisabled( 'dragDrop' ); - } - } - - // If this was not a widget we should check if we need to drag some text content. - else if ( !viewDocument.selection.isCollapsed ) { - const selectedElement = viewDocument.selection.getSelectedElement(); - - if ( !selectedElement || !isWidget( selectedElement ) ) { - this._draggedRange = LiveRange.fromRange( selection.getFirstRange()! ); - } - } + this._prepareDraggedRange( data.target ); if ( !this._draggedRange ) { data.preventDefault(); @@ -298,9 +285,7 @@ export default class DragDrop extends Plugin { this._draggingUid = uid(); - const canEditAtDraggedRange = this.isEnabled && editor.model.canEditAt( this._draggedRange ); - - data.dataTransfer.effectAllowed = canEditAtDraggedRange ? 'copyMove' : 'copy'; + data.dataTransfer.effectAllowed = this.isEnabled ? 'copyMove' : 'copy'; data.dataTransfer.setData( 'application/ckeditor5-dragging-uid', this._draggingUid ); const draggedSelection = model.createSelection( this._draggedRange.toRange() ); @@ -312,7 +297,14 @@ export default class DragDrop extends Plugin { method: 'dragstart' } ); - if ( !canEditAtDraggedRange ) { + const { dataTransfer, domTarget, domEvent } = data; + const { clientX } = domEvent; + + this._updatePreview( { dataTransfer, domTarget, clientX } ); + + data.stopPropagation(); + + if ( !this.isEnabled ) { this._draggedRange.detach(); this._draggedRange = null; this._draggingUid = ''; @@ -326,6 +318,11 @@ export default class DragDrop extends Plugin { this._finalizeDragging( !data.dataTransfer.isCanceled && data.dataTransfer.dropEffect == 'move' ); }, { priority: 'low' } ); + // Reset block dragging mode even if dropped outside the editable. + this._domEmitter.listenTo( global.document, 'dragend', () => { + this._blockMode = false; + }, { useCapture: true } ); + // Dragging over the editable. this.listenTo( viewDocument, 'dragenter', () => { if ( !this.isEnabled ) { @@ -339,7 +336,7 @@ export default class DragDrop extends Plugin { this.listenTo( viewDocument, 'dragleave', () => { // We do not know if the mouse left the editor or just some element in it, so let us wait a few milliseconds // to check if 'dragover' is not fired. - this._removeDropMarkerDelayed(); + dragDropTarget.removeDropMarkerDelayed(); } ); // Handler for moving dragged content over the target area. @@ -350,16 +347,9 @@ export default class DragDrop extends Plugin { return; } - this._removeDropMarkerDelayed.cancel(); - - const targetRange = findDropTargetRange( editor, data.targetRanges, data.target ); + const { clientX, clientY } = ( data as DomEventData ).domEvent; - // Do not drop if target place is not editable. - if ( !editor.model.canEditAt( targetRange ) ) { - data.dataTransfer.dropEffect = 'none'; - - return; - } + dragDropTarget.updateDropMarker( data.target, data.targetRanges, clientX, clientY, this._blockMode ); // If this is content being dragged from another editor, moving out of current editor instance // is not possible until 'dragend' event case will be fixed. @@ -376,10 +366,7 @@ export default class DragDrop extends Plugin { } } - /* istanbul ignore else -- @preserve */ - if ( targetRange ) { - this._updateDropMarkerThrottled( targetRange ); - } + evt.stop(); }, { priority: 'low' } ); } @@ -390,6 +377,7 @@ export default class DragDrop extends Plugin { const editor = this.editor; const view = editor.editing.view; const viewDocument = view.document; + const dragDropTarget = editor.plugins.get( DragDropTarget ); // Update the event target ranges and abort dropping if dropping over itself. this.listenTo( viewDocument, 'clipboardInput', ( evt, data ) => { @@ -397,14 +385,10 @@ export default class DragDrop extends Plugin { return; } - const targetRange = findDropTargetRange( editor, data.targetRanges, data.target ); + const { clientX, clientY } = ( data as DomEventData ).domEvent; + const targetRange = dragDropTarget.getFinalDropRange( data.target, data.targetRanges, clientX, clientY, this._blockMode ); - // The dragging markers must be removed after searching for the target range because sometimes - // the target lands on the marker itself. - this._removeDropMarker(); - - /* istanbul ignore if -- @preserve */ - if ( !targetRange || !editor.model.canEditAt( targetRange ) ) { + if ( !targetRange ) { this._finalizeDragging( false ); evt.stop(); @@ -499,15 +483,11 @@ export default class DragDrop extends Plugin { // In Firefox this is not needed. In Safari it makes the whole editable draggable (not just textual content). // Disabled in read-only mode because draggable="true" + contenteditable="false" results // in not firing selectionchange event ever, which makes the selection stuck in read-only mode. - if ( env.isBlink && !draggableElement && !viewDocument.selection.isCollapsed ) { + if ( env.isBlink && !editor.isReadOnly && !draggableElement && !viewDocument.selection.isCollapsed ) { const selectedElement = viewDocument.selection.getSelectedElement(); if ( !selectedElement || !isWidget( selectedElement ) ) { - const editableElement = viewDocument.selection.editableElement; - - if ( editableElement && !editableElement.isReadOnly ) { - draggableElement = editableElement; - } + draggableElement = viewDocument.selection.editableElement; } } @@ -545,82 +525,6 @@ export default class DragDrop extends Plugin { } ); } - /** - * Creates downcast conversion for the drop target marker. - */ - private _setupDropMarker(): void { - const editor = this.editor; - - // Drop marker conversion for hovering over widgets. - editor.conversion.for( 'editingDowncast' ).markerToHighlight( { - model: 'drop-target', - view: { - classes: [ 'ck-clipboard-drop-target-range' ] - } - } ); - - // Drop marker conversion for in text drop target. - editor.conversion.for( 'editingDowncast' ).markerToElement( { - model: 'drop-target', - view: ( data, { writer } ) => { - const inText = editor.model.schema.checkChild( data.markerRange.start, '$text' ); - - if ( !inText ) { - return; - } - - return writer.createUIElement( 'span', { class: 'ck ck-clipboard-drop-target-position' }, function( domDocument ) { - const domElement = this.toDomElement( domDocument ); - - // Using word joiner to make this marker as high as text and also making text not break on marker. - domElement.append( '\u2060', domDocument.createElement( 'span' ), '\u2060' ); - - return domElement; - } ); - } - } ); - } - - /** - * Updates the drop target marker to the provided range. - * - * @param targetRange The range to set the marker to. - */ - private _updateDropMarker( targetRange: Range ): void { - const editor = this.editor; - const markers = editor.model.markers; - - editor.model.change( writer => { - if ( markers.has( 'drop-target' ) ) { - if ( !markers.get( 'drop-target' )!.getRange().isEqual( targetRange ) ) { - writer.updateMarker( 'drop-target', { range: targetRange } ); - } - } else { - writer.addMarker( 'drop-target', { - range: targetRange, - usingOperation: false, - affectsData: false - } ); - } - } ); - } - - /** - * Removes the drop target marker. - */ - private _removeDropMarker(): void { - const model = this.editor.model; - - this._removeDropMarkerDelayed.cancel(); - this._updateDropMarkerThrottled.cancel(); - - if ( model.markers.has( 'drop-target' ) ) { - model.change( writer => { - writer.removeMarker( 'drop-target' ); - } ); - } - } - /** * Deletes the dragged content from its original range and clears the dragging state. * @@ -629,8 +533,9 @@ export default class DragDrop extends Plugin { private _finalizeDragging( moved: boolean ): void { const editor = this.editor; const model = editor.model; + const dragDropTarget = editor.plugins.get( DragDropTarget ); - this._removeDropMarker(); + dragDropTarget.removeDropMarker(); this._clearDraggableAttributes(); if ( editor.plugins.has( 'WidgetToolbarRepository' ) ) { @@ -641,6 +546,11 @@ export default class DragDrop extends Plugin { this._draggingUid = ''; + if ( this._previewContainer ) { + this._previewContainer.remove(); + this._previewContainer = undefined; + } + if ( !this._draggedRange ) { return; } @@ -653,171 +563,112 @@ export default class DragDrop extends Plugin { this._draggedRange.detach(); this._draggedRange = null; } -} - -/** - * Returns fixed selection range for given position and target element. - */ -function findDropTargetRange( editor: Editor, targetViewRanges: Array | null, targetViewElement: ViewElement ): Range | null { - const model = editor.model; - const mapper = editor.editing.mapper; - - let range: Range | null = null; - - const targetViewPosition = targetViewRanges ? targetViewRanges[ 0 ].start : null; - - // A UIElement is not a valid drop element, use parent (this could be a drop marker or any other UIElement). - if ( targetViewElement.is( 'uiElement' ) ) { - targetViewElement = targetViewElement.parent as ViewElement; - } - - // Quick win if the target is a widget (but not a nested editable). - range = findDropTargetRangeOnWidget( editor, targetViewElement ); - - if ( range ) { - return range; - } - - // The easiest part is over, now we need to move to the model space. - // Find target model element and position. - const targetModelElement = getClosestMappedModelElement( editor, targetViewElement ); - const targetModelPosition = targetViewPosition ? mapper.toModelPosition( targetViewPosition ) : null; + /** + * Sets the dragged source range based on event target and document selection. + */ + private _prepareDraggedRange( target: ViewElement ): void { + const editor = this.editor; + const model = editor.model; + const selection = model.document.selection; - // There is no target position while hovering over an empty table cell. - // In Safari, target position can be empty while hovering over a widget (e.g., a page-break). - // Find the drop position inside the element. - if ( !targetModelPosition ) { - return findDropTargetRangeInElement( editor, targetModelElement ); - } + // Check if this is dragstart over the widget (but not a nested editable). + const draggableWidget = target ? findDraggableWidget( target ) : null; - // Check if target position is between blocks and adjust drop position to the next object. - // This is because while hovering over a root element next to a widget the target position can jump in crazy places. - range = findDropTargetRangeBetweenBlocks( editor, targetModelPosition, targetModelElement ); + if ( draggableWidget ) { + const modelElement = editor.editing.mapper.toModelElement( draggableWidget )!; - if ( range ) { - return range; - } + this._draggedRange = LiveRange.fromRange( model.createRangeOn( modelElement ) ); + this._blockMode = model.schema.isBlock( modelElement ); - // Try fixing selection position. - // In Firefox, the target position lands before widgets but in other browsers it tends to land after a widget. - range = model.schema.getNearestSelectionRange( targetModelPosition, env.isGecko ? 'forward' : 'backward' ); + // Disable toolbars so they won't obscure the drop area. + if ( editor.plugins.has( 'WidgetToolbarRepository' ) ) { + const widgetToolbarRepository: WidgetToolbarRepository = editor.plugins.get( 'WidgetToolbarRepository' ); - if ( range ) { - return range; - } + widgetToolbarRepository.forceDisabled( 'dragDrop' ); + } - // There is no valid selection position inside the current limit element so find a closest object ancestor. - // This happens if the model position lands directly in the element itself (view target element was a `' + - '
` - // so a nested editable, but view target position was directly in the `
` element). - return findDropTargetRangeOnAncestorObject( editor, targetModelPosition.parent as Element ); -} + return; + } -/** - * Returns fixed selection range for a given position and a target element if it is over the widget but not over its nested editable. - */ -function findDropTargetRangeOnWidget( editor: Editor, targetViewElement: ViewElement ): Range | null { - const model = editor.model; - const mapper = editor.editing.mapper; + // If this was not a widget we should check if we need to drag some text content. + if ( selection.isCollapsed && !( selection.getFirstPosition()!.parent as Element ).isEmpty ) { + return; + } - // Quick win if the target is a widget. - if ( isWidget( targetViewElement ) ) { - return model.createRangeOn( mapper.toModelElement( targetViewElement )! ); - } + const blocks = Array.from( selection.getSelectedBlocks() ); + const draggedRange = selection.getFirstRange()!; - // Check if we are deeper over a widget (but not over a nested editable). - if ( !targetViewElement.is( 'editableElement' ) ) { - // Find a closest ancestor that is either a widget or an editable element... - const ancestor = targetViewElement.findAncestor( node => isWidget( node ) || node.is( 'editableElement' ) )!; + if ( blocks.length == 0 ) { + this._draggedRange = LiveRange.fromRange( draggedRange ); - // ...and if the widget was closer then it is a drop target. - if ( isWidget( ancestor ) ) { - return model.createRangeOn( mapper.toModelElement( ancestor! )! ); + return; } - } - return null; -} + const blockRange = getRangeIncludingFullySelectedParents( model, blocks ); -/** - * Returns fixed selection range inside a model element. - */ -function findDropTargetRangeInElement( editor: Editor, targetModelElement: Element ): Range | null { - const model = editor.model; - const schema = model.schema; - - const positionAtElementStart = model.createPositionAt( targetModelElement, 0 ); + if ( blocks.length > 1 ) { + this._draggedRange = LiveRange.fromRange( blockRange ); + this._blockMode = true; + // TODO block mode for dragging from outside editor? or inline? or both? + } else if ( blocks.length == 1 ) { + const touchesBlockEdges = draggedRange.start.isTouching( blockRange.start ) && + draggedRange.end.isTouching( blockRange.end ); - return schema.getNearestSelectionRange( positionAtElementStart, 'forward' ); -} - -/** - * Returns fixed selection range for a given position and a target element if the drop is between blocks. - */ -function findDropTargetRangeBetweenBlocks( editor: Editor, targetModelPosition: Position, targetModelElement: Element ): Range | null { - const model = editor.model; + this._draggedRange = LiveRange.fromRange( touchesBlockEdges ? blockRange : draggedRange ); + this._blockMode = touchesBlockEdges; + } - // Check if target is between blocks. - if ( !model.schema.checkChild( targetModelElement, '$block' ) ) { - return null; + model.change( writer => writer.setSelection( this._draggedRange!.toRange() ) ); } - // Find position between blocks. - const positionAtElementStart = model.createPositionAt( targetModelElement, 0 ); - - // Get the common part of the path (inside the target element and the target position). - const commonPath = targetModelPosition.path.slice( 0, positionAtElementStart.path.length ); - - // Position between the blocks. - const betweenBlocksPosition = model.createPositionFromPath( targetModelPosition.root, commonPath ); - const nodeAfter = betweenBlocksPosition.nodeAfter; - - // Adjust drop position to the next object. - // This is because while hovering over a root element next to a widget the target position can jump in crazy places. - if ( nodeAfter && model.schema.isObject( nodeAfter ) ) { - return model.createRangeOn( nodeAfter ); - } + /** + * Updates the dragged preview image. + */ + private _updatePreview( { + dataTransfer, + domTarget, + clientX + }: { + dataTransfer: DataTransfer; + domTarget: HTMLElement; + clientX: number; + } ): void { + const view = this.editor.editing.view; + const editable = view.document.selection.editableElement!; + const domEditable = view.domConverter.mapViewToDom( editable )!; + const computedStyle = global.window.getComputedStyle( domEditable ); + + if ( !this._previewContainer ) { + this._previewContainer = createElement( global.document, 'div', { + style: 'position: fixed; left: -999999px;' + } ); - return null; -} + global.document.body.appendChild( this._previewContainer ); + } else if ( this._previewContainer.firstElementChild ) { + this._previewContainer.removeChild( this._previewContainer.firstElementChild ); + } -/** - * Returns a selection range on the ancestor object. - */ -function findDropTargetRangeOnAncestorObject( editor: Editor, element: Element ): Range | null { - const model = editor.model; - let currentElement: Element | null = element; + const domRect = new Rect( domEditable ); - while ( currentElement ) { - if ( model.schema.isObject( currentElement ) ) { - return model.createRangeOn( currentElement ); + // If domTarget is inside the editable root, browsers will display the preview correctly by themselves. + if ( domEditable.contains( domTarget ) ) { + return; } - currentElement = currentElement.parent as Element | null; - } + const domEditablePaddingLeft = parseFloat( computedStyle.paddingLeft ); + const preview = createElement( global.document, 'div' ); - /* istanbul ignore next -- @preserve */ - return null; -} + preview.className = 'ck ck-content'; + preview.style.width = computedStyle.width; + preview.style.paddingLeft = `${ domRect.left - clientX + domEditablePaddingLeft }px`; -/** - * Returns the closest model element for the specified view element. - */ -function getClosestMappedModelElement( editor: Editor, element: ViewElement ): Element { - const mapper = editor.editing.mapper; - const view = editor.editing.view; + preview.innerHTML = dataTransfer.getData( 'text/html' ); - const targetModelElement = mapper.toModelElement( element ); + dataTransfer.setDragImage( preview, 0, 0 ); - if ( targetModelElement ) { - return targetModelElement; + this._previewContainer.appendChild( preview ); } - - // Find mapped ancestor if the target is inside not mapped element (for example inline code element). - const viewPosition = view.createPositionBefore( element ); - const viewElement = mapper.findMappedViewAncestor( viewPosition ); - - return mapper.toModelElement( viewElement )!; } /** @@ -861,3 +712,43 @@ function findDraggableWidget( target: ViewElement ): ViewElement | null { return null; } + +/** + * Recursively checks if common parent of provided elements doesn't have any other children. If that's the case, + * it returns range including this parent. Otherwise, it returns only the range from first to last element. + * + * Example: + * + *
+ * [Test 1 + * Test 2 + * Test 3] + *
+ * + * Because all elements inside the `blockQuote` are selected, the range is extended to include the `blockQuote` too. + * If only first and second paragraphs would be selected, the range would not include it. + */ +function getRangeIncludingFullySelectedParents( model: Model, elements: Array ): Range { + const firstElement = elements[ 0 ]; + const lastElement = elements[ elements.length - 1 ]; + const parent = firstElement.getCommonAncestor( lastElement ); + const startPosition: Position = model.createPositionBefore( firstElement ); + const endPosition: Position = model.createPositionAfter( lastElement ); + + if ( + parent && + parent.is( 'element' ) && + !model.schema.isLimit( parent ) + ) { + const parentRange = model.createRangeOn( parent ); + const touchesStart = startPosition.isTouching( parentRange.start ); + const touchesEnd = endPosition.isTouching( parentRange.end ); + + if ( touchesStart && touchesEnd ) { + // Selection includes all elements in the parent. + return getRangeIncludingFullySelectedParents( model, [ parent ] ); + } + } + + return model.createRange( startPosition, endPosition ); +} diff --git a/packages/ckeditor5-clipboard/src/dragdropexperimental.ts b/packages/ckeditor5-clipboard/src/dragdropexperimental.ts deleted file mode 100644 index f565bd7654e..00000000000 --- a/packages/ckeditor5-clipboard/src/dragdropexperimental.ts +++ /dev/null @@ -1,754 +0,0 @@ -/** - * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license - */ - -/** - * @module clipboard/dragdropexperimental - */ - -import { Plugin } from '@ckeditor/ckeditor5-core'; - -import { - LiveRange, - MouseObserver, - type DataTransfer, - type Element, - type Model, - type Range, - type Position, - type ViewDocumentMouseDownEvent, - type ViewDocumentMouseUpEvent, - type ViewElement, - type DomEventData -} from '@ckeditor/ckeditor5-engine'; - -import { - Widget, - isWidget, - type WidgetToolbarRepository -} from '@ckeditor/ckeditor5-widget'; - -import { - env, - uid, - global, - createElement, - DomEmitterMixin, - delay, - Rect, - type DelayedFunc, - type ObservableChangeEvent, - type DomEmitter -} from '@ckeditor/ckeditor5-utils'; - -import ClipboardPipeline, { - type ClipboardContentInsertionEvent, - type ViewDocumentClipboardOutputEvent -} from './clipboardpipeline'; - -import ClipboardObserver, { - type ViewDocumentDragEndEvent, - type ViewDocumentDragEnterEvent, - type ViewDocumentDraggingEvent, - type ViewDocumentDragLeaveEvent, - type ViewDocumentDragStartEvent, - type ViewDocumentClipboardInputEvent -} from './clipboardobserver'; - -import DragDropTarget from './dragdroptarget'; - -import '../theme/clipboard.css'; - -// Drag and drop events overview: -// -// ┌──────────────────┐ -// │ mousedown │ Sets the draggable attribute. -// └─────────┬────────┘ -// │ -// └─────────────────────┐ -// │ │ -// │ ┌─────────V────────┐ -// │ │ mouseup │ Dragging did not start, removes the draggable attribute. -// │ └──────────────────┘ -// │ -// ┌─────────V────────┐ Retrieves the selected model.DocumentFragment -// │ dragstart │ and converts it to view.DocumentFragment. -// └─────────┬────────┘ -// │ -// ┌─────────V────────┐ Processes view.DocumentFragment to text/html and text/plain -// │ clipboardOutput │ and stores the results in data.dataTransfer. -// └─────────┬────────┘ -// │ -// │ DOM dragover -// ┌────────────┐ -// │ │ -// ┌─────────V────────┐ │ -// │ dragging │ │ Updates the drop target marker. -// └─────────┬────────┘ │ -// │ │ -// ┌─────────────└────────────┘ -// │ │ │ -// │ ┌─────────V────────┐ │ -// │ │ dragleave │ │ Removes the drop target marker. -// │ └─────────┬────────┘ │ -// │ │ │ -// ┌───│─────────────┘ │ -// │ │ │ │ -// │ │ ┌─────────V────────┐ │ -// │ │ │ dragenter │ │ Focuses the editor view. -// │ │ └─────────┬────────┘ │ -// │ │ │ │ -// │ │ └────────────┘ -// │ │ -// │ └─────────────┐ -// │ │ │ -// │ │ ┌─────────V────────┐ -// └───┐ │ drop │ (The default handler of the clipboard pipeline). -// │ └─────────┬────────┘ -// │ │ -// │ ┌─────────V────────┐ Resolves the final data.targetRanges. -// │ │ clipboardInput │ Aborts if dropping on dragged content. -// │ └─────────┬────────┘ -// │ │ -// │ ┌─────────V────────┐ -// │ │ clipboardInput │ (The default handler of the clipboard pipeline). -// │ └─────────┬────────┘ -// │ │ -// │ ┌───────────V───────────┐ -// │ │ inputTransformation │ (The default handler of the clipboard pipeline). -// │ └───────────┬───────────┘ -// │ │ -// │ ┌──────────V──────────┐ -// │ │ contentInsertion │ Updates the document selection to drop range. -// │ └──────────┬──────────┘ -// │ │ -// │ ┌──────────V──────────┐ -// │ │ contentInsertion │ (The default handler of the clipboard pipeline). -// │ └──────────┬──────────┘ -// │ │ -// │ ┌──────────V──────────┐ -// │ │ contentInsertion │ Removes the content from the original range if the insertion was successful. -// │ └──────────┬──────────┘ -// │ │ -// └─────────────┐ -// │ -// ┌─────────V────────┐ -// │ dragend │ Removes the drop marker and cleans the state. -// └──────────────────┘ -// - -/** - * The drag and drop feature. It works on top of the {@link module:clipboard/clipboardpipeline~ClipboardPipeline}. - * - * Read more about the clipboard integration in the {@glink framework/deep-dive/clipboard clipboard deep-dive} guide. - * - * @internal - */ -export default class DragDropExperimental extends Plugin { - /** - * The live range over the original content that is being dragged. - */ - private _draggedRange!: LiveRange | null; - - /** - * The UID of current dragging that is used to verify if the drop started in the same editor as the drag start. - * - * **Note**: This is a workaround for broken 'dragend' events (they are not fired if the source text node got removed). - */ - private _draggingUid!: string; - - /** - * The reference to the model element that currently has a `draggable` attribute set (it is set while dragging). - */ - private _draggableElement!: Element | null; - - /** - * A delayed callback removing draggable attributes. - */ - private _clearDraggableAttributesDelayed: DelayedFunc<() => void> = delay( () => this._clearDraggableAttributes(), 40 ); - - /** - * Whether the dragged content can be dropped only in block context. - */ - // TODO handle drag from other editor instance - // TODO configure to use block, inline or both - private _blockMode: boolean = false; - - /** - * DOM Emitter. - */ - private _domEmitter: DomEmitter = new ( DomEmitterMixin() )(); - - /** - * The DOM element used to generate dragged preview image. - */ - private _previewContainer?: HTMLElement; - - /** - * @inheritDoc - */ - public static get pluginName() { - return 'DragDropExperimental' as const; - } - - /** - * @inheritDoc - */ - public static get requires() { - return [ ClipboardPipeline, Widget, DragDropTarget ] as const; - } - - /** - * @inheritDoc - */ - public init(): void { - const editor = this.editor; - const view = editor.editing.view; - - this._draggedRange = null; - this._draggingUid = ''; - this._draggableElement = null; - - view.addObserver( ClipboardObserver ); - view.addObserver( MouseObserver ); - - this._setupDragging(); - this._setupContentInsertionIntegration(); - this._setupClipboardInputIntegration(); - this._setupDraggableAttributeHandling(); - - this.listenTo>( editor, 'change:isReadOnly', ( evt, name, isReadOnly ) => { - if ( isReadOnly ) { - this.forceDisabled( 'readOnlyMode' ); - } else { - this.clearForceDisabled( 'readOnlyMode' ); - } - } ); - - this.on>( 'change:isEnabled', ( evt, name, isEnabled ) => { - if ( !isEnabled ) { - this._finalizeDragging( false ); - } - } ); - - if ( env.isAndroid ) { - this.forceDisabled( 'noAndroidSupport' ); - } - } - - /** - * @inheritDoc - */ - public override destroy(): void { - if ( this._draggedRange ) { - this._draggedRange.detach(); - this._draggedRange = null; - } - - if ( this._previewContainer ) { - this._previewContainer.remove(); - } - - this._domEmitter.stopListening(); - this._clearDraggableAttributesDelayed.cancel(); - - return super.destroy(); - } - - /** - * Drag and drop events handling. - */ - private _setupDragging(): void { - const editor = this.editor; - const model = editor.model; - const view = editor.editing.view; - const viewDocument = view.document; - const dragDropTarget = editor.plugins.get( DragDropTarget ); - - // The handler for the drag start; it is responsible for setting data transfer object. - this.listenTo( viewDocument, 'dragstart', ( evt, data ) => { - // Don't drag the editable element itself. - if ( data.target && data.target.is( 'editableElement' ) ) { - data.preventDefault(); - - return; - } - - this._prepareDraggedRange( data.target ); - - if ( !this._draggedRange ) { - data.preventDefault(); - - return; - } - - this._draggingUid = uid(); - - data.dataTransfer.effectAllowed = this.isEnabled ? 'copyMove' : 'copy'; - data.dataTransfer.setData( 'application/ckeditor5-dragging-uid', this._draggingUid ); - - const draggedSelection = model.createSelection( this._draggedRange.toRange() ); - const content = editor.data.toView( model.getSelectedContent( draggedSelection ) ); - - viewDocument.fire( 'clipboardOutput', { - dataTransfer: data.dataTransfer, - content, - method: 'dragstart' - } ); - - const { dataTransfer, domTarget, domEvent } = data; - const { clientX } = domEvent; - - this._updatePreview( { dataTransfer, domTarget, clientX } ); - - data.stopPropagation(); - - if ( !this.isEnabled ) { - this._draggedRange.detach(); - this._draggedRange = null; - this._draggingUid = ''; - } - }, { priority: 'low' } ); - - // The handler for finalizing drag and drop. It should always be triggered after dragging completes - // even if it was completed in a different application. - // Note: This is not fired if source text node got removed while downcasting a marker. - this.listenTo( viewDocument, 'dragend', ( evt, data ) => { - this._finalizeDragging( !data.dataTransfer.isCanceled && data.dataTransfer.dropEffect == 'move' ); - }, { priority: 'low' } ); - - // Reset block dragging mode even if dropped outside the editable. - this._domEmitter.listenTo( global.document, 'dragend', () => { - this._blockMode = false; - }, { useCapture: true } ); - - // Dragging over the editable. - this.listenTo( viewDocument, 'dragenter', () => { - if ( !this.isEnabled ) { - return; - } - - view.focus(); - } ); - - // Dragging out of the editable. - this.listenTo( viewDocument, 'dragleave', () => { - // We do not know if the mouse left the editor or just some element in it, so let us wait a few milliseconds - // to check if 'dragover' is not fired. - dragDropTarget.removeDropMarkerDelayed(); - } ); - - // Handler for moving dragged content over the target area. - this.listenTo( viewDocument, 'dragging', ( evt, data ) => { - if ( !this.isEnabled ) { - data.dataTransfer.dropEffect = 'none'; - - return; - } - - const { clientX, clientY } = ( data as DomEventData ).domEvent; - - dragDropTarget.updateDropMarker( data.target, data.targetRanges, clientX, clientY, this._blockMode ); - - // If this is content being dragged from another editor, moving out of current editor instance - // is not possible until 'dragend' event case will be fixed. - if ( !this._draggedRange ) { - data.dataTransfer.dropEffect = 'copy'; - } - - // In Firefox it is already set and effect allowed remains the same as originally set. - if ( !env.isGecko ) { - if ( data.dataTransfer.effectAllowed == 'copy' ) { - data.dataTransfer.dropEffect = 'copy'; - } else if ( [ 'all', 'copyMove' ].includes( data.dataTransfer.effectAllowed ) ) { - data.dataTransfer.dropEffect = 'move'; - } - } - - evt.stop(); - }, { priority: 'low' } ); - } - - /** - * Integration with the `clipboardInput` event. - */ - private _setupClipboardInputIntegration(): void { - const editor = this.editor; - const view = editor.editing.view; - const viewDocument = view.document; - const dragDropTarget = editor.plugins.get( DragDropTarget ); - - // Update the event target ranges and abort dropping if dropping over itself. - this.listenTo( viewDocument, 'clipboardInput', ( evt, data ) => { - if ( data.method != 'drop' ) { - return; - } - - const { clientX, clientY } = ( data as DomEventData ).domEvent; - const targetRange = dragDropTarget.getFinalDropRange( data.target, data.targetRanges, clientX, clientY, this._blockMode ); - - if ( !targetRange ) { - this._finalizeDragging( false ); - evt.stop(); - - return; - } - - // Since we cannot rely on the drag end event, we must check if the local drag range is from the current drag and drop - // or it is from some previous not cleared one. - if ( this._draggedRange && this._draggingUid != data.dataTransfer.getData( 'application/ckeditor5-dragging-uid' ) ) { - this._draggedRange.detach(); - this._draggedRange = null; - this._draggingUid = ''; - } - - // Do not do anything if some content was dragged within the same document to the same position. - const isMove = getFinalDropEffect( data.dataTransfer ) == 'move'; - - if ( isMove && this._draggedRange && this._draggedRange.containsRange( targetRange, true ) ) { - this._finalizeDragging( false ); - evt.stop(); - - return; - } - - // Override the target ranges with the one adjusted to the best one for a drop. - data.targetRanges = [ editor.editing.mapper.toViewRange( targetRange ) ]; - }, { priority: 'high' } ); - } - - /** - * Integration with the `contentInsertion` event of the clipboard pipeline. - */ - private _setupContentInsertionIntegration(): void { - const clipboardPipeline = this.editor.plugins.get( ClipboardPipeline ); - - clipboardPipeline.on( 'contentInsertion', ( evt, data ) => { - if ( !this.isEnabled || data.method !== 'drop' ) { - return; - } - - // Update the selection to the target range in the same change block to avoid selection post-fixing - // and to be able to clone text attributes for plain text dropping. - const ranges = data.targetRanges!.map( viewRange => this.editor.editing.mapper.toModelRange( viewRange ) ); - - this.editor.model.change( writer => writer.setSelection( ranges ) ); - }, { priority: 'high' } ); - - clipboardPipeline.on( 'contentInsertion', ( evt, data ) => { - if ( !this.isEnabled || data.method !== 'drop' ) { - return; - } - - // Remove dragged range content, remove markers, clean after dragging. - const isMove = getFinalDropEffect( data.dataTransfer ) == 'move'; - - // Whether any content was inserted (insertion might fail if the schema is disallowing some elements - // (for example an image caption allows only the content of a block but not blocks themselves. - // Some integrations might not return valid range (i.e., table pasting). - const isSuccess = !data.resultRange || !data.resultRange.isCollapsed; - - this._finalizeDragging( isSuccess && isMove ); - }, { priority: 'lowest' } ); - } - - /** - * Adds listeners that add the `draggable` attribute to the elements while the mouse button is down so the dragging could start. - */ - private _setupDraggableAttributeHandling(): void { - const editor = this.editor; - const view = editor.editing.view; - const viewDocument = view.document; - - // Add the 'draggable' attribute to the widget while pressing the selection handle. - // This is required for widgets to be draggable. In Chrome it will enable dragging text nodes. - this.listenTo( viewDocument, 'mousedown', ( evt, data ) => { - // The lack of data can be caused by editor tests firing fake mouse events. This should not occur - // in real-life scenarios but this greatly simplifies editor tests that would otherwise fail a lot. - if ( env.isAndroid || !data ) { - return; - } - - this._clearDraggableAttributesDelayed.cancel(); - - // Check if this is a mousedown over the widget (but not a nested editable). - let draggableElement = findDraggableWidget( data.target ); - - // Note: There is a limitation that if more than a widget is selected (a widget and some text) - // and dragging starts on the widget, then only the widget is dragged. - - // If this was not a widget then we should check if we need to drag some text content. - // In Chrome set a 'draggable' attribute on closest editable to allow immediate dragging of the selected text range. - // In Firefox this is not needed. In Safari it makes the whole editable draggable (not just textual content). - // Disabled in read-only mode because draggable="true" + contenteditable="false" results - // in not firing selectionchange event ever, which makes the selection stuck in read-only mode. - if ( env.isBlink && !editor.isReadOnly && !draggableElement && !viewDocument.selection.isCollapsed ) { - const selectedElement = viewDocument.selection.getSelectedElement(); - - if ( !selectedElement || !isWidget( selectedElement ) ) { - draggableElement = viewDocument.selection.editableElement; - } - } - - if ( draggableElement ) { - view.change( writer => { - writer.setAttribute( 'draggable', 'true', draggableElement! ); - } ); - - // Keep the reference to the model element in case the view element gets removed while dragging. - this._draggableElement = editor.editing.mapper.toModelElement( draggableElement )!; - } - } ); - - // Remove the draggable attribute in case no dragging started (only mousedown + mouseup). - this.listenTo( viewDocument, 'mouseup', () => { - if ( !env.isAndroid ) { - this._clearDraggableAttributesDelayed(); - } - } ); - } - - /** - * Removes the `draggable` attribute from the element that was used for dragging. - */ - private _clearDraggableAttributes(): void { - const editing = this.editor.editing; - - editing.view.change( writer => { - // Remove 'draggable' attribute. - if ( this._draggableElement && this._draggableElement.root.rootName != '$graveyard' ) { - writer.removeAttribute( 'draggable', editing.mapper.toViewElement( this._draggableElement )! ); - } - - this._draggableElement = null; - } ); - } - - /** - * Deletes the dragged content from its original range and clears the dragging state. - * - * @param moved Whether the move succeeded. - */ - private _finalizeDragging( moved: boolean ): void { - const editor = this.editor; - const model = editor.model; - const dragDropTarget = editor.plugins.get( DragDropTarget ); - - dragDropTarget.removeDropMarker(); - this._clearDraggableAttributes(); - - if ( editor.plugins.has( 'WidgetToolbarRepository' ) ) { - const widgetToolbarRepository: WidgetToolbarRepository = editor.plugins.get( 'WidgetToolbarRepository' ); - - widgetToolbarRepository.clearForceDisabled( 'dragDrop' ); - } - - this._draggingUid = ''; - - if ( this._previewContainer ) { - this._previewContainer.remove(); - this._previewContainer = undefined; - } - - if ( !this._draggedRange ) { - return; - } - - // Delete moved content. - if ( moved && this.isEnabled ) { - model.deleteContent( model.createSelection( this._draggedRange ), { doNotAutoparagraph: true } ); - } - - this._draggedRange.detach(); - this._draggedRange = null; - } - - /** - * Sets the dragged source range based on event target and document selection. - */ - private _prepareDraggedRange( target: ViewElement ): void { - const editor = this.editor; - const model = editor.model; - const selection = model.document.selection; - - // Check if this is dragstart over the widget (but not a nested editable). - const draggableWidget = target ? findDraggableWidget( target ) : null; - - if ( draggableWidget ) { - const modelElement = editor.editing.mapper.toModelElement( draggableWidget )!; - - this._draggedRange = LiveRange.fromRange( model.createRangeOn( modelElement ) ); - this._blockMode = model.schema.isBlock( modelElement ); - - // Disable toolbars so they won't obscure the drop area. - if ( editor.plugins.has( 'WidgetToolbarRepository' ) ) { - const widgetToolbarRepository: WidgetToolbarRepository = editor.plugins.get( 'WidgetToolbarRepository' ); - - widgetToolbarRepository.forceDisabled( 'dragDrop' ); - } - - return; - } - - // If this was not a widget we should check if we need to drag some text content. - if ( selection.isCollapsed && !( selection.getFirstPosition()!.parent as Element ).isEmpty ) { - return; - } - - const blocks = Array.from( selection.getSelectedBlocks() ); - const draggedRange = selection.getFirstRange()!; - - if ( blocks.length == 0 ) { - this._draggedRange = LiveRange.fromRange( draggedRange ); - - return; - } - - const blockRange = getRangeIncludingFullySelectedParents( model, blocks ); - - if ( blocks.length > 1 ) { - this._draggedRange = LiveRange.fromRange( blockRange ); - this._blockMode = true; - // TODO block mode for dragging from outside editor? or inline? or both? - } else if ( blocks.length == 1 ) { - const touchesBlockEdges = draggedRange.start.isTouching( blockRange.start ) && - draggedRange.end.isTouching( blockRange.end ); - - this._draggedRange = LiveRange.fromRange( touchesBlockEdges ? blockRange : draggedRange ); - this._blockMode = touchesBlockEdges; - } - - model.change( writer => writer.setSelection( this._draggedRange!.toRange() ) ); - } - - /** - * Updates the dragged preview image. - */ - private _updatePreview( { - dataTransfer, - domTarget, - clientX - }: { - dataTransfer: DataTransfer; - domTarget: HTMLElement; - clientX: number; - } ): void { - const view = this.editor.editing.view; - const editable = view.document.selection.editableElement!; - const domEditable = view.domConverter.mapViewToDom( editable )!; - const computedStyle = global.window.getComputedStyle( domEditable ); - - if ( !this._previewContainer ) { - this._previewContainer = createElement( global.document, 'div', { - style: 'position: fixed; left: -999999px;' - } ); - - global.document.body.appendChild( this._previewContainer ); - } else if ( this._previewContainer.firstElementChild ) { - this._previewContainer.removeChild( this._previewContainer.firstElementChild ); - } - - const domRect = new Rect( domEditable ); - - // If domTarget is inside the editable root, browsers will display the preview correctly by themselves. - if ( domEditable.contains( domTarget ) ) { - return; - } - - const domEditablePaddingLeft = parseFloat( computedStyle.paddingLeft ); - const preview = createElement( global.document, 'div' ); - - preview.className = 'ck ck-content'; - preview.style.width = computedStyle.width; - preview.style.paddingLeft = `${ domRect.left - clientX + domEditablePaddingLeft }px`; - - preview.innerHTML = dataTransfer.getData( 'text/html' ); - - dataTransfer.setDragImage( preview, 0, 0 ); - - this._previewContainer.appendChild( preview ); - } -} - -/** - * Returns the drop effect that should be a result of dragging the content. - * This function is handling a quirk when checking the effect in the 'drop' DOM event. - */ -function getFinalDropEffect( dataTransfer: DataTransfer ): DataTransfer[ 'dropEffect' ] { - if ( env.isGecko ) { - return dataTransfer.dropEffect; - } - - return [ 'all', 'copyMove' ].includes( dataTransfer.effectAllowed ) ? 'move' : 'copy'; -} - -/** - * Returns a widget element that should be dragged. - */ -function findDraggableWidget( target: ViewElement ): ViewElement | null { - // This is directly an editable so not a widget for sure. - if ( target.is( 'editableElement' ) ) { - return null; - } - - // TODO: Let's have a isWidgetSelectionHandleDomElement() helper in ckeditor5-widget utils. - if ( target.hasClass( 'ck-widget__selection-handle' ) ) { - return target.findAncestor( isWidget ); - } - - // Direct hit on a widget. - if ( isWidget( target ) ) { - return target; - } - - // Find closest ancestor that is either a widget or an editable element... - const ancestor = target.findAncestor( node => isWidget( node ) || node.is( 'editableElement' ) )!; - - // ...and if closer was the widget then enable dragging it. - if ( isWidget( ancestor ) ) { - return ancestor; - } - - return null; -} - -/** - * Recursively checks if common parent of provided elements doesn't have any other children. If that's the case, - * it returns range including this parent. Otherwise, it returns only the range from first to last element. - * - * Example: - * - *
- * [Test 1 - * Test 2 - * Test 3] - *
- * - * Because all elements inside the `blockQuote` are selected, the range is extended to include the `blockQuote` too. - * If only first and second paragraphs would be selected, the range would not include it. - */ -function getRangeIncludingFullySelectedParents( model: Model, elements: Array ): Range { - const firstElement = elements[ 0 ]; - const lastElement = elements[ elements.length - 1 ]; - const parent = firstElement.getCommonAncestor( lastElement ); - const startPosition: Position = model.createPositionBefore( firstElement ); - const endPosition: Position = model.createPositionAfter( lastElement ); - - if ( - parent && - parent.is( 'element' ) && - !model.schema.isLimit( parent ) - ) { - const parentRange = model.createRangeOn( parent ); - const touchesStart = startPosition.isTouching( parentRange.start ); - const touchesEnd = endPosition.isTouching( parentRange.end ); - - if ( touchesStart && touchesEnd ) { - // Selection includes all elements in the parent. - return getRangeIncludingFullySelectedParents( model, [ parent ] ); - } - } - - return model.createRange( startPosition, endPosition ); -} diff --git a/packages/ckeditor5-clipboard/src/index.ts b/packages/ckeditor5-clipboard/src/index.ts index 23439f565b5..51685ae30af 100644 --- a/packages/ckeditor5-clipboard/src/index.ts +++ b/packages/ckeditor5-clipboard/src/index.ts @@ -22,7 +22,6 @@ export type { export { default as DragDrop } from './dragdrop'; export { default as PastePlainText } from './pasteplaintext'; -export { default as DragDropExperimental } from './dragdropexperimental'; export { default as DragDropTarget } from './dragdroptarget'; export { default as DragDropBlockToolbar } from './dragdropblocktoolbar'; diff --git a/packages/ckeditor5-clipboard/tests/clipboard.js b/packages/ckeditor5-clipboard/tests/clipboard.js index 7f9b888badc..ca4cb495601 100644 --- a/packages/ckeditor5-clipboard/tests/clipboard.js +++ b/packages/ckeditor5-clipboard/tests/clipboard.js @@ -5,12 +5,12 @@ import Clipboard from '../src/clipboard'; import ClipboardPipeline from '../src/clipboardpipeline'; -import DragDropExperimental from '../src/dragdropexperimental'; +import DragDrop from '../src/dragdrop'; import PastePlainText from '../src/pasteplaintext'; describe( 'Clipboard Feature', () => { it( 'requires ClipboardPipeline, DragDrop and PastePlainText', () => { - expect( Clipboard.requires ).to.deep.equal( [ ClipboardPipeline, DragDropExperimental, PastePlainText ] ); + expect( Clipboard.requires ).to.deep.equal( [ ClipboardPipeline, DragDrop, PastePlainText ] ); } ); it( 'has proper name', () => { diff --git a/packages/ckeditor5-clipboard/tests/dragdrop.js b/packages/ckeditor5-clipboard/tests/dragdrop.js index 4d4246b6141..55da87fc88d 100644 --- a/packages/ckeditor5-clipboard/tests/dragdrop.js +++ b/packages/ckeditor5-clipboard/tests/dragdrop.js @@ -3,10 +3,11 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ -/* globals document */ +/* globals window, document, Event */ import ClipboardPipeline from '../src/clipboardpipeline'; import DragDrop from '../src/dragdrop'; +import DragDropTarget from '../src/dragdroptarget'; import PastePlainText from '../src/pasteplaintext'; import Widget from '@ckeditor/ckeditor5-widget/src/widget'; @@ -18,45 +19,27 @@ import HorizontalLine from '@ckeditor/ckeditor5-horizontal-line/src/horizontalli import ShiftEnter from '@ckeditor/ckeditor5-enter/src/shiftenter'; import BlockQuote from '@ckeditor/ckeditor5-block-quote/src/blockquote'; import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold'; +import { Image, ImageCaption } from '@ckeditor/ckeditor5-image'; import env from '@ckeditor/ckeditor5-utils/src/env'; +import { Rect } from '@ckeditor/ckeditor5-utils'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import { getData as getViewData, stringify as stringifyView } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; -describe( 'Drag and Drop', () => { +describe( 'Drag and Drop experimental', () => { let editorElement, editor, model, view, viewDocument, root, mapper, domConverter; testUtils.createSinonSandbox(); - it( 'requires ClipboardPipeline and Widget', () => { - expect( DragDrop.requires ).to.deep.equal( [ ClipboardPipeline, Widget ] ); + it( 'requires DragDropTarget, ClipboardPipeline and Widget', () => { + expect( DragDrop.requires ).to.deep.equal( [ ClipboardPipeline, Widget, DragDropTarget ] ); } ); it( 'has proper name', () => { expect( DragDrop.pluginName ).to.equal( 'DragDrop' ); } ); - it( 'should not initialize if DragDropExperimental plugin is loaded', async () => { - const editorElement = document.createElement( 'div' ); - document.body.appendChild( editorElement ); - - function DragDropExperimental() {} - - DragDropExperimental.pluginName = 'DragDropExperimental'; - - const editor = await ClassicTestEditor.create( editorElement, { - plugins: [ DragDrop, DragDropExperimental ] - } ); - - const plugin = editor.plugins.get( 'DragDrop' ); - - expect( plugin.isEnabled ).to.be.false; - - await editor.destroy(); - await editorElement.remove(); - } ); - it( 'should be disabled on Android', async () => { env.isAndroid = true; @@ -72,7 +55,7 @@ describe( 'Drag and Drop', () => { expect( plugin.isEnabled ).to.be.false; await editor.destroy(); - await editorElement.remove(); + editorElement.remove(); env.isAndroid = false; } ); @@ -83,7 +66,18 @@ describe( 'Drag and Drop', () => { document.body.appendChild( editorElement ); editor = await ClassicTestEditor.create( editorElement, { - plugins: [ DragDrop, PastePlainText, Paragraph, Table, HorizontalLine, ShiftEnter, BlockQuote, Bold ] + plugins: [ + DragDrop, + PastePlainText, + Paragraph, + Table, + HorizontalLine, + ShiftEnter, + BlockQuote, + Bold, + Image, + ImageCaption + ] } ); model = editor.model; @@ -96,7 +90,7 @@ describe( 'Drag and Drop', () => { afterEach( async () => { await editor.destroy(); - await editorElement.remove(); + editorElement.remove(); } ); it( 'should move text to other place in the same editor (not Firefox)', () => { @@ -176,6 +170,8 @@ describe( 'Drag and Drop', () => { const spyClipboardInput = sinon.spy(); let targetPosition; + sinon.stub( dataTransferMock, 'setDragImage' ).returns( () => null ); + viewDocument.on( 'clipboardOutput', ( event, data ) => spyClipboardOutput( data ) ); viewDocument.on( 'clipboardInput', ( event, data ) => spyClipboardInput( data ) ); @@ -359,14 +355,14 @@ describe( 'Drag and Drop', () => { // Dragging. - const targetRange = model.createRangeOn( root.getChild( 1 ) ); + const targetRange = model.createPositionAt( root.getChild( 1 ), 'after' ); fireDragging( dataTransferMock, targetRange ); clock.tick( 100 ); expectDraggingMarker( targetRange ); expect( getViewData( view ) ).to.equal( '

{foo}bar

' + - '
' + + '
' + '
' + '
' + '
' @@ -385,7 +381,11 @@ describe( 'Drag and Drop', () => { fireDragEnd( dataTransferMock ); expectFinalized(); - expect( getModelData( model ) ).to.equal( 'barfoo[]' ); + expect( getModelData( model ) ).to.equal( + 'bar' + + '' + + 'foo[]' + ); } ); it( 'should do nothing if dropped on dragged range', () => { @@ -507,10 +507,8 @@ describe( 'Drag and Drop', () => { } ); it( 'should not remove dragged range if insert into drop target was not allowed', () => { - editor.model.schema.register( 'caption', { - allowIn: '$root', - allowContentOf: '$block', - isObject: true + editor.model.schema.extend( 'caption', { + allowIn: '$root' } ); editor.conversion.elementToElement( { @@ -551,8 +549,8 @@ describe( 'Drag and Drop', () => { fireDrop( dataTransferMock, targetPosition ); expect( getModelData( model ) ).to.equal( - '
fo[]o
bar
' + 'foo' + + '[bar
]' ); } ); @@ -898,9 +896,8 @@ describe( 'Drag and Drop', () => { expectFinalized(); expect( getModelData( model ) ).to.equal( - 'foo' + - '[abc
]' + - 'bar' + 'foobar' + + '[abc
]' ); } ); @@ -1006,9 +1003,8 @@ describe( 'Drag and Drop', () => { expectFinalized(); expect( getModelData( model ) ).to.equal( - 'foo' + - '[]' + - 'bar' + 'foobar' + + '[]' ); } ); @@ -1064,6 +1060,7 @@ describe( 'Drag and Drop', () => { ); } ); + // TODO: what does it mean "(but not nested editable)"? it( 'should start dragging by grabbing a widget nested element (but not nested editable)', () => { setModelData( model, '[]foobar' + @@ -1120,9 +1117,8 @@ describe( 'Drag and Drop', () => { expectFinalized(); expect( getModelData( model ) ).to.equal( - 'foo' + - '[]' + - 'bar' + 'foobar' + + '[]' ); } ); @@ -1161,7 +1157,8 @@ describe( 'Drag and Drop', () => { expect( dataTransferMock.getData( 'text/html' ) ).to.be.undefined; } ); - it( 'should not start dragging a widget if it is not a target for an event (but it was selected)', () => { + // TODO: this test looks invalid, "this._draggedRange" in experimental in most cases is not undefined. + it.skip( 'should not start dragging a widget if it is not a target for an event (but it was selected)', () => { setModelData( model, 'foobar' + '[]' @@ -1196,7 +1193,7 @@ describe( 'Drag and Drop', () => { expect( dataTransferMock.getData( 'text/html' ) ).to.be.undefined; } ); - it( 'should not start dragging a widget when some element inside nested editable is dragged', () => { + it( 'should drag parent paragraph if entire content is selected', () => { setModelData( model, 'foobar' + '[]
' @@ -1231,7 +1228,7 @@ describe( 'Drag and Drop', () => { stopPropagation: () => {} } ); - expect( dataTransferMock.getData( 'text/html' ) ).to.equal( '
' ); + expect( dataTransferMock.getData( 'text/html' ) ).to.equal( '


 

' ); const modelCellElement = model.document.getRoot().getNodeByPath( [ 1, 0, 0 ] ); const viewCellElement = mapper.toViewElement( modelCellElement ); @@ -1241,7 +1238,7 @@ describe( 'Drag and Drop', () => { expect( spyClipboardOutput.called ).to.be.true; expect( spyClipboardOutput.firstCall.firstArg.method ).to.equal( 'dragstart' ); expect( spyClipboardOutput.firstCall.firstArg.dataTransfer ).to.equal( dataTransferMock ); - expect( stringifyView( spyClipboardOutput.firstCall.firstArg.content ) ).to.equal( '

' ); + expect( stringifyView( spyClipboardOutput.firstCall.firstArg.content ) ).to.equal( '



' ); dataTransferMock.dropEffect = 'move'; const targetPosition = model.createPositionAt( root.getChild( 0 ), 3 ); @@ -1255,27 +1252,71 @@ describe( 'Drag and Drop', () => { expectFinalized(); expect( getModelData( model ) ).to.equal( - 'foo[]bar' + + 'foobar []' + '
' ); } ); - it( 'should remove "draggable" attribute from widget element if mouseup before dragging start (selection handle)', () => { + it( 'should start dragging text from caption to paragraph', () => { + setModelData( model, trim` + + [World] + + Hello + ` ); + + const dataTransferMock = createDataTransfer(); + const viewElement = viewDocument.getRoot().getChild( 1 ); + const positionAfterHr = model.createPositionAt( root.getChild( 1 ), 'after' ); + + viewDocument.fire( 'dragstart', { + domTarget: domConverter.mapViewToDom( viewElement ), + target: viewElement, + domEvent: {}, + dataTransfer: dataTransferMock, + stopPropagation: () => {} + } ); + + expect( dataTransferMock.getData( 'text/html' ) ).to.equal( 'World' ); + + fireDragging( dataTransferMock, positionAfterHr ); + expectDraggingMarker( positionAfterHr ); + + fireDrop( + dataTransferMock, + model.createPositionAt( root.getChild( 1 ), 5 ) + ); + + expect( getModelData( model ) ).to.equal( trim` + + + + HelloWorld[] + ` ); + } ); + + it( 'should not drag parent paragraph when only portion of content is selected', () => { setModelData( model, - '[]foobar' + - 'abc
' + 'foobar' + + 'ba[]z
' ); - const clock = sinon.useFakeTimers(); - const domNode = view.getDomRoot().querySelector( '.ck-widget__selection-handle' ); - const widgetViewElement = viewDocument.getRoot().getChild( 1 ); - const selectionHandleElement = widgetViewElement.getChild( 0 ); + const dataTransferMock = createDataTransfer(); + const spyClipboardOutput = sinon.spy(); + const spyClipboardInput = sinon.spy(); - expect( selectionHandleElement.hasClass( 'ck-widget__selection-handle' ) ).to.be.true; + viewDocument.on( 'clipboardOutput', ( event, data ) => spyClipboardOutput( data ) ); + viewDocument.on( 'clipboardInput', ( event, data ) => spyClipboardInput( data ) ); + + const modelElement = model.document.getRoot().getNodeByPath( [ 1, 0, 0, 0, 2 ] ); + const viewElement = mapper.toViewElement( modelElement ); + const domNode = domConverter.mapViewToDom( viewElement ); + + expect( viewElement.is( 'element', 'br' ) ).to.be.true; const eventData = { domTarget: domNode, - target: selectionHandleElement, + target: viewElement, domEvent: {} }; @@ -1283,30 +1324,57 @@ describe( 'Drag and Drop', () => { ...eventData } ); - expect( widgetViewElement.getAttribute( 'draggable' ) ).to.equal( 'true' ); + viewDocument.fire( 'dragstart', { + ...eventData, + dataTransfer: dataTransferMock, + stopPropagation: () => {} + } ); - viewDocument.fire( 'mouseup' ); - clock.tick( 50 ); + expect( dataTransferMock.getData( 'text/html' ) ).to.equal( '
' ); - expect( widgetViewElement.hasAttribute( 'draggable' ) ).to.be.false; + const modelCellElement = model.document.getRoot().getNodeByPath( [ 1, 0, 0 ] ); + const viewCellElement = mapper.toViewElement( modelCellElement ); + expect( viewCellElement.is( 'editableElement', 'td' ) ).to.be.true; + expect( viewCellElement.getAttribute( 'draggable' ) ).to.equal( 'true' ); + + expect( spyClipboardOutput.called ).to.be.true; + expect( spyClipboardOutput.firstCall.firstArg.method ).to.equal( 'dragstart' ); + expect( spyClipboardOutput.firstCall.firstArg.dataTransfer ).to.equal( dataTransferMock ); + expect( stringifyView( spyClipboardOutput.firstCall.firstArg.content ) ).to.equal( '

' ); + + dataTransferMock.dropEffect = 'move'; + const targetPosition = model.createPositionAt( root.getChild( 0 ), 3 ); + fireDrop( dataTransferMock, targetPosition ); + + expect( spyClipboardInput.called ).to.be.true; + expect( spyClipboardInput.firstCall.firstArg.method ).to.equal( 'drop' ); + expect( spyClipboardInput.firstCall.firstArg.dataTransfer ).to.equal( dataTransferMock ); + + fireDragEnd( dataTransferMock ); + expectFinalized(); + + expect( getModelData( model ) ).to.equal( + 'foo[]bar' + + 'baz
' + ); } ); - it( 'should remove "draggable" attribute from widget element if mouseup before dragging start (widget)', () => { + it( 'should remove "draggable" attribute from widget element if mouseup before dragging start (selection handle)', () => { setModelData( model, '[]foobar' + - '' + 'abc
' ); const clock = sinon.useFakeTimers(); + const domNode = view.getDomRoot().querySelector( '.ck-widget__selection-handle' ); const widgetViewElement = viewDocument.getRoot().getChild( 1 ); - const viewElement = widgetViewElement.getChild( 0 ); - const domNode = domConverter.mapViewToDom( viewElement ); + const selectionHandleElement = widgetViewElement.getChild( 0 ); - expect( viewElement.is( 'element', 'hr' ) ).to.be.true; + expect( selectionHandleElement.hasClass( 'ck-widget__selection-handle' ) ).to.be.true; const eventData = { domTarget: domNode, - target: viewElement, + target: selectionHandleElement, domEvent: {} }; @@ -1322,9 +1390,7 @@ describe( 'Drag and Drop', () => { expect( widgetViewElement.hasAttribute( 'draggable' ) ).to.be.false; } ); - it( 'should do nothing on mouseup on android', () => { - env.isAndroid = true; - + it( 'should remove "draggable" attribute from widget element if mouseup before dragging start (widget)', () => { setModelData( model, '[]foobar' + '' @@ -1340,22 +1406,39 @@ describe( 'Drag and Drop', () => { const eventData = { domTarget: domNode, target: viewElement, - domEvent: {}, - preventDefault() {} + domEvent: {} }; viewDocument.fire( 'mousedown', { ...eventData } ); - expect( widgetViewElement.hasAttribute( 'draggable' ) ).to.be.false; + expect( widgetViewElement.getAttribute( 'draggable' ) ).to.equal( 'true' ); viewDocument.fire( 'mouseup' ); clock.tick( 50 ); expect( widgetViewElement.hasAttribute( 'draggable' ) ).to.be.false; + } ); + + it( 'can drag multiple elements', () => { + setModelData( model, + '
' + + '[foo' + + 'bar]' + + 'baz' + + '
' + + '' + ); + + const dataTransferMock = createDataTransfer(); + const positionAfterHr = model.createPositionAt( root.getChild( 1 ), 'after' ); - env.isAndroid = false; + fireDragStart( dataTransferMock ); + expectDragStarted( dataTransferMock, '

foo

bar

' ); + + fireDragging( dataTransferMock, positionAfterHr ); + expectDraggingMarker( positionAfterHr ); } ); it( 'should remove "draggable" attribute from editable element', () => { @@ -1385,6 +1468,92 @@ describe( 'Drag and Drop', () => { expect( editableElement.hasAttribute( 'draggable' ) ).to.be.false; } ); + + it( 'should only show one preview element when you drag element outside the editing root', () => { + setModelData( model, + '
' + + '[foo' + + 'bar]' + + 'baz' + + '
' + + '' + ); + + const pilcrow = document.createElement( 'div' ); + pilcrow.setAttribute( 'class', 'pilcrow' ); + + const dataTransferMock = createDataTransfer(); + + fireDragStart( dataTransferMock, () => {}, pilcrow ); + fireDragStart( dataTransferMock, () => {}, pilcrow ); + + const numberOfCkContentElements = Object + .keys( document.getElementsByClassName( 'ck-content' ) ) + .length; + + // There should be two elements with the `.ck-content` class - editor and drag-and-drop preview. + expect( numberOfCkContentElements ).to.equal( 2 ); + } ); + + it( 'should show preview with custom implementation if drag element outside the editing root', () => { + setModelData( editor.model, '[Foo.]' ); + + const dataTransfer = createDataTransfer( {} ); + + const spy = sinon.spy( dataTransfer, 'setDragImage' ); + const clientX = 10; + + viewDocument.fire( 'dragstart', { + dataTransfer, + preventDefault: sinon.spy(), + stopPropagation: sinon.spy(), + domEvent: { + clientX + } + } ); + + const editable = editor.editing.view.document.selection.editableElement; + const domEditable = editor.editing.view.domConverter.mapViewToDom( editable ); + const computedStyle = window.getComputedStyle( domEditable ); + const paddingLeftString = computedStyle.paddingLeft; + const paddingLeft = parseFloat( paddingLeftString ); + + const domRect = new Rect( domEditable ); + + sinon.assert.calledWith( spy, sinon.match( { + style: { + 'padding-left': `${ domRect.left - clientX + paddingLeft }px` + }, + className: 'ck ck-content', + firstChild: sinon.match( { + tagName: 'P', + innerHTML: 'Foo.' + } ) + } ), 0, 0 ); + sinon.assert.calledOnce( spy ); + } ); + + it( 'should show preview with browser implementation if drag element inside the editing root', () => { + setModelData( editor.model, '[Foo.]' ); + + const dataTransfer = createDataTransfer( {} ); + + const spy = sinon.spy( dataTransfer, 'setDragImage' ); + + const modelElement = root.getNodeByPath( [ 0 ] ); + const viewElement = mapper.toViewElement( modelElement ); + const domElement = domConverter.mapViewToDom( viewElement ); + + viewDocument.fire( 'dragstart', { + dataTransfer, + preventDefault: sinon.spy(), + stopPropagation: sinon.spy(), + domEvent: getMockedMousePosition( domElement ), + domTarget: domElement + } ); + + sinon.assert.notCalled( spy ); + } ); } ); describe( 'dragenter', () => { @@ -1484,7 +1653,8 @@ describe( 'Drag and Drop', () => { expectDraggingMarker( targetPosition ); } ); - it( 'cannot be dropped on non-editable place.', () => { + // TODO: this should be fixed in code. + it.skip( 'cannot be dropped on non-editable place.', () => { setModelData( model, '[]foobar' ); const dataTransferMock = createDataTransfer(); @@ -1515,13 +1685,14 @@ describe( 'Drag and Drop', () => { domTarget: domNode, target: viewElement, targetRanges: [ view.createRange( view.createPositionAt( viewElement.getChild( 0 ), 2 ) ) ], - dataTransfer: dataTransferMock + dataTransfer: dataTransferMock, + domEvent: getMockedMousePosition( domNode ) } ); expectDraggingMarker( model.createPositionAt( root.getChild( 0 ), 5 ) ); } ); - it( 'should find ancestor widget while hovering over the selection handle (UIElement)', () => { + it( 'should put marker before element when mouse position is on the upper half of it', () => { setModelData( model, '[]foobar' + 'abc
' @@ -1537,10 +1708,36 @@ describe( 'Drag and Drop', () => { viewDocument.fire( 'dragging', { domTarget: domNode, target: viewElement, - dataTransfer: dataTransferMock + dataTransfer: dataTransferMock, + domEvent: getMockedMousePosition( domNode ) } ); - expectDraggingMarker( model.createRangeOn( root.getChild( 1 ) ) ); + expectDraggingMarker( model.createPositionAt( root.getChild( 1 ), 'before' ) ); + } ); + + it( 'should put marker after element when mouse position is on the bottom half of it', () => { + setModelData( model, + '[]foobar' + + 'abc
' + ); + + const dataTransferMock = createDataTransfer(); + + const viewElement = viewDocument.getRoot().getChild( 1 ).getChild( 0 ); + const domNode = domConverter.mapViewToDom( viewElement ); + + expect( viewElement.hasClass( 'ck-widget__selection-handle' ) ).to.be.true; + + const widgetUIHeight = 25; + + viewDocument.fire( 'dragging', { + domTarget: domNode, + target: viewElement, + dataTransfer: dataTransferMock, + domEvent: getMockedMousePosition( domNode, 'after', widgetUIHeight ) + } ); + + expectDraggingMarker( model.createPositionAt( root.getChild( 1 ), 'after' ) ); } ); it( 'should find ancestor widget while hovering over inner content of widget (but not nested editable)', () => { @@ -1560,10 +1757,11 @@ describe( 'Drag and Drop', () => { viewDocument.fire( 'dragging', { domTarget: domNode, target: viewElement, - dataTransfer: dataTransferMock + dataTransfer: dataTransferMock, + domEvent: getMockedMousePosition( domNode ) } ); - expectDraggingMarker( model.createRangeOn( root.getChild( 1 ) ) ); + expectDraggingMarker( model.createPositionAt( root.getChild( 1 ), 'before' ) ); } ); it( 'should find drop position while hovering over empty nested editable', () => { @@ -1583,10 +1781,11 @@ describe( 'Drag and Drop', () => { viewDocument.fire( 'dragging', { domTarget: domNode, target: viewElement, - dataTransfer: dataTransferMock + dataTransfer: dataTransferMock, + domEvent: getMockedMousePosition( domNode ) } ); - expectDraggingMarker( model.createPositionAt( root.getNodeByPath( [ 1, 0, 0, 0 ] ), 0 ) ); + expectDraggingMarker( model.createPositionAt( root.getNodeByPath( [ 1, 0, 0, 0 ] ), 'before' ) ); } ); it( 'should find drop position while hovering over space between blocks', () => { @@ -1603,17 +1802,19 @@ describe( 'Drag and Drop', () => { const nestedModelParagraph = root.getNodeByPath( [ 1, 0, 0, 0 ] ); const nestedViewParagraph = mapper.toViewElement( nestedModelParagraph ); + const nestedParagraphDomNode = domConverter.mapViewToDom( nestedViewParagraph ); expect( viewElement.is( 'rootElement' ) ).to.be.true; viewDocument.fire( 'dragging', { domTarget: domNode, target: rootElement, - targetRanges: [ view.createRange( view.createPositionAt( nestedViewParagraph, 0 ) ) ], - dataTransfer: dataTransferMock + targetRanges: [ view.createRangeOn( nestedModelParagraph ) ], + dataTransfer: dataTransferMock, + domEvent: getMockedMousePosition( nestedParagraphDomNode ) } ); - expectDraggingMarker( model.createRangeOn( root.getChild( 1 ) ) ); + expectDraggingMarker( model.createPositionAt( root.getChild( 1 ), 'before' ) ); } ); it( 'should find drop position while hovering over table figure', () => { @@ -1635,36 +1836,14 @@ describe( 'Drag and Drop', () => { domTarget: domNode, target: viewElement, targetRanges: [ view.createRange( view.createPositionAt( rootElement.getChild( 1 ), 1 ) ) ], - dataTransfer: dataTransferMock - } ); - - expectDraggingMarker( model.createRangeOn( root.getChild( 1 ) ) ); - } ); - - it( 'should find drop position while hovering over table with target range inside figure', () => { - setModelData( model, - '[]foobar' + - 'abc
' - ); - - const dataTransferMock = createDataTransfer(); - - const rootElement = viewDocument.getRoot(); - const modelElement = root.getNodeByPath( [ 1, 0, 0 ] ); - const viewElement = mapper.toViewElement( modelElement ); - const domNode = domConverter.mapViewToDom( viewElement ); - - viewDocument.fire( 'dragging', { - domTarget: domNode, - target: viewElement, - targetRanges: [ view.createRange( view.createPositionAt( rootElement.getChild( 1 ), 1 ) ) ], - dataTransfer: dataTransferMock + dataTransfer: dataTransferMock, + domEvent: getMockedMousePosition( domNode ) } ); - expectDraggingMarker( model.createRangeOn( root.getChild( 1 ) ) ); + expectDraggingMarker( model.createPositionAt( root.getNodeByPath( [ 1, 0, 0, 0 ] ), 'before' ) ); } ); - it( 'should find drop position while hovering over table with target range inside tr', () => { + it( 'should find drop position while hovering over table with target position inside after paragraph', () => { setModelData( model, '[]foobar' + 'abc
' @@ -1676,17 +1855,18 @@ describe( 'Drag and Drop', () => { const viewElement = mapper.toViewElement( modelElement ); const domNode = domConverter.mapViewToDom( viewElement ); - const tableRow = root.getNodeByPath( [ 1, 0 ] ); - const tableRowView = mapper.toViewElement( tableRow ); + const paragraphModel = root.getNodeByPath( [ 1, 0, 0, 0 ] ); + const paragraphView = mapper.toViewElement( paragraphModel ); viewDocument.fire( 'dragging', { domTarget: domNode, target: viewElement, - targetRanges: [ view.createRange( view.createPositionAt( tableRowView, 0 ) ) ], - dataTransfer: dataTransferMock + targetRanges: [ view.createRange( view.createPositionAt( paragraphView, 'after' ) ) ], + dataTransfer: dataTransferMock, + domEvent: getMockedMousePosition( domNode, 'after' ) } ); - expectDraggingMarker( model.createRangeOn( root.getChild( 1 ) ) ); + expectDraggingMarker( model.createPositionAt( root.getNodeByPath( [ 1, 0, 0, 0 ] ), 'after' ) ); } ); it( 'should find drop position while hovering over space between blocks but the following element is not an object', () => { @@ -1701,19 +1881,24 @@ describe( 'Drag and Drop', () => { const viewElement = rootElement; const domNode = domConverter.mapViewToDom( viewElement ); + const firstParagraphModelElement = root.getChild( 1 ); + const firstParagraphViewElement = mapper.toViewElement( firstParagraphModelElement ); + const firstParagraphDomNode = domConverter.mapViewToDom( firstParagraphViewElement ); + expect( viewElement.is( 'rootElement' ) ).to.be.true; viewDocument.fire( 'dragging', { domTarget: domNode, target: rootElement, - targetRanges: [ view.createRange( view.createPositionAt( rootElement.getChild( 1 ), 0 ) ) ], - dataTransfer: dataTransferMock + targetRanges: [ view.createRangeOn( firstParagraphModelElement ) ], + dataTransfer: dataTransferMock, + domEvent: getMockedMousePosition( firstParagraphDomNode ) } ); - expectDraggingMarker( model.createPositionAt( root.getChild( 1 ), 0 ) ); + expectDraggingMarker( model.createPositionAt( firstParagraphModelElement, 'before' ) ); } ); - it( 'should find drop position while hovering over a widget without content (not Firefox)', () => { + it( 'should find drop position while hovering after widget without content (not Firefox)', () => { const originalEnvGecko = env.isGecko; env.isGecko = false; @@ -1729,19 +1914,24 @@ describe( 'Drag and Drop', () => { const rootElement = viewDocument.getRoot(); const domNode = domConverter.mapViewToDom( rootElement ); + const modelElement = root.getNodeByPath( [ 1 ] ); + const viewWidget = mapper.toViewElement( modelElement ); + const domWidget = domConverter.mapViewToDom( viewWidget ); + viewDocument.fire( 'dragging', { domTarget: domNode, target: rootElement, - targetRanges: [ view.createRange( view.createPositionAt( rootElement, 2 ) ) ], - dataTransfer: dataTransferMock + targetRanges: [ view.createRange( view.createPositionAt( viewWidget, 'after' ) ) ], + dataTransfer: dataTransferMock, + domEvent: getMockedMousePosition( domWidget, 'after' ) } ); - expectDraggingMarker( model.createRangeOn( root.getChild( 1 ) ) ); + expectDraggingMarker( model.createPositionAt( root.getChild( 1 ), 'after' ) ); env.isGecko = originalEnvGecko; } ); - it( 'should find drop position while hovering over a widget without content (in Firefox)', () => { + it( 'should find drop position while hovering after widget without content (in Firefox)', () => { const originalEnvGecko = env.isGecko; env.isGecko = true; @@ -1754,24 +1944,61 @@ describe( 'Drag and Drop', () => { const dataTransferMock = createDataTransfer(); - const rootElement = viewDocument.getRoot(); - const domNode = domConverter.mapViewToDom( rootElement ); + const modelWidget = root.getNodeByPath( [ 1, 0 ] ); + const viewWidget = mapper.toViewElement( modelWidget ); + const domWidget = domConverter.mapViewToDom( viewWidget ); + + const modelQuote = root.getNodeByPath( [ 1 ] ); + const viewQuote = mapper.toViewElement( modelQuote ); + const domQuote = domConverter.mapViewToDom( viewQuote ); viewDocument.fire( 'dragging', { - domTarget: domNode, - target: rootElement, - targetRanges: [ view.createRange( view.createPositionAt( rootElement.getChild( 1 ), 0 ) ) ], - dataTransfer: dataTransferMock + domTarget: domQuote, + target: viewQuote, + targetRanges: [ view.createRange( view.createPositionAt( viewWidget, 'after' ) ) ], + dataTransfer: dataTransferMock, + domEvent: getMockedMousePosition( domWidget, 'after' ) } ); - expectDraggingMarker( model.createRangeOn( root.getNodeByPath( [ 1, 0 ] ) ) ); + expectDraggingMarker( model.createPositionAt( modelWidget, 'after' ) ); env.isGecko = originalEnvGecko; } ); } ); + describe( 'dragend', () => { + it( 'should reset block dragging when dropped outside the editable', () => { + setModelData( model, + 'foobar' + + '[]' + ); + + const plugin = editor.plugins.get( 'DragDrop' ); + const modelElement = model.document.getRoot().getNodeByPath( [ 1 ] ); + const viewElement = mapper.toViewElement( modelElement ); + + // Fire the 'dragstart' event to change the '_blockMode to true + viewDocument.fire( 'dragstart', { + domTarget: domConverter.mapViewToDom( viewElement ), + target: viewElement, + domEvent: {}, + dataTransfer: createDataTransfer( {} ), + preventDefault: sinon.spy(), + stopPropagation: sinon.spy() + } ); + + // Fire the 'dragend' event on the document + document.dispatchEvent( new Event( 'dragend' ) ); + + // Check if the blockMode changes to 'false' + expect( plugin._blockMode ).to.be.false; + expect( model.markers.has( 'drop-target' ) ).to.be.false; + } ); + } ); + describe( 'drop', () => { - it( 'should update targetRanges', () => { + // TODO: to be discussed. + it.skip( 'should update targetRanges', () => { setModelData( model, '[]foobar' + '' @@ -1796,21 +2023,129 @@ describe( 'Drag and Drop', () => { } ); } ); - it( 'should not use overrided clipboardInput event in drag and drop', done => { - const dataTransferMock = createDataTransfer( { 'text/html': '

x

', 'text/plain': 'y' } ); - const preventDefaultSpy = sinon.spy(); + describe( 'extending selection range when all parent elements are selected', () => { + it( 'extends flat selection', () => { + setModelData( model, trim` +
+ [one + two + three] +
+ + ` ); - viewDocument.on( 'clipboardInput', ( evt, data ) => { - expect( preventDefaultSpy.calledOnce ).to.be.true; - expect( data.dataTransfer ).to.equal( dataTransferMock ); - expect( data.method ).to.equal( 'paste' ); + const dataTransferMock = createDataTransfer(); + const positionAfterHr = model.createPositionAt( root.getChild( 1 ), 'after' ); - done(); + fireDragStart( dataTransferMock ); + expectDragStarted( dataTransferMock, trim` +
+

one

+

two

+

three

+
+ ` ); + + fireDragging( dataTransferMock, positionAfterHr ); + expectDraggingMarker( positionAfterHr ); } ); - viewDocument.fire( 'paste', { - dataTransfer: dataTransferMock, - preventDefault: preventDefaultSpy + it( 'extends nested selection', () => { + setModelData( model, trim` +
+ [one +
+ two + three + four +
+ five] +
+ + ` ); + + const dataTransferMock = createDataTransfer(); + const positionAfterHr = model.createPositionAt( root.getChild( 1 ), 'after' ); + + fireDragStart( dataTransferMock ); + expectDragStarted( dataTransferMock, trim` +
+

one

+
+

two

+

three

+

four

+
+

five

+
+ ` ); + + fireDragging( dataTransferMock, positionAfterHr ); + expectDraggingMarker( positionAfterHr ); + } ); + + it( 'extends selection when it starts at different level than it ends', () => { + setModelData( model, trim` +
+
+ [one + two + three +
+ four] +
+ + ` ); + + const dataTransferMock = createDataTransfer(); + const positionAfterHr = model.createPositionAt( root.getChild( 1 ), 'after' ); + + fireDragStart( dataTransferMock ); + expectDragStarted( dataTransferMock, trim` +
+
+

one

+

two

+

three

+
+

four

+
+ ` ); + + fireDragging( dataTransferMock, positionAfterHr ); + expectDraggingMarker( positionAfterHr ); + } ); + + it( 'extends selection when it ends at different level than it starts', () => { + setModelData( model, trim` +
+ [one +
+ two + three + four] +
+
+ + ` ); + + const dataTransferMock = createDataTransfer(); + const positionAfterHr = model.createPositionAt( root.getChild( 1 ), 'after' ); + + fireDragStart( dataTransferMock ); + expectDragStarted( dataTransferMock, trim` +
+

one

+
+

two

+

three

+

four

+
+
+ ` ); + + fireDragging( dataTransferMock, positionAfterHr ); + expectDraggingMarker( positionAfterHr ); } ); } ); } ); @@ -1835,10 +2170,9 @@ describe( 'Drag and Drop', () => { } ); } ); - afterEach( () => { + afterEach( async () => { + await editor.destroy(); editorElement.remove(); - - return editor.destroy(); } ); describe( 'WidgetToolbarRepository#isEnabled', () => { @@ -1849,9 +2183,16 @@ describe( 'Drag and Drop', () => { it( 'is enabled when starts dragging the text node', () => { setModelData( editor.model, '[Foo.]' ); + const nodeModel = root.getNodeByPath( [ 0 ] ); + const nodeView = mapper.toViewElement( nodeModel ); + const nodeDOM = domConverter.mapViewToDom( nodeView ); + const dataTransfer = createDataTransfer( {} ); + viewDocument.fire( 'dragstart', { + dataTransfer, preventDefault: sinon.spy(), - dataTransfer: createDataTransfer( {} ) + stopPropagation: sinon.spy(), + domEvent: getMockedMousePosition( nodeDOM ) } ); expect( widgetToolbarRepository.isEnabled ).to.be.true; @@ -1860,13 +2201,19 @@ describe( 'Drag and Drop', () => { it( 'is disabled when plugin is disabled', () => { setModelData( editor.model, 'Foo.[]' ); + const nodeModel = root.getNodeByPath( [ 0 ] ); + const nodeView = mapper.toViewElement( nodeModel ); + const nodeDOM = domConverter.mapViewToDom( nodeView ); + const plugin = editor.plugins.get( 'DragDrop' ); plugin.isEnabled = false; viewDocument.fire( 'dragstart', { preventDefault: sinon.spy(), target: viewDocument.getRoot().getChild( 1 ), - dataTransfer: createDataTransfer( {} ) + dataTransfer: createDataTransfer( {} ), + stopPropagation: sinon.spy(), + domEvent: getMockedMousePosition( nodeDOM ) } ); expect( widgetToolbarRepository.isEnabled ).to.be.false; @@ -1875,10 +2222,16 @@ describe( 'Drag and Drop', () => { it( 'is disabled when starts dragging the widget', () => { setModelData( editor.model, 'Foo.[]' ); + const nodeModel = root.getNodeByPath( [ 0 ] ); + const nodeView = mapper.toViewElement( nodeModel ); + const nodeDOM = domConverter.mapViewToDom( nodeView ); + viewDocument.fire( 'dragstart', { preventDefault: sinon.spy(), target: viewDocument.getRoot().getChild( 1 ), - dataTransfer: createDataTransfer( {} ) + dataTransfer: createDataTransfer( {} ), + stopPropagation: sinon.spy(), + domEvent: getMockedMousePosition( nodeDOM ) } ); expect( widgetToolbarRepository.isEnabled ).to.be.false; @@ -1889,10 +2242,16 @@ describe( 'Drag and Drop', () => { const dataTransfer = createDataTransfer( {} ); + const nodeModel = root.getNodeByPath( [ 0 ] ); + const nodeView = mapper.toViewElement( nodeModel ); + const nodeDOM = domConverter.mapViewToDom( nodeView ); + viewDocument.fire( 'dragstart', { preventDefault: sinon.spy(), target: viewDocument.getRoot().getChild( 0 ), - dataTransfer + dataTransfer, + stopPropagation: sinon.spy(), + domEvent: getMockedMousePosition( nodeDOM ) } ); expect( widgetToolbarRepository.isEnabled ).to.be.false; @@ -1902,7 +2261,11 @@ describe( 'Drag and Drop', () => { stopPropagation: sinon.spy(), target: viewDocument.getRoot().getChild( 0 ), dataTransfer, - method: 'drop' + method: 'drop', + domEvent: { + clientX: sinon.spy(), + clientY: sinon.spy() + } } ); expect( widgetToolbarRepository.isEnabled ).to.be.true; @@ -1913,10 +2276,16 @@ describe( 'Drag and Drop', () => { const dataTransfer = createDataTransfer( {} ); + const nodeModel = root.getNodeByPath( [ 0 ] ); + const nodeView = mapper.toViewElement( nodeModel ); + const nodeDOM = domConverter.mapViewToDom( nodeView ); + viewDocument.fire( 'dragstart', { preventDefault: sinon.spy(), target: viewDocument.getRoot().getChild( 0 ), - dataTransfer + dataTransfer, + stopPropagation() {}, + domEvent: getMockedMousePosition( nodeDOM ) } ); expect( widgetToolbarRepository.isEnabled ).to.be.false; @@ -1931,8 +2300,70 @@ describe( 'Drag and Drop', () => { } ); } ); - function fireDragStart( dataTransferMock, preventDefault = () => {} ) { - const eventData = prepareEventData( model.document.selection.getLastPosition() ); + describe( 'integration with paragraph-only editor', () => { + beforeEach( async () => { + editorElement = document.createElement( 'div' ); + document.body.appendChild( editorElement ); + + editor = await ClassicTestEditor.create( editorElement, { + useInlineRoot: true, + plugins: [ DragDrop, PastePlainText, Paragraph, Bold ] + } ); + + model = editor.model; + root = model.document.getRoot(); + mapper = editor.editing.mapper; + view = editor.editing.view; + viewDocument = view.document; + domConverter = view.domConverter; + } ); + + afterEach( async () => { + await editor.destroy(); + editorElement.remove(); + } ); + + it( 'handles paste', () => { + setModelData( model, + 'foo[]' + ); + + editor.editing.view.document.fire( 'paste', { + dataTransfer: createDataTransfer( { 'text/html': 'bar' } ), + stopPropagation() {}, + preventDefault() {} + } ); + + expect( getModelData( model ) ).to.equal( 'foo<$text bold="true">bar[]' ); + } ); + + it( 'stops `clipboardInput` event', () => { + setModelData( model, + 'foo[]' + ); + + const spyClipboardInput = sinon.spy(); + const rootElement = viewDocument.getRoot(); + const domNode = domConverter.mapViewToDom( rootElement ); + + viewDocument.on( 'clipboardInput', ( event, data ) => spyClipboardInput( data ) ); + + viewDocument.fire( 'clipboardInput', { + domTarget: domNode, + target: rootElement, + method: 'drop', + dataTransfer: createDataTransfer(), + domEvent: getMockedMousePosition( domNode ), + stopPropagation: () => {}, + preventDefault: () => {} + } ); + + expect( spyClipboardInput.called ).to.be.false; + } ); + } ); + + function fireDragStart( dataTransferMock, preventDefault = () => {}, domTarget ) { + const eventData = prepareEventData( model.document.selection.getLastPosition(), domTarget ); viewDocument.fire( 'mousedown', { ...eventData @@ -1974,7 +2405,7 @@ describe( 'Drag and Drop', () => { } ); } - function prepareEventData( modelPositionOrRange ) { + function prepareEventData( modelPositionOrRange, domTarget ) { let domNode, viewElement, viewRange; if ( modelPositionOrRange.is( 'position' ) ) { @@ -1983,13 +2414,17 @@ describe( 'Drag and Drop', () => { viewRange = view.createRange( viewPosition ); viewElement = mapper.findMappedViewAncestor( viewPosition ); - domNode = viewPosition.parent.is( '$text' ) ? - domConverter.findCorrespondingDomText( viewPosition.parent ).parentNode : - domConverter.mapViewToDom( viewElement ); + if ( !domTarget ) { + domNode = viewPosition.parent.is( '$text' ) ? + domConverter.findCorrespondingDomText( viewPosition.parent ).parentNode : + domConverter.mapViewToDom( viewElement ); + } else { + domNode = domTarget; + } } else { viewRange = mapper.toViewRange( modelPositionOrRange ); viewElement = viewRange.getContainedElement(); - domNode = domConverter.mapViewToDom( viewElement ); + domNode = domTarget || domConverter.mapViewToDom( viewElement ); } return { @@ -2039,7 +2474,34 @@ describe( 'Drag and Drop', () => { getData( type ) { return data[ type ]; + }, + + setDragImage() { + return null; } }; } + + function getMockedMousePosition( domNode, position = 'before', extraOffset = 0 ) { + const { x, y, height } = domNode.getBoundingClientRect(); + + if ( position === 'after' ) { + return { + clientX: x, + clientY: y + height + extraOffset + }; + } + + return { + clientX: x, + clientY: y + extraOffset + }; + } + + function trim( strings ) { + return strings + .join( '' ) + .trim() + .replace( />\s+<' ); + } } ); diff --git a/packages/ckeditor5-clipboard/tests/dragdropexperimental.js b/packages/ckeditor5-clipboard/tests/dragdropexperimental.js deleted file mode 100644 index 8ca5bc4574e..00000000000 --- a/packages/ckeditor5-clipboard/tests/dragdropexperimental.js +++ /dev/null @@ -1,2507 +0,0 @@ -/** - * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license - */ - -/* globals window, document, Event */ - -import ClipboardPipeline from '../src/clipboardpipeline'; -import DragDropExperimental from '../src/dragdropexperimental'; -import DragDropTarget from '../src/dragdroptarget'; -import PastePlainText from '../src/pasteplaintext'; - -import Widget from '@ckeditor/ckeditor5-widget/src/widget'; -import WidgetToolbarRepository from '@ckeditor/ckeditor5-widget/src/widgettoolbarrepository'; -import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; -import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; -import Table from '@ckeditor/ckeditor5-table/src/table'; -import HorizontalLine from '@ckeditor/ckeditor5-horizontal-line/src/horizontalline'; -import ShiftEnter from '@ckeditor/ckeditor5-enter/src/shiftenter'; -import BlockQuote from '@ckeditor/ckeditor5-block-quote/src/blockquote'; -import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold'; -import { Image, ImageCaption } from '@ckeditor/ckeditor5-image'; -import env from '@ckeditor/ckeditor5-utils/src/env'; -import { Rect } from '@ckeditor/ckeditor5-utils'; - -import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; -import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; -import { getData as getViewData, stringify as stringifyView } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; - -describe( 'Drag and Drop experimental', () => { - let editorElement, editor, model, view, viewDocument, root, mapper, domConverter; - - testUtils.createSinonSandbox(); - - it( 'requires DragDropTarget, ClipboardPipeline and Widget', () => { - expect( DragDropExperimental.requires ).to.deep.equal( [ ClipboardPipeline, Widget, DragDropTarget ] ); - } ); - - it( 'has proper name', () => { - expect( DragDropExperimental.pluginName ).to.equal( 'DragDropExperimental' ); - } ); - - it( 'should be disabled on Android', async () => { - env.isAndroid = true; - - const editorElement = document.createElement( 'div' ); - document.body.appendChild( editorElement ); - - const editor = await ClassicTestEditor.create( editorElement, { - plugins: [ DragDropExperimental ] - } ); - - const plugin = editor.plugins.get( 'DragDropExperimental' ); - - expect( plugin.isEnabled ).to.be.false; - - await editor.destroy(); - editorElement.remove(); - - env.isAndroid = false; - } ); - - describe( 'dragging', () => { - beforeEach( async () => { - editorElement = document.createElement( 'div' ); - document.body.appendChild( editorElement ); - - editor = await ClassicTestEditor.create( editorElement, { - plugins: [ - DragDropExperimental, - PastePlainText, - Paragraph, - Table, - HorizontalLine, - ShiftEnter, - BlockQuote, - Bold, - Image, - ImageCaption - ] - } ); - - model = editor.model; - root = model.document.getRoot(); - mapper = editor.editing.mapper; - view = editor.editing.view; - viewDocument = view.document; - domConverter = view.domConverter; - } ); - - afterEach( async () => { - await editor.destroy(); - editorElement.remove(); - } ); - - it( 'should move text to other place in the same editor (not Firefox)', () => { - const originalEnvGecko = env.isGecko; - - env.isGecko = false; - - setModelData( model, '[foo]bar' ); - - const clock = sinon.useFakeTimers(); - const dataTransferMock = createDataTransfer(); - const spyClipboardOutput = sinon.spy(); - const spyClipboardInput = sinon.spy(); - let targetPosition; - - viewDocument.on( 'clipboardOutput', ( event, data ) => spyClipboardOutput( data ) ); - viewDocument.on( 'clipboardInput', ( event, data ) => spyClipboardInput( data ) ); - - fireDragStart( dataTransferMock ); - expectDragStarted( dataTransferMock, 'foo', spyClipboardOutput ); - - // Dragging. - - targetPosition = model.createPositionAt( root.getChild( 0 ), 3 ); - dataTransferMock.effectAllowed = 'copyMove'; - fireDragging( dataTransferMock, targetPosition ); - clock.tick( 100 ); - - expectDraggingMarker( targetPosition ); - expect( getViewData( view, { renderUIElements: true } ) ).to.equal( - '

{foo}\u2060\u2060bar

' - ); - - // Dragging. - - targetPosition = model.createPositionAt( root.getChild( 0 ), 5 ); - dataTransferMock.effectAllowed = 'copy'; - fireDragging( dataTransferMock, targetPosition ); - clock.tick( 100 ); - - expectDraggingMarker( targetPosition ); - expect( getViewData( view, { renderUIElements: true } ) ).to.equal( - '

{foo}ba\u2060\u2060r

' - ); - - // Dropping. - - dataTransferMock.effectAllowed = 'copyMove'; - dataTransferMock.dropEffect = 'move'; - targetPosition = model.createPositionAt( root.getChild( 0 ), 6 ); - fireDrop( dataTransferMock, targetPosition ); - clock.tick( 100 ); - - expect( spyClipboardInput.called ).to.be.true; - expect( spyClipboardInput.firstCall.firstArg.method ).to.equal( 'drop' ); - expect( spyClipboardInput.firstCall.firstArg.dataTransfer ).to.equal( dataTransferMock ); - - fireDragEnd( dataTransferMock ); - expectFinalized(); - - expect( getModelData( model ) ).to.equal( 'barfoo[]' ); - expect( getViewData( view ) ).to.equal( '

barfoo{}

' ); - - env.isGecko = originalEnvGecko; - } ); - - it( 'should move text to other place in the same editor (in Firefox)', () => { - const originalEnvGecko = env.isGecko; - - env.isGecko = true; - - setModelData( model, '[foo]bar' ); - - const clock = sinon.useFakeTimers(); - const dataTransferMock = createDataTransfer(); - const spyClipboardOutput = sinon.spy(); - const spyClipboardInput = sinon.spy(); - let targetPosition; - - sinon.stub( dataTransferMock, 'setDragImage' ).returns( () => null ); - - viewDocument.on( 'clipboardOutput', ( event, data ) => spyClipboardOutput( data ) ); - viewDocument.on( 'clipboardInput', ( event, data ) => spyClipboardInput( data ) ); - - fireDragStart( dataTransferMock ); - expectDragStarted( dataTransferMock, 'foo', spyClipboardOutput ); - - // Dragging. - - targetPosition = model.createPositionAt( root.getChild( 0 ), 3 ); - fireDragging( dataTransferMock, targetPosition ); - clock.tick( 100 ); - - expectDraggingMarker( targetPosition ); - expect( getViewData( view, { renderUIElements: true } ) ).to.equal( - '

{foo}\u2060\u2060bar

' - ); - - // Dragging. - - targetPosition = model.createPositionAt( root.getChild( 0 ), 5 ); - fireDragging( dataTransferMock, targetPosition ); - clock.tick( 100 ); - - expectDraggingMarker( targetPosition ); - expect( getViewData( view, { renderUIElements: true } ) ).to.equal( - '

{foo}ba\u2060\u2060r

' - ); - - // Dropping. - - dataTransferMock.dropEffect = 'move'; - targetPosition = model.createPositionAt( root.getChild( 0 ), 6 ); - fireDrop( dataTransferMock, targetPosition ); - clock.tick( 100 ); - - expect( spyClipboardInput.called ).to.be.true; - expect( spyClipboardInput.firstCall.firstArg.method ).to.equal( 'drop' ); - expect( spyClipboardInput.firstCall.firstArg.dataTransfer ).to.equal( dataTransferMock ); - - fireDragEnd( dataTransferMock ); - expectFinalized(); - - expect( getModelData( model ) ).to.equal( 'barfoo[]' ); - expect( getViewData( view ) ).to.equal( '

barfoo{}

' ); - - env.isGecko = originalEnvGecko; - } ); - - it( 'should copy text to other place in the same editor (not Firefox)', () => { - const originalEnvGecko = env.isGecko; - - env.isGecko = false; - - setModelData( model, '[foo]bar' ); - - const clock = sinon.useFakeTimers(); - const dataTransferMock = createDataTransfer(); - const spyClipboardOutput = sinon.spy(); - const spyClipboardInput = sinon.spy(); - let targetPosition; - - viewDocument.on( 'clipboardOutput', ( event, data ) => spyClipboardOutput( data ) ); - viewDocument.on( 'clipboardInput', ( event, data ) => spyClipboardInput( data ) ); - - fireDragStart( dataTransferMock ); - expectDragStarted( dataTransferMock, 'foo', spyClipboardOutput ); - - // Dragging. - - targetPosition = model.createPositionAt( root.getChild( 0 ), 3 ); - dataTransferMock.effectAllowed = 'copy'; - fireDragging( dataTransferMock, targetPosition ); - clock.tick( 100 ); - - expectDraggingMarker( targetPosition ); - expect( getViewData( view, { renderUIElements: true } ) ).to.equal( - '

{foo}\u2060\u2060bar

' - ); - - fireDragging( dataTransferMock, targetPosition ); - clock.tick( 100 ); - - expectDraggingMarker( targetPosition ); - expect( getViewData( view, { renderUIElements: true } ) ).to.equal( - '

{foo}\u2060\u2060bar

' - ); - - // Dropping. - - targetPosition = model.createPositionAt( root.getChild( 0 ), 6 ); - dataTransferMock.dropEffect = 'copy'; - dataTransferMock.effectAllowed = 'copy'; - fireDrop( dataTransferMock, targetPosition ); - clock.tick( 100 ); - - expect( spyClipboardInput.called ).to.be.true; - expect( spyClipboardInput.firstCall.firstArg.method ).to.equal( 'drop' ); - expect( spyClipboardInput.firstCall.firstArg.dataTransfer ).to.equal( dataTransferMock ); - - fireDragEnd( dataTransferMock ); - expectFinalized(); - - expect( getModelData( model ) ).to.equal( 'foobarfoo[]' ); - expect( getViewData( view ) ).to.equal( '

foobarfoo{}

' ); - - env.isGecko = originalEnvGecko; - } ); - - it( 'should copy text to other place in the same editor (in Firefox)', () => { - const originalEnvGecko = env.isGecko; - - env.isGecko = true; - - setModelData( model, '[foo]bar' ); - - const clock = sinon.useFakeTimers(); - const dataTransferMock = createDataTransfer(); - const spyClipboardOutput = sinon.spy(); - const spyClipboardInput = sinon.spy(); - let targetPosition; - - viewDocument.on( 'clipboardOutput', ( event, data ) => spyClipboardOutput( data ) ); - viewDocument.on( 'clipboardInput', ( event, data ) => spyClipboardInput( data ) ); - - fireDragStart( dataTransferMock ); - expectDragStarted( dataTransferMock, 'foo', spyClipboardOutput ); - - // Dragging. - - targetPosition = model.createPositionAt( root.getChild( 0 ), 3 ); - fireDragging( dataTransferMock, targetPosition ); - clock.tick( 100 ); - - expectDraggingMarker( targetPosition ); - expect( getViewData( view, { renderUIElements: true } ) ).to.equal( - '

{foo}\u2060\u2060bar

' - ); - - fireDragging( dataTransferMock, targetPosition ); - clock.tick( 100 ); - - expectDraggingMarker( targetPosition ); - expect( getViewData( view, { renderUIElements: true } ) ).to.equal( - '

{foo}\u2060\u2060bar

' - ); - - // Dropping. - - targetPosition = model.createPositionAt( root.getChild( 0 ), 6 ); - dataTransferMock.dropEffect = 'copy'; - dataTransferMock.effectAllowed = 'copy'; - fireDrop( dataTransferMock, targetPosition ); - clock.tick( 100 ); - - expect( spyClipboardInput.called ).to.be.true; - expect( spyClipboardInput.firstCall.firstArg.method ).to.equal( 'drop' ); - expect( spyClipboardInput.firstCall.firstArg.dataTransfer ).to.equal( dataTransferMock ); - - fireDragEnd( dataTransferMock ); - expectFinalized(); - - expect( getModelData( model ) ).to.equal( 'foobarfoo[]' ); - expect( getViewData( view ) ).to.equal( '

foobarfoo{}

' ); - - env.isGecko = originalEnvGecko; - } ); - - it( 'should move text to other place in the same editor (over some widget)', () => { - setModelData( model, '[foo]bar' ); - - const clock = sinon.useFakeTimers(); - const dataTransferMock = createDataTransfer(); - const spyClipboardOutput = sinon.spy(); - const spyClipboardInput = sinon.spy(); - - viewDocument.on( 'clipboardOutput', ( event, data ) => spyClipboardOutput( data ) ); - viewDocument.on( 'clipboardInput', ( event, data ) => spyClipboardInput( data ) ); - - fireDragStart( dataTransferMock ); - expectDragStarted( dataTransferMock, 'foo', spyClipboardOutput ); - - // Dragging. - - const targetRange = model.createPositionAt( root.getChild( 1 ), 'after' ); - fireDragging( dataTransferMock, targetRange ); - clock.tick( 100 ); - - expectDraggingMarker( targetRange ); - expect( getViewData( view ) ).to.equal( - '

{foo}bar

' + - '
' + - '
' + - '
' + - '
' - ); - - // Dropping. - - dataTransferMock.dropEffect = 'move'; - fireDrop( dataTransferMock, targetRange ); - clock.tick( 100 ); - - expect( spyClipboardInput.called ).to.be.true; - expect( spyClipboardInput.firstCall.firstArg.method ).to.equal( 'drop' ); - expect( spyClipboardInput.firstCall.firstArg.dataTransfer ).to.equal( dataTransferMock ); - - fireDragEnd( dataTransferMock ); - expectFinalized(); - - expect( getModelData( model ) ).to.equal( - 'bar' + - '' + - 'foo[]' - ); - } ); - - it( 'should do nothing if dropped on dragged range', () => { - setModelData( model, '[foo]bar' ); - - const clock = sinon.useFakeTimers(); - const dataTransferMock = createDataTransfer(); - const spyClipboardOutput = sinon.spy(); - const spyClipboardInput = sinon.spy(); - - viewDocument.on( 'clipboardOutput', ( event, data ) => spyClipboardOutput( data ) ); - viewDocument.on( 'clipboardInput', ( event, data ) => spyClipboardInput( data ) ); - - fireDragStart( dataTransferMock ); - expectDragStarted( dataTransferMock, 'foo', spyClipboardOutput ); - - // Dropping. - - const targetPosition = model.createPositionAt( root.getChild( 0 ), 2 ); - dataTransferMock.dropEffect = 'move'; - dataTransferMock.effectAllowed = 'copyMove'; - fireDrop( dataTransferMock, targetPosition ); - clock.tick( 100 ); - - expect( spyClipboardInput.called ).to.be.false; - - fireDragEnd( dataTransferMock ); - expectFinalized(); - - expect( getModelData( model ) ).to.equal( '[foo]bar' ); - expect( getViewData( view ) ).to.equal( '

{foo}bar

' ); - } ); - - it( 'should copy text to from outside the editor', () => { - setModelData( model, '[]foobar' ); - - const clock = sinon.useFakeTimers(); - const dataTransferMock = createDataTransfer( { 'text/html': 'abc' } ); - const spyClipboardInput = sinon.spy(); - let targetPosition; - - viewDocument.on( 'clipboardInput', ( event, data ) => spyClipboardInput( data ) ); - - // Dragging. - - targetPosition = model.createPositionAt( root.getChild( 0 ), 5 ); - fireDragging( dataTransferMock, targetPosition ); - clock.tick( 100 ); - - expectDraggingMarker( targetPosition ); - expect( dataTransferMock.dropEffect ).to.equal( 'copy' ); - expect( getViewData( view, { renderUIElements: true } ) ).to.equal( - '

{}fooba\u2060\u2060r

' - ); - - // Dropping. - - targetPosition = model.createPositionAt( root.getChild( 0 ), 3 ); - dataTransferMock.dropEffect = 'copy'; - dataTransferMock.effectAllowed = 'copy'; - fireDrop( dataTransferMock, targetPosition ); - clock.tick( 100 ); - - expect( spyClipboardInput.called ).to.be.true; - expect( spyClipboardInput.firstCall.firstArg.method ).to.equal( 'drop' ); - expect( spyClipboardInput.firstCall.firstArg.dataTransfer ).to.equal( dataTransferMock ); - - fireDragEnd( dataTransferMock ); - expectFinalized(); - - expect( getModelData( model ) ).to.equal( 'fooabc[]bar' ); - expect( getViewData( view ) ).to.equal( '

fooabc{}bar

' ); - } ); - - it( 'should not remove dragged range if it is from other drag session', () => { - setModelData( model, '[foo]bar' ); - - const clock = sinon.useFakeTimers(); - let dataTransferMock = createDataTransfer(); - const spyClipboardOutput = sinon.spy(); - const spyClipboardInput = sinon.spy(); - - viewDocument.on( 'clipboardOutput', ( event, data ) => spyClipboardOutput( data ) ); - viewDocument.on( 'clipboardInput', ( event, data ) => spyClipboardInput( data ) ); - - fireDragStart( dataTransferMock ); - expectDragStarted( dataTransferMock, 'foo', spyClipboardOutput ); - - // Dragging finalized outside the editor without proper dragend event. - - dataTransferMock = createDataTransfer( { 'text/html': 'abc' } ); - - let targetPosition = model.createPositionAt( root.getChild( 0 ), 2 ); - fireDragging( dataTransferMock, targetPosition ); - clock.tick( 100 ); - - expectDraggingMarker( targetPosition ); - expect( getViewData( view, { renderUIElements: true } ) ).to.equal( - '

{fo\u2060\u2060o}bar

' - ); - - // Dropping. - - targetPosition = model.createPositionAt( root.getChild( 0 ), 3 ); - dataTransferMock.dropEffect = 'copy'; - dataTransferMock.effectAllowed = 'copy'; - fireDrop( dataTransferMock, targetPosition ); - clock.tick( 100 ); - - expect( spyClipboardInput.called ).to.be.true; - expect( spyClipboardInput.firstCall.firstArg.method ).to.equal( 'drop' ); - expect( spyClipboardInput.firstCall.firstArg.dataTransfer ).to.equal( dataTransferMock ); - - fireDragEnd( dataTransferMock ); - expectFinalized(); - - expect( getModelData( model ) ).to.equal( 'fooabc[]bar' ); - expect( getViewData( view ) ).to.equal( '

fooabc{}bar

' ); - } ); - - it( 'should not remove dragged range if insert into drop target was not allowed', () => { - editor.model.schema.extend( 'caption', { - allowIn: '$root' - } ); - - editor.conversion.elementToElement( { - view: 'caption', - model: 'caption' - } ); - - setModelData( model, - 'foo' + - '[bar
]' - ); - - const dataTransferMock = createDataTransfer(); - const viewElement = viewDocument.getRoot().getChild( 1 ); - const domNode = domConverter.mapViewToDom( viewElement ); - - const eventData = { - domTarget: domNode, - target: viewElement, - domEvent: {} - }; - - viewDocument.fire( 'mousedown', { - ...eventData - } ); - - viewDocument.fire( 'dragstart', { - ...eventData, - dataTransfer: dataTransferMock, - stopPropagation: () => {} - } ); - - expect( dataTransferMock.getData( 'text/html' ) ).to.equal( - '
bar
' - ); - - const targetPosition = model.createPositionAt( root.getChild( 0 ), 2 ); - fireDrop( dataTransferMock, targetPosition ); - - expect( getModelData( model ) ).to.equal( - 'foo' + - '[bar
]' - ); - } ); - - it( 'should properly move content even if dragend event is not fired', () => { - setModelData( model, '[foo]bar' ); - - const clock = sinon.useFakeTimers(); - const dataTransferMock = createDataTransfer(); - const spyClipboardOutput = sinon.spy(); - const spyClipboardInput = sinon.spy(); - let targetPosition; - - viewDocument.on( 'clipboardOutput', ( event, data ) => spyClipboardOutput( data ) ); - viewDocument.on( 'clipboardInput', ( event, data ) => spyClipboardInput( data ) ); - - fireDragStart( dataTransferMock ); - expectDragStarted( dataTransferMock, 'foo', spyClipboardOutput ); - - // Dragging. - - targetPosition = model.createPositionAt( root.getChild( 0 ), 3 ); - fireDragging( dataTransferMock, targetPosition ); - clock.tick( 100 ); - - expectDraggingMarker( targetPosition ); - expect( getViewData( view, { renderUIElements: true } ) ).to.equal( - '

{foo}\u2060\u2060bar

' - ); - - // Dragging. - - targetPosition = model.createPositionAt( root.getChild( 0 ), 5 ); - fireDragging( dataTransferMock, targetPosition ); - clock.tick( 100 ); - - expectDraggingMarker( targetPosition ); - expect( getViewData( view, { renderUIElements: true } ) ).to.equal( - '

{foo}ba\u2060\u2060r

' - ); - - // Dropping. - - dataTransferMock.dropEffect = 'move'; - targetPosition = model.createPositionAt( root.getChild( 0 ), 6 ); - fireDrop( dataTransferMock, targetPosition ); - clock.tick( 100 ); - - expect( spyClipboardInput.called ).to.be.true; - expect( spyClipboardInput.firstCall.firstArg.method ).to.equal( 'drop' ); - expect( spyClipboardInput.firstCall.firstArg.dataTransfer ).to.equal( dataTransferMock ); - - expect( getModelData( model ) ).to.equal( 'barfoo[]' ); - expect( getViewData( view ) ).to.equal( '

barfoo{}

' ); - - expectFinalized(); - } ); - - it( 'should not allow dropping if the editor is read-only', () => { - setModelData( model, '[foo]bar' ); - - const clock = sinon.useFakeTimers(); - const dataTransferMock = createDataTransfer(); - const spyClipboardOutput = sinon.spy(); - const spyClipboardInput = sinon.spy(); - let targetPosition; - - viewDocument.on( 'clipboardOutput', ( event, data ) => spyClipboardOutput( data ) ); - viewDocument.on( 'clipboardInput', ( event, data ) => spyClipboardInput( data ) ); - - fireDragStart( dataTransferMock ); - expectDragStarted( dataTransferMock, 'foo', spyClipboardOutput ); - - // Dragging. - - targetPosition = model.createPositionAt( root.getChild( 0 ), 3 ); - fireDragging( dataTransferMock, targetPosition ); - clock.tick( 100 ); - - expectDraggingMarker( targetPosition ); - expect( getViewData( view, { renderUIElements: true } ) ).to.equal( - '

{foo}\u2060\u2060bar

' - ); - - editor.enableReadOnlyMode( 'unit-test' ); - - // Dragging. - - targetPosition = model.createPositionAt( root.getChild( 0 ), 5 ); - fireDragging( dataTransferMock, targetPosition ); - clock.tick( 100 ); - - expect( dataTransferMock.dropEffect ).to.equal( 'none' ); - expect( model.markers.has( 'drop-target' ) ).to.be.false; - expect( getViewData( view, { renderUIElements: true } ) ).to.equal( '

{foo}bar

' ); - - editor.disableReadOnlyMode( 'unit-test' ); - // Dropping. - - dataTransferMock.dropEffect = 'move'; - targetPosition = model.createPositionAt( root.getChild( 0 ), 6 ); - fireDrop( dataTransferMock, targetPosition ); - clock.tick( 100 ); - - expect( spyClipboardInput.called ).to.be.true; - expect( spyClipboardInput.firstCall.firstArg.method ).to.equal( 'drop' ); - expect( spyClipboardInput.firstCall.firstArg.dataTransfer ).to.equal( dataTransferMock ); - - fireDragEnd( dataTransferMock ); - expectFinalized(); - - expect( getModelData( model ) ).to.equal( 'foobarfoo[]' ); - expect( getViewData( view ) ).to.equal( '

foobarfoo{}

' ); - } ); - - it( 'should not allow dropping if the plugin is disabled', () => { - setModelData( model, '[foo]bar' ); - - const plugin = editor.plugins.get( 'DragDropExperimental' ); - const clock = sinon.useFakeTimers(); - const dataTransferMock = createDataTransfer(); - const spyClipboardOutput = sinon.spy(); - const spyClipboardInput = sinon.spy(); - let targetPosition; - - viewDocument.on( 'clipboardOutput', ( event, data ) => spyClipboardOutput( data ) ); - viewDocument.on( 'clipboardInput', ( event, data ) => spyClipboardInput( data ) ); - - fireDragStart( dataTransferMock ); - expectDragStarted( dataTransferMock, 'foo', spyClipboardOutput ); - - // Dragging. - - targetPosition = model.createPositionAt( root.getChild( 0 ), 3 ); - fireDragging( dataTransferMock, targetPosition ); - clock.tick( 100 ); - - expectDraggingMarker( targetPosition ); - expect( getViewData( view, { renderUIElements: true } ) ).to.equal( - '

{foo}\u2060\u2060bar

' - ); - - plugin.forceDisabled( 'test' ); - - // Dragging. - - targetPosition = model.createPositionAt( root.getChild( 0 ), 5 ); - fireDragging( dataTransferMock, targetPosition ); - clock.tick( 100 ); - - expect( dataTransferMock.dropEffect ).to.equal( 'none' ); - expect( model.markers.has( 'drop-target' ) ).to.be.false; - expect( getViewData( view, { renderUIElements: true } ) ).to.equal( '

{foo}bar

' ); - - plugin.clearForceDisabled( 'test' ); - // Dropping. - - dataTransferMock.dropEffect = 'move'; - targetPosition = model.createPositionAt( root.getChild( 0 ), 6 ); - fireDrop( dataTransferMock, targetPosition ); - clock.tick( 100 ); - - expect( spyClipboardInput.called ).to.be.true; - expect( spyClipboardInput.firstCall.firstArg.method ).to.equal( 'drop' ); - expect( spyClipboardInput.firstCall.firstArg.dataTransfer ).to.equal( dataTransferMock ); - - fireDragEnd( dataTransferMock ); - expectFinalized(); - - expect( getModelData( model ) ).to.equal( 'foobarfoo[]' ); - expect( getViewData( view ) ).to.equal( '

foobarfoo{}

' ); - } ); - - it( 'should do nothing if dragging on Android', () => { - env.isAndroid = true; - - setModelData( model, '[foo]bar' ); - - const dataTransferMock = createDataTransfer(); - const spyClipboardInput = sinon.spy(); - - viewDocument.on( 'clipboardInput', ( event, data ) => spyClipboardInput( data ) ); - - fireDragStart( dataTransferMock ); - - expect( dataTransferMock.getData( 'text/html' ) ).to.equal( 'foo' ); - expect( dataTransferMock.effectAllowed ).to.equal( 'copyMove' ); - - expect( viewDocument.getRoot().hasAttribute( 'draggable' ) ).to.be.false; - - env.isAndroid = false; - } ); - - describe( 'dragstart', () => { - it( 'should not start dragging if the selection is collapsed', () => { - setModelData( model, 'foo[]bar' ); - - const dataTransferMock = createDataTransfer(); - const spyClipboardOutput = sinon.spy(); - const spyPreventDefault = sinon.spy(); - - viewDocument.on( 'clipboardOutput', ( event, data ) => spyClipboardOutput( data ) ); - - fireDragStart( dataTransferMock, spyPreventDefault ); - - expect( spyPreventDefault.called ).to.be.true; - expect( spyClipboardOutput.notCalled ).to.be.true; - } ); - - it( 'should not start dragging if the root editable would be dragged itself', () => { - setModelData( model, '[foo]bar' ); - - const dataTransferMock = createDataTransfer(); - const spyClipboardOutput = sinon.spy(); - const spyPreventDefault = sinon.spy(); - - viewDocument.on( 'clipboardOutput', ( event, data ) => spyClipboardOutput( data ) ); - - const eventData = prepareEventData( model.createPositionAt( root.getChild( 0 ), 3 ) ); - eventData.domTarget = view.getDomRoot(); - eventData.target = domConverter.mapDomToView( view.getDomRoot() ); - - viewDocument.fire( 'mousedown', { - ...eventData - } ); - - viewDocument.fire( 'dragstart', { - ...eventData, - dataTransfer: dataTransferMock, - preventDefault: spyPreventDefault, - stopPropagation: () => { - } - } ); - - expect( spyPreventDefault.called ).to.be.true; - expect( spyClipboardOutput.notCalled ).to.be.true; - } ); - - it( 'should not start dragging if the editable would be dragged itself', () => { - setModelData( model, '[foo]bar
' ); - - const dataTransferMock = createDataTransfer(); - const spyClipboardOutput = sinon.spy(); - const spyPreventDefault = sinon.spy(); - - viewDocument.on( 'clipboardOutput', ( event, data ) => spyClipboardOutput( data ) ); - - const modelElement = root.getNodeByPath( [ 0, 0, 0 ] ); - const eventData = prepareEventData( model.createPositionAt( modelElement.getChild( 0 ), 3 ) ); - eventData.target = mapper.toViewElement( modelElement ); - eventData.domTarget = domConverter.mapViewToDom( eventData.target ); - - expect( eventData.target.is( 'editableElement', 'td' ) ).to.be.true; - - viewDocument.fire( 'mousedown', { - ...eventData - } ); - - viewDocument.fire( 'dragstart', { - ...eventData, - dataTransfer: dataTransferMock, - preventDefault: spyPreventDefault, - stopPropagation: () => { - } - } ); - - expect( spyPreventDefault.called ).to.be.true; - expect( spyClipboardOutput.notCalled ).to.be.true; - } ); - - it( 'should mark allowed effect as "copy" if the editor is read-only', () => { - setModelData( model, '[foo]bar' ); - - const dataTransferMock = createDataTransfer(); - const spyClipboardOutput = sinon.spy(); - - editor.enableReadOnlyMode( 'unit-test' ); - - viewDocument.on( 'clipboardOutput', ( event, data ) => spyClipboardOutput( data ) ); - - fireDragStart( dataTransferMock ); - - expect( viewDocument.getRoot().hasAttribute( 'draggable' ) ).to.be.false; - expect( dataTransferMock.effectAllowed ).to.equal( 'copy' ); - } ); - - it( 'should start dragging by grabbing the widget selection handle', () => { - setModelData( model, - '[]foobar' + - 'abc
' - ); - - const dataTransferMock = createDataTransfer(); - const spyClipboardOutput = sinon.spy(); - const spyClipboardInput = sinon.spy(); - - viewDocument.on( 'clipboardOutput', ( event, data ) => spyClipboardOutput( data ) ); - viewDocument.on( 'clipboardInput', ( event, data ) => spyClipboardInput( data ) ); - - const domNode = view.getDomRoot().querySelector( '.ck-widget__selection-handle' ); - const widgetViewElement = viewDocument.getRoot().getChild( 1 ); - const selectionHandleElement = widgetViewElement.getChild( 0 ); - - expect( selectionHandleElement.hasClass( 'ck-widget__selection-handle' ) ).to.be.true; - - const eventData = { - domTarget: domNode, - target: selectionHandleElement, - domEvent: {} - }; - - viewDocument.fire( 'mousedown', { - ...eventData - } ); - - viewDocument.fire( 'dragstart', { - ...eventData, - dataTransfer: dataTransferMock, - stopPropagation: () => {} - } ); - - expect( dataTransferMock.getData( 'text/html' ) ).to.equal( - '
abc
' - ); - - expect( widgetViewElement.getAttribute( 'draggable' ) ).to.equal( 'true' ); - - expect( spyClipboardOutput.called ).to.be.true; - expect( spyClipboardOutput.firstCall.firstArg.method ).to.equal( 'dragstart' ); - expect( spyClipboardOutput.firstCall.firstArg.dataTransfer ).to.equal( dataTransferMock ); - expect( stringifyView( spyClipboardOutput.firstCall.firstArg.content ) ).to.equal( - '

abc

' - ); - - dataTransferMock.dropEffect = 'move'; - const targetPosition = model.createPositionAt( root.getChild( 0 ), 3 ); - fireDrop( dataTransferMock, targetPosition ); - - expect( spyClipboardInput.called ).to.be.true; - expect( spyClipboardInput.firstCall.firstArg.method ).to.equal( 'drop' ); - expect( spyClipboardInput.firstCall.firstArg.dataTransfer ).to.equal( dataTransferMock ); - - fireDragEnd( dataTransferMock ); - expectFinalized(); - - expect( getModelData( model ) ).to.equal( - 'foobar' + - '[abc
]' - ); - } ); - - it( 'should start dragging by grabbing the widget selection handle (in read only mode)', () => { - setModelData( model, - '[]foobar' + - 'abc
' - ); - - editor.enableReadOnlyMode( 'unit-test' ); - - const dataTransferMock = createDataTransfer(); - const spyClipboardOutput = sinon.spy(); - - viewDocument.on( 'clipboardOutput', ( event, data ) => spyClipboardOutput( data ) ); - - const domNode = view.getDomRoot().querySelector( '.ck-widget__selection-handle' ); - const widgetViewElement = viewDocument.getRoot().getChild( 1 ); - const selectionHandleElement = widgetViewElement.getChild( 0 ); - - expect( selectionHandleElement.hasClass( 'ck-widget__selection-handle' ) ).to.be.true; - - const eventData = { - domTarget: domNode, - target: selectionHandleElement, - domEvent: {} - }; - - viewDocument.fire( 'mousedown', { - ...eventData - } ); - - viewDocument.fire( 'dragstart', { - ...eventData, - dataTransfer: dataTransferMock, - stopPropagation: () => {} - } ); - - expect( dataTransferMock.getData( 'text/html' ) ).to.equal( - '
abc
' - ); - - expect( widgetViewElement.getAttribute( 'draggable' ) ).to.equal( 'true' ); - - expect( spyClipboardOutput.called ).to.be.true; - expect( spyClipboardOutput.firstCall.firstArg.method ).to.equal( 'dragstart' ); - expect( spyClipboardOutput.firstCall.firstArg.dataTransfer ).to.equal( dataTransferMock ); - expect( stringifyView( spyClipboardOutput.firstCall.firstArg.content ) ).to.equal( - '

abc

' - ); - } ); - - it( 'should start dragging by grabbing the widget element directly', () => { - setModelData( model, - '[]foobar' + - '' - ); - - const dataTransferMock = createDataTransfer(); - const spyClipboardOutput = sinon.spy(); - const spyClipboardInput = sinon.spy(); - - viewDocument.on( 'clipboardOutput', ( event, data ) => spyClipboardOutput( data ) ); - viewDocument.on( 'clipboardInput', ( event, data ) => spyClipboardInput( data ) ); - - const widgetViewElement = viewDocument.getRoot().getChild( 1 ); - const domNode = domConverter.mapViewToDom( widgetViewElement ); - - const eventData = { - domTarget: domNode, - target: widgetViewElement, - domEvent: {} - }; - - viewDocument.fire( 'mousedown', { - ...eventData - } ); - - viewDocument.fire( 'dragstart', { - ...eventData, - dataTransfer: dataTransferMock, - stopPropagation: () => {} - } ); - - expect( dataTransferMock.getData( 'text/html' ) ).to.equal( '
' ); - - expect( widgetViewElement.getAttribute( 'draggable' ) ).to.equal( 'true' ); - - expect( spyClipboardOutput.called ).to.be.true; - expect( spyClipboardOutput.firstCall.firstArg.method ).to.equal( 'dragstart' ); - expect( spyClipboardOutput.firstCall.firstArg.dataTransfer ).to.equal( dataTransferMock ); - expect( stringifyView( spyClipboardOutput.firstCall.firstArg.content ) ).to.equal( '
' ); - - dataTransferMock.dropEffect = 'move'; - const targetPosition = model.createPositionAt( root.getChild( 0 ), 3 ); - fireDrop( dataTransferMock, targetPosition ); - - expect( spyClipboardInput.called ).to.be.true; - expect( spyClipboardInput.firstCall.firstArg.method ).to.equal( 'drop' ); - expect( spyClipboardInput.firstCall.firstArg.dataTransfer ).to.equal( dataTransferMock ); - - fireDragEnd( dataTransferMock ); - expectFinalized(); - - expect( getModelData( model ) ).to.equal( - 'foobar' + - '[]' - ); - } ); - - it( 'should start dragging the selected text fragment', () => { - setModelData( model, - '[foo]bar' - ); - - const dataTransferMock = createDataTransfer(); - const spyClipboardOutput = sinon.spy(); - const spyClipboardInput = sinon.spy(); - - viewDocument.on( 'clipboardOutput', ( event, data ) => spyClipboardOutput( data ) ); - viewDocument.on( 'clipboardInput', ( event, data ) => spyClipboardInput( data ) ); - - const viewNode = viewDocument.getRoot().getChild( 0 ).getChild( 0 ); - const domNode = domConverter.findCorrespondingDomText( viewNode ); - - viewDocument.fire( 'mousedown', { - domTarget: domNode.parentNode, - target: viewNode.parent, - domEvent: {} - } ); - - viewDocument.fire( 'dragstart', { - domTarget: domNode, - target: null, // text node - domEvent: {}, - dataTransfer: dataTransferMock, - stopPropagation: () => {} - } ); - - expect( dataTransferMock.getData( 'text/html' ) ).to.equal( 'foo' ); - - expect( spyClipboardOutput.called ).to.be.true; - expect( spyClipboardOutput.firstCall.firstArg.method ).to.equal( 'dragstart' ); - expect( spyClipboardOutput.firstCall.firstArg.dataTransfer ).to.equal( dataTransferMock ); - expect( stringifyView( spyClipboardOutput.firstCall.firstArg.content ) ).to.equal( 'foo' ); - - dataTransferMock.dropEffect = 'move'; - const targetPosition = model.createPositionAt( root.getChild( 0 ), 4 ); - fireDrop( dataTransferMock, targetPosition ); - - expect( spyClipboardInput.called ).to.be.true; - expect( spyClipboardInput.firstCall.firstArg.method ).to.equal( 'drop' ); - expect( spyClipboardInput.firstCall.firstArg.dataTransfer ).to.equal( dataTransferMock ); - - fireDragEnd( dataTransferMock ); - expectFinalized(); - - expect( getModelData( model ) ).to.equal( - 'bfoo[]ar' - ); - } ); - - // TODO: what does it mean "(but not nested editable)"? - it( 'should start dragging by grabbing a widget nested element (but not nested editable)', () => { - setModelData( model, - '[]foobar' + - '' - ); - - const dataTransferMock = createDataTransfer(); - const spyClipboardOutput = sinon.spy(); - const spyClipboardInput = sinon.spy(); - - viewDocument.on( 'clipboardOutput', ( event, data ) => spyClipboardOutput( data ) ); - viewDocument.on( 'clipboardInput', ( event, data ) => spyClipboardInput( data ) ); - - const widgetViewElement = viewDocument.getRoot().getChild( 1 ); - const viewElement = widgetViewElement.getChild( 0 ); - const domNode = domConverter.mapViewToDom( viewElement ); - - expect( viewElement.is( 'element', 'hr' ) ).to.be.true; - - const eventData = { - domTarget: domNode, - target: viewElement, - domEvent: {} - }; - - viewDocument.fire( 'mousedown', { - ...eventData - } ); - - viewDocument.fire( 'dragstart', { - ...eventData, - dataTransfer: dataTransferMock, - stopPropagation: () => {} - } ); - - expect( dataTransferMock.getData( 'text/html' ) ).to.equal( '
' ); - - expect( widgetViewElement.getAttribute( 'draggable' ) ).to.equal( 'true' ); - - expect( spyClipboardOutput.called ).to.be.true; - expect( spyClipboardOutput.firstCall.firstArg.method ).to.equal( 'dragstart' ); - expect( spyClipboardOutput.firstCall.firstArg.dataTransfer ).to.equal( dataTransferMock ); - expect( stringifyView( spyClipboardOutput.firstCall.firstArg.content ) ).to.equal( '
' ); - - dataTransferMock.dropEffect = 'move'; - const targetPosition = model.createPositionAt( root.getChild( 0 ), 3 ); - fireDrop( dataTransferMock, targetPosition ); - - expect( spyClipboardInput.called ).to.be.true; - expect( spyClipboardInput.firstCall.firstArg.method ).to.equal( 'drop' ); - expect( spyClipboardInput.firstCall.firstArg.dataTransfer ).to.equal( dataTransferMock ); - - fireDragEnd( dataTransferMock ); - expectFinalized(); - - expect( getModelData( model ) ).to.equal( - 'foobar' + - '[]' - ); - } ); - - it( 'should not start dragging a widget if it is not a target for an event', () => { - setModelData( model, - 'foobar' + - '[]' - ); - - const dataTransferMock = createDataTransfer(); - const spyClipboardOutput = sinon.spy(); - - viewDocument.on( 'clipboardOutput', ( event, data ) => spyClipboardOutput( data ) ); - - const editableElement = viewDocument.getRoot(); - const editableDomNode = domConverter.mapViewToDom( editableElement ); - - const eventData = { - domTarget: editableDomNode, - target: editableElement, - domEvent: {} - }; - - viewDocument.fire( 'mousedown', { - ...eventData - } ); - - viewDocument.fire( 'dragstart', { - ...eventData, - dataTransfer: dataTransferMock, - stopPropagation: () => {}, - preventDefault: () => {} - } ); - - expect( spyClipboardOutput.notCalled ).to.be.true; - expect( dataTransferMock.getData( 'text/html' ) ).to.be.undefined; - } ); - - // TODO: this test looks invalid, "this._draggedRange" in experimental in most cases is not undefined. - it.skip( 'should not start dragging a widget if it is not a target for an event (but it was selected)', () => { - setModelData( model, - 'foobar' + - '[]' - ); - - const dataTransferMock = createDataTransfer(); - const spyClipboardOutput = sinon.spy(); - - viewDocument.on( 'clipboardOutput', ( event, data ) => spyClipboardOutput( data ) ); - - const targetElement = viewDocument.getRoot().getChild( 0 ); - const targetDomNode = domConverter.mapViewToDom( targetElement ); - - const eventData = { - domTarget: targetDomNode, - target: targetElement, - domEvent: {} - }; - - viewDocument.fire( 'mousedown', { - ...eventData - } ); - - viewDocument.fire( 'dragstart', { - ...eventData, - dataTransfer: dataTransferMock, - stopPropagation: () => {}, - preventDefault: () => {} - } ); - - expect( spyClipboardOutput.notCalled ).to.be.true; - expect( dataTransferMock.getData( 'text/html' ) ).to.be.undefined; - } ); - - it( 'should drag parent paragraph if entire content is selected', () => { - setModelData( model, - 'foobar' + - '[]
' - ); - - const dataTransferMock = createDataTransfer(); - const spyClipboardOutput = sinon.spy(); - const spyClipboardInput = sinon.spy(); - - viewDocument.on( 'clipboardOutput', ( event, data ) => spyClipboardOutput( data ) ); - viewDocument.on( 'clipboardInput', ( event, data ) => spyClipboardInput( data ) ); - - const modelElement = model.document.getRoot().getNodeByPath( [ 1, 0, 0, 0, 0 ] ); - const viewElement = mapper.toViewElement( modelElement ); - const domNode = domConverter.mapViewToDom( viewElement ); - - expect( viewElement.is( 'element', 'br' ) ).to.be.true; - - const eventData = { - domTarget: domNode, - target: viewElement, - domEvent: {} - }; - - viewDocument.fire( 'mousedown', { - ...eventData - } ); - - viewDocument.fire( 'dragstart', { - ...eventData, - dataTransfer: dataTransferMock, - stopPropagation: () => {} - } ); - - expect( dataTransferMock.getData( 'text/html' ) ).to.equal( '


 

' ); - - const modelCellElement = model.document.getRoot().getNodeByPath( [ 1, 0, 0 ] ); - const viewCellElement = mapper.toViewElement( modelCellElement ); - expect( viewCellElement.is( 'editableElement', 'td' ) ).to.be.true; - expect( viewCellElement.getAttribute( 'draggable' ) ).to.equal( 'true' ); - - expect( spyClipboardOutput.called ).to.be.true; - expect( spyClipboardOutput.firstCall.firstArg.method ).to.equal( 'dragstart' ); - expect( spyClipboardOutput.firstCall.firstArg.dataTransfer ).to.equal( dataTransferMock ); - expect( stringifyView( spyClipboardOutput.firstCall.firstArg.content ) ).to.equal( '



' ); - - dataTransferMock.dropEffect = 'move'; - const targetPosition = model.createPositionAt( root.getChild( 0 ), 3 ); - fireDrop( dataTransferMock, targetPosition ); - - expect( spyClipboardInput.called ).to.be.true; - expect( spyClipboardInput.firstCall.firstArg.method ).to.equal( 'drop' ); - expect( spyClipboardInput.firstCall.firstArg.dataTransfer ).to.equal( dataTransferMock ); - - fireDragEnd( dataTransferMock ); - expectFinalized(); - - expect( getModelData( model ) ).to.equal( - 'foobar []' + - '
' - ); - } ); - - it( 'should start dragging text from caption to paragraph', () => { - setModelData( model, trim` - - [World] - - Hello - ` ); - - const dataTransferMock = createDataTransfer(); - const viewElement = viewDocument.getRoot().getChild( 1 ); - const positionAfterHr = model.createPositionAt( root.getChild( 1 ), 'after' ); - - viewDocument.fire( 'dragstart', { - domTarget: domConverter.mapViewToDom( viewElement ), - target: viewElement, - domEvent: {}, - dataTransfer: dataTransferMock, - stopPropagation: () => {} - } ); - - expect( dataTransferMock.getData( 'text/html' ) ).to.equal( 'World' ); - - fireDragging( dataTransferMock, positionAfterHr ); - expectDraggingMarker( positionAfterHr ); - - fireDrop( - dataTransferMock, - model.createPositionAt( root.getChild( 1 ), 5 ) - ); - - expect( getModelData( model ) ).to.equal( trim` - - - - HelloWorld[] - ` ); - } ); - - it( 'should not drag parent paragraph when only portion of content is selected', () => { - setModelData( model, - 'foobar' + - 'ba[]z
' - ); - - const dataTransferMock = createDataTransfer(); - const spyClipboardOutput = sinon.spy(); - const spyClipboardInput = sinon.spy(); - - viewDocument.on( 'clipboardOutput', ( event, data ) => spyClipboardOutput( data ) ); - viewDocument.on( 'clipboardInput', ( event, data ) => spyClipboardInput( data ) ); - - const modelElement = model.document.getRoot().getNodeByPath( [ 1, 0, 0, 0, 2 ] ); - const viewElement = mapper.toViewElement( modelElement ); - const domNode = domConverter.mapViewToDom( viewElement ); - - expect( viewElement.is( 'element', 'br' ) ).to.be.true; - - const eventData = { - domTarget: domNode, - target: viewElement, - domEvent: {} - }; - - viewDocument.fire( 'mousedown', { - ...eventData - } ); - - viewDocument.fire( 'dragstart', { - ...eventData, - dataTransfer: dataTransferMock, - stopPropagation: () => {} - } ); - - expect( dataTransferMock.getData( 'text/html' ) ).to.equal( '
' ); - - const modelCellElement = model.document.getRoot().getNodeByPath( [ 1, 0, 0 ] ); - const viewCellElement = mapper.toViewElement( modelCellElement ); - expect( viewCellElement.is( 'editableElement', 'td' ) ).to.be.true; - expect( viewCellElement.getAttribute( 'draggable' ) ).to.equal( 'true' ); - - expect( spyClipboardOutput.called ).to.be.true; - expect( spyClipboardOutput.firstCall.firstArg.method ).to.equal( 'dragstart' ); - expect( spyClipboardOutput.firstCall.firstArg.dataTransfer ).to.equal( dataTransferMock ); - expect( stringifyView( spyClipboardOutput.firstCall.firstArg.content ) ).to.equal( '

' ); - - dataTransferMock.dropEffect = 'move'; - const targetPosition = model.createPositionAt( root.getChild( 0 ), 3 ); - fireDrop( dataTransferMock, targetPosition ); - - expect( spyClipboardInput.called ).to.be.true; - expect( spyClipboardInput.firstCall.firstArg.method ).to.equal( 'drop' ); - expect( spyClipboardInput.firstCall.firstArg.dataTransfer ).to.equal( dataTransferMock ); - - fireDragEnd( dataTransferMock ); - expectFinalized(); - - expect( getModelData( model ) ).to.equal( - 'foo[]bar' + - 'baz
' - ); - } ); - - it( 'should remove "draggable" attribute from widget element if mouseup before dragging start (selection handle)', () => { - setModelData( model, - '[]foobar' + - 'abc
' - ); - - const clock = sinon.useFakeTimers(); - const domNode = view.getDomRoot().querySelector( '.ck-widget__selection-handle' ); - const widgetViewElement = viewDocument.getRoot().getChild( 1 ); - const selectionHandleElement = widgetViewElement.getChild( 0 ); - - expect( selectionHandleElement.hasClass( 'ck-widget__selection-handle' ) ).to.be.true; - - const eventData = { - domTarget: domNode, - target: selectionHandleElement, - domEvent: {} - }; - - viewDocument.fire( 'mousedown', { - ...eventData - } ); - - expect( widgetViewElement.getAttribute( 'draggable' ) ).to.equal( 'true' ); - - viewDocument.fire( 'mouseup' ); - clock.tick( 50 ); - - expect( widgetViewElement.hasAttribute( 'draggable' ) ).to.be.false; - } ); - - it( 'should remove "draggable" attribute from widget element if mouseup before dragging start (widget)', () => { - setModelData( model, - '[]foobar' + - '' - ); - - const clock = sinon.useFakeTimers(); - const widgetViewElement = viewDocument.getRoot().getChild( 1 ); - const viewElement = widgetViewElement.getChild( 0 ); - const domNode = domConverter.mapViewToDom( viewElement ); - - expect( viewElement.is( 'element', 'hr' ) ).to.be.true; - - const eventData = { - domTarget: domNode, - target: viewElement, - domEvent: {} - }; - - viewDocument.fire( 'mousedown', { - ...eventData - } ); - - expect( widgetViewElement.getAttribute( 'draggable' ) ).to.equal( 'true' ); - - viewDocument.fire( 'mouseup' ); - clock.tick( 50 ); - - expect( widgetViewElement.hasAttribute( 'draggable' ) ).to.be.false; - } ); - - it( 'can drag multiple elements', () => { - setModelData( model, - '
' + - '[foo' + - 'bar]' + - 'baz' + - '
' + - '' - ); - - const dataTransferMock = createDataTransfer(); - const positionAfterHr = model.createPositionAt( root.getChild( 1 ), 'after' ); - - fireDragStart( dataTransferMock ); - expectDragStarted( dataTransferMock, '

foo

bar

' ); - - fireDragging( dataTransferMock, positionAfterHr ); - expectDraggingMarker( positionAfterHr ); - } ); - - it( 'should remove "draggable" attribute from editable element', () => { - setModelData( model, '[foo]bar' ); - - const clock = sinon.useFakeTimers(); - const editableElement = viewDocument.getRoot(); - const viewElement = editableElement.getChild( 0 ); - const domNode = domConverter.mapViewToDom( viewElement ); - - expect( viewElement.is( 'element', 'p' ) ).to.be.true; - - const eventData = { - domTarget: domNode, - target: viewElement, - domEvent: {} - }; - - viewDocument.fire( 'mousedown', { - ...eventData - } ); - - expect( editableElement.getAttribute( 'draggable' ) ).to.equal( 'true' ); - - viewDocument.fire( 'mouseup' ); - clock.tick( 50 ); - - expect( editableElement.hasAttribute( 'draggable' ) ).to.be.false; - } ); - - it( 'should only show one preview element when you drag element outside the editing root', () => { - setModelData( model, - '
' + - '[foo' + - 'bar]' + - 'baz' + - '
' + - '' - ); - - const pilcrow = document.createElement( 'div' ); - pilcrow.setAttribute( 'class', 'pilcrow' ); - - const dataTransferMock = createDataTransfer(); - - fireDragStart( dataTransferMock, () => {}, pilcrow ); - fireDragStart( dataTransferMock, () => {}, pilcrow ); - - const numberOfCkContentElements = Object - .keys( document.getElementsByClassName( 'ck-content' ) ) - .length; - - // There should be two elements with the `.ck-content` class - editor and drag-and-drop preview. - expect( numberOfCkContentElements ).to.equal( 2 ); - } ); - - it( 'should show preview with custom implementation if drag element outside the editing root', () => { - setModelData( editor.model, '[Foo.]' ); - - const dataTransfer = createDataTransfer( {} ); - - const spy = sinon.spy( dataTransfer, 'setDragImage' ); - const clientX = 10; - - viewDocument.fire( 'dragstart', { - dataTransfer, - preventDefault: sinon.spy(), - stopPropagation: sinon.spy(), - domEvent: { - clientX - } - } ); - - const editable = editor.editing.view.document.selection.editableElement; - const domEditable = editor.editing.view.domConverter.mapViewToDom( editable ); - const computedStyle = window.getComputedStyle( domEditable ); - const paddingLeftString = computedStyle.paddingLeft; - const paddingLeft = parseFloat( paddingLeftString ); - - const domRect = new Rect( domEditable ); - - sinon.assert.calledWith( spy, sinon.match( { - style: { - 'padding-left': `${ domRect.left - clientX + paddingLeft }px` - }, - className: 'ck ck-content', - firstChild: sinon.match( { - tagName: 'P', - innerHTML: 'Foo.' - } ) - } ), 0, 0 ); - sinon.assert.calledOnce( spy ); - } ); - - it( 'should show preview with browser implementation if drag element inside the editing root', () => { - setModelData( editor.model, '[Foo.]' ); - - const dataTransfer = createDataTransfer( {} ); - - const spy = sinon.spy( dataTransfer, 'setDragImage' ); - - const modelElement = root.getNodeByPath( [ 0 ] ); - const viewElement = mapper.toViewElement( modelElement ); - const domElement = domConverter.mapViewToDom( viewElement ); - - viewDocument.fire( 'dragstart', { - dataTransfer, - preventDefault: sinon.spy(), - stopPropagation: sinon.spy(), - domEvent: getMockedMousePosition( domElement ), - domTarget: domElement - } ); - - sinon.assert.notCalled( spy ); - } ); - } ); - - describe( 'dragenter', () => { - it( 'should focus the editor while dragging over the editable', () => { - const stubFocus = sinon.stub( view, 'focus' ); - - viewDocument.fire( 'dragenter', {} ); - - expect( stubFocus.calledOnce ).to.be.true; - } ); - - it( 'should not focus the editor while dragging over disabled editor', () => { - const stubFocus = sinon.stub( view, 'focus' ); - - editor.enableReadOnlyMode( 'unit-test' ); - - viewDocument.fire( 'dragenter' ); - - expect( stubFocus.calledOnce ).to.be.false; - } ); - } ); - - describe( 'dragleave', () => { - it( 'should remove drop target marker', () => { - setModelData( model, '[foo]bar' ); - - const clock = sinon.useFakeTimers(); - const dataTransferMock = createDataTransfer(); - - fireDragStart( dataTransferMock ); - expectDragStarted( dataTransferMock, 'foo' ); - - fireDragging( dataTransferMock, model.createPositionAt( root.getChild( 0 ), 3 ) ); - clock.tick( 100 ); - - viewDocument.fire( 'dragleave' ); - expect( model.markers.has( 'drop-target' ) ).to.be.true; - - clock.tick( 100 ); - expect( model.markers.has( 'drop-target' ) ).to.be.false; - } ); - - it( 'should not remove drop target marker if dragging left some nested element', () => { - setModelData( model, '[foo]bar' ); - - const spy = sinon.spy(); - const clock = sinon.useFakeTimers(); - const dataTransferMock = createDataTransfer(); - - model.markers.on( 'update:drop-target', ( evt, marker, oldRange, newRange ) => { - if ( !newRange ) { - spy(); - } - } ); - - fireDragStart( dataTransferMock ); - expectDragStarted( dataTransferMock, 'foo' ); - - let targetPosition = model.createPositionAt( root.getChild( 0 ), 3 ); - fireDragging( dataTransferMock, targetPosition ); - clock.tick( 100 ); - - viewDocument.fire( 'dragleave' ); - expectDraggingMarker( targetPosition ); - - clock.tick( 10 ); - expectDraggingMarker( targetPosition ); - - targetPosition = model.createPositionAt( root.getChild( 0 ), 4 ); - fireDragging( dataTransferMock, targetPosition ); - clock.tick( 60 ); - - expectDraggingMarker( targetPosition ); - expect( spy.notCalled ).to.be.true; - } ); - - it( 'should not focus the editor while dragging over disabled editor', () => { - const stubFocus = sinon.stub( view, 'focus' ); - - editor.enableReadOnlyMode( 'unit-test' ); - - viewDocument.fire( 'dragenter' ); - - expect( stubFocus.calledOnce ).to.be.false; - } ); - } ); - - describe( 'dragover', () => { - it( 'should put drop target marker inside a text node', () => { - setModelData( model, '[]foobar' ); - - const dataTransferMock = createDataTransfer(); - const targetPosition = model.createPositionAt( root.getChild( 0 ), 2 ); - - fireDragging( dataTransferMock, targetPosition ); - - expectDraggingMarker( targetPosition ); - } ); - - // TODO: this should be fixed in code. - it.skip( 'cannot be dropped on non-editable place.', () => { - setModelData( model, '[]foobar' ); - - const dataTransferMock = createDataTransfer(); - const targetPosition = model.createPositionAt( root.getChild( 0 ), 2 ); - - // For selection to be in non-editable place by overwriting `canEditAt()`. - model.on( 'canEditAt', evt => { - evt.return = false; - evt.stop(); - }, { priority: 'highest' } ); - - fireDragging( dataTransferMock, targetPosition ); - - expect( model.markers.has( 'drop-target' ) ).to.be.false; - } ); - - it( 'should put drop target marker inside and attribute element', () => { - setModelData( model, '[]foo<$text bold="true">bar' ); - - const dataTransferMock = createDataTransfer(); - - const viewElement = viewDocument.getRoot().getChild( 0 ).getChild( 1 ); - const domNode = domConverter.mapViewToDom( viewElement ); - - expect( viewElement.is( 'attributeElement' ) ).to.be.true; - - viewDocument.fire( 'dragging', { - domTarget: domNode, - target: viewElement, - targetRanges: [ view.createRange( view.createPositionAt( viewElement.getChild( 0 ), 2 ) ) ], - dataTransfer: dataTransferMock, - domEvent: getMockedMousePosition( domNode ) - } ); - - expectDraggingMarker( model.createPositionAt( root.getChild( 0 ), 5 ) ); - } ); - - it( 'should put marker before element when mouse position is on the upper half of it', () => { - setModelData( model, - '[]foobar' + - 'abc
' - ); - - const dataTransferMock = createDataTransfer(); - - const viewElement = viewDocument.getRoot().getChild( 1 ).getChild( 0 ); - const domNode = domConverter.mapViewToDom( viewElement ); - - expect( viewElement.hasClass( 'ck-widget__selection-handle' ) ).to.be.true; - - viewDocument.fire( 'dragging', { - domTarget: domNode, - target: viewElement, - dataTransfer: dataTransferMock, - domEvent: getMockedMousePosition( domNode ) - } ); - - expectDraggingMarker( model.createPositionAt( root.getChild( 1 ), 'before' ) ); - } ); - - it( 'should put marker after element when mouse position is on the bottom half of it', () => { - setModelData( model, - '[]foobar' + - 'abc
' - ); - - const dataTransferMock = createDataTransfer(); - - const viewElement = viewDocument.getRoot().getChild( 1 ).getChild( 0 ); - const domNode = domConverter.mapViewToDom( viewElement ); - - expect( viewElement.hasClass( 'ck-widget__selection-handle' ) ).to.be.true; - - const widgetUIHeight = 25; - - viewDocument.fire( 'dragging', { - domTarget: domNode, - target: viewElement, - dataTransfer: dataTransferMock, - domEvent: getMockedMousePosition( domNode, 'after', widgetUIHeight ) - } ); - - expectDraggingMarker( model.createPositionAt( root.getChild( 1 ), 'after' ) ); - } ); - - it( 'should find ancestor widget while hovering over inner content of widget (but not nested editable)', () => { - setModelData( model, - '[]foobar' + - 'abc
' - ); - - const dataTransferMock = createDataTransfer(); - - const modelElement = root.getNodeByPath( [ 1, 0 ] ); - const viewElement = mapper.toViewElement( modelElement ); - const domNode = domConverter.mapViewToDom( viewElement ); - - expect( viewElement.is( 'element', 'tr' ) ).to.be.true; - - viewDocument.fire( 'dragging', { - domTarget: domNode, - target: viewElement, - dataTransfer: dataTransferMock, - domEvent: getMockedMousePosition( domNode ) - } ); - - expectDraggingMarker( model.createPositionAt( root.getChild( 1 ), 'before' ) ); - } ); - - it( 'should find drop position while hovering over empty nested editable', () => { - setModelData( model, - '[]foobar' + - '
' - ); - - const dataTransferMock = createDataTransfer(); - - const modelElement = root.getNodeByPath( [ 1, 0, 0 ] ); - const viewElement = mapper.toViewElement( modelElement ); - const domNode = domConverter.mapViewToDom( viewElement ); - - expect( viewElement.is( 'element', 'td' ) ).to.be.true; - - viewDocument.fire( 'dragging', { - domTarget: domNode, - target: viewElement, - dataTransfer: dataTransferMock, - domEvent: getMockedMousePosition( domNode ) - } ); - - expectDraggingMarker( model.createPositionAt( root.getNodeByPath( [ 1, 0, 0, 0 ] ), 'before' ) ); - } ); - - it( 'should find drop position while hovering over space between blocks', () => { - setModelData( model, - '[]foobar' + - '
' - ); - - const dataTransferMock = createDataTransfer(); - - const rootElement = viewDocument.getRoot(); - const viewElement = rootElement; - const domNode = domConverter.mapViewToDom( viewElement ); - - const nestedModelParagraph = root.getNodeByPath( [ 1, 0, 0, 0 ] ); - const nestedViewParagraph = mapper.toViewElement( nestedModelParagraph ); - const nestedParagraphDomNode = domConverter.mapViewToDom( nestedViewParagraph ); - - expect( viewElement.is( 'rootElement' ) ).to.be.true; - - viewDocument.fire( 'dragging', { - domTarget: domNode, - target: rootElement, - targetRanges: [ view.createRangeOn( nestedModelParagraph ) ], - dataTransfer: dataTransferMock, - domEvent: getMockedMousePosition( nestedParagraphDomNode ) - } ); - - expectDraggingMarker( model.createPositionAt( root.getChild( 1 ), 'before' ) ); - } ); - - it( 'should find drop position while hovering over table figure', () => { - setModelData( model, - '[]foobar' + - 'abc
' - ); - - const dataTransferMock = createDataTransfer(); - - const rootElement = viewDocument.getRoot(); - const modelElement = root.getNodeByPath( [ 1, 0, 0 ] ); - const viewElement = mapper.toViewElement( modelElement ); - const domNode = domConverter.mapViewToDom( viewElement ); - - expect( viewElement.is( 'element', 'td' ) ).to.be.true; - - viewDocument.fire( 'dragging', { - domTarget: domNode, - target: viewElement, - targetRanges: [ view.createRange( view.createPositionAt( rootElement.getChild( 1 ), 1 ) ) ], - dataTransfer: dataTransferMock, - domEvent: getMockedMousePosition( domNode ) - } ); - - expectDraggingMarker( model.createPositionAt( root.getNodeByPath( [ 1, 0, 0, 0 ] ), 'before' ) ); - } ); - - it( 'should find drop position while hovering over table with target position inside after paragraph', () => { - setModelData( model, - '[]foobar' + - 'abc
' - ); - - const dataTransferMock = createDataTransfer(); - - const modelElement = root.getNodeByPath( [ 1, 0, 0 ] ); - const viewElement = mapper.toViewElement( modelElement ); - const domNode = domConverter.mapViewToDom( viewElement ); - - const paragraphModel = root.getNodeByPath( [ 1, 0, 0, 0 ] ); - const paragraphView = mapper.toViewElement( paragraphModel ); - - viewDocument.fire( 'dragging', { - domTarget: domNode, - target: viewElement, - targetRanges: [ view.createRange( view.createPositionAt( paragraphView, 'after' ) ) ], - dataTransfer: dataTransferMock, - domEvent: getMockedMousePosition( domNode, 'after' ) - } ); - - expectDraggingMarker( model.createPositionAt( root.getNodeByPath( [ 1, 0, 0, 0 ] ), 'after' ) ); - } ); - - it( 'should find drop position while hovering over space between blocks but the following element is not an object', () => { - setModelData( model, - '[]foo' + - 'bar' - ); - - const dataTransferMock = createDataTransfer(); - - const rootElement = viewDocument.getRoot(); - const viewElement = rootElement; - const domNode = domConverter.mapViewToDom( viewElement ); - - const firstParagraphModelElement = root.getChild( 1 ); - const firstParagraphViewElement = mapper.toViewElement( firstParagraphModelElement ); - const firstParagraphDomNode = domConverter.mapViewToDom( firstParagraphViewElement ); - - expect( viewElement.is( 'rootElement' ) ).to.be.true; - - viewDocument.fire( 'dragging', { - domTarget: domNode, - target: rootElement, - targetRanges: [ view.createRangeOn( firstParagraphModelElement ) ], - dataTransfer: dataTransferMock, - domEvent: getMockedMousePosition( firstParagraphDomNode ) - } ); - - expectDraggingMarker( model.createPositionAt( firstParagraphModelElement, 'before' ) ); - } ); - - it( 'should find drop position while hovering after widget without content (not Firefox)', () => { - const originalEnvGecko = env.isGecko; - - env.isGecko = false; - - setModelData( model, - '[]foo' + - '' + - 'bar' - ); - - const dataTransferMock = createDataTransfer(); - - const rootElement = viewDocument.getRoot(); - const domNode = domConverter.mapViewToDom( rootElement ); - - const modelElement = root.getNodeByPath( [ 1 ] ); - const viewWidget = mapper.toViewElement( modelElement ); - const domWidget = domConverter.mapViewToDom( viewWidget ); - - viewDocument.fire( 'dragging', { - domTarget: domNode, - target: rootElement, - targetRanges: [ view.createRange( view.createPositionAt( viewWidget, 'after' ) ) ], - dataTransfer: dataTransferMock, - domEvent: getMockedMousePosition( domWidget, 'after' ) - } ); - - expectDraggingMarker( model.createPositionAt( root.getChild( 1 ), 'after' ) ); - - env.isGecko = originalEnvGecko; - } ); - - it( 'should find drop position while hovering after widget without content (in Firefox)', () => { - const originalEnvGecko = env.isGecko; - - env.isGecko = true; - - setModelData( model, - '[]foo' + - '
' + - 'bar' - ); - - const dataTransferMock = createDataTransfer(); - - const modelWidget = root.getNodeByPath( [ 1, 0 ] ); - const viewWidget = mapper.toViewElement( modelWidget ); - const domWidget = domConverter.mapViewToDom( viewWidget ); - - const modelQuote = root.getNodeByPath( [ 1 ] ); - const viewQuote = mapper.toViewElement( modelQuote ); - const domQuote = domConverter.mapViewToDom( viewQuote ); - - viewDocument.fire( 'dragging', { - domTarget: domQuote, - target: viewQuote, - targetRanges: [ view.createRange( view.createPositionAt( viewWidget, 'after' ) ) ], - dataTransfer: dataTransferMock, - domEvent: getMockedMousePosition( domWidget, 'after' ) - } ); - - expectDraggingMarker( model.createPositionAt( modelWidget, 'after' ) ); - - env.isGecko = originalEnvGecko; - } ); - } ); - - describe( 'dragend', () => { - it( 'should reset block dragging when dropped outside the editable', () => { - setModelData( model, - 'foobar' + - '[]' - ); - - const plugin = editor.plugins.get( 'DragDropExperimental' ); - const modelElement = model.document.getRoot().getNodeByPath( [ 1 ] ); - const viewElement = mapper.toViewElement( modelElement ); - - // Fire the 'dragstart' event to change the '_blockMode to true - viewDocument.fire( 'dragstart', { - domTarget: domConverter.mapViewToDom( viewElement ), - target: viewElement, - domEvent: {}, - dataTransfer: createDataTransfer( {} ), - preventDefault: sinon.spy(), - stopPropagation: sinon.spy() - } ); - - // Fire the 'dragend' event on the document - document.dispatchEvent( new Event( 'dragend' ) ); - - // Check if the blockMode changes to 'false' - expect( plugin._blockMode ).to.be.false; - expect( model.markers.has( 'drop-target' ) ).to.be.false; - } ); - } ); - - describe( 'drop', () => { - // TODO: to be discussed. - it.skip( 'should update targetRanges', () => { - setModelData( model, - '[]foobar' + - '' - ); - - const dataTransferMock = createDataTransfer(); - const spyClipboardInput = sinon.spy(); - - viewDocument.on( 'clipboardInput', ( event, data ) => spyClipboardInput( data ) ); - - dataTransferMock.dropEffect = 'move'; - const targetPosition = model.createPositionAt( root.getChild( 1 ), 0 ); - fireDrop( dataTransferMock, targetPosition ); - - expect( spyClipboardInput.called ).to.be.true; - - const data = spyClipboardInput.firstCall.firstArg; - expect( data.method ).to.equal( 'drop' ); - expect( data.dataTransfer ).to.equal( dataTransferMock ); - expect( data.targetRanges.length ).to.equal( 1 ); - expect( data.targetRanges[ 0 ].isEqual( view.createRangeOn( viewDocument.getRoot().getChild( 1 ) ) ) ).to.be.true; - } ); - } ); - - describe( 'extending selection range when all parent elements are selected', () => { - it( 'extends flat selection', () => { - setModelData( model, trim` -
- [one - two - three] -
- - ` ); - - const dataTransferMock = createDataTransfer(); - const positionAfterHr = model.createPositionAt( root.getChild( 1 ), 'after' ); - - fireDragStart( dataTransferMock ); - expectDragStarted( dataTransferMock, trim` -
-

one

-

two

-

three

-
- ` ); - - fireDragging( dataTransferMock, positionAfterHr ); - expectDraggingMarker( positionAfterHr ); - } ); - - it( 'extends nested selection', () => { - setModelData( model, trim` -
- [one -
- two - three - four -
- five] -
- - ` ); - - const dataTransferMock = createDataTransfer(); - const positionAfterHr = model.createPositionAt( root.getChild( 1 ), 'after' ); - - fireDragStart( dataTransferMock ); - expectDragStarted( dataTransferMock, trim` -
-

one

-
-

two

-

three

-

four

-
-

five

-
- ` ); - - fireDragging( dataTransferMock, positionAfterHr ); - expectDraggingMarker( positionAfterHr ); - } ); - - it( 'extends selection when it starts at different level than it ends', () => { - setModelData( model, trim` -
-
- [one - two - three -
- four] -
- - ` ); - - const dataTransferMock = createDataTransfer(); - const positionAfterHr = model.createPositionAt( root.getChild( 1 ), 'after' ); - - fireDragStart( dataTransferMock ); - expectDragStarted( dataTransferMock, trim` -
-
-

one

-

two

-

three

-
-

four

-
- ` ); - - fireDragging( dataTransferMock, positionAfterHr ); - expectDraggingMarker( positionAfterHr ); - } ); - - it( 'extends selection when it ends at different level than it starts', () => { - setModelData( model, trim` -
- [one -
- two - three - four] -
-
- - ` ); - - const dataTransferMock = createDataTransfer(); - const positionAfterHr = model.createPositionAt( root.getChild( 1 ), 'after' ); - - fireDragStart( dataTransferMock ); - expectDragStarted( dataTransferMock, trim` -
-

one

-
-

two

-

three

-

four

-
-
- ` ); - - fireDragging( dataTransferMock, positionAfterHr ); - expectDraggingMarker( positionAfterHr ); - } ); - } ); - } ); - - describe( 'integration with the WidgetToolbarRepository plugin', () => { - let editor, widgetToolbarRepository, editorElement, viewDocument; - - beforeEach( () => { - editorElement = document.createElement( 'div' ); - document.body.appendChild( editorElement ); - - return ClassicTestEditor - .create( editorElement, { - plugins: [ Paragraph, WidgetToolbarRepository, DragDropExperimental, HorizontalLine ] - } ) - .then( newEditor => { - editor = newEditor; - viewDocument = editor.editing.view.document; - widgetToolbarRepository = editor.plugins.get( WidgetToolbarRepository ); - - editor.setData( '

' ); - } ); - } ); - - afterEach( async () => { - await editor.destroy(); - editorElement.remove(); - } ); - - describe( 'WidgetToolbarRepository#isEnabled', () => { - it( 'is enabled by default', () => { - expect( widgetToolbarRepository.isEnabled ).to.be.true; - } ); - - it( 'is enabled when starts dragging the text node', () => { - setModelData( editor.model, '[Foo.]' ); - - const nodeModel = root.getNodeByPath( [ 0 ] ); - const nodeView = mapper.toViewElement( nodeModel ); - const nodeDOM = domConverter.mapViewToDom( nodeView ); - const dataTransfer = createDataTransfer( {} ); - - viewDocument.fire( 'dragstart', { - dataTransfer, - preventDefault: sinon.spy(), - stopPropagation: sinon.spy(), - domEvent: getMockedMousePosition( nodeDOM ) - } ); - - expect( widgetToolbarRepository.isEnabled ).to.be.true; - } ); - - it( 'is disabled when plugin is disabled', () => { - setModelData( editor.model, 'Foo.[]' ); - - const nodeModel = root.getNodeByPath( [ 0 ] ); - const nodeView = mapper.toViewElement( nodeModel ); - const nodeDOM = domConverter.mapViewToDom( nodeView ); - - const plugin = editor.plugins.get( 'DragDropExperimental' ); - plugin.isEnabled = false; - - viewDocument.fire( 'dragstart', { - preventDefault: sinon.spy(), - target: viewDocument.getRoot().getChild( 1 ), - dataTransfer: createDataTransfer( {} ), - stopPropagation: sinon.spy(), - domEvent: getMockedMousePosition( nodeDOM ) - } ); - - expect( widgetToolbarRepository.isEnabled ).to.be.false; - } ); - - it( 'is disabled when starts dragging the widget', () => { - setModelData( editor.model, 'Foo.[]' ); - - const nodeModel = root.getNodeByPath( [ 0 ] ); - const nodeView = mapper.toViewElement( nodeModel ); - const nodeDOM = domConverter.mapViewToDom( nodeView ); - - viewDocument.fire( 'dragstart', { - preventDefault: sinon.spy(), - target: viewDocument.getRoot().getChild( 1 ), - dataTransfer: createDataTransfer( {} ), - stopPropagation: sinon.spy(), - domEvent: getMockedMousePosition( nodeDOM ) - } ); - - expect( widgetToolbarRepository.isEnabled ).to.be.false; - } ); - - it( 'is enabled when ends dragging (drop in the editable)', () => { - setModelData( editor.model, '[]' ); - - const dataTransfer = createDataTransfer( {} ); - - const nodeModel = root.getNodeByPath( [ 0 ] ); - const nodeView = mapper.toViewElement( nodeModel ); - const nodeDOM = domConverter.mapViewToDom( nodeView ); - - viewDocument.fire( 'dragstart', { - preventDefault: sinon.spy(), - target: viewDocument.getRoot().getChild( 0 ), - dataTransfer, - stopPropagation: sinon.spy(), - domEvent: getMockedMousePosition( nodeDOM ) - } ); - - expect( widgetToolbarRepository.isEnabled ).to.be.false; - - viewDocument.fire( 'drop', { - preventDefault: sinon.spy(), - stopPropagation: sinon.spy(), - target: viewDocument.getRoot().getChild( 0 ), - dataTransfer, - method: 'drop', - domEvent: { - clientX: sinon.spy(), - clientY: sinon.spy() - } - } ); - - expect( widgetToolbarRepository.isEnabled ).to.be.true; - } ); - - it( 'is enabled when ends dragging (drop outside the editable)', () => { - setModelData( editor.model, '[]' ); - - const dataTransfer = createDataTransfer( {} ); - - const nodeModel = root.getNodeByPath( [ 0 ] ); - const nodeView = mapper.toViewElement( nodeModel ); - const nodeDOM = domConverter.mapViewToDom( nodeView ); - - viewDocument.fire( 'dragstart', { - preventDefault: sinon.spy(), - target: viewDocument.getRoot().getChild( 0 ), - dataTransfer, - stopPropagation() {}, - domEvent: getMockedMousePosition( nodeDOM ) - } ); - - expect( widgetToolbarRepository.isEnabled ).to.be.false; - - viewDocument.fire( 'dragend', { - preventDefault: sinon.spy(), - dataTransfer - } ); - - expect( widgetToolbarRepository.isEnabled ).to.be.true; - } ); - } ); - } ); - - describe( 'integration with paragraph-only editor', () => { - beforeEach( async () => { - editorElement = document.createElement( 'div' ); - document.body.appendChild( editorElement ); - - editor = await ClassicTestEditor.create( editorElement, { - useInlineRoot: true, - plugins: [ DragDropExperimental, PastePlainText, Paragraph, Bold ] - } ); - - model = editor.model; - root = model.document.getRoot(); - mapper = editor.editing.mapper; - view = editor.editing.view; - viewDocument = view.document; - domConverter = view.domConverter; - } ); - - afterEach( async () => { - await editor.destroy(); - editorElement.remove(); - } ); - - it( 'handles paste', () => { - setModelData( model, - 'foo[]' - ); - - editor.editing.view.document.fire( 'paste', { - dataTransfer: createDataTransfer( { 'text/html': 'bar' } ), - stopPropagation() {}, - preventDefault() {} - } ); - - expect( getModelData( model ) ).to.equal( 'foo<$text bold="true">bar[]' ); - } ); - - it( 'stops `clipboardInput` event', () => { - setModelData( model, - 'foo[]' - ); - - const spyClipboardInput = sinon.spy(); - const rootElement = viewDocument.getRoot(); - const domNode = domConverter.mapViewToDom( rootElement ); - - viewDocument.on( 'clipboardInput', ( event, data ) => spyClipboardInput( data ) ); - - viewDocument.fire( 'clipboardInput', { - domTarget: domNode, - target: rootElement, - method: 'drop', - dataTransfer: createDataTransfer(), - domEvent: getMockedMousePosition( domNode ), - stopPropagation: () => {}, - preventDefault: () => {} - } ); - - expect( spyClipboardInput.called ).to.be.false; - } ); - } ); - - function fireDragStart( dataTransferMock, preventDefault = () => {}, domTarget ) { - const eventData = prepareEventData( model.document.selection.getLastPosition(), domTarget ); - - viewDocument.fire( 'mousedown', { - ...eventData - } ); - - viewDocument.fire( 'dragstart', { - ...eventData, - dataTransfer: dataTransferMock, - stopPropagation: () => {}, - preventDefault - } ); - } - - function fireDragging( dataTransferMock, modelPositionOrRange ) { - viewDocument.fire( 'dragging', { - ...prepareEventData( modelPositionOrRange ), - method: 'dragging', - dataTransfer: dataTransferMock, - stopPropagation: () => {}, - preventDefault: () => {} - } ); - } - - function fireDrop( dataTransferMock, modelPosition ) { - viewDocument.fire( 'clipboardInput', { - ...prepareEventData( modelPosition ), - method: 'drop', - dataTransfer: dataTransferMock, - stopPropagation: () => {}, - preventDefault: () => {} - } ); - } - - function fireDragEnd( dataTransferMock ) { - viewDocument.fire( 'dragend', { - dataTransfer: dataTransferMock, - stopPropagation: () => {}, - preventDefault: () => {} - } ); - } - - function prepareEventData( modelPositionOrRange, domTarget ) { - let domNode, viewElement, viewRange; - - if ( modelPositionOrRange.is( 'position' ) ) { - const viewPosition = mapper.toViewPosition( modelPositionOrRange ); - - viewRange = view.createRange( viewPosition ); - viewElement = mapper.findMappedViewAncestor( viewPosition ); - - if ( !domTarget ) { - domNode = viewPosition.parent.is( '$text' ) ? - domConverter.findCorrespondingDomText( viewPosition.parent ).parentNode : - domConverter.mapViewToDom( viewElement ); - } else { - domNode = domTarget; - } - } else { - viewRange = mapper.toViewRange( modelPositionOrRange ); - viewElement = viewRange.getContainedElement(); - domNode = domTarget || domConverter.mapViewToDom( viewElement ); - } - - return { - domTarget: domNode, - target: viewElement, - targetRanges: [ viewRange ], - domEvent: {} - }; - } - - function expectDragStarted( dataTransferMock, data, spyClipboardOutput, effectAllowed = 'copyMove' ) { - expect( dataTransferMock.getData( 'text/html' ) ).to.equal( data ); - expect( dataTransferMock.effectAllowed ).to.equal( effectAllowed ); - - expect( viewDocument.getRoot().getAttribute( 'draggable' ) ).to.equal( 'true' ); - - if ( spyClipboardOutput ) { - expect( spyClipboardOutput.called ).to.be.true; - expect( spyClipboardOutput.firstCall.firstArg.method ).to.equal( 'dragstart' ); - expect( spyClipboardOutput.firstCall.firstArg.dataTransfer ).to.equal( dataTransferMock ); - expect( stringifyView( spyClipboardOutput.firstCall.firstArg.content ) ).to.equal( data ); - } - } - - function expectDraggingMarker( targetPositionOrRange ) { - expect( model.markers.has( 'drop-target' ) ).to.be.true; - - if ( targetPositionOrRange.is( 'position' ) ) { - expect( model.markers.get( 'drop-target' ).getRange().isCollapsed ).to.be.true; - expect( model.markers.get( 'drop-target' ).getRange().start.isEqual( targetPositionOrRange ) ).to.be.true; - } else { - expect( model.markers.get( 'drop-target' ).getRange().isEqual( targetPositionOrRange ) ).to.be.true; - } - } - - function expectFinalized() { - expect( viewDocument.getRoot().hasAttribute( 'draggable' ) ).to.be.false; - - expect( model.markers.has( 'drop-target' ) ).to.be.false; - } - - function createDataTransfer( data = {} ) { - return { - setData( type, value ) { - data[ type ] = value; - }, - - getData( type ) { - return data[ type ]; - }, - - setDragImage() { - return null; - } - }; - } - - function getMockedMousePosition( domNode, position = 'before', extraOffset = 0 ) { - const { x, y, height } = domNode.getBoundingClientRect(); - - if ( position === 'after' ) { - return { - clientX: x, - clientY: y + height + extraOffset - }; - } - - return { - clientX: x, - clientY: y + extraOffset - }; - } - - function trim( strings ) { - return strings - .join( '' ) - .trim() - .replace( />\s+<' ); - } -} ); diff --git a/packages/ckeditor5-clipboard/tests/dragdroptarget.js b/packages/ckeditor5-clipboard/tests/dragdroptarget.js index 621fc65cb21..71488ed4914 100644 --- a/packages/ckeditor5-clipboard/tests/dragdroptarget.js +++ b/packages/ckeditor5-clipboard/tests/dragdroptarget.js @@ -5,7 +5,7 @@ /* globals document, Event, setTimeout */ -import DragDropExperimental from '../src/dragdropexperimental'; +import DragDrop from '../src/dragdrop'; import DragDropTarget from '../src/dragdroptarget'; import PastePlainText from '../src/pasteplaintext'; @@ -31,7 +31,7 @@ describe( 'Drag and Drop target', () => { document.body.appendChild( editorElement ); editor = await ClassicTestEditor.create( editorElement, { - plugins: [ DragDropExperimental, PastePlainText, Paragraph, Table, HorizontalLine, ShiftEnter, BlockQuote, Bold ] + plugins: [ DragDrop, PastePlainText, Paragraph, Table, HorizontalLine, ShiftEnter, BlockQuote, Bold ] } ); model = editor.model; diff --git a/packages/ckeditor5-clipboard/tests/manual/dragdrop.html b/packages/ckeditor5-clipboard/tests/manual/dragdrop.html index 151cb8e728e..43f7d1467a5 100644 --- a/packages/ckeditor5-clipboard/tests/manual/dragdrop.html +++ b/packages/ckeditor5-clipboard/tests/manual/dragdrop.html @@ -1,178 +1,287 @@ - - - +

Classic Editor

-
- -
+
+

CKEditor 5 is a project that allows you to quickly and easily initialize one of the many types of editors it + offers in your application. At the same time, it is a framework for creating custom-tailored rich-text editing solutions. The former + requirement is met thanks to the predefined + CKEditor 5 builds. The latter — thanks to CKEditor 5 Framework.

+

CKEditor 5 Framework is a highly-flexible and universal platform that provides a set of components allowing + you to create any kind of rich text editing solution. bar It enables the building + of different, custom-tailored editors that suit specific needs. It also provides tools for the creation and integration of user-made + features and for customizing existing ones.

+

This guide explains how the framework is built and how to start using it.

+

Please note that the CKEditor 5 Framework documentation is constantly updated and expanded, but it may still + be lacking some things. Feel free to suggest documentation + enhancements and share your feedback about the framework.

+

If the documentation is insufficient, do not be afraid to look into the source code of CKEditor 5 packages. For example, if you + plan to create a new feature, check if a similar one already exists and try to take inspiration from its source code.

+

When to use the framework?

+

The CKEditor 5 predefined builds can + be customized, but certain types of customizations require using the framework.

+
    +
  • Writing your own features. New features are implemented using the framework.
  • +
  • Customizing existing features. Changing the behavior or look of existing features can be done thanks to the + framework’s capabilities. +
  • +
  • Creating new types of editors. You can create new editor types using the framework.
  • +
+

To sum up: you need to start using the framework as soon as existing builds do not meet your requirements or + cannot be customized to the extent you need.

-
-
-

The Flavorful Tuscany Meetup

-

Welcome letter

-

Dear Guest,

-

We are delighted to welcome you to the annual Flavorful Tuscany Meetup and hope you will enjoy the program as well as - your stay at the Bilancino Hotel.

-

Please find attached the full schedule of the event.

-

The annual Flavorful Tuscany meetups are always a culinary discovery. You get the best of Tuscan flavors during an - intense one-day stay at one of the top hotels of the region. All the sessions are lead by top chefs passionate about their - profession. I would certainly recommend to save the date in your calendar for this one!

-

Angelina Calvino, food journalist

-

Please arrive at the Bilancino Hotel reception desk at least half an hour - earlier to make sure that the registration process goes as smoothly as possible.

-

We look forward to welcoming you to the event.

-

Robinson Crusoe+45 2345 234 235 -

-
-

Lists in the table

-
- - - - - - - - - - - - - -
Bulleted listNumbered listNo list
-
    -
  • UL List item 1
  • -
  • UL List item 2
  • -
-
-
    -
  1. OL List item 1
  2. -
  3. OL List item 2
  4. -
-
 
-
-

Basic features overview

-

Lorem ipsum dolor sit amet, consectetur adipisicing elit.1

-
bar -
Caption
-
-

Blockquote

-

Quote

-
    -
  • Quoted UL List item 1
  • -
  • Quoted UL List item 2
  • -
-

Quote

-
 
-
body {
+	

Lists in the table

+
+ + + + + + + + + + + +
Bulleted listNumbered list
+
    +
  • UL List item 1
  • +
  • UL List item 2
  • +
+
+
    +
  1. OL List item 1
  2. +
  3. OL List item 2
  4. +
+
+
+ +

Basic features overview

+

Lorem ipsum dolor sit amet, consectetur adipisicing elit.1

+

Basic styles

+

Ad alias, architecto culpa cumque dignissimos dolor eos incidunt ipsa itaque laboriosam magnam molestias nihil numquam odit quam, suscipit veritatis voluptate voluptatum.2

+

Image

+
+ bar +
Caption
+
+

Blockquote

+
+

Quote

+
    +
  • Quoted UL List item 1
  • +
  • Quoted UL List item 2
  • +
+

Quote

+
+ +
 
+ +

Code blocks in the table

+
+ + + + + + + + + + + +
+
body {
 	color: red;
 }
 
 p {
 	font-size: 10px;
-}
-		
-
-
function foo() {
+}
<?php
+
+function dump( array ...$args ) {
+	foreach ( $args as $item ) {
+		var_dump( $item );
+	}
+
+	die;
+}
+
function foo() {
 	console.log( 'indented using 1 tab' );
 }
 
 function bar() {
-	console.log( 'indented using spaces' );
-}
-		
- + console.log( 'indented using spaces' ); +}
Plain text
+
+ +
-
-

List of Droppable Contacts

-
    -
    Drop here
    +

    HTML embed

    + +

    <details> and <summary> tags as HTML snippet

    +
    +
    + Details + Something small enough to escape casual notice. +
    +

    Decoupled Editor

    + +

    The toolbar

    +
    + +

    The editable

    +
    + + +

    Balloon Block Editor

    + +
    +
    +

    The three greatest things you learn from traveling

    +

    + Like all the great things on earth traveling teaches us by example. Here are some of the most precious lessons + I’ve learned over the years of traveling. +

    + +
    + Three monks ascending the stairs of an ancient temple. +
    Three monks ascending the stairs of an ancient temple.
    +
    + +

    Appreciation of diversity

    +

    + Getting used to an entirely different culture can be challenging. While it’s also nice to learn about + cultures online or from books, nothing comes close to experiencing cultural diversity in person. + You learn to appreciate each and every single one of the differences while you become more culturally fluid. +

    + +

    Improvisation

    +

    + Life doesn't allow us to execute every single plan perfectly. This especially seems to be the case when + you travel. You plan it down to every minute with a big checklist. But when it comes to executing it, + something always comes up and you’re left with your improvising skills. You learn to adapt as you go. + Here’s how my travel checklist looks now: +

    + +
      +
    • buy the ticket
    • +
    • start your adventure
    • +
    + +

    Confidence

    +

    + Going to a new place can be quite terrifying. While change and uncertainty makes us scared, traveling + teaches us how ridiculous it is to be afraid of something before it happens. The moment you face your + fear and see there is nothing to be afraid of, is the moment you discover bliss. +

    +
    +
    + +

    Balloon Block Editor with custom icon

    + +
    +
    +

    The three greatest things you learn from traveling

    +

    + Like all the great things on earth traveling teaches us by example. Here are some of the most precious + lessons + I’ve learned over the years of traveling. +

    + +
    + Three monks ascending the stairs of an ancient temple. +
    Three monks ascending the stairs of an ancient temple.
    +
    + +

    Appreciation of diversity

    +

    + Getting used to an entirely different culture can be challenging. While it’s also nice to learn about + cultures online or from books, nothing comes close to experiencing cultural diversity in person. + You learn to appreciate each and every single one of the differences while you become more culturally fluid. +

    + +

    Improvisation

    +

    + Life doesn't allow us to execute every single plan perfectly. This especially seems to be the case when + you travel. You plan it down to every minute with a big checklist. But when it comes to executing it, + something always comes up and you’re left with your improvising skills. You learn to adapt as you go. + Here’s how my travel checklist looks now: +

    + +
      +
    • buy the ticket
    • +
    • start your adventure
    • +
    + +

    Confidence

    +

    + Going to a new place can be quite terrifying. While change and uncertainty makes us scared, traveling + teaches us how ridiculous it is to be afraid of something before it happens. The moment you face your + fear and see there is nothing to be afraid of, is the moment you discover bliss. +

    +
    +
    + + diff --git a/packages/ckeditor5-clipboard/tests/manual/dragdrop.js b/packages/ckeditor5-clipboard/tests/manual/dragdrop.js index a9fcafc98c9..775b13752b4 100644 --- a/packages/ckeditor5-clipboard/tests/manual/dragdrop.js +++ b/packages/ckeditor5-clipboard/tests/manual/dragdrop.js @@ -3,276 +3,341 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ -/* globals console, window, document */ +/* globals console, window, document, CKEditorInspector */ import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; -import Alignment from '@ckeditor/ckeditor5-alignment/src/alignment'; -import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset'; -import AutoImage from '@ckeditor/ckeditor5-image/src/autoimage'; -import AutoLink from '@ckeditor/ckeditor5-link/src/autolink'; +import { Essentials } from '@ckeditor/ckeditor5-essentials'; +import { Autoformat } from '@ckeditor/ckeditor5-autoformat'; +import { BlockQuote } from '@ckeditor/ckeditor5-block-quote'; +import { Bold, Italic } from '@ckeditor/ckeditor5-basic-styles'; +import { Heading } from '@ckeditor/ckeditor5-heading'; +import { Image, ImageCaption, ImageStyle, ImageToolbar } from '@ckeditor/ckeditor5-image'; +import { Indent } from '@ckeditor/ckeditor5-indent'; +import { Link } from '@ckeditor/ckeditor5-link'; +import { DocumentList, DocumentListProperties } from '@ckeditor/ckeditor5-list'; +import { Paragraph, ParagraphButtonUI } from '@ckeditor/ckeditor5-paragraph'; +import { Table, TableToolbar } from '@ckeditor/ckeditor5-table'; +import Underline from '@ckeditor/ckeditor5-basic-styles/src/underline'; +import Strikethrough from '@ckeditor/ckeditor5-basic-styles/src/strikethrough'; +import Superscript from '@ckeditor/ckeditor5-basic-styles/src/superscript'; +import Subscript from '@ckeditor/ckeditor5-basic-styles/src/subscript'; import Code from '@ckeditor/ckeditor5-basic-styles/src/code'; +import RemoveFormat from '@ckeditor/ckeditor5-remove-format/src/removeformat'; +import FindAndReplace from '@ckeditor/ckeditor5-find-and-replace/src/findandreplace'; +import FontColor from '@ckeditor/ckeditor5-font/src/fontcolor'; +import FontBackgroundColor from '@ckeditor/ckeditor5-font/src/fontbackgroundcolor'; +import FontFamily from '@ckeditor/ckeditor5-font/src/fontfamily'; +import FontSize from '@ckeditor/ckeditor5-font/src/fontsize'; +import Highlight from '@ckeditor/ckeditor5-highlight/src/highlight'; import CodeBlock from '@ckeditor/ckeditor5-code-block/src/codeblock'; +import TableProperties from '@ckeditor/ckeditor5-table/src/tableproperties'; +import TableCellProperties from '@ckeditor/ckeditor5-table/src/tablecellproperties'; +import TableCaption from '@ckeditor/ckeditor5-table/src/tablecaption'; +import TableColumnResize from '@ckeditor/ckeditor5-table/src/tablecolumnresize'; import EasyImage from '@ckeditor/ckeditor5-easy-image/src/easyimage'; -import HorizontalLine from '@ckeditor/ckeditor5-horizontal-line/src/horizontalline'; import ImageResize from '@ckeditor/ckeditor5-image/src/imageresize'; +import ImageInsert from '@ckeditor/ckeditor5-image/src/imageinsert'; import LinkImage from '@ckeditor/ckeditor5-link/src/linkimage'; -import PageBreak from '@ckeditor/ckeditor5-page-break/src/pagebreak'; -import PasteFromOffice from '@ckeditor/ckeditor5-paste-from-office/src/pastefromoffice'; -import RemoveFormat from '@ckeditor/ckeditor5-remove-format/src/removeformat'; +import AutoImage from '@ckeditor/ckeditor5-image/src/autoimage'; +import HtmlEmbed from '@ckeditor/ckeditor5-html-embed/src/htmlembed'; +import AutoLink from '@ckeditor/ckeditor5-link/src/autolink'; +import Mention from '@ckeditor/ckeditor5-mention/src/mention'; import TextTransformation from '@ckeditor/ckeditor5-typing/src/texttransformation'; +import Alignment from '@ckeditor/ckeditor5-alignment/src/alignment'; +import IndentBlock from '@ckeditor/ckeditor5-indent/src/indentblock'; +import PageBreak from '@ckeditor/ckeditor5-page-break/src/pagebreak'; +import HorizontalLine from '@ckeditor/ckeditor5-horizontal-line/src/horizontalline'; import CloudServices from '@ckeditor/ckeditor5-cloud-services/src/cloudservices'; +import TextPartLanguage from '@ckeditor/ckeditor5-language/src/textpartlanguage'; +import SourceEditing from '@ckeditor/ckeditor5-source-editing/src/sourceediting'; +import Style from '@ckeditor/ckeditor5-style/src/style'; +import GeneralHtmlSupport from '@ckeditor/ckeditor5-html-support/src/generalhtmlsupport'; +import DecoupledEditor from '@ckeditor/ckeditor5-editor-decoupled/src/decouplededitor'; +import Enter from '@ckeditor/ckeditor5-enter/src/enter'; +import Typing from '@ckeditor/ckeditor5-typing/src/typing'; +import Undo from '@ckeditor/ckeditor5-undo/src/undo'; +import BalloonEditor from '@ckeditor/ckeditor5-editor-balloon/src/ballooneditor'; +import List from '@ckeditor/ckeditor5-list/src/list'; +import HeadingButtonsUI from '@ckeditor/ckeditor5-heading/src/headingbuttonsui'; +import BlockToolbar from '@ckeditor/ckeditor5-ui/src/toolbar/block/blocktoolbar'; import ImageUpload from '@ckeditor/ckeditor5-image/src/imageupload'; -import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; -import Widget from '@ckeditor/ckeditor5-widget/src/widget'; -import { UpcastWriter } from '@ckeditor/ckeditor5-engine'; -import { toWidget, viewToModelPositionOutsideModelElement } from '@ckeditor/ckeditor5-widget'; -import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config'; - -const contacts = [ - { name: 'Huckleberry Finn', tel: '+48 1345 234 235', email: 'h.finn@example.com', avatar: 'hfin' }, - { name: 'D\'Artagnan', tel: '+45 2345 234 235', email: 'dartagnan@example.com', avatar: 'dartagnan' }, - { name: 'Phileas Fogg', tel: '+44 3345 234 235', email: 'p.fogg@example.com', avatar: 'pfog' }, - { name: 'Alice', tel: '+20 4345 234 235', email: 'alice@example.com', avatar: 'alice' }, - { name: 'Little Red Riding Hood', tel: '+45 2345 234 235', email: 'lrrh@example.com', avatar: 'lrrh' }, - { name: 'Winnetou', tel: '+44 3345 234 235', email: 'winnetou@example.com', avatar: 'winetou' }, - { name: 'Edmond Dantès', tel: '+20 4345 234 235', email: 'count@example.com', avatar: 'edantes' }, - { name: 'Robinson Crusoe', tel: '+45 2345 234 235', email: 'r.crusoe@example.com', avatar: 'rcrusoe' } -]; -class HCardEditing extends Plugin { - static get requires() { - return [ Widget ]; - } - - init() { - this._defineSchema(); - this._defineConverters(); - this._defineClipboardInputOutput(); - - this.editor.editing.mapper.on( - 'viewToModelPosition', - viewToModelPositionOutsideModelElement( this.editor.model, viewElement => viewElement.hasClass( 'h-card' ) ) - ); - } - - _defineSchema() { - this.editor.model.schema.register( 'h-card', { - allowWhere: '$text', - isInline: true, - isObject: true, - allowAttributes: [ 'email', 'name', 'tel' ] - } ); - } - - _defineConverters() { - const conversion = this.editor.conversion; - - conversion.for( 'upcast' ).elementToElement( { - view: { - name: 'span', - classes: [ 'h-card' ] - }, - model: ( viewElement, { writer } ) => { - return writer.createElement( 'h-card', getCardDataFromViewElement( viewElement ) ); - } - } ); - - conversion.for( 'editingDowncast' ).elementToElement( { - model: 'h-card', - view: ( modelItem, { writer: viewWriter } ) => toWidget( createCardView( modelItem, viewWriter ), viewWriter ) - } ); - - conversion.for( 'dataDowncast' ).elementToElement( { - model: 'h-card', - view: ( modelItem, { writer: viewWriter } ) => createCardView( modelItem, viewWriter ) - } ); - - // Helper method for both downcast converters. - function createCardView( modelItem, viewWriter ) { - const email = modelItem.getAttribute( 'email' ); - const name = modelItem.getAttribute( 'name' ); - const tel = modelItem.getAttribute( 'tel' ); - - const cardView = viewWriter.createContainerElement( 'span', { class: 'h-card' } ); - const linkView = viewWriter.createContainerElement( 'a', { href: `mailto:${ email }`, class: 'p-name u-email' } ); - const phoneView = viewWriter.createContainerElement( 'span', { class: 'p-tel' } ); - - viewWriter.insert( viewWriter.createPositionAt( linkView, 0 ), viewWriter.createText( name ) ); - viewWriter.insert( viewWriter.createPositionAt( phoneView, 0 ), viewWriter.createText( tel ) ); - - viewWriter.insert( viewWriter.createPositionAt( cardView, 0 ), linkView ); - viewWriter.insert( viewWriter.createPositionAt( cardView, 'end' ), phoneView ); - - return cardView; - } - } +import { Clipboard, DragDropBlockToolbar, DragDropExperimental } from '../../src'; - _defineClipboardInputOutput() { - const view = this.editor.editing.view; - const viewDocument = view.document; - - this.listenTo( viewDocument, 'clipboardInput', ( evt, data ) => { - const contactData = data.dataTransfer.getData( 'contact' ); - - // There is no contact data or the clipboard content was already processed by the listener on the higher priority - // (for example while pasting into code-block). - if ( !contactData || data.content ) { - return; - } - - const contact = JSON.parse( contactData ); - const writer = new UpcastWriter( viewDocument ); - const fragment = writer.createDocumentFragment(); - - writer.appendChild( - writer.createElement( 'span', { class: 'h-card' }, [ - writer.createElement( 'a', { href: `mailto:${ contact.email }`, class: 'p-name u-email' }, contact.name ), - writer.createElement( 'span', { class: 'p-tel' }, contact.tel ) - ] ), - fragment - ); - - data.content = fragment; - } ); - - this.listenTo( document, 'clipboardOutput', ( evt, data ) => { - if ( data.content.childCount != 1 ) { - return; - } - - const viewElement = data.content.getChild( 0 ); - - if ( viewElement.is( 'element', 'span' ) && viewElement.hasClass( 'h-card' ) ) { - data.dataTransfer.setData( 'contact', JSON.stringify( getCardDataFromViewElement( viewElement ) ) ); - } - } ); - } -} - -function getCardDataFromViewElement( viewElement ) { - const children = Array.from( viewElement.getChildren() ); - const linkElement = children.find( element => element.is( 'element', 'a' ) && element.hasClass( 'p-name' ) ); - const telElement = children.find( element => element.is( 'element', 'span' ) && element.hasClass( 'p-tel' ) ); - - return { - name: getText( linkElement ), - tel: getText( telElement ), - email: linkElement.getAttribute( 'href' ).replace( /^mailto:/i, '' ) - }; -} - -function getText( viewElement ) { - return Array.from( viewElement.getChildren() ) - .map( node => node.is( '$text' ) ? node.data : '' ) - .join( '' ); -} +import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config'; ClassicEditor - .create( document.querySelector( '#editor' ), { + .create( document.querySelector( '#editor-classic' ), { plugins: [ - ArticlePluginSet, Code, RemoveFormat, CodeBlock, EasyImage, ImageResize, LinkImage, - AutoImage, AutoLink, TextTransformation, Alignment, PasteFromOffice, PageBreak, - HorizontalLine, ImageUpload, CloudServices, HCardEditing + Essentials, Autoformat, BlockQuote, Bold, Heading, Image, ImageCaption, ImageStyle, ImageToolbar, Indent, Italic, Link, + DocumentList, Paragraph, Table, TableToolbar, Underline, Strikethrough, Superscript, Subscript, Code, RemoveFormat, + FindAndReplace, FontColor, FontBackgroundColor, FontFamily, FontSize, Highlight, + CodeBlock, DocumentListProperties, TableProperties, TableCellProperties, TableCaption, TableColumnResize, + EasyImage, ImageResize, ImageInsert, LinkImage, AutoImage, HtmlEmbed, + AutoLink, Mention, TextTransformation, Alignment, IndentBlock, PageBreak, HorizontalLine, + CloudServices, TextPartLanguage, SourceEditing, Style, GeneralHtmlSupport, DragDropExperimental ], toolbar: [ - 'heading', + 'heading', 'style', + '|', + 'removeFormat', 'bold', 'italic', 'strikethrough', 'underline', 'code', 'subscript', 'superscript', 'link', '|', - 'removeFormat', 'bold', 'italic', 'code', 'link', + 'highlight', 'fontSize', 'fontFamily', 'fontColor', 'fontBackgroundColor', '|', 'bulletedList', 'numberedList', '|', - 'blockQuote', 'uploadImage', 'insertTable', 'mediaEmbed', 'codeBlock', + 'blockQuote', 'insertImage', 'insertTable', 'codeBlock', '|', - 'alignment', + 'htmlEmbed', + '|', + 'alignment', 'outdent', 'indent', '|', 'pageBreak', 'horizontalLine', '|', - 'undo', 'redo' + 'textPartLanguage', + '|', + 'sourceEditing', + '|', + 'undo', 'redo', 'findAndReplace' ], cloudServices: CS_CONFIG, table: { - contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ] + contentToolbar: [ + 'tableColumn', 'tableRow', 'mergeTableCells', 'tableProperties', 'tableCellProperties', 'toggleTableCaption' + ] }, image: { + styles: [ + 'alignCenter', + 'alignLeft', + 'alignRight' + ], + resizeOptions: [ + { + name: 'resizeImage:original', + label: 'Original size', + value: null + }, + { + name: 'resizeImage:50', + label: '50%', + value: '50' + }, + { + name: 'resizeImage:75', + label: '75%', + value: '75' + } + ], toolbar: [ - 'imageTextAlternative', '|', - 'imageStyle:inline', 'imageStyle:wrapText', 'imageStyle:breakText', '|', + 'imageTextAlternative', 'toggleImageCaption', '|', + 'imageStyle:inline', 'imageStyle:wrapText', 'imageStyle:breakText', 'imageStyle:side', '|', 'resizeImage' + ], + insert: { + integrations: [ + 'insertImageViaUrl' + ] + } + }, + placeholder: 'Type the content here!', + mention: { + feeds: [ + { + marker: '@', + feed: [ + '@apple', '@bears', '@brownie', '@cake', '@cake', '@candy', '@canes', '@chocolate', '@cookie', '@cotton', '@cream', + '@cupcake', '@danish', '@donut', '@dragée', '@fruitcake', '@gingerbread', '@gummi', '@ice', '@jelly-o', + '@liquorice', '@macaroon', '@marzipan', '@oat', '@pie', '@plum', '@pudding', '@sesame', '@snaps', '@soufflé', + '@sugar', '@sweet', '@topping', '@wafer' + ], + minimumCharacters: 1 + } ] }, - placeholder: 'Type the content here!' + link: { + decorators: { + isExternal: { + mode: 'manual', + label: 'Open in a new tab', + attributes: { + target: '_blank', + rel: 'noopener noreferrer' + } + }, + isDownloadable: { + mode: 'manual', + label: 'Downloadable', + attributes: { + download: 'download' + } + }, + isGallery: { + mode: 'manual', + label: 'Gallery link', + classes: 'gallery' + } + } + }, + htmlEmbed: { + showPreviews: true, + sanitizeHtml: html => ( { html, hasChange: false } ) + }, + list: { + properties: { + styles: true, + startIndex: true, + reversed: true + } + }, + style: { + definitions: [ + { + name: 'Article category', + element: 'h3', + classes: [ 'category' ] + }, + { + name: 'Title', + element: 'h2', + classes: [ 'document-title' ] + }, + { + name: 'Subtitle', + element: 'h3', + classes: [ 'document-subtitle' ] + }, + { + name: 'Info box', + element: 'p', + classes: [ 'info-box' ] + }, + { + name: 'Side quote', + element: 'blockquote', + classes: [ 'side-quote' ] + }, + { + name: 'Marker', + element: 'span', + classes: [ 'marker' ] + }, + { + name: 'Spoiler', + element: 'span', + classes: [ 'spoiler' ] + }, + { + name: 'Code (dark)', + element: 'pre', + classes: [ 'fancy-code', 'fancy-code-dark' ] + }, + { + name: 'Code (bright)', + element: 'pre', + classes: [ 'fancy-code', 'fancy-code-bright' ] + } + ] + } } ) .then( editor => { - window.editor = editor; - - const button = document.getElementById( 'read-only' ); - let isReadOnly = false; - - button.addEventListener( 'click', () => { - isReadOnly = !isReadOnly; - - if ( isReadOnly ) { - editor.enableReadOnlyMode( 'manual-test' ); - } else { - editor.disableReadOnlyMode( 'manual-test' ); - } - - button.textContent = isReadOnly ? - 'Turn off read-only mode' : - 'Turn on read-only mode'; + window.editorClassic = editor; - editor.editing.view.focus(); - } ); + CKEditorInspector.attach( { classic: editor } ); } ) .catch( err => { console.error( err.stack ); } ); -const contactsContainer = document.querySelector( '#contactList' ); +const editorData = document.querySelector( '#editor-classic' ).innerHTML; -contactsContainer.addEventListener( 'dragstart', event => { - const target = event.target.nodeType == 1 ? event.target : event.target.parentElement; - const draggable = target.closest( '[draggable]' ); - - event.dataTransfer.setData( 'text/plain', draggable.innerText ); - event.dataTransfer.setData( 'text/html', draggable.innerText ); - event.dataTransfer.setData( 'contact', JSON.stringify( contacts[ draggable.dataset.contact ] ) ); - - event.dataTransfer.setDragImage( draggable, 0, 0 ); -} ); - -contacts.forEach( ( contact, id ) => { - const li = document.createElement( 'li' ); - - li.innerHTML = - `
    ` + - `avatar` + - contact.name + - '
    '; - - contactsContainer.appendChild( li ); -} ); +DecoupledEditor + .create( editorData, { + plugins: [ Enter, Typing, Paragraph, Undo, Heading, Bold, Italic, Clipboard, Table, DragDropExperimental ], + toolbar: [ 'heading', '|', 'bold', 'italic', 'insertTable', 'undo', 'redo' ] + } ) + .then( editor => { + document.querySelector( '.toolbar-container' ).appendChild( editor.ui.view.toolbar.element ); + document.querySelector( '.editable-container' ).appendChild( editor.ui.view.editable.element ); -const dropArea = document.querySelector( '#drop-area' ); + window.editorDecoupled = editor; -dropArea.addEventListener( 'dragover', event => { - event.preventDefault(); - event.dataTransfer.dropEffect = 'copy'; - dropArea.classList.add( 'dragover' ); -} ); + CKEditorInspector.attach( { decoupled: editor } ); + } ) + .catch( err => { + console.error( err.stack ); + } ); -dropArea.addEventListener( 'dragleave', () => { - dropArea.classList.remove( 'dragover' ); -} ); +BalloonEditor + .create( document.querySelector( '#editor-balloon' ), { + plugins: [ + Essentials, List, Paragraph, Heading, + Image, ImageResize, ImageStyle, ImageToolbar, ImageCaption, + HeadingButtonsUI, ParagraphButtonUI, BlockToolbar, Table, TableToolbar, + CloudServices, ImageUpload, EasyImage, DragDropBlockToolbar, DragDropExperimental + ], + cloudServices: CS_CONFIG, + blockToolbar: [ + 'paragraph', 'heading1', 'heading2', 'heading3', 'bulletedList', 'numberedList', 'paragraph', + 'heading1', 'heading2', 'heading3', 'bulletedList', 'numberedList', 'paragraph', 'heading1', 'heading2', 'heading3', + 'bulletedList', 'numberedList', 'insertTable', 'uploadImage' + ], + image: { + toolbar: [ + 'imageTextAlternative', 'toggleImageCaption', '|', + 'imageStyle:inline', 'imageStyle:wrapText', 'imageStyle:breakText', 'imageStyle:side', '|', + 'resizeImage' + ] + }, + table: { + contentToolbar: [ + 'tableColumn', 'tableRow', 'mergeTableCells', 'toggleTableCaption' + ] + } + } ) + .then( editor => { + window.editorBalloon = editor; -dropArea.addEventListener( 'drop', event => { - const contact = event.dataTransfer.getData( 'contact' ); + CKEditorInspector.attach( { balloon: editor } ); + } ) + .catch( err => { + console.error( err.stack ); + } ); - dropArea.innerText = - '-- text/plain --\n' + event.dataTransfer.getData( 'text/plain' ) + '\n\n' + - '-- text/html --\n' + event.dataTransfer.getData( 'text/html' ) + '\n\n' + - '-- contact --\n' + ( contact ? JSON.stringify( JSON.parse( contact ), 0, 2 ) : '' ) + '\n'; - dropArea.classList.remove( 'dragover' ); +BalloonEditor + .create( document.querySelector( '#editor-balloon-custom-icon' ), { + plugins: [ + Essentials, List, Paragraph, Heading, + Image, ImageResize, ImageStyle, ImageToolbar, ImageCaption, + HeadingButtonsUI, ParagraphButtonUI, BlockToolbar, Table, TableToolbar, + CloudServices, ImageUpload, EasyImage, DragDropBlockToolbar, DragDropExperimental + ], + cloudServices: CS_CONFIG, + blockToolbar: { + items: [ 'paragraph', 'heading1', 'heading2', 'heading3', 'bulletedList', 'numberedList', + 'paragraph', 'heading1', 'heading2', 'heading3', 'bulletedList', 'numberedList', 'paragraph', 'heading1', 'heading2', + 'heading3', 'bulletedList', 'numberedList', 'insertTable', 'uploadImage' ], + icon: '' + + '' + }, + image: { + toolbar: [ + 'imageTextAlternative', 'toggleImageCaption', '|', + 'imageStyle:inline', 'imageStyle:wrapText', 'imageStyle:breakText', 'imageStyle:side', '|', + 'resizeImage' + ] + }, + table: { + contentToolbar: [ + 'tableColumn', 'tableRow', 'mergeTableCells', 'toggleTableCaption' + ] + } + } ) + .then( editor => { + window.editorBalloonCustomIcon = editor; - event.preventDefault(); -} ); + CKEditorInspector.attach( { balloonCustomIcon: editor } ); + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/packages/ckeditor5-clipboard/tests/manual/dragdrop.md b/packages/ckeditor5-clipboard/tests/manual/dragdrop.md index fd18b15967f..e69de29bb2d 100644 --- a/packages/ckeditor5-clipboard/tests/manual/dragdrop.md +++ b/packages/ckeditor5-clipboard/tests/manual/dragdrop.md @@ -1,7 +0,0 @@ -# Drag & Drop - -Main focus of this test is accurate finding the proper drop target. - -* Use the draggable elements on the right-side of the editor to drop them to the editor. -* Move different widgets and text fragments within the editor or outside from it. - diff --git a/packages/ckeditor5-clipboard/tests/manual/dragdropexperimental.html b/packages/ckeditor5-clipboard/tests/manual/dragdropexperimental.html deleted file mode 100644 index 43f7d1467a5..00000000000 --- a/packages/ckeditor5-clipboard/tests/manual/dragdropexperimental.html +++ /dev/null @@ -1,287 +0,0 @@ -

    Classic Editor

    - -
    -

    CKEditor 5 is a project that allows you to quickly and easily initialize one of the many types of editors it - offers in your application. At the same time, it is a framework for creating custom-tailored rich-text editing solutions. The former - requirement is met thanks to the predefined - CKEditor 5 builds. The latter — thanks to CKEditor 5 Framework.

    -

    CKEditor 5 Framework is a highly-flexible and universal platform that provides a set of components allowing - you to create any kind of rich text editing solution. bar It enables the building - of different, custom-tailored editors that suit specific needs. It also provides tools for the creation and integration of user-made - features and for customizing existing ones.

    -

    This guide explains how the framework is built and how to start using it.

    -

    Please note that the CKEditor 5 Framework documentation is constantly updated and expanded, but it may still - be lacking some things. Feel free to suggest documentation - enhancements and share your feedback about the framework.

    -

    If the documentation is insufficient, do not be afraid to look into the source code of CKEditor 5 packages. For example, if you - plan to create a new feature, check if a similar one already exists and try to take inspiration from its source code.

    -

    When to use the framework?

    -

    The CKEditor 5 predefined builds can - be customized, but certain types of customizations require using the framework.

    -
      -
    • Writing your own features. New features are implemented using the framework.
    • -
    • Customizing existing features. Changing the behavior or look of existing features can be done thanks to the - framework’s capabilities. -
    • -
    • Creating new types of editors. You can create new editor types using the framework.
    • -
    -

    To sum up: you need to start using the framework as soon as existing builds do not meet your requirements or - cannot be customized to the extent you need.

    - -

    Lists in the table

    -
    - - - - - - - - - - - -
    Bulleted listNumbered list
    -
      -
    • UL List item 1
    • -
    • UL List item 2
    • -
    -
    -
      -
    1. OL List item 1
    2. -
    3. OL List item 2
    4. -
    -
    -
    - -

    Basic features overview

    -

    Lorem ipsum dolor sit amet, consectetur adipisicing elit.1

    -

    Basic styles

    -

    Ad alias, architecto culpa cumque dignissimos dolor eos incidunt ipsa itaque laboriosam magnam molestias nihil numquam odit quam, suscipit veritatis voluptate voluptatum.2

    -

    Image

    -
    - bar -
    Caption
    -
    -

    Blockquote

    -
    -

    Quote

    -
      -
    • Quoted UL List item 1
    • -
    • Quoted UL List item 2
    • -
    -

    Quote

    -
    - -
     
    - -

    Code blocks in the table

    -
    - - - - - - - - - - - -
    -
    body {
    -	color: red;
    -}
    -
    -p {
    -	font-size: 10px;
    -}
    <?php
    -
    -function dump( array ...$args ) {
    -	foreach ( $args as $item ) {
    -		var_dump( $item );
    -	}
    -
    -	die;
    -}
    -
    function foo() {
    -	console.log( 'indented using 1 tab' );
    -}
    -
    -function bar() {
    -    console.log( 'indented using spaces' );
    -}
    Plain text
    -
    - -
    - -

    HTML embed

    - -

    <details> and <summary> tags as HTML snippet

    -
    -
    - Details - Something small enough to escape casual notice. -
    -
    -
    - -

    Decoupled Editor

    - -

    The toolbar

    -
    - -

    The editable

    -
    - - - -

    Balloon Block Editor

    - -
    -
    -

    The three greatest things you learn from traveling

    -

    - Like all the great things on earth traveling teaches us by example. Here are some of the most precious lessons - I’ve learned over the years of traveling. -

    - -
    - Three monks ascending the stairs of an ancient temple. -
    Three monks ascending the stairs of an ancient temple.
    -
    - -

    Appreciation of diversity

    -

    - Getting used to an entirely different culture can be challenging. While it’s also nice to learn about - cultures online or from books, nothing comes close to experiencing cultural diversity in person. - You learn to appreciate each and every single one of the differences while you become more culturally fluid. -

    - -

    Improvisation

    -

    - Life doesn't allow us to execute every single plan perfectly. This especially seems to be the case when - you travel. You plan it down to every minute with a big checklist. But when it comes to executing it, - something always comes up and you’re left with your improvising skills. You learn to adapt as you go. - Here’s how my travel checklist looks now: -

    - -
      -
    • buy the ticket
    • -
    • start your adventure
    • -
    - -

    Confidence

    -

    - Going to a new place can be quite terrifying. While change and uncertainty makes us scared, traveling - teaches us how ridiculous it is to be afraid of something before it happens. The moment you face your - fear and see there is nothing to be afraid of, is the moment you discover bliss. -

    -
    -
    - -

    Balloon Block Editor with custom icon

    - -
    -
    -

    The three greatest things you learn from traveling

    -

    - Like all the great things on earth traveling teaches us by example. Here are some of the most precious - lessons - I’ve learned over the years of traveling. -

    - -
    - Three monks ascending the stairs of an ancient temple. -
    Three monks ascending the stairs of an ancient temple.
    -
    - -

    Appreciation of diversity

    -

    - Getting used to an entirely different culture can be challenging. While it’s also nice to learn about - cultures online or from books, nothing comes close to experiencing cultural diversity in person. - You learn to appreciate each and every single one of the differences while you become more culturally fluid. -

    - -

    Improvisation

    -

    - Life doesn't allow us to execute every single plan perfectly. This especially seems to be the case when - you travel. You plan it down to every minute with a big checklist. But when it comes to executing it, - something always comes up and you’re left with your improvising skills. You learn to adapt as you go. - Here’s how my travel checklist looks now: -

    - -
      -
    • buy the ticket
    • -
    • start your adventure
    • -
    - -

    Confidence

    -

    - Going to a new place can be quite terrifying. While change and uncertainty makes us scared, traveling - teaches us how ridiculous it is to be afraid of something before it happens. The moment you face your - fear and see there is nothing to be afraid of, is the moment you discover bliss. -

    -
    -
    - - diff --git a/packages/ckeditor5-clipboard/tests/manual/dragdropexperimental.js b/packages/ckeditor5-clipboard/tests/manual/dragdropexperimental.js deleted file mode 100644 index 775b13752b4..00000000000 --- a/packages/ckeditor5-clipboard/tests/manual/dragdropexperimental.js +++ /dev/null @@ -1,343 +0,0 @@ -/** - * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license - */ - -/* globals console, window, document, CKEditorInspector */ - -import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; -import { Essentials } from '@ckeditor/ckeditor5-essentials'; -import { Autoformat } from '@ckeditor/ckeditor5-autoformat'; -import { BlockQuote } from '@ckeditor/ckeditor5-block-quote'; -import { Bold, Italic } from '@ckeditor/ckeditor5-basic-styles'; -import { Heading } from '@ckeditor/ckeditor5-heading'; -import { Image, ImageCaption, ImageStyle, ImageToolbar } from '@ckeditor/ckeditor5-image'; -import { Indent } from '@ckeditor/ckeditor5-indent'; -import { Link } from '@ckeditor/ckeditor5-link'; -import { DocumentList, DocumentListProperties } from '@ckeditor/ckeditor5-list'; -import { Paragraph, ParagraphButtonUI } from '@ckeditor/ckeditor5-paragraph'; -import { Table, TableToolbar } from '@ckeditor/ckeditor5-table'; -import Underline from '@ckeditor/ckeditor5-basic-styles/src/underline'; -import Strikethrough from '@ckeditor/ckeditor5-basic-styles/src/strikethrough'; -import Superscript from '@ckeditor/ckeditor5-basic-styles/src/superscript'; -import Subscript from '@ckeditor/ckeditor5-basic-styles/src/subscript'; -import Code from '@ckeditor/ckeditor5-basic-styles/src/code'; -import RemoveFormat from '@ckeditor/ckeditor5-remove-format/src/removeformat'; -import FindAndReplace from '@ckeditor/ckeditor5-find-and-replace/src/findandreplace'; -import FontColor from '@ckeditor/ckeditor5-font/src/fontcolor'; -import FontBackgroundColor from '@ckeditor/ckeditor5-font/src/fontbackgroundcolor'; -import FontFamily from '@ckeditor/ckeditor5-font/src/fontfamily'; -import FontSize from '@ckeditor/ckeditor5-font/src/fontsize'; -import Highlight from '@ckeditor/ckeditor5-highlight/src/highlight'; -import CodeBlock from '@ckeditor/ckeditor5-code-block/src/codeblock'; -import TableProperties from '@ckeditor/ckeditor5-table/src/tableproperties'; -import TableCellProperties from '@ckeditor/ckeditor5-table/src/tablecellproperties'; -import TableCaption from '@ckeditor/ckeditor5-table/src/tablecaption'; -import TableColumnResize from '@ckeditor/ckeditor5-table/src/tablecolumnresize'; -import EasyImage from '@ckeditor/ckeditor5-easy-image/src/easyimage'; -import ImageResize from '@ckeditor/ckeditor5-image/src/imageresize'; -import ImageInsert from '@ckeditor/ckeditor5-image/src/imageinsert'; -import LinkImage from '@ckeditor/ckeditor5-link/src/linkimage'; -import AutoImage from '@ckeditor/ckeditor5-image/src/autoimage'; -import HtmlEmbed from '@ckeditor/ckeditor5-html-embed/src/htmlembed'; -import AutoLink from '@ckeditor/ckeditor5-link/src/autolink'; -import Mention from '@ckeditor/ckeditor5-mention/src/mention'; -import TextTransformation from '@ckeditor/ckeditor5-typing/src/texttransformation'; -import Alignment from '@ckeditor/ckeditor5-alignment/src/alignment'; -import IndentBlock from '@ckeditor/ckeditor5-indent/src/indentblock'; -import PageBreak from '@ckeditor/ckeditor5-page-break/src/pagebreak'; -import HorizontalLine from '@ckeditor/ckeditor5-horizontal-line/src/horizontalline'; -import CloudServices from '@ckeditor/ckeditor5-cloud-services/src/cloudservices'; -import TextPartLanguage from '@ckeditor/ckeditor5-language/src/textpartlanguage'; -import SourceEditing from '@ckeditor/ckeditor5-source-editing/src/sourceediting'; -import Style from '@ckeditor/ckeditor5-style/src/style'; -import GeneralHtmlSupport from '@ckeditor/ckeditor5-html-support/src/generalhtmlsupport'; -import DecoupledEditor from '@ckeditor/ckeditor5-editor-decoupled/src/decouplededitor'; -import Enter from '@ckeditor/ckeditor5-enter/src/enter'; -import Typing from '@ckeditor/ckeditor5-typing/src/typing'; -import Undo from '@ckeditor/ckeditor5-undo/src/undo'; -import BalloonEditor from '@ckeditor/ckeditor5-editor-balloon/src/ballooneditor'; -import List from '@ckeditor/ckeditor5-list/src/list'; -import HeadingButtonsUI from '@ckeditor/ckeditor5-heading/src/headingbuttonsui'; -import BlockToolbar from '@ckeditor/ckeditor5-ui/src/toolbar/block/blocktoolbar'; -import ImageUpload from '@ckeditor/ckeditor5-image/src/imageupload'; - -import { Clipboard, DragDropBlockToolbar, DragDropExperimental } from '../../src'; - -import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config'; - -ClassicEditor - .create( document.querySelector( '#editor-classic' ), { - plugins: [ - Essentials, Autoformat, BlockQuote, Bold, Heading, Image, ImageCaption, ImageStyle, ImageToolbar, Indent, Italic, Link, - DocumentList, Paragraph, Table, TableToolbar, Underline, Strikethrough, Superscript, Subscript, Code, RemoveFormat, - FindAndReplace, FontColor, FontBackgroundColor, FontFamily, FontSize, Highlight, - CodeBlock, DocumentListProperties, TableProperties, TableCellProperties, TableCaption, TableColumnResize, - EasyImage, ImageResize, ImageInsert, LinkImage, AutoImage, HtmlEmbed, - AutoLink, Mention, TextTransformation, Alignment, IndentBlock, PageBreak, HorizontalLine, - CloudServices, TextPartLanguage, SourceEditing, Style, GeneralHtmlSupport, DragDropExperimental - ], - toolbar: [ - 'heading', 'style', - '|', - 'removeFormat', 'bold', 'italic', 'strikethrough', 'underline', 'code', 'subscript', 'superscript', 'link', - '|', - 'highlight', 'fontSize', 'fontFamily', 'fontColor', 'fontBackgroundColor', - '|', - 'bulletedList', 'numberedList', - '|', - 'blockQuote', 'insertImage', 'insertTable', 'codeBlock', - '|', - 'htmlEmbed', - '|', - 'alignment', 'outdent', 'indent', - '|', - 'pageBreak', 'horizontalLine', - '|', - 'textPartLanguage', - '|', - 'sourceEditing', - '|', - 'undo', 'redo', 'findAndReplace' - ], - cloudServices: CS_CONFIG, - table: { - contentToolbar: [ - 'tableColumn', 'tableRow', 'mergeTableCells', 'tableProperties', 'tableCellProperties', 'toggleTableCaption' - ] - }, - image: { - styles: [ - 'alignCenter', - 'alignLeft', - 'alignRight' - ], - resizeOptions: [ - { - name: 'resizeImage:original', - label: 'Original size', - value: null - }, - { - name: 'resizeImage:50', - label: '50%', - value: '50' - }, - { - name: 'resizeImage:75', - label: '75%', - value: '75' - } - ], - toolbar: [ - 'imageTextAlternative', 'toggleImageCaption', '|', - 'imageStyle:inline', 'imageStyle:wrapText', 'imageStyle:breakText', 'imageStyle:side', '|', - 'resizeImage' - ], - insert: { - integrations: [ - 'insertImageViaUrl' - ] - } - }, - placeholder: 'Type the content here!', - mention: { - feeds: [ - { - marker: '@', - feed: [ - '@apple', '@bears', '@brownie', '@cake', '@cake', '@candy', '@canes', '@chocolate', '@cookie', '@cotton', '@cream', - '@cupcake', '@danish', '@donut', '@dragée', '@fruitcake', '@gingerbread', '@gummi', '@ice', '@jelly-o', - '@liquorice', '@macaroon', '@marzipan', '@oat', '@pie', '@plum', '@pudding', '@sesame', '@snaps', '@soufflé', - '@sugar', '@sweet', '@topping', '@wafer' - ], - minimumCharacters: 1 - } - ] - }, - link: { - decorators: { - isExternal: { - mode: 'manual', - label: 'Open in a new tab', - attributes: { - target: '_blank', - rel: 'noopener noreferrer' - } - }, - isDownloadable: { - mode: 'manual', - label: 'Downloadable', - attributes: { - download: 'download' - } - }, - isGallery: { - mode: 'manual', - label: 'Gallery link', - classes: 'gallery' - } - } - }, - htmlEmbed: { - showPreviews: true, - sanitizeHtml: html => ( { html, hasChange: false } ) - }, - list: { - properties: { - styles: true, - startIndex: true, - reversed: true - } - }, - style: { - definitions: [ - { - name: 'Article category', - element: 'h3', - classes: [ 'category' ] - }, - { - name: 'Title', - element: 'h2', - classes: [ 'document-title' ] - }, - { - name: 'Subtitle', - element: 'h3', - classes: [ 'document-subtitle' ] - }, - { - name: 'Info box', - element: 'p', - classes: [ 'info-box' ] - }, - { - name: 'Side quote', - element: 'blockquote', - classes: [ 'side-quote' ] - }, - { - name: 'Marker', - element: 'span', - classes: [ 'marker' ] - }, - { - name: 'Spoiler', - element: 'span', - classes: [ 'spoiler' ] - }, - { - name: 'Code (dark)', - element: 'pre', - classes: [ 'fancy-code', 'fancy-code-dark' ] - }, - { - name: 'Code (bright)', - element: 'pre', - classes: [ 'fancy-code', 'fancy-code-bright' ] - } - ] - } - } ) - .then( editor => { - window.editorClassic = editor; - - CKEditorInspector.attach( { classic: editor } ); - } ) - .catch( err => { - console.error( err.stack ); - } ); - -const editorData = document.querySelector( '#editor-classic' ).innerHTML; - -DecoupledEditor - .create( editorData, { - plugins: [ Enter, Typing, Paragraph, Undo, Heading, Bold, Italic, Clipboard, Table, DragDropExperimental ], - toolbar: [ 'heading', '|', 'bold', 'italic', 'insertTable', 'undo', 'redo' ] - } ) - .then( editor => { - document.querySelector( '.toolbar-container' ).appendChild( editor.ui.view.toolbar.element ); - document.querySelector( '.editable-container' ).appendChild( editor.ui.view.editable.element ); - - window.editorDecoupled = editor; - - CKEditorInspector.attach( { decoupled: editor } ); - } ) - .catch( err => { - console.error( err.stack ); - } ); - -BalloonEditor - .create( document.querySelector( '#editor-balloon' ), { - plugins: [ - Essentials, List, Paragraph, Heading, - Image, ImageResize, ImageStyle, ImageToolbar, ImageCaption, - HeadingButtonsUI, ParagraphButtonUI, BlockToolbar, Table, TableToolbar, - CloudServices, ImageUpload, EasyImage, DragDropBlockToolbar, DragDropExperimental - ], - cloudServices: CS_CONFIG, - blockToolbar: [ - 'paragraph', 'heading1', 'heading2', 'heading3', 'bulletedList', 'numberedList', 'paragraph', - 'heading1', 'heading2', 'heading3', 'bulletedList', 'numberedList', 'paragraph', 'heading1', 'heading2', 'heading3', - 'bulletedList', 'numberedList', 'insertTable', 'uploadImage' - ], - image: { - toolbar: [ - 'imageTextAlternative', 'toggleImageCaption', '|', - 'imageStyle:inline', 'imageStyle:wrapText', 'imageStyle:breakText', 'imageStyle:side', '|', - 'resizeImage' - ] - }, - table: { - contentToolbar: [ - 'tableColumn', 'tableRow', 'mergeTableCells', 'toggleTableCaption' - ] - } - } ) - .then( editor => { - window.editorBalloon = editor; - - CKEditorInspector.attach( { balloon: editor } ); - } ) - .catch( err => { - console.error( err.stack ); - } ); - -BalloonEditor - .create( document.querySelector( '#editor-balloon-custom-icon' ), { - plugins: [ - Essentials, List, Paragraph, Heading, - Image, ImageResize, ImageStyle, ImageToolbar, ImageCaption, - HeadingButtonsUI, ParagraphButtonUI, BlockToolbar, Table, TableToolbar, - CloudServices, ImageUpload, EasyImage, DragDropBlockToolbar, DragDropExperimental - ], - cloudServices: CS_CONFIG, - blockToolbar: { - items: [ 'paragraph', 'heading1', 'heading2', 'heading3', 'bulletedList', 'numberedList', - 'paragraph', 'heading1', 'heading2', 'heading3', 'bulletedList', 'numberedList', 'paragraph', 'heading1', 'heading2', - 'heading3', 'bulletedList', 'numberedList', 'insertTable', 'uploadImage' ], - icon: '' + - '' - }, - image: { - toolbar: [ - 'imageTextAlternative', 'toggleImageCaption', '|', - 'imageStyle:inline', 'imageStyle:wrapText', 'imageStyle:breakText', 'imageStyle:side', '|', - 'resizeImage' - ] - }, - table: { - contentToolbar: [ - 'tableColumn', 'tableRow', 'mergeTableCells', 'toggleTableCaption' - ] - } - } ) - .then( editor => { - window.editorBalloonCustomIcon = editor; - - CKEditorInspector.attach( { balloonCustomIcon: editor } ); - } ) - .catch( err => { - console.error( err.stack ); - } ); diff --git a/packages/ckeditor5-clipboard/tests/manual/dragdropexperimental.md b/packages/ckeditor5-clipboard/tests/manual/dragdropexperimental.md deleted file mode 100644 index e69de29bb2d..00000000000 From a75d3cdc013b02f2edd1c1c27d913694f332edca Mon Sep 17 00:00:00 2001 From: Illia Sheremetov Date: Tue, 19 Sep 2023 16:58:33 +0200 Subject: [PATCH 06/12] Provide 100% coverage --- packages/ckeditor5-code-block/tests/codeblockediting.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ckeditor5-code-block/tests/codeblockediting.js b/packages/ckeditor5-code-block/tests/codeblockediting.js index dc5fd9f4c7b..071ef40dc46 100644 --- a/packages/ckeditor5-code-block/tests/codeblockediting.js +++ b/packages/ckeditor5-code-block/tests/codeblockediting.js @@ -1670,6 +1670,7 @@ describe( 'CodeBlockEditing', () => { dataTransfer: dataTransferMock, targetRanges: [ targetViewRange ], target: targetViewRange.start.parent.parent, + domEvent: sinon.spy(), stop: sinon.spy() } ); From 4c628ba8deb4782f56b39fb8a878bbae496194e9 Mon Sep 17 00:00:00 2001 From: Arkadiusz Filipczak Date: Wed, 20 Sep 2023 16:09:07 +0200 Subject: [PATCH 07/12] Fixed broken manual test in drag&drop. --- .../ckeditor5-clipboard/src/dragdropblocktoolbar.ts | 2 +- packages/ckeditor5-clipboard/tests/dragdrop.js | 2 +- packages/ckeditor5-clipboard/tests/manual/dragdrop.js | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/ckeditor5-clipboard/src/dragdropblocktoolbar.ts b/packages/ckeditor5-clipboard/src/dragdropblocktoolbar.ts index 3f88385ecc1..2d84b45bdde 100644 --- a/packages/ckeditor5-clipboard/src/dragdropblocktoolbar.ts +++ b/packages/ckeditor5-clipboard/src/dragdropblocktoolbar.ts @@ -24,7 +24,7 @@ import type { BlockToolbar } from '@ckeditor/ckeditor5-ui'; import ClipboardObserver from './clipboardobserver'; /** - * Integration of an experimental block Drag and drop support with the block toolbar. + * Integration of a block Drag and Drop support with the block toolbar. * * @internal */ diff --git a/packages/ckeditor5-clipboard/tests/dragdrop.js b/packages/ckeditor5-clipboard/tests/dragdrop.js index 55da87fc88d..bbe4079cfe1 100644 --- a/packages/ckeditor5-clipboard/tests/dragdrop.js +++ b/packages/ckeditor5-clipboard/tests/dragdrop.js @@ -27,7 +27,7 @@ import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import { getData as getViewData, stringify as stringifyView } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; -describe( 'Drag and Drop experimental', () => { +describe( 'Drag and Drop', () => { let editorElement, editor, model, view, viewDocument, root, mapper, domConverter; testUtils.createSinonSandbox(); diff --git a/packages/ckeditor5-clipboard/tests/manual/dragdrop.js b/packages/ckeditor5-clipboard/tests/manual/dragdrop.js index 775b13752b4..4ea9b3aee8e 100644 --- a/packages/ckeditor5-clipboard/tests/manual/dragdrop.js +++ b/packages/ckeditor5-clipboard/tests/manual/dragdrop.js @@ -62,7 +62,7 @@ import HeadingButtonsUI from '@ckeditor/ckeditor5-heading/src/headingbuttonsui'; import BlockToolbar from '@ckeditor/ckeditor5-ui/src/toolbar/block/blocktoolbar'; import ImageUpload from '@ckeditor/ckeditor5-image/src/imageupload'; -import { Clipboard, DragDropBlockToolbar, DragDropExperimental } from '../../src'; +import { Clipboard, DragDropBlockToolbar, DragDrop } from '../../src'; import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config'; @@ -75,7 +75,7 @@ ClassicEditor CodeBlock, DocumentListProperties, TableProperties, TableCellProperties, TableCaption, TableColumnResize, EasyImage, ImageResize, ImageInsert, LinkImage, AutoImage, HtmlEmbed, AutoLink, Mention, TextTransformation, Alignment, IndentBlock, PageBreak, HorizontalLine, - CloudServices, TextPartLanguage, SourceEditing, Style, GeneralHtmlSupport, DragDropExperimental + CloudServices, TextPartLanguage, SourceEditing, Style, GeneralHtmlSupport, DragDrop ], toolbar: [ 'heading', 'style', @@ -253,7 +253,7 @@ const editorData = document.querySelector( '#editor-classic' ).innerHTML; DecoupledEditor .create( editorData, { - plugins: [ Enter, Typing, Paragraph, Undo, Heading, Bold, Italic, Clipboard, Table, DragDropExperimental ], + plugins: [ Enter, Typing, Paragraph, Undo, Heading, Bold, Italic, Clipboard, Table, DragDrop ], toolbar: [ 'heading', '|', 'bold', 'italic', 'insertTable', 'undo', 'redo' ] } ) .then( editor => { @@ -274,7 +274,7 @@ BalloonEditor Essentials, List, Paragraph, Heading, Image, ImageResize, ImageStyle, ImageToolbar, ImageCaption, HeadingButtonsUI, ParagraphButtonUI, BlockToolbar, Table, TableToolbar, - CloudServices, ImageUpload, EasyImage, DragDropBlockToolbar, DragDropExperimental + CloudServices, ImageUpload, EasyImage, DragDropBlockToolbar, DragDrop ], cloudServices: CS_CONFIG, blockToolbar: [ @@ -310,7 +310,7 @@ BalloonEditor Essentials, List, Paragraph, Heading, Image, ImageResize, ImageStyle, ImageToolbar, ImageCaption, HeadingButtonsUI, ParagraphButtonUI, BlockToolbar, Table, TableToolbar, - CloudServices, ImageUpload, EasyImage, DragDropBlockToolbar, DragDropExperimental + CloudServices, ImageUpload, EasyImage, DragDropBlockToolbar, DragDrop ], cloudServices: CS_CONFIG, blockToolbar: { From e07b8b772010b72d4eac63be9acf49a06c181193 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Mon, 25 Sep 2023 11:57:54 +0200 Subject: [PATCH 08/12] Docs changes. --- .../docs/features/drag-drop.md | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/packages/ckeditor5-clipboard/docs/features/drag-drop.md b/packages/ckeditor5-clipboard/docs/features/drag-drop.md index a61218e99af..6d3dfa79d03 100644 --- a/packages/ckeditor5-clipboard/docs/features/drag-drop.md +++ b/packages/ckeditor5-clipboard/docs/features/drag-drop.md @@ -59,10 +59,6 @@ The {@link module:clipboard/dragdrop~DragDrop `DragDrop`} plugin will activate a In the v38.0.0 release, we introduced plugins that enable dragging content blocks such as paragraphs, tables, or lists inside the editor. This allows you to select an entire block or multiple blocks, and move them before or after other blocks. - - This block drag and drop is still an **experimental feature**. It is available for users, developers, and enthusiasts, who want to test the new functionality and provide feedback to the product team. Usage in production environments may result in errors. - - Functions introduced in the initial release include: * Selection of the text, elements, multiple blocks, and moving these around. @@ -79,18 +75,9 @@ Select a block or blocks, and drag them across the document. You can place block #### Installation -To enable the block drag and drop in a classic editor, you need to add the {@link module:clipboard/dragdrop~DragDrop `DragDrop`} module to your editor configuration: - -```js -import { ClassicEditor } from '@ckeditor/ckeditor5-editor-classic'; -import { Clipboard, DragDrop } from '@ckeditor/ckeditor5-clipboard'; - -ClassicEditor.create( document.querySelector( '#editor' ), { - plugins: [ Clipboard, DragDrop, /* ... */ ], -}) - .then( /* ... */ ) - .catch( /* ... */ ); -``` + + This feature is required by the clipboard plugin and is enabled by default in all {@link installation/getting-started/predefined-builds predefined builds}. These installation instructions are for developers interested in building their own custom rich-text editor. + ### Balloon block editor demo From 454687fb1cd13508092ba907f1fa9ef37e99efb5 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Mon, 25 Sep 2023 12:08:22 +0200 Subject: [PATCH 09/12] Reverted removed manual test. --- packages/ckeditor5-clipboard/package.json | 1 + .../tests/manual/dragdrop-blocks.html | 287 ++++++++++ .../tests/manual/dragdrop-blocks.js | 343 ++++++++++++ .../tests/manual/dragdrop-blocks.md | 0 .../tests/manual/dragdrop.html | 409 +++++--------- .../tests/manual/dragdrop.js | 513 ++++++++---------- .../tests/manual/dragdrop.md | 7 + 7 files changed, 1012 insertions(+), 548 deletions(-) create mode 100644 packages/ckeditor5-clipboard/tests/manual/dragdrop-blocks.html create mode 100644 packages/ckeditor5-clipboard/tests/manual/dragdrop-blocks.js create mode 100644 packages/ckeditor5-clipboard/tests/manual/dragdrop-blocks.md diff --git a/packages/ckeditor5-clipboard/package.json b/packages/ckeditor5-clipboard/package.json index 36979208c0d..a3593152f1b 100644 --- a/packages/ckeditor5-clipboard/package.json +++ b/packages/ckeditor5-clipboard/package.json @@ -47,6 +47,7 @@ "@ckeditor/ckeditor5-mention": "39.0.2", "@ckeditor/ckeditor5-page-break": "39.0.2", "@ckeditor/ckeditor5-paragraph": "39.0.2", + "@ckeditor/ckeditor5-paste-from-office": "39.0.2", "@ckeditor/ckeditor5-remove-format": "39.0.2", "@ckeditor/ckeditor5-source-editing": "39.0.2", "@ckeditor/ckeditor5-style": "39.0.2", diff --git a/packages/ckeditor5-clipboard/tests/manual/dragdrop-blocks.html b/packages/ckeditor5-clipboard/tests/manual/dragdrop-blocks.html new file mode 100644 index 00000000000..43f7d1467a5 --- /dev/null +++ b/packages/ckeditor5-clipboard/tests/manual/dragdrop-blocks.html @@ -0,0 +1,287 @@ +

    Classic Editor

    + +
    +

    CKEditor 5 is a project that allows you to quickly and easily initialize one of the many types of editors it + offers in your application. At the same time, it is a framework for creating custom-tailored rich-text editing solutions. The former + requirement is met thanks to the predefined + CKEditor 5 builds. The latter — thanks to CKEditor 5 Framework.

    +

    CKEditor 5 Framework is a highly-flexible and universal platform that provides a set of components allowing + you to create any kind of rich text editing solution. bar It enables the building + of different, custom-tailored editors that suit specific needs. It also provides tools for the creation and integration of user-made + features and for customizing existing ones.

    +

    This guide explains how the framework is built and how to start using it.

    +

    Please note that the CKEditor 5 Framework documentation is constantly updated and expanded, but it may still + be lacking some things. Feel free to suggest documentation + enhancements and share your feedback about the framework.

    +

    If the documentation is insufficient, do not be afraid to look into the source code of CKEditor 5 packages. For example, if you + plan to create a new feature, check if a similar one already exists and try to take inspiration from its source code.

    +

    When to use the framework?

    +

    The CKEditor 5 predefined builds can + be customized, but certain types of customizations require using the framework.

    +
      +
    • Writing your own features. New features are implemented using the framework.
    • +
    • Customizing existing features. Changing the behavior or look of existing features can be done thanks to the + framework’s capabilities. +
    • +
    • Creating new types of editors. You can create new editor types using the framework.
    • +
    +

    To sum up: you need to start using the framework as soon as existing builds do not meet your requirements or + cannot be customized to the extent you need.

    + +

    Lists in the table

    +
    + + + + + + + + + + + +
    Bulleted listNumbered list
    +
      +
    • UL List item 1
    • +
    • UL List item 2
    • +
    +
    +
      +
    1. OL List item 1
    2. +
    3. OL List item 2
    4. +
    +
    +
    + +

    Basic features overview

    +

    Lorem ipsum dolor sit amet, consectetur adipisicing elit.1

    +

    Basic styles

    +

    Ad alias, architecto culpa cumque dignissimos dolor eos incidunt ipsa itaque laboriosam magnam molestias nihil numquam odit quam, suscipit veritatis voluptate voluptatum.2

    +

    Image

    +
    + bar +
    Caption
    +
    +

    Blockquote

    +
    +

    Quote

    +
      +
    • Quoted UL List item 1
    • +
    • Quoted UL List item 2
    • +
    +

    Quote

    +
    + +
     
    + +

    Code blocks in the table

    +
    + + + + + + + + + + + +
    +
    body {
    +	color: red;
    +}
    +
    +p {
    +	font-size: 10px;
    +}
    <?php
    +
    +function dump( array ...$args ) {
    +	foreach ( $args as $item ) {
    +		var_dump( $item );
    +	}
    +
    +	die;
    +}
    +
    function foo() {
    +	console.log( 'indented using 1 tab' );
    +}
    +
    +function bar() {
    +    console.log( 'indented using spaces' );
    +}
    Plain text
    +
    + +
    + +

    HTML embed

    + +

    <details> and <summary> tags as HTML snippet

    +
    +
    + Details + Something small enough to escape casual notice. +
    +
    +
    + +

    Decoupled Editor

    + +

    The toolbar

    +
    + +

    The editable

    +
    + + + +

    Balloon Block Editor

    + +
    +
    +

    The three greatest things you learn from traveling

    +

    + Like all the great things on earth traveling teaches us by example. Here are some of the most precious lessons + I’ve learned over the years of traveling. +

    + +
    + Three monks ascending the stairs of an ancient temple. +
    Three monks ascending the stairs of an ancient temple.
    +
    + +

    Appreciation of diversity

    +

    + Getting used to an entirely different culture can be challenging. While it’s also nice to learn about + cultures online or from books, nothing comes close to experiencing cultural diversity in person. + You learn to appreciate each and every single one of the differences while you become more culturally fluid. +

    + +

    Improvisation

    +

    + Life doesn't allow us to execute every single plan perfectly. This especially seems to be the case when + you travel. You plan it down to every minute with a big checklist. But when it comes to executing it, + something always comes up and you’re left with your improvising skills. You learn to adapt as you go. + Here’s how my travel checklist looks now: +

    + +
      +
    • buy the ticket
    • +
    • start your adventure
    • +
    + +

    Confidence

    +

    + Going to a new place can be quite terrifying. While change and uncertainty makes us scared, traveling + teaches us how ridiculous it is to be afraid of something before it happens. The moment you face your + fear and see there is nothing to be afraid of, is the moment you discover bliss. +

    +
    +
    + +

    Balloon Block Editor with custom icon

    + +
    +
    +

    The three greatest things you learn from traveling

    +

    + Like all the great things on earth traveling teaches us by example. Here are some of the most precious + lessons + I’ve learned over the years of traveling. +

    + +
    + Three monks ascending the stairs of an ancient temple. +
    Three monks ascending the stairs of an ancient temple.
    +
    + +

    Appreciation of diversity

    +

    + Getting used to an entirely different culture can be challenging. While it’s also nice to learn about + cultures online or from books, nothing comes close to experiencing cultural diversity in person. + You learn to appreciate each and every single one of the differences while you become more culturally fluid. +

    + +

    Improvisation

    +

    + Life doesn't allow us to execute every single plan perfectly. This especially seems to be the case when + you travel. You plan it down to every minute with a big checklist. But when it comes to executing it, + something always comes up and you’re left with your improvising skills. You learn to adapt as you go. + Here’s how my travel checklist looks now: +

    + +
      +
    • buy the ticket
    • +
    • start your adventure
    • +
    + +

    Confidence

    +

    + Going to a new place can be quite terrifying. While change and uncertainty makes us scared, traveling + teaches us how ridiculous it is to be afraid of something before it happens. The moment you face your + fear and see there is nothing to be afraid of, is the moment you discover bliss. +

    +
    +
    + + diff --git a/packages/ckeditor5-clipboard/tests/manual/dragdrop-blocks.js b/packages/ckeditor5-clipboard/tests/manual/dragdrop-blocks.js new file mode 100644 index 00000000000..6d58ec438c5 --- /dev/null +++ b/packages/ckeditor5-clipboard/tests/manual/dragdrop-blocks.js @@ -0,0 +1,343 @@ +/** + * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* globals console, window, document, CKEditorInspector */ + +import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; +import { Essentials } from '@ckeditor/ckeditor5-essentials'; +import { Autoformat } from '@ckeditor/ckeditor5-autoformat'; +import { BlockQuote } from '@ckeditor/ckeditor5-block-quote'; +import { Bold, Italic } from '@ckeditor/ckeditor5-basic-styles'; +import { Heading } from '@ckeditor/ckeditor5-heading'; +import { Image, ImageCaption, ImageStyle, ImageToolbar } from '@ckeditor/ckeditor5-image'; +import { Indent } from '@ckeditor/ckeditor5-indent'; +import { Link } from '@ckeditor/ckeditor5-link'; +import { DocumentList, DocumentListProperties } from '@ckeditor/ckeditor5-list'; +import { Paragraph, ParagraphButtonUI } from '@ckeditor/ckeditor5-paragraph'; +import { Table, TableToolbar } from '@ckeditor/ckeditor5-table'; +import Underline from '@ckeditor/ckeditor5-basic-styles/src/underline'; +import Strikethrough from '@ckeditor/ckeditor5-basic-styles/src/strikethrough'; +import Superscript from '@ckeditor/ckeditor5-basic-styles/src/superscript'; +import Subscript from '@ckeditor/ckeditor5-basic-styles/src/subscript'; +import Code from '@ckeditor/ckeditor5-basic-styles/src/code'; +import RemoveFormat from '@ckeditor/ckeditor5-remove-format/src/removeformat'; +import FindAndReplace from '@ckeditor/ckeditor5-find-and-replace/src/findandreplace'; +import FontColor from '@ckeditor/ckeditor5-font/src/fontcolor'; +import FontBackgroundColor from '@ckeditor/ckeditor5-font/src/fontbackgroundcolor'; +import FontFamily from '@ckeditor/ckeditor5-font/src/fontfamily'; +import FontSize from '@ckeditor/ckeditor5-font/src/fontsize'; +import Highlight from '@ckeditor/ckeditor5-highlight/src/highlight'; +import CodeBlock from '@ckeditor/ckeditor5-code-block/src/codeblock'; +import TableProperties from '@ckeditor/ckeditor5-table/src/tableproperties'; +import TableCellProperties from '@ckeditor/ckeditor5-table/src/tablecellproperties'; +import TableCaption from '@ckeditor/ckeditor5-table/src/tablecaption'; +import TableColumnResize from '@ckeditor/ckeditor5-table/src/tablecolumnresize'; +import EasyImage from '@ckeditor/ckeditor5-easy-image/src/easyimage'; +import ImageResize from '@ckeditor/ckeditor5-image/src/imageresize'; +import ImageInsert from '@ckeditor/ckeditor5-image/src/imageinsert'; +import LinkImage from '@ckeditor/ckeditor5-link/src/linkimage'; +import AutoImage from '@ckeditor/ckeditor5-image/src/autoimage'; +import HtmlEmbed from '@ckeditor/ckeditor5-html-embed/src/htmlembed'; +import AutoLink from '@ckeditor/ckeditor5-link/src/autolink'; +import Mention from '@ckeditor/ckeditor5-mention/src/mention'; +import TextTransformation from '@ckeditor/ckeditor5-typing/src/texttransformation'; +import Alignment from '@ckeditor/ckeditor5-alignment/src/alignment'; +import IndentBlock from '@ckeditor/ckeditor5-indent/src/indentblock'; +import PageBreak from '@ckeditor/ckeditor5-page-break/src/pagebreak'; +import HorizontalLine from '@ckeditor/ckeditor5-horizontal-line/src/horizontalline'; +import CloudServices from '@ckeditor/ckeditor5-cloud-services/src/cloudservices'; +import TextPartLanguage from '@ckeditor/ckeditor5-language/src/textpartlanguage'; +import SourceEditing from '@ckeditor/ckeditor5-source-editing/src/sourceediting'; +import Style from '@ckeditor/ckeditor5-style/src/style'; +import GeneralHtmlSupport from '@ckeditor/ckeditor5-html-support/src/generalhtmlsupport'; +import DecoupledEditor from '@ckeditor/ckeditor5-editor-decoupled/src/decouplededitor'; +import Enter from '@ckeditor/ckeditor5-enter/src/enter'; +import Typing from '@ckeditor/ckeditor5-typing/src/typing'; +import Undo from '@ckeditor/ckeditor5-undo/src/undo'; +import BalloonEditor from '@ckeditor/ckeditor5-editor-balloon/src/ballooneditor'; +import List from '@ckeditor/ckeditor5-list/src/list'; +import HeadingButtonsUI from '@ckeditor/ckeditor5-heading/src/headingbuttonsui'; +import BlockToolbar from '@ckeditor/ckeditor5-ui/src/toolbar/block/blocktoolbar'; +import ImageUpload from '@ckeditor/ckeditor5-image/src/imageupload'; + +import { Clipboard, DragDropBlockToolbar, DragDrop } from '../../src'; + +import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config'; + +ClassicEditor + .create( document.querySelector( '#editor-classic' ), { + plugins: [ + Essentials, Autoformat, BlockQuote, Bold, Heading, Image, ImageCaption, ImageStyle, ImageToolbar, Indent, Italic, Link, + DocumentList, Paragraph, Table, TableToolbar, Underline, Strikethrough, Superscript, Subscript, Code, RemoveFormat, + FindAndReplace, FontColor, FontBackgroundColor, FontFamily, FontSize, Highlight, + CodeBlock, DocumentListProperties, TableProperties, TableCellProperties, TableCaption, TableColumnResize, + EasyImage, ImageResize, ImageInsert, LinkImage, AutoImage, HtmlEmbed, + AutoLink, Mention, TextTransformation, Alignment, IndentBlock, PageBreak, HorizontalLine, + CloudServices, TextPartLanguage, SourceEditing, Style, GeneralHtmlSupport, DragDrop + ], + toolbar: [ + 'heading', 'style', + '|', + 'removeFormat', 'bold', 'italic', 'strikethrough', 'underline', 'code', 'subscript', 'superscript', 'link', + '|', + 'highlight', 'fontSize', 'fontFamily', 'fontColor', 'fontBackgroundColor', + '|', + 'bulletedList', 'numberedList', + '|', + 'blockQuote', 'insertImage', 'insertTable', 'codeBlock', + '|', + 'htmlEmbed', + '|', + 'alignment', 'outdent', 'indent', + '|', + 'pageBreak', 'horizontalLine', + '|', + 'textPartLanguage', + '|', + 'sourceEditing', + '|', + 'undo', 'redo', 'findAndReplace' + ], + cloudServices: CS_CONFIG, + table: { + contentToolbar: [ + 'tableColumn', 'tableRow', 'mergeTableCells', 'tableProperties', 'tableCellProperties', 'toggleTableCaption' + ] + }, + image: { + styles: [ + 'alignCenter', + 'alignLeft', + 'alignRight' + ], + resizeOptions: [ + { + name: 'resizeImage:original', + label: 'Original size', + value: null + }, + { + name: 'resizeImage:50', + label: '50%', + value: '50' + }, + { + name: 'resizeImage:75', + label: '75%', + value: '75' + } + ], + toolbar: [ + 'imageTextAlternative', 'toggleImageCaption', '|', + 'imageStyle:inline', 'imageStyle:wrapText', 'imageStyle:breakText', 'imageStyle:side', '|', + 'resizeImage' + ], + insert: { + integrations: [ + 'insertImageViaUrl' + ] + } + }, + placeholder: 'Type the content here!', + mention: { + feeds: [ + { + marker: '@', + feed: [ + '@apple', '@bears', '@brownie', '@cake', '@cake', '@candy', '@canes', '@chocolate', '@cookie', '@cotton', '@cream', + '@cupcake', '@danish', '@donut', '@dragée', '@fruitcake', '@gingerbread', '@gummi', '@ice', '@jelly-o', + '@liquorice', '@macaroon', '@marzipan', '@oat', '@pie', '@plum', '@pudding', '@sesame', '@snaps', '@soufflé', + '@sugar', '@sweet', '@topping', '@wafer' + ], + minimumCharacters: 1 + } + ] + }, + link: { + decorators: { + isExternal: { + mode: 'manual', + label: 'Open in a new tab', + attributes: { + target: '_blank', + rel: 'noopener noreferrer' + } + }, + isDownloadable: { + mode: 'manual', + label: 'Downloadable', + attributes: { + download: 'download' + } + }, + isGallery: { + mode: 'manual', + label: 'Gallery link', + classes: 'gallery' + } + } + }, + htmlEmbed: { + showPreviews: true, + sanitizeHtml: html => ( { html, hasChange: false } ) + }, + list: { + properties: { + styles: true, + startIndex: true, + reversed: true + } + }, + style: { + definitions: [ + { + name: 'Article category', + element: 'h3', + classes: [ 'category' ] + }, + { + name: 'Title', + element: 'h2', + classes: [ 'document-title' ] + }, + { + name: 'Subtitle', + element: 'h3', + classes: [ 'document-subtitle' ] + }, + { + name: 'Info box', + element: 'p', + classes: [ 'info-box' ] + }, + { + name: 'Side quote', + element: 'blockquote', + classes: [ 'side-quote' ] + }, + { + name: 'Marker', + element: 'span', + classes: [ 'marker' ] + }, + { + name: 'Spoiler', + element: 'span', + classes: [ 'spoiler' ] + }, + { + name: 'Code (dark)', + element: 'pre', + classes: [ 'fancy-code', 'fancy-code-dark' ] + }, + { + name: 'Code (bright)', + element: 'pre', + classes: [ 'fancy-code', 'fancy-code-bright' ] + } + ] + } + } ) + .then( editor => { + window.editorClassic = editor; + + CKEditorInspector.attach( { classic: editor } ); + } ) + .catch( err => { + console.error( err.stack ); + } ); + +const editorData = document.querySelector( '#editor-classic' ).innerHTML; + +DecoupledEditor + .create( editorData, { + plugins: [ Enter, Typing, Paragraph, Undo, Heading, Bold, Italic, Clipboard, Table, DragDrop ], + toolbar: [ 'heading', '|', 'bold', 'italic', 'insertTable', 'undo', 'redo' ] + } ) + .then( editor => { + document.querySelector( '.toolbar-container' ).appendChild( editor.ui.view.toolbar.element ); + document.querySelector( '.editable-container' ).appendChild( editor.ui.view.editable.element ); + + window.editorDecoupled = editor; + + CKEditorInspector.attach( { decoupled: editor } ); + } ) + .catch( err => { + console.error( err.stack ); + } ); + +BalloonEditor + .create( document.querySelector( '#editor-balloon' ), { + plugins: [ + Essentials, List, Paragraph, Heading, + Image, ImageResize, ImageStyle, ImageToolbar, ImageCaption, + HeadingButtonsUI, ParagraphButtonUI, BlockToolbar, Table, TableToolbar, + CloudServices, ImageUpload, EasyImage, DragDropBlockToolbar, DragDrop + ], + cloudServices: CS_CONFIG, + blockToolbar: [ + 'paragraph', 'heading1', 'heading2', 'heading3', 'bulletedList', 'numberedList', 'paragraph', + 'heading1', 'heading2', 'heading3', 'bulletedList', 'numberedList', 'paragraph', 'heading1', 'heading2', 'heading3', + 'bulletedList', 'numberedList', 'insertTable', 'uploadImage' + ], + image: { + toolbar: [ + 'imageTextAlternative', 'toggleImageCaption', '|', + 'imageStyle:inline', 'imageStyle:wrapText', 'imageStyle:breakText', 'imageStyle:side', '|', + 'resizeImage' + ] + }, + table: { + contentToolbar: [ + 'tableColumn', 'tableRow', 'mergeTableCells', 'toggleTableCaption' + ] + } + } ) + .then( editor => { + window.editorBalloon = editor; + + CKEditorInspector.attach( { balloon: editor } ); + } ) + .catch( err => { + console.error( err.stack ); + } ); + +BalloonEditor + .create( document.querySelector( '#editor-balloon-custom-icon' ), { + plugins: [ + Essentials, List, Paragraph, Heading, + Image, ImageResize, ImageStyle, ImageToolbar, ImageCaption, + HeadingButtonsUI, ParagraphButtonUI, BlockToolbar, Table, TableToolbar, + CloudServices, ImageUpload, EasyImage, DragDropBlockToolbar, DragDrop + ], + cloudServices: CS_CONFIG, + blockToolbar: { + items: [ 'paragraph', 'heading1', 'heading2', 'heading3', 'bulletedList', 'numberedList', + 'paragraph', 'heading1', 'heading2', 'heading3', 'bulletedList', 'numberedList', 'paragraph', 'heading1', 'heading2', + 'heading3', 'bulletedList', 'numberedList', 'insertTable', 'uploadImage' ], + icon: '' + + '' + }, + image: { + toolbar: [ + 'imageTextAlternative', 'toggleImageCaption', '|', + 'imageStyle:inline', 'imageStyle:wrapText', 'imageStyle:breakText', 'imageStyle:side', '|', + 'resizeImage' + ] + }, + table: { + contentToolbar: [ + 'tableColumn', 'tableRow', 'mergeTableCells', 'toggleTableCaption' + ] + } + } ) + .then( editor => { + window.editorBalloonCustomIcon = editor; + + CKEditorInspector.attach( { balloonCustomIcon: editor } ); + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/packages/ckeditor5-clipboard/tests/manual/dragdrop-blocks.md b/packages/ckeditor5-clipboard/tests/manual/dragdrop-blocks.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/ckeditor5-clipboard/tests/manual/dragdrop.html b/packages/ckeditor5-clipboard/tests/manual/dragdrop.html index 43f7d1467a5..151cb8e728e 100644 --- a/packages/ckeditor5-clipboard/tests/manual/dragdrop.html +++ b/packages/ckeditor5-clipboard/tests/manual/dragdrop.html @@ -1,287 +1,178 @@ -

    Classic Editor

    + + + -
    -

    CKEditor 5 is a project that allows you to quickly and easily initialize one of the many types of editors it - offers in your application. At the same time, it is a framework for creating custom-tailored rich-text editing solutions. The former - requirement is met thanks to the predefined - CKEditor 5 builds. The latter — thanks to CKEditor 5 Framework.

    -

    CKEditor 5 Framework is a highly-flexible and universal platform that provides a set of components allowing - you to create any kind of rich text editing solution. bar It enables the building - of different, custom-tailored editors that suit specific needs. It also provides tools for the creation and integration of user-made - features and for customizing existing ones.

    -

    This guide explains how the framework is built and how to start using it.

    -

    Please note that the CKEditor 5 Framework documentation is constantly updated and expanded, but it may still - be lacking some things. Feel free to suggest documentation - enhancements and share your feedback about the framework.

    -

    If the documentation is insufficient, do not be afraid to look into the source code of CKEditor 5 packages. For example, if you - plan to create a new feature, check if a similar one already exists and try to take inspiration from its source code.

    -

    When to use the framework?

    -

    The CKEditor 5 predefined builds can - be customized, but certain types of customizations require using the framework.

    -
      -
    • Writing your own features. New features are implemented using the framework.
    • -
    • Customizing existing features. Changing the behavior or look of existing features can be done thanks to the - framework’s capabilities. -
    • -
    • Creating new types of editors. You can create new editor types using the framework.
    • -
    -

    To sum up: you need to start using the framework as soon as existing builds do not meet your requirements or - cannot be customized to the extent you need.

    - -

    Lists in the table

    -
    - - - - - - - - - - - -
    Bulleted listNumbered list
    -
      -
    • UL List item 1
    • -
    • UL List item 2
    • -
    -
    -
      -
    1. OL List item 1
    2. -
    3. OL List item 2
    4. -
    -
    -
    - -

    Basic features overview

    -

    Lorem ipsum dolor sit amet, consectetur adipisicing elit.1

    -

    Basic styles

    -

    Ad alias, architecto culpa cumque dignissimos dolor eos incidunt ipsa itaque laboriosam magnam molestias nihil numquam odit quam, suscipit veritatis voluptate voluptatum.2

    -

    Image

    -
    - bar -
    Caption
    -
    -

    Blockquote

    -
    -

    Quote

    -
      -
    • Quoted UL List item 1
    • -
    • Quoted UL List item 2
    • -
    -

    Quote

    -
    - -
     
    +
    + +
    -

    Code blocks in the table

    -
    - - - - - - - - - - - -
    -
    body {
    +
    +
    +

    The Flavorful Tuscany Meetup

    +

    Welcome letter

    +

    Dear Guest,

    +

    We are delighted to welcome you to the annual Flavorful Tuscany Meetup and hope you will enjoy the program as well as + your stay at the Bilancino Hotel.

    +

    Please find attached the full schedule of the event.

    +

    The annual Flavorful Tuscany meetups are always a culinary discovery. You get the best of Tuscan flavors during an + intense one-day stay at one of the top hotels of the region. All the sessions are lead by top chefs passionate about their + profession. I would certainly recommend to save the date in your calendar for this one!

    +

    Angelina Calvino, food journalist

    +

    Please arrive at the Bilancino Hotel reception desk at least half an hour + earlier to make sure that the registration process goes as smoothly as possible.

    +

    We look forward to welcoming you to the event.

    +

    Robinson Crusoe+45 2345 234 235 +

    +
    +

    Lists in the table

    +
    + + + + + + + + + + + + + +
    Bulleted listNumbered listNo list
    +
      +
    • UL List item 1
    • +
    • UL List item 2
    • +
    +
    +
      +
    1. OL List item 1
    2. +
    3. OL List item 2
    4. +
    +
     
    +
    +

    Basic features overview

    +

    Lorem ipsum dolor sit amet, consectetur adipisicing elit.1

    +
    bar +
    Caption
    +
    +

    Blockquote

    +

    Quote

    +
      +
    • Quoted UL List item 1
    • +
    • Quoted UL List item 2
    • +
    +

    Quote

    +
     
    +
    body {
     	color: red;
     }
     
     p {
     	font-size: 10px;
    -}
    <?php
    -
    -function dump( array ...$args ) {
    -	foreach ( $args as $item ) {
    -		var_dump( $item );
    -	}
    -
    -	die;
    -}
    -
    function foo() {
    +}
    +		
    +
    +
    function foo() {
     	console.log( 'indented using 1 tab' );
     }
     
     function bar() {
    -    console.log( 'indented using spaces' );
    -}
    Plain text
    -
    - -
    - -

    HTML embed

    + console.log( 'indented using spaces' ); +} +
    +
    -

    <details> and <summary> tags as HTML snippet

    -
    -
    - Details - Something small enough to escape casual notice. -
    +
    +

    List of Droppable Contacts

    +
      +
      Drop here
      -

      Decoupled Editor

      - -

      The toolbar

      -
      - -

      The editable

      -
      - - -

      Balloon Block Editor

      - -
      -
      -

      The three greatest things you learn from traveling

      -

      - Like all the great things on earth traveling teaches us by example. Here are some of the most precious lessons - I’ve learned over the years of traveling. -

      - -
      - Three monks ascending the stairs of an ancient temple. -
      Three monks ascending the stairs of an ancient temple.
      -
      - -

      Appreciation of diversity

      -

      - Getting used to an entirely different culture can be challenging. While it’s also nice to learn about - cultures online or from books, nothing comes close to experiencing cultural diversity in person. - You learn to appreciate each and every single one of the differences while you become more culturally fluid. -

      - -

      Improvisation

      -

      - Life doesn't allow us to execute every single plan perfectly. This especially seems to be the case when - you travel. You plan it down to every minute with a big checklist. But when it comes to executing it, - something always comes up and you’re left with your improvising skills. You learn to adapt as you go. - Here’s how my travel checklist looks now: -

      - -
        -
      • buy the ticket
      • -
      • start your adventure
      • -
      - -

      Confidence

      -

      - Going to a new place can be quite terrifying. While change and uncertainty makes us scared, traveling - teaches us how ridiculous it is to be afraid of something before it happens. The moment you face your - fear and see there is nothing to be afraid of, is the moment you discover bliss. -

      -
      -
      - -

      Balloon Block Editor with custom icon

      - -
      -
      -

      The three greatest things you learn from traveling

      -

      - Like all the great things on earth traveling teaches us by example. Here are some of the most precious - lessons - I’ve learned over the years of traveling. -

      - -
      - Three monks ascending the stairs of an ancient temple. -
      Three monks ascending the stairs of an ancient temple.
      -
      - -

      Appreciation of diversity

      -

      - Getting used to an entirely different culture can be challenging. While it’s also nice to learn about - cultures online or from books, nothing comes close to experiencing cultural diversity in person. - You learn to appreciate each and every single one of the differences while you become more culturally fluid. -

      - -

      Improvisation

      -

      - Life doesn't allow us to execute every single plan perfectly. This especially seems to be the case when - you travel. You plan it down to every minute with a big checklist. But when it comes to executing it, - something always comes up and you’re left with your improvising skills. You learn to adapt as you go. - Here’s how my travel checklist looks now: -

      - -
        -
      • buy the ticket
      • -
      • start your adventure
      • -
      - -

      Confidence

      -

      - Going to a new place can be quite terrifying. While change and uncertainty makes us scared, traveling - teaches us how ridiculous it is to be afraid of something before it happens. The moment you face your - fear and see there is nothing to be afraid of, is the moment you discover bliss. -

      -
      -
      - - diff --git a/packages/ckeditor5-clipboard/tests/manual/dragdrop.js b/packages/ckeditor5-clipboard/tests/manual/dragdrop.js index 4ea9b3aee8e..a9fcafc98c9 100644 --- a/packages/ckeditor5-clipboard/tests/manual/dragdrop.js +++ b/packages/ckeditor5-clipboard/tests/manual/dragdrop.js @@ -3,341 +3,276 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ -/* globals console, window, document, CKEditorInspector */ +/* globals console, window, document */ import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; -import { Essentials } from '@ckeditor/ckeditor5-essentials'; -import { Autoformat } from '@ckeditor/ckeditor5-autoformat'; -import { BlockQuote } from '@ckeditor/ckeditor5-block-quote'; -import { Bold, Italic } from '@ckeditor/ckeditor5-basic-styles'; -import { Heading } from '@ckeditor/ckeditor5-heading'; -import { Image, ImageCaption, ImageStyle, ImageToolbar } from '@ckeditor/ckeditor5-image'; -import { Indent } from '@ckeditor/ckeditor5-indent'; -import { Link } from '@ckeditor/ckeditor5-link'; -import { DocumentList, DocumentListProperties } from '@ckeditor/ckeditor5-list'; -import { Paragraph, ParagraphButtonUI } from '@ckeditor/ckeditor5-paragraph'; -import { Table, TableToolbar } from '@ckeditor/ckeditor5-table'; -import Underline from '@ckeditor/ckeditor5-basic-styles/src/underline'; -import Strikethrough from '@ckeditor/ckeditor5-basic-styles/src/strikethrough'; -import Superscript from '@ckeditor/ckeditor5-basic-styles/src/superscript'; -import Subscript from '@ckeditor/ckeditor5-basic-styles/src/subscript'; +import Alignment from '@ckeditor/ckeditor5-alignment/src/alignment'; +import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset'; +import AutoImage from '@ckeditor/ckeditor5-image/src/autoimage'; +import AutoLink from '@ckeditor/ckeditor5-link/src/autolink'; import Code from '@ckeditor/ckeditor5-basic-styles/src/code'; -import RemoveFormat from '@ckeditor/ckeditor5-remove-format/src/removeformat'; -import FindAndReplace from '@ckeditor/ckeditor5-find-and-replace/src/findandreplace'; -import FontColor from '@ckeditor/ckeditor5-font/src/fontcolor'; -import FontBackgroundColor from '@ckeditor/ckeditor5-font/src/fontbackgroundcolor'; -import FontFamily from '@ckeditor/ckeditor5-font/src/fontfamily'; -import FontSize from '@ckeditor/ckeditor5-font/src/fontsize'; -import Highlight from '@ckeditor/ckeditor5-highlight/src/highlight'; import CodeBlock from '@ckeditor/ckeditor5-code-block/src/codeblock'; -import TableProperties from '@ckeditor/ckeditor5-table/src/tableproperties'; -import TableCellProperties from '@ckeditor/ckeditor5-table/src/tablecellproperties'; -import TableCaption from '@ckeditor/ckeditor5-table/src/tablecaption'; -import TableColumnResize from '@ckeditor/ckeditor5-table/src/tablecolumnresize'; import EasyImage from '@ckeditor/ckeditor5-easy-image/src/easyimage'; +import HorizontalLine from '@ckeditor/ckeditor5-horizontal-line/src/horizontalline'; import ImageResize from '@ckeditor/ckeditor5-image/src/imageresize'; -import ImageInsert from '@ckeditor/ckeditor5-image/src/imageinsert'; import LinkImage from '@ckeditor/ckeditor5-link/src/linkimage'; -import AutoImage from '@ckeditor/ckeditor5-image/src/autoimage'; -import HtmlEmbed from '@ckeditor/ckeditor5-html-embed/src/htmlembed'; -import AutoLink from '@ckeditor/ckeditor5-link/src/autolink'; -import Mention from '@ckeditor/ckeditor5-mention/src/mention'; -import TextTransformation from '@ckeditor/ckeditor5-typing/src/texttransformation'; -import Alignment from '@ckeditor/ckeditor5-alignment/src/alignment'; -import IndentBlock from '@ckeditor/ckeditor5-indent/src/indentblock'; import PageBreak from '@ckeditor/ckeditor5-page-break/src/pagebreak'; -import HorizontalLine from '@ckeditor/ckeditor5-horizontal-line/src/horizontalline'; +import PasteFromOffice from '@ckeditor/ckeditor5-paste-from-office/src/pastefromoffice'; +import RemoveFormat from '@ckeditor/ckeditor5-remove-format/src/removeformat'; +import TextTransformation from '@ckeditor/ckeditor5-typing/src/texttransformation'; import CloudServices from '@ckeditor/ckeditor5-cloud-services/src/cloudservices'; -import TextPartLanguage from '@ckeditor/ckeditor5-language/src/textpartlanguage'; -import SourceEditing from '@ckeditor/ckeditor5-source-editing/src/sourceediting'; -import Style from '@ckeditor/ckeditor5-style/src/style'; -import GeneralHtmlSupport from '@ckeditor/ckeditor5-html-support/src/generalhtmlsupport'; -import DecoupledEditor from '@ckeditor/ckeditor5-editor-decoupled/src/decouplededitor'; -import Enter from '@ckeditor/ckeditor5-enter/src/enter'; -import Typing from '@ckeditor/ckeditor5-typing/src/typing'; -import Undo from '@ckeditor/ckeditor5-undo/src/undo'; -import BalloonEditor from '@ckeditor/ckeditor5-editor-balloon/src/ballooneditor'; -import List from '@ckeditor/ckeditor5-list/src/list'; -import HeadingButtonsUI from '@ckeditor/ckeditor5-heading/src/headingbuttonsui'; -import BlockToolbar from '@ckeditor/ckeditor5-ui/src/toolbar/block/blocktoolbar'; import ImageUpload from '@ckeditor/ckeditor5-image/src/imageupload'; +import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; +import Widget from '@ckeditor/ckeditor5-widget/src/widget'; +import { UpcastWriter } from '@ckeditor/ckeditor5-engine'; +import { toWidget, viewToModelPositionOutsideModelElement } from '@ckeditor/ckeditor5-widget'; +import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config'; -import { Clipboard, DragDropBlockToolbar, DragDrop } from '../../src'; +const contacts = [ + { name: 'Huckleberry Finn', tel: '+48 1345 234 235', email: 'h.finn@example.com', avatar: 'hfin' }, + { name: 'D\'Artagnan', tel: '+45 2345 234 235', email: 'dartagnan@example.com', avatar: 'dartagnan' }, + { name: 'Phileas Fogg', tel: '+44 3345 234 235', email: 'p.fogg@example.com', avatar: 'pfog' }, + { name: 'Alice', tel: '+20 4345 234 235', email: 'alice@example.com', avatar: 'alice' }, + { name: 'Little Red Riding Hood', tel: '+45 2345 234 235', email: 'lrrh@example.com', avatar: 'lrrh' }, + { name: 'Winnetou', tel: '+44 3345 234 235', email: 'winnetou@example.com', avatar: 'winetou' }, + { name: 'Edmond Dantès', tel: '+20 4345 234 235', email: 'count@example.com', avatar: 'edantes' }, + { name: 'Robinson Crusoe', tel: '+45 2345 234 235', email: 'r.crusoe@example.com', avatar: 'rcrusoe' } +]; -import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config'; +class HCardEditing extends Plugin { + static get requires() { + return [ Widget ]; + } + + init() { + this._defineSchema(); + this._defineConverters(); + this._defineClipboardInputOutput(); + + this.editor.editing.mapper.on( + 'viewToModelPosition', + viewToModelPositionOutsideModelElement( this.editor.model, viewElement => viewElement.hasClass( 'h-card' ) ) + ); + } + + _defineSchema() { + this.editor.model.schema.register( 'h-card', { + allowWhere: '$text', + isInline: true, + isObject: true, + allowAttributes: [ 'email', 'name', 'tel' ] + } ); + } + + _defineConverters() { + const conversion = this.editor.conversion; + + conversion.for( 'upcast' ).elementToElement( { + view: { + name: 'span', + classes: [ 'h-card' ] + }, + model: ( viewElement, { writer } ) => { + return writer.createElement( 'h-card', getCardDataFromViewElement( viewElement ) ); + } + } ); + + conversion.for( 'editingDowncast' ).elementToElement( { + model: 'h-card', + view: ( modelItem, { writer: viewWriter } ) => toWidget( createCardView( modelItem, viewWriter ), viewWriter ) + } ); + + conversion.for( 'dataDowncast' ).elementToElement( { + model: 'h-card', + view: ( modelItem, { writer: viewWriter } ) => createCardView( modelItem, viewWriter ) + } ); + + // Helper method for both downcast converters. + function createCardView( modelItem, viewWriter ) { + const email = modelItem.getAttribute( 'email' ); + const name = modelItem.getAttribute( 'name' ); + const tel = modelItem.getAttribute( 'tel' ); + + const cardView = viewWriter.createContainerElement( 'span', { class: 'h-card' } ); + const linkView = viewWriter.createContainerElement( 'a', { href: `mailto:${ email }`, class: 'p-name u-email' } ); + const phoneView = viewWriter.createContainerElement( 'span', { class: 'p-tel' } ); + + viewWriter.insert( viewWriter.createPositionAt( linkView, 0 ), viewWriter.createText( name ) ); + viewWriter.insert( viewWriter.createPositionAt( phoneView, 0 ), viewWriter.createText( tel ) ); + + viewWriter.insert( viewWriter.createPositionAt( cardView, 0 ), linkView ); + viewWriter.insert( viewWriter.createPositionAt( cardView, 'end' ), phoneView ); + + return cardView; + } + } + + _defineClipboardInputOutput() { + const view = this.editor.editing.view; + const viewDocument = view.document; + + this.listenTo( viewDocument, 'clipboardInput', ( evt, data ) => { + const contactData = data.dataTransfer.getData( 'contact' ); + + // There is no contact data or the clipboard content was already processed by the listener on the higher priority + // (for example while pasting into code-block). + if ( !contactData || data.content ) { + return; + } + + const contact = JSON.parse( contactData ); + const writer = new UpcastWriter( viewDocument ); + const fragment = writer.createDocumentFragment(); + + writer.appendChild( + writer.createElement( 'span', { class: 'h-card' }, [ + writer.createElement( 'a', { href: `mailto:${ contact.email }`, class: 'p-name u-email' }, contact.name ), + writer.createElement( 'span', { class: 'p-tel' }, contact.tel ) + ] ), + fragment + ); + + data.content = fragment; + } ); + + this.listenTo( document, 'clipboardOutput', ( evt, data ) => { + if ( data.content.childCount != 1 ) { + return; + } + + const viewElement = data.content.getChild( 0 ); + + if ( viewElement.is( 'element', 'span' ) && viewElement.hasClass( 'h-card' ) ) { + data.dataTransfer.setData( 'contact', JSON.stringify( getCardDataFromViewElement( viewElement ) ) ); + } + } ); + } +} + +function getCardDataFromViewElement( viewElement ) { + const children = Array.from( viewElement.getChildren() ); + const linkElement = children.find( element => element.is( 'element', 'a' ) && element.hasClass( 'p-name' ) ); + const telElement = children.find( element => element.is( 'element', 'span' ) && element.hasClass( 'p-tel' ) ); + + return { + name: getText( linkElement ), + tel: getText( telElement ), + email: linkElement.getAttribute( 'href' ).replace( /^mailto:/i, '' ) + }; +} + +function getText( viewElement ) { + return Array.from( viewElement.getChildren() ) + .map( node => node.is( '$text' ) ? node.data : '' ) + .join( '' ); +} ClassicEditor - .create( document.querySelector( '#editor-classic' ), { + .create( document.querySelector( '#editor' ), { plugins: [ - Essentials, Autoformat, BlockQuote, Bold, Heading, Image, ImageCaption, ImageStyle, ImageToolbar, Indent, Italic, Link, - DocumentList, Paragraph, Table, TableToolbar, Underline, Strikethrough, Superscript, Subscript, Code, RemoveFormat, - FindAndReplace, FontColor, FontBackgroundColor, FontFamily, FontSize, Highlight, - CodeBlock, DocumentListProperties, TableProperties, TableCellProperties, TableCaption, TableColumnResize, - EasyImage, ImageResize, ImageInsert, LinkImage, AutoImage, HtmlEmbed, - AutoLink, Mention, TextTransformation, Alignment, IndentBlock, PageBreak, HorizontalLine, - CloudServices, TextPartLanguage, SourceEditing, Style, GeneralHtmlSupport, DragDrop + ArticlePluginSet, Code, RemoveFormat, CodeBlock, EasyImage, ImageResize, LinkImage, + AutoImage, AutoLink, TextTransformation, Alignment, PasteFromOffice, PageBreak, + HorizontalLine, ImageUpload, CloudServices, HCardEditing ], toolbar: [ - 'heading', 'style', - '|', - 'removeFormat', 'bold', 'italic', 'strikethrough', 'underline', 'code', 'subscript', 'superscript', 'link', + 'heading', '|', - 'highlight', 'fontSize', 'fontFamily', 'fontColor', 'fontBackgroundColor', + 'removeFormat', 'bold', 'italic', 'code', 'link', '|', 'bulletedList', 'numberedList', '|', - 'blockQuote', 'insertImage', 'insertTable', 'codeBlock', + 'blockQuote', 'uploadImage', 'insertTable', 'mediaEmbed', 'codeBlock', '|', - 'htmlEmbed', - '|', - 'alignment', 'outdent', 'indent', + 'alignment', '|', 'pageBreak', 'horizontalLine', '|', - 'textPartLanguage', - '|', - 'sourceEditing', - '|', - 'undo', 'redo', 'findAndReplace' + 'undo', 'redo' ], cloudServices: CS_CONFIG, table: { - contentToolbar: [ - 'tableColumn', 'tableRow', 'mergeTableCells', 'tableProperties', 'tableCellProperties', 'toggleTableCaption' - ] + contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ] }, image: { - styles: [ - 'alignCenter', - 'alignLeft', - 'alignRight' - ], - resizeOptions: [ - { - name: 'resizeImage:original', - label: 'Original size', - value: null - }, - { - name: 'resizeImage:50', - label: '50%', - value: '50' - }, - { - name: 'resizeImage:75', - label: '75%', - value: '75' - } - ], toolbar: [ - 'imageTextAlternative', 'toggleImageCaption', '|', - 'imageStyle:inline', 'imageStyle:wrapText', 'imageStyle:breakText', 'imageStyle:side', '|', + 'imageTextAlternative', '|', + 'imageStyle:inline', 'imageStyle:wrapText', 'imageStyle:breakText', '|', 'resizeImage' - ], - insert: { - integrations: [ - 'insertImageViaUrl' - ] - } - }, - placeholder: 'Type the content here!', - mention: { - feeds: [ - { - marker: '@', - feed: [ - '@apple', '@bears', '@brownie', '@cake', '@cake', '@candy', '@canes', '@chocolate', '@cookie', '@cotton', '@cream', - '@cupcake', '@danish', '@donut', '@dragée', '@fruitcake', '@gingerbread', '@gummi', '@ice', '@jelly-o', - '@liquorice', '@macaroon', '@marzipan', '@oat', '@pie', '@plum', '@pudding', '@sesame', '@snaps', '@soufflé', - '@sugar', '@sweet', '@topping', '@wafer' - ], - minimumCharacters: 1 - } ] }, - link: { - decorators: { - isExternal: { - mode: 'manual', - label: 'Open in a new tab', - attributes: { - target: '_blank', - rel: 'noopener noreferrer' - } - }, - isDownloadable: { - mode: 'manual', - label: 'Downloadable', - attributes: { - download: 'download' - } - }, - isGallery: { - mode: 'manual', - label: 'Gallery link', - classes: 'gallery' - } - } - }, - htmlEmbed: { - showPreviews: true, - sanitizeHtml: html => ( { html, hasChange: false } ) - }, - list: { - properties: { - styles: true, - startIndex: true, - reversed: true - } - }, - style: { - definitions: [ - { - name: 'Article category', - element: 'h3', - classes: [ 'category' ] - }, - { - name: 'Title', - element: 'h2', - classes: [ 'document-title' ] - }, - { - name: 'Subtitle', - element: 'h3', - classes: [ 'document-subtitle' ] - }, - { - name: 'Info box', - element: 'p', - classes: [ 'info-box' ] - }, - { - name: 'Side quote', - element: 'blockquote', - classes: [ 'side-quote' ] - }, - { - name: 'Marker', - element: 'span', - classes: [ 'marker' ] - }, - { - name: 'Spoiler', - element: 'span', - classes: [ 'spoiler' ] - }, - { - name: 'Code (dark)', - element: 'pre', - classes: [ 'fancy-code', 'fancy-code-dark' ] - }, - { - name: 'Code (bright)', - element: 'pre', - classes: [ 'fancy-code', 'fancy-code-bright' ] - } - ] - } + placeholder: 'Type the content here!' } ) .then( editor => { - window.editorClassic = editor; + window.editor = editor; - CKEditorInspector.attach( { classic: editor } ); - } ) - .catch( err => { - console.error( err.stack ); - } ); + const button = document.getElementById( 'read-only' ); + let isReadOnly = false; -const editorData = document.querySelector( '#editor-classic' ).innerHTML; + button.addEventListener( 'click', () => { + isReadOnly = !isReadOnly; -DecoupledEditor - .create( editorData, { - plugins: [ Enter, Typing, Paragraph, Undo, Heading, Bold, Italic, Clipboard, Table, DragDrop ], - toolbar: [ 'heading', '|', 'bold', 'italic', 'insertTable', 'undo', 'redo' ] - } ) - .then( editor => { - document.querySelector( '.toolbar-container' ).appendChild( editor.ui.view.toolbar.element ); - document.querySelector( '.editable-container' ).appendChild( editor.ui.view.editable.element ); + if ( isReadOnly ) { + editor.enableReadOnlyMode( 'manual-test' ); + } else { + editor.disableReadOnlyMode( 'manual-test' ); + } - window.editorDecoupled = editor; + button.textContent = isReadOnly ? + 'Turn off read-only mode' : + 'Turn on read-only mode'; - CKEditorInspector.attach( { decoupled: editor } ); + editor.editing.view.focus(); + } ); } ) .catch( err => { console.error( err.stack ); } ); -BalloonEditor - .create( document.querySelector( '#editor-balloon' ), { - plugins: [ - Essentials, List, Paragraph, Heading, - Image, ImageResize, ImageStyle, ImageToolbar, ImageCaption, - HeadingButtonsUI, ParagraphButtonUI, BlockToolbar, Table, TableToolbar, - CloudServices, ImageUpload, EasyImage, DragDropBlockToolbar, DragDrop - ], - cloudServices: CS_CONFIG, - blockToolbar: [ - 'paragraph', 'heading1', 'heading2', 'heading3', 'bulletedList', 'numberedList', 'paragraph', - 'heading1', 'heading2', 'heading3', 'bulletedList', 'numberedList', 'paragraph', 'heading1', 'heading2', 'heading3', - 'bulletedList', 'numberedList', 'insertTable', 'uploadImage' - ], - image: { - toolbar: [ - 'imageTextAlternative', 'toggleImageCaption', '|', - 'imageStyle:inline', 'imageStyle:wrapText', 'imageStyle:breakText', 'imageStyle:side', '|', - 'resizeImage' - ] - }, - table: { - contentToolbar: [ - 'tableColumn', 'tableRow', 'mergeTableCells', 'toggleTableCaption' - ] - } - } ) - .then( editor => { - window.editorBalloon = editor; +const contactsContainer = document.querySelector( '#contactList' ); - CKEditorInspector.attach( { balloon: editor } ); - } ) - .catch( err => { - console.error( err.stack ); - } ); +contactsContainer.addEventListener( 'dragstart', event => { + const target = event.target.nodeType == 1 ? event.target : event.target.parentElement; + const draggable = target.closest( '[draggable]' ); -BalloonEditor - .create( document.querySelector( '#editor-balloon-custom-icon' ), { - plugins: [ - Essentials, List, Paragraph, Heading, - Image, ImageResize, ImageStyle, ImageToolbar, ImageCaption, - HeadingButtonsUI, ParagraphButtonUI, BlockToolbar, Table, TableToolbar, - CloudServices, ImageUpload, EasyImage, DragDropBlockToolbar, DragDrop - ], - cloudServices: CS_CONFIG, - blockToolbar: { - items: [ 'paragraph', 'heading1', 'heading2', 'heading3', 'bulletedList', 'numberedList', - 'paragraph', 'heading1', 'heading2', 'heading3', 'bulletedList', 'numberedList', 'paragraph', 'heading1', 'heading2', - 'heading3', 'bulletedList', 'numberedList', 'insertTable', 'uploadImage' ], - icon: '' + - '' - }, - image: { - toolbar: [ - 'imageTextAlternative', 'toggleImageCaption', '|', - 'imageStyle:inline', 'imageStyle:wrapText', 'imageStyle:breakText', 'imageStyle:side', '|', - 'resizeImage' - ] - }, - table: { - contentToolbar: [ - 'tableColumn', 'tableRow', 'mergeTableCells', 'toggleTableCaption' - ] - } - } ) - .then( editor => { - window.editorBalloonCustomIcon = editor; + event.dataTransfer.setData( 'text/plain', draggable.innerText ); + event.dataTransfer.setData( 'text/html', draggable.innerText ); + event.dataTransfer.setData( 'contact', JSON.stringify( contacts[ draggable.dataset.contact ] ) ); - CKEditorInspector.attach( { balloonCustomIcon: editor } ); - } ) - .catch( err => { - console.error( err.stack ); - } ); + event.dataTransfer.setDragImage( draggable, 0, 0 ); +} ); + +contacts.forEach( ( contact, id ) => { + const li = document.createElement( 'li' ); + + li.innerHTML = + `
      ` + + `avatar` + + contact.name + + '
      '; + + contactsContainer.appendChild( li ); +} ); + +const dropArea = document.querySelector( '#drop-area' ); + +dropArea.addEventListener( 'dragover', event => { + event.preventDefault(); + event.dataTransfer.dropEffect = 'copy'; + dropArea.classList.add( 'dragover' ); +} ); + +dropArea.addEventListener( 'dragleave', () => { + dropArea.classList.remove( 'dragover' ); +} ); + +dropArea.addEventListener( 'drop', event => { + const contact = event.dataTransfer.getData( 'contact' ); + + dropArea.innerText = + '-- text/plain --\n' + event.dataTransfer.getData( 'text/plain' ) + '\n\n' + + '-- text/html --\n' + event.dataTransfer.getData( 'text/html' ) + '\n\n' + + '-- contact --\n' + ( contact ? JSON.stringify( JSON.parse( contact ), 0, 2 ) : '' ) + '\n'; + dropArea.classList.remove( 'dragover' ); + + event.preventDefault(); +} ); diff --git a/packages/ckeditor5-clipboard/tests/manual/dragdrop.md b/packages/ckeditor5-clipboard/tests/manual/dragdrop.md index e69de29bb2d..fd18b15967f 100644 --- a/packages/ckeditor5-clipboard/tests/manual/dragdrop.md +++ b/packages/ckeditor5-clipboard/tests/manual/dragdrop.md @@ -0,0 +1,7 @@ +# Drag & Drop + +Main focus of this test is accurate finding the proper drop target. + +* Use the draggable elements on the right-side of the editor to drop them to the editor. +* Move different widgets and text fragments within the editor or outside from it. + From 70db2ddbe1cd7ffab64b7c10ef5cb89e33870fd6 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Mon, 25 Sep 2023 12:37:42 +0200 Subject: [PATCH 10/12] The drag and drop should be enabled by default for the balloon block toolbar. --- packages/ckeditor5-clipboard/src/dragdrop.ts | 3 ++- .../ckeditor5-clipboard/src/dragdropblocktoolbar.ts | 13 ++++++++----- packages/ckeditor5-clipboard/tests/dragdrop.js | 3 ++- .../tests/manual/dragdrop-blocks.js | 6 +++--- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/ckeditor5-clipboard/src/dragdrop.ts b/packages/ckeditor5-clipboard/src/dragdrop.ts index 08fefe04b47..c046589ac78 100644 --- a/packages/ckeditor5-clipboard/src/dragdrop.ts +++ b/packages/ckeditor5-clipboard/src/dragdrop.ts @@ -57,6 +57,7 @@ import ClipboardObserver, { } from './clipboardobserver'; import DragDropTarget from './dragdroptarget'; +import DragDropBlockToolbar from './dragdropblocktoolbar'; import '../theme/clipboard.css'; @@ -196,7 +197,7 @@ export default class DragDrop extends Plugin { * @inheritDoc */ public static get requires() { - return [ ClipboardPipeline, Widget, DragDropTarget ] as const; + return [ ClipboardPipeline, Widget, DragDropTarget, DragDropBlockToolbar ] as const; } /** diff --git a/packages/ckeditor5-clipboard/src/dragdropblocktoolbar.ts b/packages/ckeditor5-clipboard/src/dragdropblocktoolbar.ts index 2d84b45bdde..bdd92278541 100644 --- a/packages/ckeditor5-clipboard/src/dragdropblocktoolbar.ts +++ b/packages/ckeditor5-clipboard/src/dragdropblocktoolbar.ts @@ -7,8 +7,6 @@ * @module clipboard/dragdropblocktoolbar */ -/* istanbul ignore file -- @preserve */ - import { Plugin } from '@ckeditor/ckeditor5-core'; import { @@ -69,12 +67,18 @@ export default class DragDropBlockToolbar extends Plugin { const blockToolbar: BlockToolbar = editor.plugins.get( 'BlockToolbar' ); const element = blockToolbar.buttonView.element!; - element.setAttribute( 'draggable', 'true' ); - this._domEmitter.listenTo( element, 'dragstart', ( evt, data ) => this._handleBlockDragStart( data ) ); this._domEmitter.listenTo( global.document, 'dragover', ( evt, data ) => this._handleBlockDragging( data ) ); this._domEmitter.listenTo( global.document, 'drop', ( evt, data ) => this._handleBlockDragging( data ) ); this._domEmitter.listenTo( global.document, 'dragend', () => this._handleBlockDragEnd(), { useCapture: true } ); + + if ( this.isEnabled ) { + element.setAttribute( 'draggable', 'true' ); + } + + this.on>( 'change:isEnabled', ( evt, name, isEnabled ) => { + element.setAttribute( 'draggable', isEnabled ? 'true' : 'false' ); + } ); } } @@ -146,4 +150,3 @@ export default class DragDropBlockToolbar extends Plugin { this._isBlockDragging = false; } } - diff --git a/packages/ckeditor5-clipboard/tests/dragdrop.js b/packages/ckeditor5-clipboard/tests/dragdrop.js index bbe4079cfe1..42f95a5615b 100644 --- a/packages/ckeditor5-clipboard/tests/dragdrop.js +++ b/packages/ckeditor5-clipboard/tests/dragdrop.js @@ -9,6 +9,7 @@ import ClipboardPipeline from '../src/clipboardpipeline'; import DragDrop from '../src/dragdrop'; import DragDropTarget from '../src/dragdroptarget'; import PastePlainText from '../src/pasteplaintext'; +import DragDropBlockToolbar from '../src/dragdropblocktoolbar'; import Widget from '@ckeditor/ckeditor5-widget/src/widget'; import WidgetToolbarRepository from '@ckeditor/ckeditor5-widget/src/widgettoolbarrepository'; @@ -33,7 +34,7 @@ describe( 'Drag and Drop', () => { testUtils.createSinonSandbox(); it( 'requires DragDropTarget, ClipboardPipeline and Widget', () => { - expect( DragDrop.requires ).to.deep.equal( [ ClipboardPipeline, Widget, DragDropTarget ] ); + expect( DragDrop.requires ).to.deep.equal( [ ClipboardPipeline, Widget, DragDropTarget, DragDropBlockToolbar ] ); } ); it( 'has proper name', () => { diff --git a/packages/ckeditor5-clipboard/tests/manual/dragdrop-blocks.js b/packages/ckeditor5-clipboard/tests/manual/dragdrop-blocks.js index 6d58ec438c5..b21a4e650e4 100644 --- a/packages/ckeditor5-clipboard/tests/manual/dragdrop-blocks.js +++ b/packages/ckeditor5-clipboard/tests/manual/dragdrop-blocks.js @@ -62,7 +62,7 @@ import HeadingButtonsUI from '@ckeditor/ckeditor5-heading/src/headingbuttonsui'; import BlockToolbar from '@ckeditor/ckeditor5-ui/src/toolbar/block/blocktoolbar'; import ImageUpload from '@ckeditor/ckeditor5-image/src/imageupload'; -import { Clipboard, DragDropBlockToolbar, DragDrop } from '../../src'; +import { Clipboard, DragDrop } from '../../src'; import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config'; @@ -274,7 +274,7 @@ BalloonEditor Essentials, List, Paragraph, Heading, Image, ImageResize, ImageStyle, ImageToolbar, ImageCaption, HeadingButtonsUI, ParagraphButtonUI, BlockToolbar, Table, TableToolbar, - CloudServices, ImageUpload, EasyImage, DragDropBlockToolbar, DragDrop + CloudServices, ImageUpload, EasyImage, DragDrop ], cloudServices: CS_CONFIG, blockToolbar: [ @@ -310,7 +310,7 @@ BalloonEditor Essentials, List, Paragraph, Heading, Image, ImageResize, ImageStyle, ImageToolbar, ImageCaption, HeadingButtonsUI, ParagraphButtonUI, BlockToolbar, Table, TableToolbar, - CloudServices, ImageUpload, EasyImage, DragDropBlockToolbar, DragDrop + CloudServices, ImageUpload, EasyImage, DragDrop ], cloudServices: CS_CONFIG, blockToolbar: { From 96e76c7b677fe45d5fa94b6b1a8710d1fbf2bef9 Mon Sep 17 00:00:00 2001 From: Illia Sheremetov Date: Mon, 25 Sep 2023 19:32:01 +0200 Subject: [PATCH 11/12] Back coverage to 100% --- .../tests/dragdropblocktoolbar.js | 309 ++++++++++++++++++ 1 file changed, 309 insertions(+) create mode 100644 packages/ckeditor5-clipboard/tests/dragdropblocktoolbar.js diff --git a/packages/ckeditor5-clipboard/tests/dragdropblocktoolbar.js b/packages/ckeditor5-clipboard/tests/dragdropblocktoolbar.js new file mode 100644 index 00000000000..1ad54ee94ef --- /dev/null +++ b/packages/ckeditor5-clipboard/tests/dragdropblocktoolbar.js @@ -0,0 +1,309 @@ +/** + * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* globals document, DragEvent, DataTransfer */ + +import DragDropTarget from '../src/dragdroptarget'; +import DragDrop from '../src/dragdrop'; +import PastePlainText from '../src/pasteplaintext'; +import DragDropBlockToolbar from '../src/dragdropblocktoolbar'; + +import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; +import Table from '@ckeditor/ckeditor5-table/src/table'; +import HorizontalLine from '@ckeditor/ckeditor5-horizontal-line/src/horizontalline'; +import ShiftEnter from '@ckeditor/ckeditor5-enter/src/shiftenter'; +import BlockQuote from '@ckeditor/ckeditor5-block-quote/src/blockquote'; +import BlockToolbar from '@ckeditor/ckeditor5-ui/src/toolbar/block/blocktoolbar'; +import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold'; +import { Image, ImageCaption } from '@ckeditor/ckeditor5-image'; +import env from '@ckeditor/ckeditor5-utils/src/env'; + +import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; +import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; + +describe( 'Drag and Drop Block Toolbar', () => { + let editorElement, editor, model, view, viewDocument, root, mapper, domConverter, dragDropBlockToolbar, + blockToolbar, blockToolbarButton; + + testUtils.createSinonSandbox(); + + beforeEach( async () => { + editorElement = document.createElement( 'div' ); + document.body.appendChild( editorElement ); + + editor = await ClassicTestEditor.create( editorElement, { + plugins: [ + DragDrop, + DragDropBlockToolbar, + DragDropTarget, + PastePlainText, + Paragraph, + Table, + HorizontalLine, + ShiftEnter, + BlockQuote, + Bold, + Image, + ImageCaption, + BlockToolbar + ], + blockToolbar: [ 'bold' ] + } ); + + editor.ui.focusTracker.isFocused = true; + + model = editor.model; + root = model.document.getRoot(); + mapper = editor.editing.mapper; + view = editor.editing.view; + viewDocument = view.document; + domConverter = view.domConverter; + dragDropBlockToolbar = editor.plugins.get( DragDropBlockToolbar ); + + blockToolbar = editor.plugins.get( BlockToolbar ); + blockToolbarButton = blockToolbar.buttonView.element; + } ); + + afterEach( async () => { + await editor.destroy(); + editorElement.remove(); + } ); + + describe( 'init', () => { + it( 'should toggle read only mode', () => { + expect( dragDropBlockToolbar.isEnabled ).to.be.true; + + editor.enableReadOnlyMode( 'test' ); + + expect( dragDropBlockToolbar.isEnabled ).to.be.false; + + editor.disableReadOnlyMode( 'test' ); + + expect( dragDropBlockToolbar.isEnabled ).to.be.true; + } ); + + it( 'should be disabled on android', async () => { + env.isAndroid = true; + + const editor = await ClassicTestEditor.create( editorElement, { + plugins: [ DragDrop, DragDropBlockToolbar ] + } ); + + expect( editor.plugins.get( DragDropBlockToolbar ).isEnabled ).to.be.false; + + await editor.destroy(); + + env.isAndroid = false; + } ); + + it( 'should display block toolbar button', () => { + setModelData( model, '[foo]bar' ); + + expect( blockToolbarButton ).not.to.be.null; + expect( blockToolbarButton.className.includes( 'ck-hidden' ) ).to.be.false; + } ); + } ); + + describe( 'dragging', () => { + it( 'should set selection on drag block toolbar button', () => { + setModelData( model, '[foo]bar' ); + + const dragEvent = new DragEvent( 'dragstart' ); + + viewDocument.on( 'dragstart', ( evt, data ) => { + expect( data.domEvent ).to.equal( dragEvent ); + evt.stop(); + }, 'highest' ); + + blockToolbarButton.dispatchEvent( dragEvent ); + + const modelSelection = model.document.selection; + const { focus, anchor } = modelSelection; + + expect( focus.path ).to.have.same.members( [ 0, 6 ] ); + expect( anchor.path ).to.have.same.members( [ 0, 0 ] ); + expect( dragDropBlockToolbar._isBlockDragging ).to.be.true; + } ); + + it( 'should not set selection if plugin is disabled', () => { + setModelData( model, '[foo]bar' ); + + editor.enableReadOnlyMode( 'test' ); + + const dragEvent = new DragEvent( 'dragstart' ); + + blockToolbarButton.dispatchEvent( dragEvent ); + + const modelSelection = model.document.selection; + const { focus, anchor } = modelSelection; + + expect( focus.path ).to.have.same.members( [ 0, 3 ] ); + expect( anchor.path ).to.have.same.members( [ 0, 0 ] ); + } ); + + it( 'should display dragging marker', () => { + setModelData( model, '[foo]bar' ); + + const dragStartEvent = new DragEvent( 'dragstart', { + dataTransfer: new DataTransfer() + } ); + const modelParagraph = root.getNodeByPath( [ 0 ] ); + const viewParagraph = mapper.toViewElement( modelParagraph ); + const domNode = domConverter.mapViewToDom( viewParagraph ); + + const { x: clientX, y: clientY } = domNode.getBoundingClientRect(); + + blockToolbarButton.dispatchEvent( dragStartEvent ); + + const dragOverEvent = new DragEvent( 'dragover', { + clientX, + clientY, + dataTransfer: new DataTransfer() + } ); + + document.dispatchEvent( dragOverEvent ); + + const targetPosition = model.createPositionAt( root.getChild( 0 ), 'before' ); + + expectDraggingMarker( targetPosition ); + } ); + + it( 'should not drag if block toolbar is disabled', () => { + setModelData( model, '[foo]bar' ); + + editor.enableReadOnlyMode( 'test' ); + + const dragStartEvent = new DragEvent( 'dragstart', { + dataTransfer: new DataTransfer() + } ); + const modelParagraph = root.getNodeByPath( [ 0 ] ); + const viewParagraph = mapper.toViewElement( modelParagraph ); + const domNode = domConverter.mapViewToDom( viewParagraph ); + + const { x: clientX, y: clientY } = domNode.getBoundingClientRect(); + + blockToolbarButton.dispatchEvent( dragStartEvent ); + + const dragOverEvent = new DragEvent( 'dragover', { + clientX, + clientY, + dataTransfer: new DataTransfer() + } ); + + document.dispatchEvent( dragOverEvent ); + + expect( model.markers.has( 'drop-target' ) ).to.be.false; + } ); + + it( 'should not display dragging marker', () => { + setModelData( model, '[foo]bar' ); + + const dragStartEvent = new DragEvent( 'dragstart', { + dataTransfer: new DataTransfer() + } ); + + blockToolbarButton.dispatchEvent( dragStartEvent ); + + const dragOverEvent = new DragEvent( 'dragover', { + clientX: -99999, + clientY: -99999, + dataTransfer: new DataTransfer() + } ); + + document.dispatchEvent( dragOverEvent ); + + expect( model.markers.has( 'drop-target' ) ).to.be.false; + } ); + + it( 'should drop element', () => { + setModelData( model, '[foo]bar' ); + + const modelParagraph = root.getNodeByPath( [ 0 ] ); + const viewParagraph = mapper.toViewElement( modelParagraph ); + const domNode = domConverter.mapViewToDom( viewParagraph ); + const spyClipboardInput = sinon.spy(); + + const { x: clientX, y: clientY } = domNode.getBoundingClientRect(); + const dragStartEvent = new DragEvent( 'dragstart', { + dataTransfer: new DataTransfer() + } ); + + viewDocument.on( 'clipboardInput', ( event, data ) => spyClipboardInput( data ) ); + + blockToolbarButton.dispatchEvent( dragStartEvent ); + + const dragOverEvent = new DragEvent( 'dragover', { + clientX, + clientY, + dataTransfer: new DataTransfer() + } ); + + document.dispatchEvent( dragOverEvent ); + + const dropEvent = new DragEvent( 'drop', { + clientX, + clientY, + dataTransfer: new DataTransfer() + } ); + + viewDocument.on( 'drop', ( evt, data ) => { + data.stopPropagation(); + }, 'highest' ); + + document.dispatchEvent( dropEvent ); + + expect( spyClipboardInput.called ).to.be.true; + expect( spyClipboardInput.firstCall.firstArg.method ).to.equal( 'drop' ); + } ); + + it( 'should end drag and drop', () => { + setModelData( model, '[foo]bar' ); + + const modelParagraph = root.getNodeByPath( [ 0 ] ); + const viewParagraph = mapper.toViewElement( modelParagraph ); + const domNode = domConverter.mapViewToDom( viewParagraph ); + const spyClipboardInput = sinon.spy(); + + const { x: clientX, y: clientY } = domNode.getBoundingClientRect(); + const dragStartEvent = new DragEvent( 'dragstart', { + dataTransfer: new DataTransfer() + } ); + + viewDocument.on( 'clipboardInput', ( event, data ) => spyClipboardInput( data ) ); + + blockToolbarButton.dispatchEvent( dragStartEvent ); + + const dragOverEvent = new DragEvent( 'dragover', { + clientX, + clientY, + dataTransfer: new DataTransfer() + } ); + + document.dispatchEvent( dragOverEvent ); + + const dragEndEvent = new DragEvent( 'dragend', { + clientX, + clientY, + dataTransfer: new DataTransfer() + } ); + + document.dispatchEvent( dragEndEvent ); + + expect( dragDropBlockToolbar._isBlockDragging ).to.be.false; + } ); + } ); + + function expectDraggingMarker( targetPositionOrRange ) { + expect( model.markers.has( 'drop-target' ) ).to.be.true; + + if ( targetPositionOrRange.is( 'position' ) ) { + expect( model.markers.get( 'drop-target' ).getRange().isCollapsed ).to.be.true; + expect( model.markers.get( 'drop-target' ).getRange().start.isEqual( targetPositionOrRange ) ).to.be.true; + } else { + expect( model.markers.get( 'drop-target' ).getRange().isEqual( targetPositionOrRange ) ).to.be.true; + } + } +} ); From dae7824fdb096957ae4db031fd9c5ee75f732618 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Tue, 26 Sep 2023 12:00:55 +0200 Subject: [PATCH 12/12] Test fixes. --- .../ckeditor5-clipboard/tests/dragdropblocktoolbar.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/ckeditor5-clipboard/tests/dragdropblocktoolbar.js b/packages/ckeditor5-clipboard/tests/dragdropblocktoolbar.js index 1ad54ee94ef..5fc26d69c89 100644 --- a/packages/ckeditor5-clipboard/tests/dragdropblocktoolbar.js +++ b/packages/ckeditor5-clipboard/tests/dragdropblocktoolbar.js @@ -123,8 +123,8 @@ describe( 'Drag and Drop Block Toolbar', () => { const modelSelection = model.document.selection; const { focus, anchor } = modelSelection; - expect( focus.path ).to.have.same.members( [ 0, 6 ] ); - expect( anchor.path ).to.have.same.members( [ 0, 0 ] ); + expect( focus.path ).to.deep.equal( [ 0, 6 ] ); + expect( anchor.path ).to.deep.equal( [ 0, 0 ] ); expect( dragDropBlockToolbar._isBlockDragging ).to.be.true; } ); @@ -140,8 +140,8 @@ describe( 'Drag and Drop Block Toolbar', () => { const modelSelection = model.document.selection; const { focus, anchor } = modelSelection; - expect( focus.path ).to.have.same.members( [ 0, 3 ] ); - expect( anchor.path ).to.have.same.members( [ 0, 0 ] ); + expect( focus.path ).to.deep.equal( [ 0, 3 ] ); + expect( anchor.path ).to.deep.equal( [ 0, 0 ] ); } ); it( 'should display dragging marker', () => {