From fba3fe7a138ee81613553883276d57d30af190aa Mon Sep 17 00:00:00 2001 From: Ian Storm Taylor Date: Wed, 27 Jul 2016 13:33:36 -0700 Subject: [PATCH] move onCopy and onCut logic to core plugin, refactor onDrop --- lib/components/content.js | 121 ++++++++++++-------------------------- lib/components/editor.js | 2 + lib/plugins/core.js | 98 ++++++++++++++++++++++++++++++ 3 files changed, 138 insertions(+), 83 deletions(-) diff --git a/lib/components/content.js b/lib/components/content.js index 0ae6da247..5915a17a1 100644 --- a/lib/components/content.js +++ b/lib/components/content.js @@ -222,7 +222,16 @@ class Content extends React.Component { */ onCopy = (e) => { - this.onCutCopy(e) + this.tmp.isCopying = true + window.requestAnimationFrame(() => { + this.tmp.isCopying = false + }) + + const { state } = this.props + const data = {} + data.type = 'fragment' + data.fragment = state.fragment + this.props.onCopy(e, data) } /** @@ -233,69 +242,17 @@ class Content extends React.Component { onCut = (e) => { if (this.props.readOnly) return - this.onCutCopy(e) - // Once the cut has successfully executed, delete the current selection. - window.requestAnimationFrame(() => { - const state = this.props.state.transform().delete().apply() - this.onChange(state) - }) - } - - /** - * On cut and copy, add the currently selected fragment to the currently - * selected DOM, so that it will show up when pasted. - * - * @param {Event} e - */ - - onCutCopy = (e) => { - const native = window.getSelection() - if (!native.rangeCount) return - - const { state } = this.props - const { fragment } = state - const encoded = Base64.serializeNode(fragment) - - // Wrap the first character of the selection in a span that has the encoded - // fragment attached as an attribute, so it will show up in the copied HTML. - const range = native.getRangeAt(0) - const contents = range.cloneContents() - const wrapper = window.document.createElement('span') - const text = contents.childNodes[0] - const char = text.textContent.slice(0, 1) - const first = window.document.createTextNode(char) - const rest = text.textContent.slice(1) - text.textContent = rest - wrapper.appendChild(first) - wrapper.setAttribute('data-fragment', encoded) - contents.insertBefore(wrapper, text) - - // Add the phony content to the DOM, and select it, so it will be copied. - const body = window.document.querySelector('body') - const div = window.document.createElement('div') - div.setAttribute('contenteditable', true) - div.style.position = 'absolute' - div.style.left = '-9999px' - div.appendChild(contents) - body.appendChild(div) - - // Set the `isCopying` flag, so our `onSelect` logic doesn't fire. this.tmp.isCopying = true - const r = window.document.createRange() - // COMPAT: In Firefox, trying to use the terser `native.selectAllChildren` - // throws an error, so we use the older `range` equivalent. (2016/06/21) - r.selectNodeContents(div) - native.removeAllRanges() - native.addRange(r) - - // Revert to the previous selection right after copying. window.requestAnimationFrame(() => { - body.removeChild(div) - native.removeAllRanges() - native.addRange(range) this.tmp.isCopying = false }) + + const { state } = this.props + const data = {} + data.type = 'fragment' + data.fragment = state.fragment + this.props.onCut(e, data) } /** @@ -364,14 +321,13 @@ class Content extends React.Component { const { state, renderDecorations } = this.props const { selection } = state - const data = e.nativeEvent.dataTransfer - const drop = {} + const { dataTransfer, x, y } = e.nativeEvent + const data = {} // COMPAT: In Firefox, `types` is array-like. (2016/06/21) - const types = Array.from(data.types) + const types = Array.from(dataTransfer.types) // Resolve the point where the drop occured. - const { x, y } = e.nativeEvent let range // COMPAT: In Firefox, `caretRangeFromPoint` doesn't exist. (2016/07/25) @@ -398,45 +354,44 @@ class Content extends React.Component { // Handle Slate fragments. if (includes(types, TYPES.FRAGMENT)) { - const encoded = data.getData(TYPES.FRAGMENT) + const encoded = dataTransfer.getData(TYPES.FRAGMENT) const fragment = Base64.deserializeNode(encoded) - drop.type = 'fragment' - drop.fragment = fragment - drop.isInternal = this.tmp.isInternalDrag + data.type = 'fragment' + data.fragment = fragment + data.isInternal = this.tmp.isInternalDrag } // Handle Slate nodes. else if (includes(types, TYPES.NODE)) { - const encoded = data.getData(TYPES.NODE) + const encoded = dataTransfer.getData(TYPES.NODE) const node = Base64.deserializeNode(encoded) - drop.type = 'node' - drop.node = node - drop.isInternal = this.tmp.isInternalDrag + data.type = 'node' + data.node = node + data.isInternal = this.tmp.isInternalDrag } // Handle files. - else if (data.files.length) { - drop.type = 'files' - drop.files = data.files + else if (dataTransfer.files.length) { + data.type = 'files' + data.files = dataTransfer.files } // Handle HTML. else if (includes(types, TYPES.HTML)) { - drop.type = 'html' - drop.text = data.getData(TYPES.TEXT) - drop.html = data.getData(TYPES.HTML) + data.type = 'html' + data.text = dataTransfer.getData(TYPES.TEXT) + data.html = dataTransfer.getData(TYPES.HTML) } // Handle plain text. else { - drop.type = 'text' - drop.text = data.getData(TYPES.TEXT) + data.type = 'text' + data.text = dataTransfer.getData(TYPES.TEXT) } - drop.data = data - drop.target = target - drop.effect = data.dropEffect - this.props.onDrop(e, drop) + data.target = target + data.effect = dataTransfer.dropEffect + this.props.onDrop(e, data) } /** diff --git a/lib/components/editor.js b/lib/components/editor.js index 5a1a58e5a..875ff0370 100644 --- a/lib/components/editor.js +++ b/lib/components/editor.js @@ -21,6 +21,8 @@ function noop() {} const EVENT_HANDLERS = [ 'onBeforeInput', 'onBlur', + 'onCopy', + 'onCut', 'onDrop', 'onKeyDown', 'onPaste', diff --git a/lib/plugins/core.js b/lib/plugins/core.js index d419f5d79..abec23dd0 100644 --- a/lib/plugins/core.js +++ b/lib/plugins/core.js @@ -1,4 +1,5 @@ +import Base64 from '../serializers/base-64' import Character from '../models/character' import Key from '../utils/key' import Placeholder from '../components/placeholder' @@ -136,6 +137,100 @@ function Plugin(options = {}) { .apply({ isNative: true }) } + /** + * On copy. + * + * @param {Event} e + * @param {Object} data + * @param {State} state + * @return {State} + */ + + function onCopy(e, data, state) { + onCutOrCopy(e, data, state) + } + + /** + * On cut. + * + * @param {Event} e + * @param {Object} data + * @param {State} state + * @param {Editor} editor + * @return {State} + */ + + function onCut(e, data, state, editor) { + onCutOrCopy(e, data, state) + + // Once the fake cut content has successfully been added to the clipboard, + // delete the content in the current selection. + window.requestAnimationFrame(() => { + const next = editor + .getState() + .transform() + .delete() + .apply() + + editor.onChange(next) + }) + } + + /** + * On cut or copy, create a fake selection so that we can add a Base 64 + * encoded copy of the fragment to the HTML, to decode on future pastes. + * + * @param {Event} e + * @param {Object} data + * @param {State} state + * @return {State} + */ + + function onCutOrCopy(e, data, state) { + const native = window.getSelection() + if (!native.rangeCount) return + + const { fragment } = data + const encoded = Base64.serializeNode(fragment) + + // Wrap the first character of the selection in a span that has the encoded + // fragment attached as an attribute, so it will show up in the copied HTML. + const range = native.getRangeAt(0) + const contents = range.cloneContents() + const wrapper = window.document.createElement('span') + const text = contents.childNodes[0] + const char = text.textContent.slice(0, 1) + const first = window.document.createTextNode(char) + const rest = text.textContent.slice(1) + text.textContent = rest + wrapper.appendChild(first) + wrapper.setAttribute('data-fragment', encoded) + contents.insertBefore(wrapper, text) + + // Add the phony content to the DOM, and select it, so it will be copied. + const body = window.document.querySelector('body') + const div = window.document.createElement('div') + div.setAttribute('contenteditable', true) + div.style.position = 'absolute' + div.style.left = '-9999px' + div.appendChild(contents) + body.appendChild(div) + + // COMPAT: In Firefox, trying to use the terser `native.selectAllChildren` + // throws an error, so we use the older `range` equivalent. (2016/06/21) + const r = window.document.createRange() + r.selectNodeContents(div) + native.removeAllRanges() + native.addRange(r) + + // Revert to the previous selection right after copying. + window.requestAnimationFrame(() => { + body.removeChild(div) + native.removeAllRanges() + native.addRange(range) + }) + } + /** * On drop. * @@ -440,6 +535,9 @@ function Plugin(options = {}) { return { onBeforeInput, + onBlur, + onCopy, + onCut, onDrop, onKeyDown, onPaste,