Skip to content

Commit 8a1ae77

Browse files
committed
[CLEANUP] DRY copy/paste and drop event handling. Add editor#serializeTo
* Remove now-unused `supportsStandardClipboardAPI` test helper * Add `editor#serializeTo(format)` * Add `editor#serializePost(post, format)` * Add deprecate helper * Add `post#trimTo(range)` and deprecate `post#cloneRange`. `cloneRange` returns a mobiledoc, which is confusing. * Enable all copy-paste acceptance tests in IE. They all now use the copy/paste mocks from the test helpers. * Add an IE-compatibility test for `getContentFromPasteEvent` to ensure it will use `window.clipboardData` * Add an IE-compatibility test for `setClipboardData` to ensure it will use `window.clipboardData`
1 parent 0f6d232 commit 8a1ae77

File tree

11 files changed

+353
-206
lines changed

11 files changed

+353
-206
lines changed

src/js/editor/editor.js

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ import { MOBILEDOC_VERSION } from 'mobiledoc-kit/renderers/mobiledoc';
3737
import EditHistory from 'mobiledoc-kit/editor/edit-history';
3838
import EventManager from 'mobiledoc-kit/editor/event-manager';
3939
import EditState from 'mobiledoc-kit/editor/edit-state';
40+
import HTMLRenderer from 'mobiledoc-html-renderer';
41+
import TextRenderer from 'mobiledoc-text-renderer';
4042
import Logger from 'mobiledoc-kit/utils/logger';
4143
let log = Logger.for('editor'); /* jshint ignore:line */
4244

@@ -441,8 +443,59 @@ class Editor {
441443
return this.activeMarkups;
442444
}
443445

446+
/**
447+
* @public
448+
* @param {string} version The mobiledoc version to serialize to.
449+
* @return {Object} Serialized mobiledoc
450+
*/
444451
serialize(version=MOBILEDOC_VERSION) {
445-
return mobiledocRenderers.render(this.post, version);
452+
return this.serializePost(this.post, 'mobiledoc', {version});
453+
}
454+
455+
/**
456+
* @public
457+
* Note that only mobiledoc format is lossless. If cards or atoms are present
458+
* in the post, the html and text formats will omit them in output because
459+
* the editor does not have access to the html and text versions of the
460+
* cards/atoms.
461+
* @param {string} format The format to serialize ('mobiledoc', 'text', 'html')
462+
* @return {Object|String} The editor's post, serialized to {format}
463+
*/
464+
serializeTo(format) {
465+
let post = this.post;
466+
return this.serializePost(post, format);
467+
}
468+
469+
/**
470+
* @param {Post}
471+
* @param {String} format Same as {serializeTo}
472+
* @param {[Object]} version to serialize to (default: MOBILEDOC_VERSION}
473+
* @return {Object|String}
474+
*/
475+
serializePost(post, format, options={}) {
476+
const validFormats = ['mobiledoc', 'html', 'text'];
477+
assert(`Unrecognized serialiation format ${format}`,
478+
contains(validFormats, format));
479+
480+
if (format === 'mobiledoc') {
481+
let version = options.version || MOBILEDOC_VERSION;
482+
return mobiledocRenderers.render(post, version);
483+
} else {
484+
let rendered;
485+
let mobiledoc = this.serializePost(post, 'mobiledoc');
486+
let unknownCardHandler = () => {};
487+
let unknownAtomHandler = () => {};
488+
let rendererOptions = { unknownCardHandler, unknownAtomHandler };
489+
490+
switch (format) {
491+
case 'html':
492+
rendered = new HTMLRenderer(rendererOptions).render(mobiledoc);
493+
return rendered.result;
494+
case 'text':
495+
rendered = new TextRenderer(rendererOptions).render(mobiledoc);
496+
return rendered.result;
497+
}
498+
}
446499
}
447500

448501
removeAllViews() {

src/js/editor/event-manager.js

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import assert from 'mobiledoc-kit/utils/assert';
22
import {
33
parsePostFromPaste,
4-
setClipboardCopyData,
4+
setClipboardData,
55
parsePostFromDrop
66
} from 'mobiledoc-kit/utils/parse-utils';
77
import Range from 'mobiledoc-kit/utils/cursor/range';
@@ -144,13 +144,25 @@ export default class EventManager {
144144
}
145145

146146
cut(event) {
147+
event.preventDefault();
148+
147149
this.copy(event);
148150
this.editor.handleDeletion();
149151
}
150152

151153
copy(event) {
152154
event.preventDefault();
153-
setClipboardCopyData(event, this.editor);
155+
156+
let { editor, editor: { range, post } } = this;
157+
post = post.trimTo(range);
158+
159+
let data = {
160+
html: editor.serializePost(post, 'html'),
161+
text: editor.serializePost(post, 'text'),
162+
mobiledoc: editor.serializePost(post, 'mobiledoc')
163+
};
164+
165+
setClipboardData(event, data, window);
154166
}
155167

156168
paste(event) {
@@ -159,10 +171,6 @@ export default class EventManager {
159171
let { editor } = this;
160172
let range = editor.range;
161173

162-
// FIXME this can go, it will be handled by insertPost
163-
if (range.head.section.isCardSection) {
164-
return;
165-
}
166174
if (!range.isCollapsed) {
167175
editor.handleDeletion();
168176
}

src/js/models/post.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import Set from 'mobiledoc-kit/utils/set';
55
import mobiledocRenderers from 'mobiledoc-kit/renderers/mobiledoc';
66
import Range from 'mobiledoc-kit/utils/cursor/range';
77
import Position from 'mobiledoc-kit/utils/cursor/position';
8+
import deprecate from 'mobiledoc-kit/utils/deprecate';
89

910
export default class Post {
1011
constructor() {
@@ -223,10 +224,18 @@ export default class Post {
223224
}
224225

225226
/**
226-
* @param {Range} range
227-
* @return {Mobiledoc} A mobiledoc representation of the range (JSON)
227+
* @deprecated
228228
*/
229229
cloneRange(range) {
230+
deprecate('post#cloneRange is deprecated. See post#trimTo(range) and editor#serializePost');
231+
return mobiledocRenderers.render(this.trimTo(range));
232+
}
233+
234+
/**
235+
* @param {Range} range
236+
* @return {Post} A new post, constrained to {range}
237+
*/
238+
trimTo(range) {
230239
const post = this.builder.createPost();
231240
const { builder } = this;
232241

@@ -263,6 +272,6 @@ export default class Post {
263272
sectionParent.sections.append(newSection);
264273
}
265274
});
266-
return mobiledocRenderers.render(post);
275+
return post;
267276
}
268277
}

src/js/utils/deprecate.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function deprecate(message) {
2+
console.log(`DEPRECATED: ${message}`); // jshint ignore:line
3+
}

src/js/utils/parse-utils.js

Lines changed: 65 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
import mobiledocParsers from '../parsers/mobiledoc';
33
import HTMLParser from '../parsers/html';
44
import TextParser from '../parsers/text';
5-
import HTMLRenderer from 'mobiledoc-html-renderer';
6-
import TextRenderer from 'mobiledoc-text-renderer';
75
import Logger from 'mobiledoc-kit/utils/logger';
86

97
export const MIME_TEXT_PLAIN = 'text/plain';
@@ -13,6 +11,9 @@ export const NONSTANDARD_IE_TEXT_TYPE = 'Text';
1311
const log = Logger.for('parse-utils');
1412
const MOBILEDOC_REGEX = new RegExp(/data\-mobiledoc='(.*?)'>/);
1513

14+
/**
15+
* @return {Post}
16+
*/
1617
function parsePostFromHTML(html, builder, plugins) {
1718
let post;
1819

@@ -27,36 +28,26 @@ function parsePostFromHTML(html, builder, plugins) {
2728
return post;
2829
}
2930

31+
/**
32+
* @return {Post}
33+
*/
3034
function parsePostFromText(text, builder, plugins) {
3135
let parser = new TextParser(builder, {plugins});
3236
let post = parser.parse(text);
3337
return post;
3438
}
3539

36-
// Sets the clipboard data in a cross-browser way.
37-
function setClipboardData(clipboardData, html, plain) {
38-
if (clipboardData && clipboardData.setData) {
39-
clipboardData.setData(MIME_TEXT_HTML, html);
40-
clipboardData.setData(MIME_TEXT_PLAIN, plain);
41-
} else if (window.clipboardData && window.clipboardData.setData) { // IE
42-
// The Internet Explorers (including Edge) have a non-standard way of interacting with the
43-
// Clipboard API (see http://caniuse.com/#feat=clipboard). In short, they expose a global window.clipboardData
44-
// object instead of the per-event event.clipboardData object on the other browsers.
45-
window.clipboardData.setData(NONSTANDARD_IE_TEXT_TYPE, html);
46-
}
47-
}
40+
/**
41+
* @return {{html: String, text: String}}
42+
*/
43+
export function getContentFromPasteEvent(event, window) {
44+
let html = '', text = '';
4845

49-
// Gets the clipboard data in a cross-browser way.
50-
function getClipboardData(clipboardData) {
51-
let html;
52-
let text;
46+
let { clipboardData } = event;
5347

5448
if (clipboardData && clipboardData.getData) {
5549
html = clipboardData.getData(MIME_TEXT_HTML);
56-
57-
if (!html || html.length === 0) { // Fallback to 'text/plain'
58-
text = clipboardData.getData(MIME_TEXT_PLAIN);
59-
}
50+
text = clipboardData.getData(MIME_TEXT_PLAIN);
6051
} else if (window.clipboardData && window.clipboardData.getData) { // IE
6152
// The Internet Explorers (including Edge) have a non-standard way of interacting with the
6253
// Clipboard API (see http://caniuse.com/#feat=clipboard). In short, they expose a global window.clipboardData
@@ -68,67 +59,74 @@ function getClipboardData(clipboardData) {
6859
}
6960

7061
/**
71-
* @param {Event} copyEvent
72-
* @param {Editor}
73-
* @return null
62+
* @return {{html: String, text: String}}
7463
*/
75-
export function setClipboardCopyData(copyEvent, editor) {
76-
const { range, post } = editor;
64+
function getContentFromDropEvent(event) {
65+
let html = '', text = '';
7766

78-
const mobiledoc = post.cloneRange(range);
67+
try {
68+
html = event.dataTransfer.getData(MIME_TEXT_HTML);
69+
text = event.dataTransfer.getData(MIME_TEXT_PLAIN);
70+
} catch (e) {
71+
// FIXME IE11 does not include any data in the 'text/html' or 'text/plain'
72+
// mimetypes. It throws an error 'Invalid argument' when attempting to read
73+
// these properties.
74+
log('Error getting drop data: ', e);
75+
}
76+
77+
return { html, text };
78+
}
7979

80-
const unknownCardHandler = () => {}; // ignore unknown cards
81-
const unknownAtomHandler = () => {}; // ignore unknown atoms
82-
const {result: innerHTML} =
83-
new HTMLRenderer({unknownCardHandler, unknownAtomHandler}).render(mobiledoc);
80+
/**
81+
* @param {CopyEvent|CutEvent}
82+
* @param {Editor}
83+
* @param {Window}
84+
*/
85+
export function setClipboardData(event, {mobiledoc, html, text}, window) {
86+
if (mobiledoc && html) {
87+
html = `<div data-mobiledoc='${JSON.stringify(mobiledoc)}'>${html}</div>`;
88+
}
8489

85-
const html =
86-
`<div data-mobiledoc='${JSON.stringify(mobiledoc)}'>${innerHTML}</div>`;
87-
const {result: plain} =
88-
new TextRenderer({unknownCardHandler, unknownAtomHandler}).render(mobiledoc);
90+
let { clipboardData } = event;
91+
let { clipboardData: nonstandardClipboardData } = window;
8992

90-
setClipboardData(copyEvent.clipboardData, html, plain);
93+
if (clipboardData && clipboardData.setData) {
94+
clipboardData.setData(MIME_TEXT_HTML, html);
95+
clipboardData.setData(MIME_TEXT_PLAIN, text);
96+
} else if (nonstandardClipboardData && nonstandardClipboardData.setData) {
97+
// The Internet Explorers (including Edge) have a non-standard way of interacting with the
98+
// Clipboard API (see http://caniuse.com/#feat=clipboard). In short, they expose a global window.clipboardData
99+
// object instead of the per-event event.clipboardData object on the other browsers.
100+
nonstandardClipboardData.setData(NONSTANDARD_IE_TEXT_TYPE, html);
101+
}
91102
}
92103

93104
/**
94-
* @param {Event} pasteEvent
95-
* @param {PostNodeBuilder} builder
96-
* @param {Array} plugins parser plugins
105+
* @param {PasteEvent}
106+
* @param {{builder: Builder, _parserPlugins: Array}} options
97107
* @return {Post}
98108
*/
99109
export function parsePostFromPaste(pasteEvent, {builder, _parserPlugins: plugins}) {
100-
let post;
110+
let { html, text } = getContentFromPasteEvent(pasteEvent, window);
101111

102-
const { html, text } = getClipboardData(pasteEvent.clipboardData);
103-
if (html && html.length > 0) {
104-
post = parsePostFromHTML(html, builder, plugins);
105-
} else if (text && text.length > 0) {
106-
post = parsePostFromText(text, builder, plugins);
112+
if (html && html.length) {
113+
return parsePostFromHTML(html, builder, plugins);
114+
} else if (text && text.length) {
115+
return parsePostFromText(text, builder, plugins);
107116
}
108-
109-
return post;
110117
}
111118

119+
/**
120+
* @param {DropEvent}
121+
* @param {{builder: Builder, _parserPlugins: Array}} options
122+
* @return {Post}
123+
*/
112124
export function parsePostFromDrop(dropEvent, {builder, _parserPlugins: plugins}) {
113-
let post;
125+
let { html, text } = getContentFromDropEvent(dropEvent);
114126

115-
let html, text;
116-
try {
117-
html = dropEvent.dataTransfer.getData('text/html');
118-
text = dropEvent.dataTransfer.getData('text/plain');
119-
} catch (e) {
120-
// FIXME IE11 does not include any data in the 'text/html' or 'text/plain'
121-
// mimetypes. It throws an error 'Invalid argument' when attempting to read
122-
// these properties.
123-
log('Error getting drop data: ', e);
124-
return;
125-
}
126-
127-
if (html && html.length > 0) {
128-
post = parsePostFromHTML(html, builder, plugins);
129-
} else if (text && text.length > 0) {
130-
post = parsePostFromText(text, builder, plugins);
127+
if (html && html.length) {
128+
return parsePostFromHTML(html, builder, plugins);
129+
} else if (text && text.length) {
130+
return parsePostFromText(text, builder, plugins);
131131
}
132-
133-
return post;
134132
}

0 commit comments

Comments
 (0)