mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-22 23:12:52 +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 isImage from 'is-image'
|
||||||
import isUrl from 'is-url'
|
import isUrl from 'is-url'
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default block to be inserted when the document is empty,
|
* Default block to be inserted when the document is empty,
|
||||||
* and after an image is the last node in the document.
|
* and after an image is the last node in the document.
|
||||||
@@ -41,7 +40,7 @@ const schema = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
rules: [
|
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) => {
|
match: (node) => {
|
||||||
return node.kind == 'document'
|
return node.kind == 'document'
|
||||||
@@ -51,12 +50,11 @@ const schema = {
|
|||||||
},
|
},
|
||||||
normalize: (transform, document) => {
|
normalize: (transform, document) => {
|
||||||
const block = Block.create(defaultBlock)
|
const block = Block.create(defaultBlock)
|
||||||
transform
|
transform.insertNodeByKey(document.key, 0, block)
|
||||||
.insertNodeByKey(document.key, 0, block)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Rule to insert a paragraph below a void node (the image)
|
// Rule to insert a paragraph below a void node (the image) if that node is
|
||||||
// if that node is the last one in the document
|
// the last one in the document.
|
||||||
{
|
{
|
||||||
match: (node) => {
|
match: (node) => {
|
||||||
return node.kind == 'document'
|
return node.kind == 'document'
|
||||||
@@ -67,8 +65,7 @@ const schema = {
|
|||||||
},
|
},
|
||||||
normalize: (transform, document) => {
|
normalize: (transform, document) => {
|
||||||
const block = Block.create(defaultBlock)
|
const block = Block.create(defaultBlock)
|
||||||
transform
|
transform.insertNodeByKey(document.key, document.nodes.size, block)
|
||||||
.insertNodeByKey(document.key, document.nodes.size, block)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@@ -7,7 +7,8 @@ import getPoint from '../utils/get-point'
|
|||||||
import Placeholder from '../components/placeholder'
|
import Placeholder from '../components/placeholder'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import getWindow from 'get-window'
|
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.
|
* Debug.
|
||||||
@@ -249,29 +250,54 @@ function Plugin(options = {}) {
|
|||||||
function onCutOrCopy(e, data, state) {
|
function onCutOrCopy(e, data, state) {
|
||||||
const window = getWindow(e.target)
|
const window = getWindow(e.target)
|
||||||
const native = window.getSelection()
|
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 { fragment } = data
|
||||||
const encoded = Base64.serializeNode(fragment)
|
const encoded = Base64.serializeNode(fragment)
|
||||||
const range = native.getRangeAt(0)
|
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
|
// 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]'))
|
const zws = [].slice.call(contents.querySelectorAll('[data-slate-zero-width]'))
|
||||||
zws.forEach(zw => zw.parentNode.removeChild(zw))
|
zws.forEach(zw => zw.parentNode.removeChild(zw))
|
||||||
|
|
||||||
// Wrap the first character of the selection in a span that has the encoded
|
// COMPAT: In Chrome and Safari, if the last element in the selection to
|
||||||
// fragment attached as an attribute, so it will show up in the copied HTML.
|
// copy has `contenteditable="false"` the copy will fail, and nothing will
|
||||||
const wrapper = window.document.createElement('span')
|
// be put in the clipboard. So we remove them all. (2017/05/04)
|
||||||
const text = contents.childNodes[0]
|
if (IS_CHROME || IS_SAFARI) {
|
||||||
const char = text.textContent.slice(0, 1)
|
const els = [].slice.call(contents.querySelectorAll('[contenteditable="false"]'))
|
||||||
const first = window.document.createTextNode(char)
|
els.forEach(el => el.removeAttribute('contenteditable'))
|
||||||
const rest = text.textContent.slice(1)
|
}
|
||||||
text.textContent = rest
|
|
||||||
wrapper.appendChild(first)
|
// Set a `data-slate-fragment` attribute on a non-empty node, so it shows up
|
||||||
wrapper.setAttribute('data-slate-fragment', encoded)
|
// in the HTML, and can be used for intra-Slate pasting. If it's a text
|
||||||
contents.insertBefore(wrapper, 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.
|
// Add the phony content to the DOM, and select it, so it will be copied.
|
||||||
const body = window.document.querySelector('body')
|
const body = window.document.querySelector('body')
|
||||||
|
@@ -185,7 +185,7 @@ Transforms.insertFragment = (transform, fragment) => {
|
|||||||
let { state } = transform
|
let { state } = transform
|
||||||
let { document, selection } = state
|
let { document, selection } = state
|
||||||
|
|
||||||
if (!fragment.length) return
|
if (!fragment.nodes.size) return
|
||||||
|
|
||||||
const { startText, endText } = state
|
const { startText, endText } = state
|
||||||
const lastText = fragment.getLastText()
|
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 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
|
// 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
|
// 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 firstBlock = blocks.first()
|
||||||
const lastBlock = blocks.last()
|
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
|
// 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.
|
// nodes after the fragment's first block at the index.
|
||||||
if (firstBlock != lastBlock) {
|
if (firstBlock != lastBlock) {
|
||||||
|
@@ -8,7 +8,7 @@ import TYPES from '../constants/types'
|
|||||||
* @type {RegExp}
|
* @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`.
|
* Get the data and type from a native data `transfer`.
|
||||||
@@ -30,7 +30,7 @@ function getTransferData(transfer) {
|
|||||||
if (
|
if (
|
||||||
!fragment &&
|
!fragment &&
|
||||||
html &&
|
html &&
|
||||||
~html.indexOf('<span data-slate-fragment="')
|
~html.indexOf(' data-slate-fragment="')
|
||||||
) {
|
) {
|
||||||
const matches = FRAGMENT_MATCHER.exec(html)
|
const matches = FRAGMENT_MATCHER.exec(html)
|
||||||
const [ full, encoded ] = matches // eslint-disable-line no-unused-vars
|
const [ full, encoded ] = matches // eslint-disable-line no-unused-vars
|
||||||
|
Reference in New Issue
Block a user