Skip to content

Commit

Permalink
Handle drop events semantically
Browse files Browse the repository at this point in the history
 * Rename paste-utils to parse-utils
  • Loading branch information
bantic committed Mar 23, 2016
1 parent f374958 commit b2a49c9
Show file tree
Hide file tree
Showing 8 changed files with 286 additions and 32 deletions.
13 changes: 12 additions & 1 deletion src/js/editor/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { setData } from '../utils/element-utils';
import mixin from '../utils/mixin';
import Cursor from '../utils/cursor';
import Range from '../utils/cursor/range';
import Position from '../utils/cursor/position';
import PostNodeBuilder from '../models/post-node-builder';
import {
DEFAULT_TEXT_EXPANSIONS, findExpansion, validateExpansion
Expand All @@ -42,7 +43,8 @@ let log = Logger.for('editor'); /* jshint ignore:line */
Logger.enableTypes([
'mutation-handler',
'event-manager',
'editor'
'editor',
'parse-utils'
]);
Logger.disable();

Expand Down Expand Up @@ -731,6 +733,15 @@ class Editor {
});
}

/**
* @param {integer} x x-position in viewport
* @param {integer} y y-position in viewport
* @return {Position|null}
*/
positionAtPoint(x, y) {
return Position.atPoint(x, y, this);
}

// @private
_setCardMode(cardSection, mode) {
const renderNode = cardSection.renderNode;
Expand Down
38 changes: 34 additions & 4 deletions src/js/editor/event-manager.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import assert from 'mobiledoc-kit/utils/assert';
import {
parsePostFromPaste,
setClipboardCopyData
} from '../utils/paste-utils';
setClipboardCopyData,
parsePostFromDrop
} from 'mobiledoc-kit/utils/parse-utils';
import Range from 'mobiledoc-kit/utils/cursor/range';
import { filter, forEach, contains } from 'mobiledoc-kit/utils/array-utils';
import Key from 'mobiledoc-kit/utils/key';
import { TAB } from 'mobiledoc-kit/utils/characters';
import Logger from 'mobiledoc-kit/utils/logger';
let log = Logger.for('event-manager'); /* jshint ignore:line */

const ELEMENT_EVENT_TYPES = ['keydown', 'keyup', 'cut', 'copy', 'paste', 'keypress'];
const ELEMENT_EVENT_TYPES = [
'keydown', 'keyup', 'cut', 'copy', 'paste', 'keypress', 'drop'
];
const DOCUMENT_EVENT_TYPES = ['mouseup'];

export default class EventManager {
Expand Down Expand Up @@ -154,14 +159,15 @@ export default class EventManager {
let { editor } = this;
let range = editor.range;

// FIXME this can go, it will be handled by insertPost
if (range.head.section.isCardSection) {
return;
}
if (!range.isCollapsed) {
editor.handleDeletion();
}
let position = editor.range.head;
let pastedPost = parsePostFromPaste(event, editor.builder, editor._parserPlugins);
let pastedPost = parsePostFromPaste(event, editor);

editor.run(postEditor => {
let nextPosition = postEditor.insertPost(position, pastedPost);
Expand All @@ -173,4 +179,28 @@ export default class EventManager {
// mouseup does not correctly report a selection until the next tick
setTimeout(() => this.editor._resetRange(), 0);
}

drop(event) {
event.preventDefault();

let { clientX: x, clientY: y } = event;
let { editor } = this;

let position = editor.positionAtPoint(x, y);
if (!position) {
log('Could not find drop position');
return;
}

let post = parsePostFromDrop(event, editor);
if (!post) {
log('Could not determine post from drop event');
return;
}

editor.run(postEditor => {
let nextPosition = postEditor.insertPost(position, post);
postEditor.setRange(new Range(nextPosition));
});
}
}
23 changes: 20 additions & 3 deletions src/js/utils/cursor/position.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import {
isTextNode
} from 'mobiledoc-kit/utils/dom-utils';
import { isTextNode } from 'mobiledoc-kit/utils/dom-utils';
import { DIRECTION } from 'mobiledoc-kit/utils/key';
import assert from 'mobiledoc-kit/utils/assert';
import {
HIGH_SURROGATE_RANGE,
LOW_SURROGATE_RANGE
} from 'mobiledoc-kit/models/marker';
import { containsNode } from 'mobiledoc-kit/utils/dom-utils';
import { findOffsetInNode } from 'mobiledoc-kit/utils/selection-utils';

function findParentSectionFromNode(renderTree, node) {
let renderNode = renderTree.findRenderNodeFromElement(
Expand Down Expand Up @@ -67,6 +67,23 @@ const Position = class Position {
this.isBlank = false;
}

/**
* @param {integer} x x-position in current viewport
* @param {integer} y y-position in current viewport
* @param {Editor} editor
* @return {Position|null}
*/
static atPoint(x, y, editor) {
let { _renderTree, element: rootElement } = editor;
let elementFromPoint = document.elementFromPoint(x, y);
if (!containsNode(rootElement, elementFromPoint)) {
return;
}

let { node, offset } = findOffsetInNode(elementFromPoint, {left: x, top: y});
return Position.fromNode(_renderTree, node, offset);
}

static blankPosition() {
return {
section: null,
Expand Down
36 changes: 32 additions & 4 deletions src/js/utils/paste-utils.js → src/js/utils/parse-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ import HTMLParser from '../parsers/html';
import TextParser from '../parsers/text';
import HTMLRenderer from 'mobiledoc-html-renderer';
import TextRenderer from 'mobiledoc-text-renderer';
import Logger from 'mobiledoc-kit/utils/logger';

const MOBILEDOC_REGEX = new RegExp(/data\-mobiledoc='(.*?)'>/);
export const MIME_TEXT_PLAIN = 'text/plain';
export const MIME_TEXT_HTML = 'text/html';
export const NONSTANDARD_IE_TEXT_TYPE = 'Text';

const log = Logger.for('parse-utils');
const MOBILEDOC_REGEX = new RegExp(/data\-mobiledoc='(.*?)'>/);

function parsePostFromHTML(html, builder, plugins) {
let post;
Expand Down Expand Up @@ -38,7 +42,7 @@ function setClipboardData(clipboardData, html, plain) {
// The Internet Explorers (including Edge) have a non-standard way of interacting with the
// Clipboard API (see http://caniuse.com/#feat=clipboard). In short, they expose a global window.clipboardData
// object instead of the per-event event.clipboardData object on the other browsers.
window.clipboardData.setData('Text', html);
window.clipboardData.setData(NONSTANDARD_IE_TEXT_TYPE, html);
}
}

Expand All @@ -57,7 +61,7 @@ function getClipboardData(clipboardData) {
// The Internet Explorers (including Edge) have a non-standard way of interacting with the
// Clipboard API (see http://caniuse.com/#feat=clipboard). In short, they expose a global window.clipboardData
// object instead of the per-event event.clipboardData object on the other browsers.
html = window.clipboardData.getData('Text');
html = window.clipboardData.getData(NONSTANDARD_IE_TEXT_TYPE);
}

return { html, text };
Expand Down Expand Up @@ -92,7 +96,7 @@ export function setClipboardCopyData(copyEvent, editor) {
* @param {Array} plugins parser plugins
* @return {Post}
*/
export function parsePostFromPaste(pasteEvent, builder, plugins=[]) {
export function parsePostFromPaste(pasteEvent, {builder, _parserPlugins: plugins}) {
let post;

const { html, text } = getClipboardData(pasteEvent.clipboardData);
Expand All @@ -104,3 +108,27 @@ export function parsePostFromPaste(pasteEvent, builder, plugins=[]) {

return post;
}

export function parsePostFromDrop(dropEvent, {builder, _parserPlugins: plugins}) {
let post;

let html, text;
try {
html = dropEvent.dataTransfer.getData('text/html');
text = dropEvent.dataTransfer.getData('text/plain');
} catch (e) {
// FIXME IE11 does not include any data in the 'text/html' or 'text/plain'
// mimetypes. It throws an error 'Invalid argument' when attempting to read
// these properties.
log('Error getting drop data: ', e);
return;
}

if (html && html.length > 0) {
post = parsePostFromHTML(html, builder, plugins);
} else if (text && text.length > 0) {
post = parsePostFromText(text, builder, plugins);
}

return post;
}
80 changes: 78 additions & 2 deletions src/js/utils/selection-utils.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,85 @@
import { DIRECTION } from '../utils/key';
import { isTextNode, isElementNode } from 'mobiledoc-kit/utils/dom-utils';

function clearSelection() {
// FIXME-IE ensure this works on IE 9. It works on IE10.
window.getSelection().removeAllRanges();
}

function textNodeRects(node) {
let range = document.createRange();
range.setEnd(node, node.nodeValue.length);
range.setStart(node, 0);
return range.getClientRects();
}

function findOffsetInTextNode(node, coords) {
let len = node.nodeValue.length;
let range = document.createRange();
for (let i = 0; i < len; i++) {
range.setEnd(node, i + 1);
range.setStart(node, i);
let rect = range.getBoundingClientRect();
if (rect.top === rect.bottom) {
continue;
}
if (rect.left <= coords.left && rect.right >= coords.left &&
rect.top <= coords.top && rect.bottom >= coords.top) {
return {node, offset: i + (coords.left >= (rect.left + rect.right) / 2 ? 1 : 0)};
}
}
return {node, offset: 0};
}

/*
* @param {Object} coords with `top` and `left`
* @see https://github.com/ProseMirror/prosemirror/blob/4c22e3fe97d87a355a0534e25d65aaf0c0d83e57/src/edit/dompos.js
* @return {Object} {node, offset}
*/
function findOffsetInNode(node, coords) {
let closest, dyClosest = 1e8, coordsClosest, offset = 0;
for (let child = node.firstChild; child; child = child.nextSibling) {
let rects;
if (isElementNode(child)) {
rects = child.getClientRects();
} else if (isTextNode(child)) {
rects = textNodeRects(child);
} else {
continue;
}

for (let i = 0; i < rects.length; i++) {
let rect = rects[i];
if (rect.left <= coords.left && rect.right >= coords.left) {
let dy = rect.top > coords.top ? rect.top - coords.top
: rect.bottom < coords.top ? coords.top - rect.bottom : 0;
if (dy < dyClosest) {
closest = child;
dyClosest = dy;
coordsClosest = dy ? {left: coords.left, top: rect.top} : coords;
if (isElementNode(child) && !child.firstChild) {
offset = i + (coords.left >= (rect.left + rect.right) / 2 ? 1 : 0);
}
continue;
}
}
if (!closest &&
(coords.top >= rect.bottom || coords.top >= rect.top && coords.left >= rect.right)) {
offset = i + 1;
}
}
}
if (!closest) {
return {node, offset};
}
if (isTextNode(closest)) {
return findOffsetInTextNode(closest, coordsClosest);
}
if (closest.firstChild) {
return findOffsetInNode(closest, coordsClosest);
}
return {node, offset};
}

function comparePosition(selection) {
let { anchorNode, focusNode, anchorOffset, focusOffset } = selection;
let headNode, tailNode, headOffset, tailOffset, direction;
Expand Down Expand Up @@ -69,5 +144,6 @@ function comparePosition(selection) {

export {
clearSelection,
comparePosition
comparePosition,
findOffsetInNode
};
2 changes: 1 addition & 1 deletion tests/acceptance/editor-copy-paste-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { supportsStandardClipboardAPI } from '../helpers/browsers';
import {
MIME_TEXT_PLAIN,
MIME_TEXT_HTML
} from 'mobiledoc-kit/utils/paste-utils';
} from 'mobiledoc-kit/utils/parse-utils';

const { module, skipInIE11 } = Helpers;

Expand Down
60 changes: 60 additions & 0 deletions tests/acceptance/editor-drag-drop-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import Helpers from '../test-helpers';

const { module, test } = Helpers;

let editor, editorElement;

function findCenterPointOfTextNode(node) {
let range = document.createRange();
range.setStart(node, 0);
range.setEnd(node, node.textContent.length);

let {left, top, width, height} = range.getBoundingClientRect();

let clientX = left + width/2;
let clientY = top + height/2;

return {clientX, clientY};
}

module('Acceptance: editor: drag-drop', {
beforeEach() {
editorElement = $('#editor')[0];
},
afterEach() {
if (editor) {
editor.destroy();
editor = null;
}
}
});

test('inserts dropped HTML content at the drop position', (assert) => {
let expected;
editor = Helpers.mobiledoc.renderInto(editorElement, ({post, markupSection, marker}) => {
expected = post([markupSection('h2', [marker('--->some text<---')])]);
return post([markupSection('h2', [marker('---><---')])]);
});

let html = '<p>some text</p>';
let node = Helpers.dom.findTextNode(editorElement, '---><---');
let {clientX, clientY} = findCenterPointOfTextNode(node);
Helpers.dom.triggerDropEvent(editor, {html, clientX, clientY});

assert.postIsSimilar(editor.post, expected);
});

test('inserts dropped text content at the drop position if no html data', (assert) => {
let expected;
editor = Helpers.mobiledoc.renderInto(editorElement, ({post, markupSection, marker}) => {
expected = post([markupSection('h2', [marker('--->some text<---')])]);
return post([markupSection('h2', [marker('---><---')])]);
});

let text = 'some text';
let node = Helpers.dom.findTextNode(editorElement, '---><---');
let {clientX, clientY} = findCenterPointOfTextNode(node);
Helpers.dom.triggerDropEvent(editor, {text, clientX, clientY});

assert.postIsSimilar(editor.post, expected);
});
Loading

0 comments on commit b2a49c9

Please sign in to comment.