diff --git a/examples/images/index.js b/examples/images/index.js index 31b31dc9e..abc313de1 100644 --- a/examples/images/index.js +++ b/examples/images/index.js @@ -5,7 +5,6 @@ import initialState from './state.json' import isImage from 'is-image' import isUrl from 'is-url' - /** * Default block to be inserted when the document is empty, * and after an image is the last node in the document. @@ -41,7 +40,7 @@ const schema = { } }, rules: [ - // Rule to insert a paragraph block if the document is empty + // Rule to insert a paragraph block if the document is empty. { match: (node) => { return node.kind == 'document' @@ -51,12 +50,11 @@ const schema = { }, normalize: (transform, document) => { const block = Block.create(defaultBlock) - transform - .insertNodeByKey(document.key, 0, block) + transform.insertNodeByKey(document.key, 0, block) } }, - // Rule to insert a paragraph below a void node (the image) - // if that node is the last one in the document + // Rule to insert a paragraph below a void node (the image) if that node is + // the last one in the document. { match: (node) => { return node.kind == 'document' @@ -67,8 +65,7 @@ const schema = { }, normalize: (transform, document) => { const block = Block.create(defaultBlock) - transform - .insertNodeByKey(document.key, document.nodes.size, block) + transform.insertNodeByKey(document.key, document.nodes.size, block) } } ] diff --git a/src/plugins/core.js b/src/plugins/core.js index e0b51198b..e606c3e2a 100644 --- a/src/plugins/core.js +++ b/src/plugins/core.js @@ -7,7 +7,8 @@ import getPoint from '../utils/get-point' import Placeholder from '../components/placeholder' import React from 'react' import getWindow from 'get-window' -import { IS_MAC } from '../constants/environment' +import findDOMNode from '../utils/find-dom-node' +import { IS_CHROME, IS_MAC, IS_SAFARI } from '../constants/environment' /** * Debug. @@ -249,29 +250,54 @@ function Plugin(options = {}) { function onCutOrCopy(e, data, state) { const window = getWindow(e.target) const native = window.getSelection() - if (native.isCollapsed) return + const { endBlock, endInline } = state + const isVoidBlock = endBlock && endBlock.isVoid + const isVoidInline = endInline && endInline.isVoid + const isVoid = isVoidBlock || isVoidInline + + // If the selection is collapsed, and it isn't inside a void node, abort. + if (native.isCollapsed && !isVoid) return const { fragment } = data const encoded = Base64.serializeNode(fragment) const range = native.getRangeAt(0) - const contents = range.cloneContents() + let contents = range.cloneContents() + let attach = contents.childNodes[0] + + // If the end node is a void node, we need to move the end of the range from + // the void node's spacer span, to the end of the void node's content. + if (isVoid) { + const r = range.cloneRange() + const node = findDOMNode(isVoidBlock ? endBlock : endInline) + r.setEndAfter(node) + contents = r.cloneContents() + attach = node + } // Remove any zero-width space spans from the cloned DOM so that they don't - // show up elsewhere when copied. + // show up elsewhere when pasted. const zws = [].slice.call(contents.querySelectorAll('[data-slate-zero-width]')) zws.forEach(zw => zw.parentNode.removeChild(zw)) - // 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 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-slate-fragment', encoded) - contents.insertBefore(wrapper, text) + // COMPAT: In Chrome and Safari, if the last element in the selection to + // copy has `contenteditable="false"` the copy will fail, and nothing will + // be put in the clipboard. So we remove them all. (2017/05/04) + if (IS_CHROME || IS_SAFARI) { + const els = [].slice.call(contents.querySelectorAll('[contenteditable="false"]')) + els.forEach(el => el.removeAttribute('contenteditable')) + } + + // Set a `data-slate-fragment` attribute on a non-empty node, so it shows up + // in the HTML, and can be used for intra-Slate pasting. If it's a text + // node, wrap it in a `` so we have something to set an attribute on. + if (attach.nodeType == 3) { + const span = window.document.createElement('span') + span.appendChild(attach) + contents.appendChild(span) + attach = span + } + + attach.setAttribute('data-slate-fragment', encoded) // Add the phony content to the DOM, and select it, so it will be copied. const body = window.document.querySelector('body') diff --git a/src/transforms/at-current-range.js b/src/transforms/at-current-range.js index 850b37758..80909395d 100644 --- a/src/transforms/at-current-range.js +++ b/src/transforms/at-current-range.js @@ -185,7 +185,7 @@ Transforms.insertFragment = (transform, fragment) => { let { state } = transform let { document, selection } = state - if (!fragment.length) return + if (!fragment.nodes.size) return const { startText, endText } = state const lastText = fragment.getLastText() diff --git a/src/transforms/at-range.js b/src/transforms/at-range.js index 0ebf1af9d..5d8b3993f 100644 --- a/src/transforms/at-range.js +++ b/src/transforms/at-range.js @@ -571,7 +571,7 @@ Transforms.insertFragmentAtRange = (transform, range, fragment, options = {}) => } // If the fragment is empty, there's nothing to do after deleting. - if (!fragment.length) return + if (!fragment.nodes.size) return // Regenerate the keys for all of the fragments nodes, so that they're // guaranteed not to collide with the existing keys in the document. Otherwise @@ -597,6 +597,12 @@ Transforms.insertFragmentAtRange = (transform, range, fragment, options = {}) => const firstBlock = blocks.first() const lastBlock = blocks.last() + // If the fragment only contains a void block, use `insertBlock` instead. + if (firstBlock == lastBlock && firstBlock.isVoid) { + transform.insertBlockAtRange(range, firstBlock, options) + return + } + // If the first and last block aren't the same, we need to insert all of the // nodes after the fragment's first block at the index. if (firstBlock != lastBlock) { diff --git a/src/utils/get-transfer-data.js b/src/utils/get-transfer-data.js index 3eaa1cc35..6c0ffb208 100644 --- a/src/utils/get-transfer-data.js +++ b/src/utils/get-transfer-data.js @@ -8,7 +8,7 @@ import TYPES from '../constants/types' * @type {RegExp} */ -const FRAGMENT_MATCHER = /data-slate-fragment="([^\s]+)"/ +const FRAGMENT_MATCHER = / data-slate-fragment="([^\s]+)"/ /** * Get the data and type from a native data `transfer`. @@ -30,7 +30,7 @@ function getTransferData(transfer) { if ( !fragment && html && - ~html.indexOf('