From b4c46c3b914b0473c1bde01833c3b8ce1bed4b61 Mon Sep 17 00:00:00 2001 From: Yoran Brondsema Date: Tue, 16 Feb 2016 15:34:40 -0300 Subject: [PATCH 1/3] Fix bug #329: Copy-pasting does not work on IE11 --- src/js/utils/paste-utils.js | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/js/utils/paste-utils.js b/src/js/utils/paste-utils.js index 4b558435f..e3ed2046a 100644 --- a/src/js/utils/paste-utils.js +++ b/src/js/utils/paste-utils.js @@ -50,8 +50,12 @@ export function setClipboardCopyData(copyEvent, editor) { const {result: plain} = new TextRenderer({unknownCardHandler, unknownAtomHandler}).render(mobiledoc); - clipboardData.setData(MIME_TEXT_PLAIN, plain); - clipboardData.setData(MIME_TEXT_HTML, html); + if (clipboardData && clipboardData.setData) { + clipboardData.setData(MIME_TEXT_PLAIN, plain); + clipboardData.setData(MIME_TEXT_HTML, html); + } else if (window.clipboardData && window.clipboardData.setData) { // IE + window.clipboardData.setData('Text', html); + } } /** @@ -62,13 +66,23 @@ export function setClipboardCopyData(copyEvent, editor) { */ export function parsePostFromPaste(pasteEvent, builder, plugins=[]) { let post; - let html = pasteEvent.clipboardData.getData(MIME_TEXT_HTML); + let html; + let text; - if (!html || html.length === 0) { // Fallback to 'text/plain' - let text = pasteEvent.clipboardData.getData(MIME_TEXT_PLAIN); - post = parsePostFromText(text, builder, plugins); - } else { + if (pasteEvent.clipboardData && pasteEvent.clipboardData.getData) { + html = pasteEvent.clipboardData.getData(MIME_TEXT_HTML); + + if (!html || html.length === 0) { // Fallback to 'text/plain' + text = pasteEvent.clipboardData.getData(MIME_TEXT_PLAIN); + } + } else if (window.clipboardData && window.clipboardData.getData) { // IE + html = window.clipboardData.getData('Text'); + } + + if (html && html.length > 0) { post = parsePostFromHTML(html, builder, plugins); + } else if (text && text.length > 0) { + post = parsePostFromText(text, builder, plugins); } return post; From 2a743fabbf2df031897d78bc8e692867e25c0344 Mon Sep 17 00:00:00 2001 From: Yoran Brondsema Date: Tue, 16 Feb 2016 16:18:02 -0300 Subject: [PATCH 2/3] Add tests for copy-pasting on IE11. --- tests/acceptance/editor-copy-paste-test.js | 28 ++++++++++---- tests/helpers/browsers.js | 6 +++ tests/helpers/dom.js | 45 ++++++++++++++++------ 3 files changed, 59 insertions(+), 20 deletions(-) diff --git a/tests/acceptance/editor-copy-paste-test.js b/tests/acceptance/editor-copy-paste-test.js index 617f4b9ee..38bdad4b9 100644 --- a/tests/acceptance/editor-copy-paste-test.js +++ b/tests/acceptance/editor-copy-paste-test.js @@ -1,6 +1,7 @@ import { Editor } from 'mobiledoc-kit'; import Helpers from '../test-helpers'; import Range from 'mobiledoc-kit/utils/cursor/range'; +import { supportsStandardClipboardAPI } from '../helpers/browsers'; import { MIME_TEXT_PLAIN, MIME_TEXT_HTML @@ -83,9 +84,14 @@ test('paste plain text with line breaks', (assert) => { Helpers.dom.setCopyData(MIME_TEXT_PLAIN, ['abc', 'def'].join('\n')); Helpers.dom.triggerPasteEvent(editor); - assert.hasElement('#editor p:contains(abcabc)', 'pastes the text'); - assert.hasElement('#editor p:contains(def)', 'second section is pasted'); - assert.equal($('#editor p').length, 2, 'adds a second section'); + if (supportsStandardClipboardAPI()) { + assert.hasElement('#editor p:contains(abcabc)', 'pastes the text'); + assert.hasElement('#editor p:contains(def)', 'second section is pasted'); + assert.equal($('#editor p').length, 2, 'adds a second section'); + } else { + assert.hasElement('#editor p:contains(abcabc\ndef)', 'pastes the text'); + assert.equal($('#editor p').length, 1, 'adds a second section'); + } }); test('paste plain text with list items', (assert) => { @@ -103,8 +109,12 @@ test('paste plain text with list items', (assert) => { Helpers.dom.setCopyData(MIME_TEXT_PLAIN, ['* abc', '* def'].join('\n')); Helpers.dom.triggerPasteEvent(editor); - assert.hasElement('#editor p:contains(abcabc)', 'pastes the text'); - assert.hasElement('#editor ul li:contains(def)', 'list item is pasted'); + if (supportsStandardClipboardAPI()) { + assert.hasElement('#editor p:contains(abcabc)', 'pastes the text'); + assert.hasElement('#editor ul li:contains(def)', 'list item is pasted'); + } else { + assert.hasElement('#editor p:contains(abc* abc\n* def)', 'pastes the text'); + } }); test('can cut and then paste content', (assert) => { @@ -300,10 +310,12 @@ test('copy sets html & text for pasting externally', (assert) => { Helpers.dom.triggerCopyEvent(editor); - let text = Helpers.dom.getCopyData(MIME_TEXT_PLAIN); let html = Helpers.dom.getCopyData(MIME_TEXT_HTML); - assert.equal(text, ["heading", "h2 subheader", "The text" ].join('\n'), - 'gets plain text'); + if (supportsStandardClipboardAPI()) { + let text = Helpers.dom.getCopyData(MIME_TEXT_PLAIN); + assert.equal(text, ["heading", "h2 subheader", "The text" ].join('\n'), + 'gets plain text'); + } assert.ok(html.indexOf("

heading") !== -1, 'html has h1'); assert.ok(html.indexOf("

h2 subheader") !== -1, 'html has h2'); diff --git a/tests/helpers/browsers.js b/tests/helpers/browsers.js index bf06d4aa6..d8a6a5ac8 100644 --- a/tests/helpers/browsers.js +++ b/tests/helpers/browsers.js @@ -7,3 +7,9 @@ export function supportsSelectionExtend() { let selection = window.getSelection(); return !!selection.extend; } + +// See http://caniuse.com/#feat=clipboard +// This rules out the Internet Explorers. +export function supportsStandardClipboardAPI() { + return !window.clipboardData; +} diff --git a/tests/helpers/dom.js b/tests/helpers/dom.js index f345f6ad7..3700d1103 100644 --- a/tests/helpers/dom.js +++ b/tests/helpers/dom.js @@ -4,6 +4,7 @@ import KEY_CODES from 'mobiledoc-kit/utils/keycodes'; import { DIRECTION, MODIFIERS } from 'mobiledoc-kit/utils/key'; import { isTextNode } from 'mobiledoc-kit/utils/dom-utils'; import { merge } from 'mobiledoc-kit/utils/merge'; +import { supportsStandardClipboardAPI } from './browsers'; // walks DOWN the dom from node to childNodes, returning the element // for which `conditionFn(element)` is true @@ -270,11 +271,17 @@ function triggerLeftArrowKey(editor, modifier) { // Allows our fake copy and paste events to communicate with each other. const lastCopyData = {}; function triggerCopyEvent(editor) { - let event = createMockEvent('copy', editor.element, { - clipboardData: { - setData(type, value) { lastCopyData[type] = value; } - } - }); + let eventData = {}; + + if (supportsStandardClipboardAPI()) { + eventData = { + clipboardData: { + setData(type, value) { lastCopyData[type] = value; } + } + }; + } + + let event = createMockEvent('copy', editor.element, eventData); editor.triggerEvent(editor.element, 'copy', event); } @@ -288,20 +295,34 @@ function triggerCutEvent(editor) { } function triggerPasteEvent(editor) { - let event = createMockEvent('copy', editor.element, { - clipboardData: { - getData(type) { return lastCopyData[type]; } - } - }); + let eventData = {}; + + if (supportsStandardClipboardAPI()) { + eventData = { + clipboardData: { + getData(type) { return lastCopyData[type]; } + } + }; + } + + let event = createMockEvent('copy', editor.element, eventData); editor.triggerEvent(editor.element, 'paste', event); } function getCopyData(type) { - return lastCopyData[type]; + if (supportsStandardClipboardAPI()) { + return lastCopyData[type]; + } else { + return window.clipboardData.getData('Text'); + } } function setCopyData(type, value) { - lastCopyData[type] = value; + if (supportsStandardClipboardAPI()) { + lastCopyData[type] = value; + } else { + window.clipboardData.setData('Text', value); + } } function clearCopyData() { From f6307eafae3425d62a20ec9a584052b2f72c3429 Mon Sep 17 00:00:00 2001 From: Yoran Brondsema Date: Tue, 16 Feb 2016 18:32:04 -0300 Subject: [PATCH 3/3] Extract clipboard getters/setters into utility functions. --- src/js/utils/paste-utils.js | 65 +++++++++++++++++++++++-------------- 1 file changed, 41 insertions(+), 24 deletions(-) diff --git a/src/js/utils/paste-utils.js b/src/js/utils/paste-utils.js index e3ed2046a..0bcf72846 100644 --- a/src/js/utils/paste-utils.js +++ b/src/js/utils/paste-utils.js @@ -29,20 +29,53 @@ function parsePostFromText(text, builder, plugins) { return post; } +// Sets the clipboard data in a cross-browser way. +function setClipboardData(clipboardData, html, plain) { + if (clipboardData && clipboardData.setData) { + clipboardData.setData(MIME_TEXT_HTML, html); + clipboardData.setData(MIME_TEXT_PLAIN, plain); + } else if (window.clipboardData && window.clipboardData.setData) { // IE + // 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); + } +} + +// Gets the clipboard data in a cross-browser way. +function getClipboardData(clipboardData) { + let html; + let text; + + if (clipboardData && clipboardData.getData) { + html = clipboardData.getData(MIME_TEXT_HTML); + + if (!html || html.length === 0) { // Fallback to 'text/plain' + text = clipboardData.getData(MIME_TEXT_PLAIN); + } + } else if (window.clipboardData && window.clipboardData.getData) { // IE + // 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'); + } + + return { html, text }; +} + /** * @param {Event} copyEvent * @param {Editor} * @return null */ export function setClipboardCopyData(copyEvent, editor) { - let { range, post } = editor; - let { clipboardData } = copyEvent; + const { range, post } = editor; - let mobiledoc = post.cloneRange(range); + const mobiledoc = post.cloneRange(range); - let unknownCardHandler = () => {}; // ignore unknown cards - let unknownAtomHandler = () => {}; // ignore unknown atoms - let {result: innerHTML} = + const unknownCardHandler = () => {}; // ignore unknown cards + const unknownAtomHandler = () => {}; // ignore unknown atoms + const {result: innerHTML} = new HTMLRenderer({unknownCardHandler, unknownAtomHandler}).render(mobiledoc); const html = @@ -50,12 +83,7 @@ export function setClipboardCopyData(copyEvent, editor) { const {result: plain} = new TextRenderer({unknownCardHandler, unknownAtomHandler}).render(mobiledoc); - if (clipboardData && clipboardData.setData) { - clipboardData.setData(MIME_TEXT_PLAIN, plain); - clipboardData.setData(MIME_TEXT_HTML, html); - } else if (window.clipboardData && window.clipboardData.setData) { // IE - window.clipboardData.setData('Text', html); - } + setClipboardData(copyEvent.clipboardData, html, plain); } /** @@ -66,19 +94,8 @@ export function setClipboardCopyData(copyEvent, editor) { */ export function parsePostFromPaste(pasteEvent, builder, plugins=[]) { let post; - let html; - let text; - - if (pasteEvent.clipboardData && pasteEvent.clipboardData.getData) { - html = pasteEvent.clipboardData.getData(MIME_TEXT_HTML); - - if (!html || html.length === 0) { // Fallback to 'text/plain' - text = pasteEvent.clipboardData.getData(MIME_TEXT_PLAIN); - } - } else if (window.clipboardData && window.clipboardData.getData) { // IE - html = window.clipboardData.getData('Text'); - } + const { html, text } = getClipboardData(pasteEvent.clipboardData); if (html && html.length > 0) { post = parsePostFromHTML(html, builder, plugins); } else if (text && text.length > 0) {