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:
@@ -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)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@@ -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')
|
||||
|
@@ -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()
|
||||
|
@@ -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) {
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user