DragDropOverrides.js 6.99 KB
/**
 * DragDropOverrides.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

/**
 * This module contains logic overriding the drag/drop logic of the editor.
 *
 * @private
 * @class tinymce.DragDropOverrides
 */
define("tinymce/DragDropOverrides", [
	"tinymce/dom/NodeType",
	"tinymce/util/Arr",
	"tinymce/util/Fun",
	"tinymce/util/Delay",
	"tinymce/dom/DOMUtils",
	"tinymce/dom/MousePosition"
], function(
	NodeType, Arr, Fun, Delay, DOMUtils, MousePosition
) {
	var isContentEditableFalse = NodeType.isContentEditableFalse,
		isContentEditableTrue = NodeType.isContentEditableTrue;

	var isDraggable = function (elm) {
		return isContentEditableFalse(elm);
	};

	var isValidDropTarget = function (editor, targetElement, dragElement) {
		if (targetElement === dragElement || editor.dom.isChildOf(targetElement, dragElement)) {
			return false;
		}

		if (isContentEditableFalse(targetElement)) {
			return false;
		}

		return true;
	};

	var cloneElement = function (elm) {
		var cloneElm = elm.cloneNode(true);
		cloneElm.removeAttribute('data-mce-selected');
		return cloneElm;
	};

	var createGhost = function (editor, elm, width, height) {
		var clonedElm = elm.cloneNode(true);

		editor.dom.setStyles(clonedElm, {width: width, height: height});
		editor.dom.setAttrib(clonedElm, 'data-mce-selected', null);

		var ghostElm = editor.dom.create('div', {
			'class': 'mce-drag-container',
			'data-mce-bogus': 'all',
			unselectable: 'on',
			contenteditable: 'false'
		});

		editor.dom.setStyles(ghostElm, {
			position: 'absolute',
			opacity: 0.5,
			overflow: 'hidden',
			border: 0,
			padding: 0,
			margin: 0,
			width: width,
			height: height
		});

		editor.dom.setStyles(clonedElm, {
			margin: 0,
			boxSizing: 'border-box'
		});

		ghostElm.appendChild(clonedElm);

		return ghostElm;
	};

	var appendGhostToBody = function (ghostElm, bodyElm) {
		if (ghostElm.parentNode !== bodyElm) {
			bodyElm.appendChild(ghostElm);
		}
	};

	var moveGhost = function (ghostElm, position, width, height, maxX, maxY) {
		var overflowX = 0, overflowY = 0;

		ghostElm.style.left = position.pageX + 'px';
		ghostElm.style.top = position.pageY + 'px';

		if (position.pageX + width > maxX) {
			overflowX = (position.pageX + width) - maxX;
		}

		if (position.pageY + height > maxY) {
			overflowY = (position.pageY + height) - maxY;
		}

		ghostElm.style.width = (width - overflowX) + 'px';
		ghostElm.style.height = (height - overflowY) + 'px';
	};

	var removeElement = function (elm) {
		if (elm && elm.parentNode) {
			elm.parentNode.removeChild(elm);
		}
	};

	var isLeftMouseButtonPressed = function (e) {
		return e.button === 0;
	};

	var hasDraggableElement = function (state) {
		return state.element;
	};

	var applyRelPos = function (state, position) {
		return {
			pageX: position.pageX - state.relX,
			pageY: position.pageY + 5
		};
	};

	var start = function (state, editor) {
		return function (e) {
			if (isLeftMouseButtonPressed(e)) {
				var ceElm = Arr.find(editor.dom.getParents(e.target), Fun.or(isContentEditableFalse, isContentEditableTrue));

				if (isDraggable(ceElm)) {
					var elmPos = editor.dom.getPos(ceElm);
					var bodyElm = editor.getBody();
					var docElm = editor.getDoc().documentElement;

					state.element = ceElm;
					state.screenX = e.screenX;
					state.screenY = e.screenY;
					state.maxX = (editor.inline ? bodyElm.scrollWidth : docElm.offsetWidth) - 2;
					state.maxY = (editor.inline ? bodyElm.scrollHeight : docElm.offsetHeight) - 2;
					state.relX = e.pageX - elmPos.x;
					state.relY = e.pageY - elmPos.y;
					state.width = ceElm.offsetWidth;
					state.height = ceElm.offsetHeight;
					state.ghost = createGhost(editor, ceElm, state.width, state.height);
				}
			}
		};
	};

	var move = function (state, editor) {
		// Reduces laggy drag behavior on Gecko
		var throttledPlaceCaretAt = Delay.throttle(function (clientX, clientY) {
			editor._selectionOverrides.hideFakeCaret();
			editor.selection.placeCaretAt(clientX, clientY);
		}, 0);

		return function (e) {
			var movement = Math.max(Math.abs(e.screenX - state.screenX), Math.abs(e.screenY - state.screenY));

			if (hasDraggableElement(state) && !state.dragging && movement > 10) {
				var args = editor.fire('dragstart', {target: state.element});
				if (args.isDefaultPrevented()) {
					return;
				}

				state.dragging = true;
				editor.focus();
			}

			if (state.dragging) {
				var targetPos = applyRelPos(state, MousePosition.calc(editor, e));

				appendGhostToBody(state.ghost, editor.getBody());
				moveGhost(state.ghost, targetPos, state.width, state.height, state.maxX, state.maxY);

				throttledPlaceCaretAt(e.clientX, e.clientY);
			}
		};
	};

	var drop = function (state, editor) {
		return function (e) {
			if (state.dragging) {
				if (isValidDropTarget(editor, editor.selection.getNode(), state.element)) {
					var targetClone = cloneElement(state.element);

					var args = editor.fire('drop', {
						targetClone: targetClone,
						clientX: e.clientX,
						clientY: e.clientY
					});

					if (!args.isDefaultPrevented()) {
						targetClone = args.targetClone;

						editor.undoManager.transact(function() {
							removeElement(state.element);
							editor.insertContent(editor.dom.getOuterHTML(targetClone));
							editor._selectionOverrides.hideFakeCaret();
						});
					}
				}
			}

			removeDragState(state);
		};
	};

	var stop = function (state, editor) {
		return function () {
			removeDragState(state);
			if (state.dragging) {
				editor.fire('dragend');
			}
		};
	};

	var removeDragState = function (state) {
		state.dragging = false;
		state.element = null;
		removeElement(state.ghost);
	};

	var bindFakeDragEvents = function (editor) {
		var state = {}, pageDom, dragStartHandler, dragHandler, dropHandler, dragEndHandler, rootDocument;

		pageDom = DOMUtils.DOM;
		rootDocument = document;
		dragStartHandler = start(state, editor);
		dragHandler = move(state, editor);
		dropHandler = drop(state, editor);
		dragEndHandler = stop(state, editor);

		editor.on('mousedown', dragStartHandler);
		editor.on('mousemove', dragHandler);
		editor.on('mouseup', dropHandler);

		pageDom.bind(rootDocument, 'mousemove', dragHandler);
		pageDom.bind(rootDocument, 'mouseup', dragEndHandler);

		editor.on('remove', function () {
			pageDom.unbind(rootDocument, 'mousemove', dragHandler);
			pageDom.unbind(rootDocument, 'mouseup', dragEndHandler);
		});
	};

	var blockIeDrop = function (editor) {
		editor.on('drop', function(e) {
			// FF doesn't pass out clientX/clientY for drop since this is for IE we just use null instead
			var realTarget = typeof e.clientX !== 'undefined' ? editor.getDoc().elementFromPoint(e.clientX, e.clientY) : null;

			if (isContentEditableFalse(realTarget) || isContentEditableFalse(editor.dom.getContentEditableParent(realTarget))) {
				e.preventDefault();
			}
		});
	};

	var init = function (editor) {
		bindFakeDragEvents(editor);
		blockIeDrop(editor);
	};

	return {
		init: init
	};
});