Skip to content

Commit

Permalink
WIP Adds tests for copy and pasting markers in a single section
Browse files Browse the repository at this point in the history
refs #111
  • Loading branch information
bantic committed Sep 17, 2015
1 parent c22a663 commit ec00f06
Show file tree
Hide file tree
Showing 7 changed files with 214 additions and 10 deletions.
34 changes: 32 additions & 2 deletions src/js/editor/editor.js
Expand Up @@ -17,7 +17,7 @@ import MobiledocRenderer from '../renderers/mobiledoc';

import { mergeWithOptions } from 'content-kit-utils';
import { clearChildNodes, addClassName, parseHTML } from '../utils/dom-utils';
import { forEach, filter } from '../utils/array-utils';
import { reduce, forEach, filter } from '../utils/array-utils';
import { setData } from '../utils/element-utils';
import mixin from '../utils/mixin';
import EventListenerMixin from '../utils/event-listener';
Expand Down Expand Up @@ -476,7 +476,7 @@ class Editor {
}

_setupListeners() {
const elementEvents = ['keydown', 'keyup', 'input', 'dragover', 'drop', 'paste'];
const elementEvents = ['keydown', 'keyup', 'input', 'dragover', 'drop', 'copy', 'paste'];
const documentEvents = ['mouseup'];

elementEvents.forEach(eventName => {
Expand Down Expand Up @@ -579,8 +579,38 @@ class Editor {
}
}

handleCopy(event) {
const { clipboardData } = event;
const range = this.cursor.offsets;
const mobiledoc = this.post.cloneRange(range);
const html = `<div data-mobiledoc='${JSON.stringify(mobiledoc)}'></div>`;
clipboardData.setData('text/html', html);
event.preventDefault();
}

handlePaste(event) {
event.preventDefault(); // FIXME for now, just prevent pasting
const html = event.clipboardData.getData('text/html');
const mobiledocRegex = new RegExp(/data\-mobiledoc='(.*)'>/);
let mobiledoc, post;
if (mobiledocRegex.test(html)) {
const mobiledocString = html.match(mobiledocRegex)[1];
mobiledoc = JSON.parse(mobiledocString);
post = new MobiledocParser(this.builder).parse(mobiledoc);

const range = this.cursor.offsets;
const nextPosition = range.head.clone();
this.run(postEditor => {
const markers = post.sections.head.markers.toArray();
postEditor.insertMarkersAtPosition(range.head, markers);
const length = reduce(markers, (prev, m) => prev = m.length, 0);

nextPosition.offset += length;
});
this.cursor.moveToPosition(nextPosition);
}
// read the mobiledoc out of the event
// if it's there, walk that mobiledoc and apply it to what is here
}
}

Expand Down
17 changes: 15 additions & 2 deletions src/js/editor/post.js
Expand Up @@ -579,10 +579,23 @@ class PostEditor {
});
}

insertMarkersAtPosition(position, markers=[]) {
const { section, offset } = position;
this.splitSectionMarkerAtOffset(section, offset);
let {marker:prevMarker} = section.markerPositionAtOffset(offset);
let currentMarker = offset === 0 ? prevMarker : prevMarker.next;

markers.forEach(marker => {
marker = marker.clone();
section.markers.insertBefore(marker, currentMarker);
this._markDirty(marker);
});
}

/**
* Toggle the given markup on the current selection. If anything in the current
* selection has the markup, it will be removed. If nothing in the selection
* has the markup, it will be added to everything in the selection.
* selection has the markup, the markup will be removed from it. If nothing in the selection
* has the markup, the markup will be added to everything in the selection.
*
* Usage:
*
Expand Down
2 changes: 1 addition & 1 deletion src/js/models/_markerable.js
Expand Up @@ -143,7 +143,7 @@ export default class Markerable extends Section {

/**
* @return {Array} New markers that match the boundaries of the
* range.
* range. Does not change the existing markers in this section.
*/
markersFor(headOffset, tailOffset) {
const range = {head: {section:this, offset:headOffset},
Expand Down
25 changes: 21 additions & 4 deletions src/js/models/post.js
@@ -1,7 +1,8 @@
import { POST_TYPE } from './types';
import LinkedList from 'content-kit-editor/utils/linked-list';
import { forEach, compact } from 'content-kit-editor/utils/array-utils';
import Set from 'content-kit-editor/utils/set';
import LinkedList from '../utils/linked-list';
import { forEach, compact } from '../utils/array-utils';
import Set from '../utils/set';
import MobiledocRenderer from '../renderers/mobiledoc';

export default class Post {
constructor() {
Expand Down Expand Up @@ -142,7 +143,8 @@ export default class Post {
return containedSections;
}

// return the next section that has markers after this one
// return the next section that has markers after this one,
// possibly skipping non-markerable sections
_nextMarkerableSection(section) {
if (!section) { return null; }
const isMarkerable = s => !!s.markers;
Expand Down Expand Up @@ -171,4 +173,19 @@ export default class Post {
}
}
}

cloneRange(range) {
const post = this.builder.createPost();
const { builder } = this;
this.walkMarkerableSections(range, section => {
let newSection = builder.createMarkupSection(section.tagName);
let currentRange = range.trimTo(section);
forEach(
section.markersFor(currentRange.headSectionOffset, currentRange.tailSectionOffset),
m => newSection.markers.append(m)
);
post.sections.append(newSection);
});
return MobiledocRenderer.render(post);
}
}
100 changes: 100 additions & 0 deletions tests/acceptance/editor-copy-paste-test.js
@@ -0,0 +1,100 @@
import { Editor } from 'content-kit-editor';
import Helpers from '../test-helpers';

const { test, module } = Helpers;

let editor, editorElement;

module('Acceptance: editor: basic', {
beforeEach() {
editorElement = $('<div id="editor"></div>').appendTo('#qunit-fixture')[0];
},
afterEach() {
if (editor) { editor.destroy(); }
}
});

test('simple copy-paste at end of section works', (assert) => {
const mobiledoc = Helpers.mobiledoc.build(({post, markupSection, marker}) => {
return post([markupSection('p', [marker('abc')])]);
});
editor = new Editor({mobiledoc});
editor.render(editorElement);

Helpers.dom.selectText('abc', editorElement);
Helpers.dom.triggerCopyEvent(editor);

let textNode = $('#editor p')[0].childNodes[0];
assert.equal(textNode.textContent, 'abc'); //precond
Helpers.dom.moveCursorTo(textNode, textNode.length);

Helpers.dom.triggerPasteEvent(editor);

assert.hasElement('#editor p:contains(abcabc)', 'pastes the text');
});

test('simple copy-paste with markup at end of section works', (assert) => {
const mobiledoc = Helpers.mobiledoc.build(({post, markupSection, marker, markup}) => {
return post([markupSection('p', [
marker('a', [markup('strong')]),
marker('bc')
])]);
});
editor = new Editor({mobiledoc});
editor.render(editorElement);

Helpers.dom.selectText('a', editorElement, 'b', editorElement);
Helpers.dom.triggerCopyEvent(editor);

let textNode = $('#editor p')[0].childNodes[1];
assert.equal(textNode.textContent, 'bc'); //precond
Helpers.dom.moveCursorTo(textNode, textNode.length);

Helpers.dom.triggerPasteEvent(editor);

assert.hasElement('#editor p:contains(abcab)', 'pastes the text');
assert.equal($('#editor p strong:contains(a)').length, 2, 'two bold As');
});

test('simple copy-paste in middle of section works', (assert) => {
const mobiledoc = Helpers.mobiledoc.build(({post, markupSection, marker}) => {
return post([markupSection('p', [marker('abcd')])]);
});
editor = new Editor({mobiledoc});
editor.render(editorElement);

Helpers.dom.selectText('c', editorElement);
Helpers.dom.triggerCopyEvent(editor);

let textNode = $('#editor p')[0].childNodes[0];
assert.equal(textNode.textContent, 'abcd'); //precond
Helpers.dom.moveCursorTo(textNode, 1);

Helpers.dom.triggerPasteEvent(editor);

assert.hasElement('#editor p:contains(acbcd)', 'pastes the text');
Helpers.dom.insertText(editor, 'X');
assert.hasElement('#editor p:contains(acXbcd)', 'inserts text in right spot');
});

test('simple copy-paste at start of section works', (assert) => {
const mobiledoc = Helpers.mobiledoc.build(({post, markupSection, marker}) => {
return post([markupSection('p', [marker('abcd')])]);
});
editor = new Editor({mobiledoc});
editor.render(editorElement);

Helpers.dom.selectText('c', editorElement);
Helpers.dom.triggerCopyEvent(editor);

let textNode = $('#editor p')[0].childNodes[0];
assert.equal(textNode.textContent, 'abcd'); //precond
Helpers.dom.moveCursorTo(textNode, 0);

Helpers.dom.triggerPasteEvent(editor);

assert.hasElement('#editor p:contains(cabcd)', 'pastes the text');
Helpers.dom.insertText(editor, 'X');
assert.hasElement('#editor p:contains(cXabcd)', 'inserts text in right spot');

});
28 changes: 27 additions & 1 deletion tests/helpers/dom.js
Expand Up @@ -171,6 +171,30 @@ function triggerKeyCommand(editor, string, modifier) {
editor.triggerEvent(editor.element, 'keydown', keyEvent);
}

// Allows our fake copy and paste events to communicate with each other.
const lastCopyData = {};
function triggerCopyEvent(editor) {
const event = {
preventDefault() {},
clipboardData: {
setData(type, value) { lastCopyData[type] = value; }
}
};
editor.triggerEvent(editor.element, 'copy', event);
document.execCommand('copy', false);
}

function triggerPasteEvent(editor) {
const event = {
preventDefault() {},
clipboardData: {
getData(type) { return lastCopyData[type]; }
}
};
editor.triggerEvent(editor.element, 'paste', event);
document.execCommand('paste', false);
}

const DOMHelper = {
moveCursorTo,
selectText,
Expand All @@ -185,7 +209,9 @@ const DOMHelper = {
triggerForwardDelete,
triggerEnter,
insertText,
triggerKeyCommand
triggerKeyCommand,
triggerCopyEvent,
triggerPasteEvent
};

export { triggerEvent };
Expand Down
18 changes: 18 additions & 0 deletions tests/unit/models/post-test.js
Expand Up @@ -393,3 +393,21 @@ test('#sectionsContainedBy when range starts/ends in list item', (assert) => {
assert.ok(containedSections.indexOf(card) !== -1, 'contains card');
assert.ok(containedSections.indexOf(s1) !== -1, 'contains section');
});

test('#cloneRange creates a mobiledoc from the given range', (assert) => {
const post = Helpers.postAbstract.build(
({post, markupSection, marker}) => {
return post([markupSection('p', [marker('abc')])]);
});
const section = post.sections.head;
const range = Range.create(section,1,section,2); // "b"

const mobiledoc = post.cloneRange(range);
const expectedMobiledoc = Helpers.mobiledoc.build(({post, marker, markupSection}) => {
return post([markupSection('p',[marker('b')])]);
});

assert.deepEqual(mobiledoc, expectedMobiledoc);
});

// test that it works for non-markerable sections

0 comments on commit ec00f06

Please sign in to comment.