File: //home/arjun/projects/buyercall/node_modules/@ckeditor/ckeditor5-clipboard/src/dragdrop.js
/**
* @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @module clipboard/dragdrop
*/
/* globals setTimeout, clearTimeout */
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import LiveRange from '@ckeditor/ckeditor5-engine/src/model/liverange';
import MouseObserver from '@ckeditor/ckeditor5-engine/src/view/observer/mouseobserver';
import Widget from '@ckeditor/ckeditor5-widget/src/widget';
import uid from '@ckeditor/ckeditor5-utils/src/uid';
import env from '@ckeditor/ckeditor5-utils/src/env';
import { isWidget } from '@ckeditor/ckeditor5-widget/src/utils';
import ClipboardPipeline from './clipboardpipeline';
import ClipboardObserver from './clipboardobserver';
import { throttle } from 'lodash-es';
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/guides/deep-dive/clipboard clipboard deep dive guide}.
*
* @extends module:core/plugin~Plugin
*/
export default class DragDrop extends Plugin {
/**
* @inheritDoc
*/
static get pluginName() {
return 'DragDrop';
}
/**
* @inheritDoc
*/
static get requires() {
return [ ClipboardPipeline, Widget ];
}
/**
* @inheritDoc
*/
init() {
const editor = this.editor;
const view = editor.editing.view;
/**
* The live range over the original content that is being dragged.
*
* @private
* @type {module:engine/model/liverange~LiveRange}
*/
this._draggedRange = 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
* @type {String}
*/
this._draggingUid = '';
/**
* The reference to the model element that currently has a `draggable` attribute set (it is set while dragging).
*
* @private
* @type {module:engine/model/element~Element}
*/
this._draggableElement = null;
/**
* A throttled callback updating the drop marker.
*
* @private
* @type {Function}
*/
this._updateDropMarkerThrottled = throttle( targetRange => this._updateDropMarker( targetRange ), 40 );
/**
* A delayed callback removing the drop marker.
*
* @private
* @type {Function}
*/
this._removeDropMarkerDelayed = delay( () => this._removeDropMarker(), 40 );
/**
* A delayed callback removing draggable attributes.
*
* @private
* @type {Function}
*/
this._clearDraggableAttributesDelayed = delay( () => this._clearDraggableAttributes(), 40 );
view.addObserver( ClipboardObserver );
view.addObserver( MouseObserver );
this._setupDragging();
this._setupContentInsertionIntegration();
this._setupClipboardInputIntegration();
this._setupDropMarker();
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
*/
destroy() {
if ( this._draggedRange ) {
this._draggedRange.detach();
this._draggedRange = null;
}
this._updateDropMarkerThrottled.cancel();
this._removeDropMarkerDelayed.cancel();
this._clearDraggableAttributesDelayed.cancel();
return super.destroy();
}
/**
* Drag and drop events handling.
*
* @private
*/
_setupDragging() {
const editor = this.editor;
const model = editor.model;
const modelDocument = model.document;
const view = editor.editing.view;
const viewDocument = view.document;
// 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();
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' ) ) {
editor.plugins.get( '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() );
}
}
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: evt.name } );
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' } );
// 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.
this._removeDropMarkerDelayed();
} );
// Handler for moving dragged content over the target area.
this.listenTo( viewDocument, 'dragging', ( evt, data ) => {
if ( !this.isEnabled ) {
data.dataTransfer.dropEffect = 'none';
return;
}
this._removeDropMarkerDelayed.cancel();
const targetRange = findDropTargetRange( editor, data.targetRanges, data.target );
// 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';
}
}
/* istanbul ignore else */
if ( targetRange ) {
this._updateDropMarkerThrottled( targetRange );
}
}, { priority: 'low' } );
}
/**
* Integration with the `clipboardInput` event.
*
* @private
*/
_setupClipboardInputIntegration() {
const editor = this.editor;
const view = editor.editing.view;
const viewDocument = view.document;
// Update the event target ranges and abort dropping if dropping over itself.
this.listenTo( viewDocument, 'clipboardInput', ( evt, data ) => {
if ( data.method != 'drop' ) {
return;
}
const targetRange = findDropTargetRange( editor, data.targetRanges, data.target );
// 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 */
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() {
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() {
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() {
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;
} );
}
/**
* Creates downcast conversion for the drop target marker.
*
* @private
*/
_setupDropMarker() {
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.innerHTML = '⁠<span></span>⁠';
return domElement;
} );
}
} );
}
/**
* Updates the drop target marker to the provided range.
*
* @private
* @param {module:engine/model/range~Range} targetRange The range to set the marker to.
*/
_updateDropMarker( targetRange ) {
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() {
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.
*
* @private
* @param {Boolean} moved Whether the move succeeded.
*/
_finalizeDragging( moved ) {
const editor = this.editor;
const model = editor.model;
this._removeDropMarker();
this._clearDraggableAttributes();
if ( editor.plugins.has( 'WidgetToolbarRepository' ) ) {
editor.plugins.get( 'WidgetToolbarRepository' ).clearForceDisabled( 'dragDrop' );
}
this._draggingUid = '';
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;
}
}
// Returns fixed selection range for given position and target element.
//
// @param {module:core/editor/editor~Editor} editor
// @param {Array.<module:engine/view/range~Range>} targetViewRanges
// @param {module:engine/view/element~Element} targetViewElement
// @returns {module:engine/model/range~Range|null}
function findDropTargetRange( editor, targetViewRanges, targetViewElement ) {
const model = editor.model;
const mapper = editor.editing.mapper;
let range = 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;
}
// 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;
// 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 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 ( range ) {
return range;
}
// 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' );
if ( range ) {
return range;
}
// 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 <table> element itself (view target element was a `<td>`
// so a nested editable, but view target position was directly in the `<figure>` element).
return findDropTargetRangeOnAncestorObject( editor, targetModelPosition.parent );
}
// Returns fixed selection range for a given position and a target element if it is over the widget but not over its nested editable.
//
// @param {module:core/editor/editor~Editor} editor
// @param {module:engine/view/element~Element} targetViewElement
// @returns {module:engine/model/range~Range|null}
function findDropTargetRangeOnWidget( editor, targetViewElement ) {
const model = editor.model;
const mapper = editor.editing.mapper;
// Quick win if the target is a widget.
if ( isWidget( targetViewElement ) ) {
return model.createRangeOn( mapper.toModelElement( targetViewElement ) );
}
// 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' ) );
// ...and if the widget was closer then it is a drop target.
if ( isWidget( ancestor ) ) {
return model.createRangeOn( mapper.toModelElement( ancestor ) );
}
}
return null;
}
// Returns fixed selection range inside a model element.
//
// @param {module:core/editor/editor~Editor} editor
// @param {module:engine/model/element~Element} targetModelElement
// @returns {module:engine/model/range~Range}
function findDropTargetRangeInElement( editor, targetModelElement ) {
const model = editor.model;
const schema = model.schema;
const positionAtElementStart = model.createPositionAt( targetModelElement, 0 );
return schema.getNearestSelectionRange( positionAtElementStart, 'forward' );
}
// Returns fixed selection range for a given position and a target element if the drop is between blocks.
//
// @param {module:core/editor/editor~Editor} editor
// @param {module:engine/model/position~Position} targetModelPosition
// @param {module:engine/model/element~Element} targetModelElement
// @returns {module:engine/model/range~Range|null}
function findDropTargetRangeBetweenBlocks( editor, targetModelPosition, targetModelElement ) {
const model = editor.model;
// Check if target is between blocks.
if ( !model.schema.checkChild( targetModelElement, '$block' ) ) {
return null;
}
// 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 );
}
return null;
}
// Returns a selection range on the ancestor object.
//
// @param {module:core/editor/editor~Editor} editor
// @param {module:engine/model/element~Element} element
// @returns {module:engine/model/range~Range}
function findDropTargetRangeOnAncestorObject( editor, element ) {
const model = editor.model;
while ( element ) {
if ( model.schema.isObject( element ) ) {
return model.createRangeOn( element );
}
element = element.parent;
}
}
// Returns the closest model element for the specified view element.
//
// @param {module:core/editor/editor~Editor} editor
// @param {module:engine/view/element~Element} element
// @returns {module:engine/model/element~Element}
function getClosestMappedModelElement( editor, element ) {
const mapper = editor.editing.mapper;
const view = editor.editing.view;
const targetModelElement = mapper.toModelElement( element );
if ( targetModelElement ) {
return targetModelElement;
}
// 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 );
}
// 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 ) {
if ( env.isGecko ) {
return dataTransfer.dropEffect;
}
return [ 'all', 'copyMove' ].includes( dataTransfer.effectAllowed ) ? 'move' : 'copy';
}
// Returns a function wrapper that will trigger a function after a specified wait time.
// The timeout can be canceled by calling the cancel function on the returned wrapped function.
//
// @param {Function} func The function to wrap.
// @param {Number} wait The timeout in ms.
// @returns {Function}
function delay( func, wait ) {
let timer;
function delayed( ...args ) {
delayed.cancel();
timer = setTimeout( () => func( ...args ), wait );
}
delayed.cancel = () => {
clearTimeout( timer );
};
return delayed;
}
// Returns a widget element that should be dragged.
//
// @param {module:engine/view/element~Element} target
// @returns {module:engine/view/element~Element}
function findDraggableWidget( target ) {
// 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;
}