1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-22 23:12:52 +02:00

fix fragment pasting

This commit is contained in:
Ian Storm Taylor
2016-06-27 14:08:30 -07:00
parent 0caa49fb10
commit f6c1e8de28
8 changed files with 148 additions and 35 deletions

View File

@@ -14,7 +14,7 @@ import Data from './data'
import Inline from './inline' import Inline from './inline'
import Node from './node' import Node from './node'
import Text from './text' import Text from './text'
import uid from 'uid' import uid from '../utils/uid'
import Immutable, { Map, List, Record } from 'immutable' import Immutable, { Map, List, Record } from 'immutable'
/** /**

View File

@@ -14,7 +14,7 @@ import Block from './block'
import Data from './data' import Data from './data'
import Node from './node' import Node from './node'
import Text from './text' import Text from './text'
import uid from 'uid' import uid from '../utils/uid'
import { List, Map, Record } from 'immutable' import { List, Map, Record } from 'immutable'
/** /**

View File

@@ -8,6 +8,7 @@ import Mark from './mark'
import Selection from './selection' import Selection from './selection'
import Transforms from './transforms' import Transforms from './transforms'
import Text from './text' import Text from './text'
import uid from '../utils/uid'
import { List, Map, OrderedSet, Set } from 'immutable' import { List, Map, OrderedSet, Set } from 'immutable'
/** /**
@@ -88,6 +89,23 @@ const Node = {
}, Block.createList()) }, Block.createList())
}, },
/**
* Get the closest block nodes for each text node in the node.
*
* @return {List} nodes
*/
getBlocks() {
return this
.getTextNodes()
.reduce((blocks, text) => {
const block = this.getClosestBlock(text)
return blocks.includes(block)
? blocks
: blocks.push(block)
}, Block.createList())
},
/** /**
* Get the closest block nodes for each text node in a `range`. * Get the closest block nodes for each text node in a `range`.
* *
@@ -340,6 +358,8 @@ const Node = {
: node.getHighestChild(endKey) : node.getHighestChild(endKey)
nodes = node.getChildrenBetweenIncluding(startNode, endNode) nodes = node.getChildrenBetweenIncluding(startNode, endNode)
// Return a new document fragment.
return Document.create({ nodes }) return Document.create({ nodes })
}, },
@@ -700,7 +720,7 @@ const Node = {
}, },
/** /**
* Insert child `nodes` after child by `key`. * Insert child `nodes` before child by `key`.
* *
* @param {String or Node} key * @param {String or Node} key
* @param {List} nodes * @param {List} nodes
@@ -710,7 +730,7 @@ const Node = {
insertChildrenBefore(key, nodes) { insertChildrenBefore(key, nodes) {
key = normalizeKey(key) key = normalizeKey(key)
const child = this.getChild(key) const child = this.getChild(key)
const index = this.nodex.indexOf(child) const index = this.nodes.indexOf(child)
nodes = this.nodes nodes = this.nodes
.slice(0, index) .slice(0, index)
@@ -736,6 +756,22 @@ const Node = {
return range.isAtStartOf(start) || range.isAtEndOf(start) return range.isAtStartOf(start) || range.isAtEndOf(start)
}, },
/**
* Map all children nodes, updating them in their parents.
*
* @param {Function} iterator
* @return {Node} node
*/
mapDescendants(iterator) {
const nodes = this.nodes.map((node, i, nodes) => {
if (node.kind != 'text') node = node.mapDescendants(iterator)
return iterator(node, i, nodes)
})
return this.merge({ nodes })
},
/** /**
* Normalize the node by joining any two adjacent text child nodes. * Normalize the node by joining any two adjacent text child nodes.
* *
@@ -746,13 +782,21 @@ const Node = {
let node = this let node = this
const texts = node.getTextNodes() const texts = node.getTextNodes()
// If there are no text nodes, add one. // If this node has no children, add a text node.
if (!texts.size) { if (node.nodes.size == 0) {
const text = Text.create() const text = Text.create()
const nodes = node.nodes.push(text) const nodes = node.nodes.push(text)
return node.merge({ nodes }) return node.merge({ nodes })
} }
// Map this node's descendants, ensuring there are no duplicate keys.
const keys = []
node = node.mapDescendants((node) => {
if (keys.includes(node.key)) node = node.set('key', uid())
keys.push(node.key)
return node
})
// See if there are any adjacent text nodes. // See if there are any adjacent text nodes.
let firstAdjacent = node.findDescendant((child) => { let firstAdjacent = node.findDescendant((child) => {
if (child.kind != 'text') return if (child.kind != 'text') return
@@ -773,13 +817,7 @@ const Node = {
// Then remove the second node. // Then remove the second node.
parent = parent.removeDescendant(second) parent = parent.removeDescendant(second)
// If the parent isn't this node, it needs to be updated.
if (parent != node) {
node = node.updateDescendant(parent) node = node.updateDescendant(parent)
} else {
node = parent
}
// Recurse by normalizing again. // Recurse by normalizing again.
return node.normalize() return node.normalize()

View File

@@ -2,6 +2,7 @@
import Document from './document' import Document from './document'
import Selection from './selection' import Selection from './selection'
import Transform from './transform' import Transform from './transform'
import uid from '../utils/uid'
import { Record, Stack } from 'immutable' import { Record, Stack } from 'immutable'
/** /**
@@ -411,21 +412,63 @@ class State extends Record(DEFAULTS) {
insertFragment(fragment) { insertFragment(fragment) {
let state = this let state = this
let { document, selection } = state let { document, selection } = state
let after = selection
// If there's nothing in the fragment, do nothing. // If there's nothing in the fragment, do nothing.
if (!fragment.length) return state if (!fragment.length) return state
// Lookup some nodes for determining the selection next.
const texts = fragment.getTextNodes()
const lastText = texts.last()
const lastInline = fragment.getClosestInline(lastText)
const startText = document.getDescendant(selection.startKey)
const startBlock = document.getClosestBlock(startText)
const startInline = document.getClosestInline(startText)
const nextText = document.getNextText(startText)
const nextBlock = nextText ? document.getClosestBlock(nextText) : null
const nextNextText = nextText ? document.getNextText(nextText) : null
// Insert the fragment. // Insert the fragment.
document = document.insertFragmentAtRange(selection, fragment) document = document.insertFragmentAtRange(selection, fragment)
// Determine what the selection should be after inserting. // Determine what the selection should be after inserting.
const texts = fragment.getTextNodes() if (texts.size == 1) {
const first = texts.first() after = selection
const last = texts.last() .moveToStart()
selection = first == last .moveForward(fragment.length)
? selection.moveForward(fragment.length) }
: selection.moveToEndOf(last)
else if (!nextText) {
const text = document.getTextNodes().last()
after = selection
.moveToStartOf(text)
.moveForward(lastText.length)
}
else if (lastInline || startInline) {
const text = document.getPreviousText(nextText)
after = selection.moveToEndOf(text)
}
else if (nextBlock != startBlock) {
const text = document.getPreviousText(nextText)
after = selection
.moveToStartOf(text)
.moveForward(lastText.length)
}
else {
const text = nextNextText
? document.getPreviousText(nextNextText)
: document.getPreviousText(document.getTextNodes().last())
after = selection
.moveToStartOf(text)
.moveForward(lastText.length)
}
// Update the document and selection.
selection = after
state = state.merge({ document, selection }) state = state.merge({ document, selection })
return state return state
} }

View File

@@ -1,7 +1,7 @@
import Character from './character' import Character from './character'
import Mark from './mark' import Mark from './mark'
import uid from 'uid' import uid from '../utils/uid'
import { List, Record } from 'immutable' import { List, Record } from 'immutable'
/** /**

View File

@@ -7,6 +7,7 @@ import Inline from './inline'
import Mark from './mark' import Mark from './mark'
import Selection from './selection' import Selection from './selection'
import Text from './text' import Text from './text'
import uid from '../utils/uid'
import { List, Map, Set } from 'immutable' import { List, Map, Set } from 'immutable'
/** /**
@@ -180,20 +181,25 @@ const Transforms = {
node = node.splitInlineAtRange(range) node = node.splitInlineAtRange(range)
} }
// Make sure each node in the fragment has a new key.
fragment = fragment.mapDescendants(node => node.set('key', uid()))
// Insert the contents of the first block into the block at the cursor. // Insert the contents of the first block into the block at the cursor.
const { startKey, endKey } = range const { startKey, endKey } = range
let block = node.getClosestBlock(startKey) let block = node.getClosestBlock(startKey)
let start = node.getDescendant(startKey) let start = node.getDescendant(startKey)
if (!range.isAtEndOf(start)) start = node.getPreviousText(start) if (!range.isAtEndOf(start)) start = node.getPreviousText(start)
const startChild = block.getHighestChild(start) const startChild = start ? block.getHighestChild(start) : null
const nextChild = block.getNextSibling(startChild) const nextChild = startChild
? block.getNextSibling(startChild)
: node.getClosestBlock(node.getTextNodes().first())
const blocks = fragment.getDeepestBlocks() const blocks = fragment.getDeepestBlocks()
const firstBlock = blocks.first() const firstBlock = blocks.first()
let lastBlock = blocks.last() let lastBlock = blocks.last()
block = block.insertChildrenAfter(startChild, firstBlock.nodes) block = block.insertChildrenBefore(nextChild, firstBlock.nodes)
node = node.updateDescendant(block) node = node.updateDescendant(block)
// If there are no other siblings, that's it. // If there are no other siblings, that's it.
@@ -205,11 +211,14 @@ const Transforms = {
// Then, add the inlines after the cursor from the current block to the // Then, add the inlines after the cursor from the current block to the
// start of the last block in the fragment. // start of the last block in the fragment.
if (nextChild) {
lastBlock = lastBlock.concatChildren(block.getChildrenAfterIncluding(nextChild)) lastBlock = lastBlock.concatChildren(block.getChildrenAfterIncluding(nextChild))
fragment = fragment.updateDescendant(lastBlock) fragment = fragment.updateDescendant(lastBlock)
block = block.removeChildrenAfterIncluding(nextChild) block = block.removeChildrenAfterIncluding(nextChild)
node = node.updateDescendant(block) node = node.updateDescendant(block)
}
// Finally, add the fragment's children after the block. // Finally, add the fragment's children after the block.
node = node.insertChildrenAfter(block, fragment.nodes) node = node.insertChildrenAfter(block, fragment.nodes)

View File

@@ -151,18 +151,23 @@ export default {
*/ */
onPaste(e, paste, state, editor) { onPaste(e, paste, state, editor) {
const { fragment } = editor
// Don't handle files in core.
if (paste.type == 'files') return if (paste.type == 'files') return
// If pasting html and the text matches the current fragment, use that. // If pasting html and the text matches the current fragment, use that.
if (paste.type == 'html' && paste.text == fragment.text) { if (paste.type == 'html') {
const { fragment } = editor
const text = fragment
.getBlocks()
.map(block => block.text)
.join('\n')
if (paste.text == text) {
return state return state
.transform() .transform()
.insertFragment(fragment) .insertFragment(fragment)
.apply() .apply()
} }
}
// Otherwise, just insert the plain text splitting at new lines. // Otherwise, just insert the plain text splitting at new lines.
let transform = state.transform() let transform = state.transform()

18
lib/utils/uid.js Normal file
View File

@@ -0,0 +1,18 @@
import generate from 'uid'
/**
* Create a unique identifier.
*
* @return {String} uid
*/
function uid() {
return generate(4)
}
/**
* Export.
*/
export default uid