1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-21 22:45:18 +02:00

Copy void (#788)

* Fix getFragmentAtRange to not return early when selection is in void node or collapsed

* Detecting when cut or copy is made in a void node, and not returning early

* Checking if text exists before using it for cut/copy

* Moving inVoidNode check to onCutOrCopy

as per @ianstormtaylor note.

* If/else for dom operations when copying text vs void node

* Adding fragment span to the contents

* more work on copy-pasting void nodes
This commit is contained in:
Ian Storm Taylor
2017-05-04 14:42:47 -07:00
committed by GitHub
parent a338b36c76
commit 94e0e6254a
5 changed files with 56 additions and 27 deletions

View File

@@ -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)
}
}
]

View File

@@ -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 `<span>` 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')

View File

@@ -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()

View File

@@ -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) {

View File

@@ -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('<span data-slate-fragment="')
~html.indexOf(' data-slate-fragment="')
) {
const matches = FRAGMENT_MATCHER.exec(html)
const [ full, encoded ] = matches // eslint-disable-line no-unused-vars