Source: dragdrop.js

/**
 * dragdrop.js is part of Aloha Editor project http://aloha-editor.org
 *
 * Aloha Editor is a WYSIWYG HTML5 inline editing library and editor.
 * Copyright (c) 2010-2014 Gentics Software GmbH, Vienna, Austria.
 * Contributors http://aloha-editor.org/contribution.php
 *
 * @reference
 * http://www.whatwg.org/specs/web-apps/current-work/#dnd
 * http://www.html5rocks.com/en/tutorials/dnd/basics/
 * https://developer.mozilla.org/en-US/docs/Drag_and_drop_events
 * https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer
 */
define([
	'dom',
	'maps',
	'arrays',
	'ranges',
	'editing',
	'selections'
], /** @exports DragDrop */ function DragDrop(
	Dom,
	Maps,
	Arrays,
	Ranges,
	Editing,
	Selections
) {
	'use strict';

	/**
	 * The pixel distance from the pointer of where the caret should be
	 * rendered when dragging.
	 */
	var DRAGGING_CARET_OFFSET = -10;

	/**
	 * Default drag and drop context properites.
	 *
	 * These are the default attributes from which drag and drop contexts will
	 * be created.
	 *
	 * @type {Object.<string, *>}
	 */
	var DEFAULTS = {
		'dropEffect' : 'none',
		'element'    : null,
		'target '    : null,
		'data'       : ['text/plain', '']
	};

	/**
	 * Creates a new drag and drop context.
	 *
	 * The following attributes are supported in the options object that is
	 * passed to this function:
	 *
	 *	`dropEffect`
	 *		The dropEffect attribute controls the drag-and-drop feedback that
	 *		the user is given during a drag-and-drop operation.  If the
	 *		`dropEffect` value is set to "copy", for example, the user agent
	 *		may rendered the drag icon with a "+" (plus) sign.
	 *		The supported values are "none", "copy", "link", or "move".  All
	 *		other values will be ignored.
	 *
	 *	`element`
	 *		The element on which dragging was initiated on.  If the drag and
	 *		drop operation is a moving operation, this element will be
	 *		relocated into the range boundary at the point at which the drop
	 *		event is fired.
	 *		If the drag and drop operation is a copying operation, then this
	 *		attribute should a reference to a deep clone of the element on
	 *		which dragging was initiated.
	 *
	 *	`data`
	 *		A tuple describing the data that will be set to the drag data
	 *		store.  See:
	 *		http://www.whatwg.org/specs/web-apps/current-work/multipage/dnd.html#drag-data-store
	 *
	 * @param  {Object} options
	 * @return {Object}
	 */
	function Context(options) {
		return Maps.merge({}, DEFAULTS, options);
	}

	/**
	 * Whether or not the given node is draggable.
	 *
	 * In an attempt to follow the implementation on most browsers, text
	 * selections, IMG elements, and anchor elements with an href attribute are
	 * draggable by default.
	 *
	 * @param  {Element} node
	 * @return {boolean}
	 */
	function isDraggable(node) {
		if (!Dom.isElementNode(node)) {
			return false;
		}

		var attr = node.getAttribute('draggable');

		if ('false' === attr) {
			return false;
		}

		if ('true' === attr) {
			return true;
		}

		if ('IMG' === node.nodeName) {
			return true;
		}

		return ('A' === node.nodeName) && node.getAttribute('href');
	}

	/**
	 * Moves the given node, into the given range.
	 *
	 * @param {Range}   range
	 * @param {Element} node
	 */
	function moveNode(range, node) {
		var prev = node.previousSibling;
		Editing.insert(range, node);
		if (prev && prev.nextSibling) {
			Dom.merge(prev, prev.nextSibling);
		}
		Ranges.collapseToEnd(range);
	}

	/**
	 * Processes drag and drop events operations.
	 *
	 * @param  {Object} alohaEvent
	 * @return {Object}
	 */
	function handle(alohaEvent) {
		var context = alohaEvent.editor.dndContext;
		var event = alohaEvent.nativeEvent;
		var x, y;
		var carets;

		if (!context) {
			return alohaEvent;
		}

		var doc = event.target.ownerDocument;

		switch (alohaEvent.type) {

		case 'dragstart':

			// Because this is required for FF for dragging to start on
			// elements other than IMG elements or anchor elements with href
			// values.
			event.dataTransfer.setData(context.data[0], context.data[1]);

			break;

		case 'dragover':

			x = event.clientX + DRAGGING_CARET_OFFSET;
			y = event.clientY + DRAGGING_CARET_OFFSET;
			carets = Selections.hideCarets(doc);
			alohaEvent.range = Ranges.fromPosition(x, y, doc);
			Selections.unhideCarets(carets);

			// Because this is necessary to enable dropping to work
			event.preventDefault();

			break;

		case 'drop':

			x = event.clientX + DRAGGING_CARET_OFFSET;
			y = event.clientY + DRAGGING_CARET_OFFSET;
			carets = Selections.hideCarets(doc);
			alohaEvent.range = Ranges.fromPosition(x, y, doc);
			Selections.unhideCarets(carets);

			if (alohaEvent.range) {
				moveNode(
					alohaEvent.range,
					alohaEvent.editor.dndContext.element
				);
			}

			if (event.stopPropagation) {
				event.stopPropagation();
			}

			// Because some browsers will redirect otherwise
			event.preventDefault();

			break;
		}

		return alohaEvent;
	}

	return {
		handle      : handle,
		Context     : Context,
		isDraggable : isDraggable
	};
});