mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-13 10:44:02 +02:00
fix selecting behavior, got delete and backspace working
This commit is contained in:
@@ -114,7 +114,8 @@ class App extends React.Component {
|
|||||||
renderMark={renderMark}
|
renderMark={renderMark}
|
||||||
state={this.state.state}
|
state={this.state.state}
|
||||||
onChange={(state) => {
|
onChange={(state) => {
|
||||||
console.log('Change:', state.toJS())
|
console.log('Selection:', state.selection.toJS())
|
||||||
|
console.log('Text:', state.nodes.last().children.first().characters.map(c => c.text).toJS())
|
||||||
this.setState({ state })
|
this.setState({ state })
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@@ -25,24 +25,6 @@ class Content extends React.Component {
|
|||||||
state: React.PropTypes.object.isRequired,
|
state: React.PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Before the component updates.
|
|
||||||
*/
|
|
||||||
|
|
||||||
componentWillUpdate() {
|
|
||||||
this.updating = true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* After the component updates.
|
|
||||||
*/
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
this.updating = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* On change, bubble up.
|
* On change, bubble up.
|
||||||
*
|
*
|
||||||
@@ -60,11 +42,6 @@ class Content extends React.Component {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
onKeyDown(e) {
|
onKeyDown(e) {
|
||||||
// COMPAT: Certain keys should never be handled by the browser's mechanism,
|
|
||||||
// because using the native contenteditable behavior introduces quirks.
|
|
||||||
const key = keycode(e.which)
|
|
||||||
if (key === 'escape' || key === 'return') e.preventDefault()
|
|
||||||
|
|
||||||
this.props.onKeyDown(e)
|
this.props.onKeyDown(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,10 +52,6 @@ class Content extends React.Component {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
onSelect(e) {
|
onSelect(e) {
|
||||||
// don't handle the selection if we're rendering, since it is about to be
|
|
||||||
// set by the
|
|
||||||
if (this.updating) return
|
|
||||||
|
|
||||||
let { state } = this.props
|
let { state } = this.props
|
||||||
let { selection } = state
|
let { selection } = state
|
||||||
const native = window.getSelection()
|
const native = window.getSelection()
|
||||||
@@ -111,12 +84,15 @@ class Content extends React.Component {
|
|||||||
(startAndEnd.size == 1 && anchor.offset > focus.offset)
|
(startAndEnd.size == 1 && anchor.offset > focus.offset)
|
||||||
)
|
)
|
||||||
|
|
||||||
selection = selection.set('anchorKey', anchor.key)
|
selection = selection.merge({
|
||||||
selection = selection.set('anchorOffset', anchor.offset)
|
anchorKey: anchor.key,
|
||||||
selection = selection.set('focusKey', focus.key)
|
anchorOffset: anchor.offset,
|
||||||
selection = selection.set('focusOffset', focus.offset)
|
focusKey: focus.key,
|
||||||
selection = selection.set('isBackward', isBackward)
|
focusOffset: focus.offset,
|
||||||
selection = selection.set('isFocused', true)
|
isBackward: isBackward,
|
||||||
|
isFocused: true
|
||||||
|
})
|
||||||
|
|
||||||
state = state.set('selection', selection)
|
state = state.set('selection', selection)
|
||||||
this.onChange(state)
|
this.onChange(state)
|
||||||
return
|
return
|
||||||
@@ -136,13 +112,19 @@ class Content extends React.Component {
|
|||||||
.toArray()
|
.toArray()
|
||||||
.map(node => this.renderNode(node))
|
.map(node => this.renderNode(node))
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
whiteSpace: 'pre-wrap' // preserve adjacent whitespace and new lines
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
contentEditable
|
contentEditable
|
||||||
suppressContentEditableWarning
|
suppressContentEditableWarning
|
||||||
|
spellCheck={false}
|
||||||
data-type='content'
|
data-type='content'
|
||||||
onKeyDown={(e) => this.onKeyDown(e)}
|
onKeyDown={(e) => this.onKeyDown(e)}
|
||||||
onSelect={(e) => this.onSelect(e)}
|
onSelect={(e) => this.onSelect(e)}
|
||||||
|
style={style}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
@@ -24,10 +24,8 @@ class Editor extends React.Component {
|
|||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
this.state = {
|
this.state = {}
|
||||||
plugins: this.resolvePlugins(props),
|
this.state.plugins = this.resolvePlugins(props)
|
||||||
state: props.state
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(props) {
|
componentWillReceiveProps(props) {
|
||||||
@@ -36,6 +34,8 @@ class Editor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onChange(state) {
|
onChange(state) {
|
||||||
|
if (state == this.props.state) return
|
||||||
|
|
||||||
for (const plugin of this.state.plugins) {
|
for (const plugin of this.state.plugins) {
|
||||||
if (!plugin.onChange) continue
|
if (!plugin.onChange) continue
|
||||||
const newState = plugin.onChange(state, this)
|
const newState = plugin.onChange(state, this)
|
||||||
@@ -53,7 +53,7 @@ class Editor extends React.Component {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
getState() {
|
getState() {
|
||||||
return this.state.state
|
return this.props.state
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -4,10 +4,10 @@ import ReactDOM from 'react-dom'
|
|||||||
import createOffsetKey from '../utils/create-offset-key'
|
import createOffsetKey from '../utils/create-offset-key'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LeafNode.
|
* Leaf.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class LeafNode extends React.Component {
|
class Leaf extends React.Component {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
node: React.PropTypes.object.isRequired,
|
node: React.PropTypes.object.isRequired,
|
||||||
@@ -16,11 +16,11 @@ class LeafNode extends React.Component {
|
|||||||
state: React.PropTypes.object.isRequired,
|
state: React.PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentDidMount() {
|
||||||
this.updateSelection()
|
this.updateSelection()
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidUpdate() {
|
||||||
this.updateSelection()
|
this.updateSelection()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,4 +133,4 @@ class LeafNode extends React.Component {
|
|||||||
* Export.
|
* Export.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default LeafNode
|
export default Leaf
|
||||||
|
@@ -5,10 +5,10 @@ import convertCharactersToRanges from '../utils/convert-characters-to-ranges'
|
|||||||
import createOffsetKey from '../utils/create-offset-key'
|
import createOffsetKey from '../utils/create-offset-key'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TextNode.
|
* Text.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class TextNode extends React.Component {
|
class Text extends React.Component {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
node: React.PropTypes.object.isRequired,
|
node: React.PropTypes.object.isRequired,
|
||||||
@@ -53,4 +53,4 @@ class TextNode extends React.Component {
|
|||||||
* Export.
|
* Export.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default TextNode
|
export default Text
|
||||||
|
@@ -53,6 +53,47 @@ class Node extends NodeRecord {
|
|||||||
}, {}))
|
}, {}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a new value for a child node by `key`.
|
||||||
|
*
|
||||||
|
* @param {String} key
|
||||||
|
* @param {Node} node
|
||||||
|
* @return {Node} node
|
||||||
|
*/
|
||||||
|
|
||||||
|
setNode(key, node) {
|
||||||
|
if (this.children.get(key)) {
|
||||||
|
const children = this.children.set(key, node)
|
||||||
|
return this.set('children', children)
|
||||||
|
}
|
||||||
|
|
||||||
|
const children = this.children.map((child) => {
|
||||||
|
return child instanceof Node
|
||||||
|
? child.setNode(key, node)
|
||||||
|
: child
|
||||||
|
})
|
||||||
|
|
||||||
|
return this.set('children', children)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively find children nodes by `iterator`.
|
||||||
|
*
|
||||||
|
* @param {Function} iterator
|
||||||
|
* @return {Node} node
|
||||||
|
*/
|
||||||
|
|
||||||
|
findNode(iterator) {
|
||||||
|
const shallow = this.children.find(iterator)
|
||||||
|
if (shallow != null) return shallow
|
||||||
|
|
||||||
|
const deep = this.children
|
||||||
|
.map(node => node instanceof Node ? node.findNode(iterator) : null)
|
||||||
|
.filter(node => node)
|
||||||
|
.first()
|
||||||
|
return deep
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively filter children nodes with `iterator`.
|
* Recursively filter children nodes with `iterator`.
|
||||||
*
|
*
|
||||||
|
@@ -32,6 +32,57 @@ class State extends StateRecord {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* NODES HELPERS.
|
||||||
|
* ==============
|
||||||
|
*
|
||||||
|
* These are all nodes-like helper functions that help with actions related to
|
||||||
|
* the recursively-nested node tree.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a new value for a child node by `key`.
|
||||||
|
*
|
||||||
|
* @param {String} key
|
||||||
|
* @param {Node} node
|
||||||
|
* @return {Node} node
|
||||||
|
*/
|
||||||
|
|
||||||
|
setNode(key, node) {
|
||||||
|
if (this.nodes.get(key)) {
|
||||||
|
const nodes = this.nodes.set(key, node)
|
||||||
|
return this.set('nodes', nodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodes = this.nodes.map((child) => {
|
||||||
|
return child instanceof Node
|
||||||
|
? child.setNode(key, node)
|
||||||
|
: child
|
||||||
|
})
|
||||||
|
|
||||||
|
return this.set('nodes', nodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively find children nodes by `iterator`.
|
||||||
|
*
|
||||||
|
* @param {Function} iterator
|
||||||
|
* @return {OrderedMap} matches
|
||||||
|
*/
|
||||||
|
|
||||||
|
findNode(iterator) {
|
||||||
|
const shallow = this.nodes.find(iterator)
|
||||||
|
if (shallow != null) return shallow
|
||||||
|
|
||||||
|
const deep = this.nodes
|
||||||
|
.map(node => node instanceof Node ? node.findNode(iterator) : null)
|
||||||
|
.filter(node => node)
|
||||||
|
.first()
|
||||||
|
return deep
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively filter children nodes with `iterator`.
|
* Recursively filter children nodes with `iterator`.
|
||||||
*
|
*
|
||||||
@@ -51,6 +102,81 @@ class State extends StateRecord {
|
|||||||
return deep
|
return deep
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* TRANSFORMS.
|
||||||
|
* -----------
|
||||||
|
*
|
||||||
|
* These are all transform helper functions that map to a specific transform
|
||||||
|
* type that you can apply to a state.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backspace a single character.
|
||||||
|
*
|
||||||
|
* @param {Selection} selection (optional)
|
||||||
|
* @return {State} state
|
||||||
|
*/
|
||||||
|
|
||||||
|
backspace(selection = this.selection) {
|
||||||
|
// when not collapsed, remove the entire selection
|
||||||
|
if (!selection.isCollapsed) {
|
||||||
|
return this
|
||||||
|
.removeSelection(selection)
|
||||||
|
.collapseBackward()
|
||||||
|
}
|
||||||
|
|
||||||
|
// when already at the start of the content, there's nothing to do
|
||||||
|
if (selection.isAtStartOf(this)) return this
|
||||||
|
|
||||||
|
// otherwise, remove one character behind of the cursor
|
||||||
|
let { startKey, endOffset } = selection
|
||||||
|
let { nodes } = this
|
||||||
|
let node = this.findNode(node => node.key == startKey)
|
||||||
|
let startOffset = endOffset - 1
|
||||||
|
return this
|
||||||
|
.removeText(node, startOffset, endOffset)
|
||||||
|
.moveTo({
|
||||||
|
anchorOffset: startOffset,
|
||||||
|
focusOffset: startOffset
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collapse the current selection backward, towards it's anchor point.
|
||||||
|
*
|
||||||
|
* @return {State} state
|
||||||
|
*/
|
||||||
|
|
||||||
|
collapseBackward() {
|
||||||
|
let { selection } = this
|
||||||
|
let { anchorKey, anchorOffset } = selection
|
||||||
|
selection = selection.merge({
|
||||||
|
focusKey: anchorKey,
|
||||||
|
focusOffset: anchorOffset
|
||||||
|
})
|
||||||
|
let state = this.set('selection', selection)
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collapse the current selection forward, towards it's focus point.
|
||||||
|
*
|
||||||
|
* @return {State} state
|
||||||
|
*/
|
||||||
|
|
||||||
|
collapseForward() {
|
||||||
|
let { selection } = this
|
||||||
|
let { focusKey, focusOffset } = selection
|
||||||
|
selection = selection.merge({
|
||||||
|
anchorKey: focusKey,
|
||||||
|
anchorOffset: focusOffset
|
||||||
|
})
|
||||||
|
let state = this.set('selection', selection)
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a single character.
|
* Delete a single character.
|
||||||
*
|
*
|
||||||
@@ -60,7 +186,11 @@ class State extends StateRecord {
|
|||||||
|
|
||||||
delete(selection = this.selection) {
|
delete(selection = this.selection) {
|
||||||
// when not collapsed, remove the entire selection
|
// when not collapsed, remove the entire selection
|
||||||
if (!selection.isCollapsed) return this.removeSelection(selection)
|
if (!selection.isCollapsed) {
|
||||||
|
return this
|
||||||
|
.removeSelection(selection)
|
||||||
|
.collapseBackward()
|
||||||
|
}
|
||||||
|
|
||||||
// when already at the end of the content, there's nothing to do
|
// when already at the end of the content, there's nothing to do
|
||||||
if (selection.isAtEndOf(this)) return this
|
if (selection.isAtEndOf(this)) return this
|
||||||
@@ -68,11 +198,28 @@ class State extends StateRecord {
|
|||||||
// otherwise, remove one character ahead of the cursor
|
// otherwise, remove one character ahead of the cursor
|
||||||
let { startKey, startOffset } = selection
|
let { startKey, startOffset } = selection
|
||||||
let { nodes } = this
|
let { nodes } = this
|
||||||
let node = nodes.get(startKey)
|
let node = this.findNode(node => node.key == startKey)
|
||||||
let endOffset = startOffset + 1
|
let endOffset = startOffset + 1
|
||||||
return this.removeText(node, startOffset, endOffset)
|
return this.removeText(node, startOffset, endOffset)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the selection to a specific anchor and focus.
|
||||||
|
*
|
||||||
|
* @param {Object} properties
|
||||||
|
* @property {String} anchorKey (optional)
|
||||||
|
* @property {Number} anchorOffset (optional)
|
||||||
|
* @property {String} focusKey (optional)
|
||||||
|
* @property {String} focusOffset (optional)
|
||||||
|
* @return {State} state
|
||||||
|
*/
|
||||||
|
|
||||||
|
moveTo(properties) {
|
||||||
|
let selection = this.selection.merge(properties)
|
||||||
|
let state = this.merge({ selection })
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove the existing selection's content.
|
* Remove the existing selection's content.
|
||||||
*
|
*
|
||||||
@@ -116,15 +263,14 @@ class State extends StateRecord {
|
|||||||
|
|
||||||
removeText(node, startOffset, endOffset) {
|
removeText(node, startOffset, endOffset) {
|
||||||
let { nodes } = this
|
let { nodes } = this
|
||||||
let { text } = node
|
let { characters } = node
|
||||||
|
|
||||||
text = text.filter((char, i) => {
|
characters = characters.filterNot((char, i) => {
|
||||||
return i > startOffset && i < endOffset
|
return startOffset <= i && i < endOffset
|
||||||
})
|
})
|
||||||
|
|
||||||
node = node.set('text', text)
|
node = node.set('characters', characters)
|
||||||
nodes = nodes.set(node.key, node)
|
let state = this.setNode(node.key, node)
|
||||||
let state = this.set('nodes', nodes)
|
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -22,12 +22,14 @@ const CORE_PLUGIN = {
|
|||||||
|
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'enter': {
|
case 'enter': {
|
||||||
|
e.preventDefault()
|
||||||
return state.split()
|
return state.split()
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'backspace': {
|
case 'backspace': {
|
||||||
// COMPAT: Windows has a special "cut" behavior for the shift key.
|
// COMPAT: Windows has a special "cut" behavior for the shift key.
|
||||||
if (IS_WINDOWS && e.shiftKey) return
|
if (IS_WINDOWS && e.shiftKey) return
|
||||||
|
e.preventDefault()
|
||||||
return isWord(e)
|
return isWord(e)
|
||||||
? state.backspaceWord()
|
? state.backspaceWord()
|
||||||
: state.backspace()
|
: state.backspace()
|
||||||
@@ -36,6 +38,7 @@ const CORE_PLUGIN = {
|
|||||||
case 'delete': {
|
case 'delete': {
|
||||||
// COMPAT: Windows has a special "cut" behavior for the shift key.
|
// COMPAT: Windows has a special "cut" behavior for the shift key.
|
||||||
if (IS_WINDOWS && e.shiftKey) return
|
if (IS_WINDOWS && e.shiftKey) return
|
||||||
|
e.preventDefault()
|
||||||
return isWord(e)
|
return isWord(e)
|
||||||
? state.deleteWord()
|
? state.deleteWord()
|
||||||
: state.delete()
|
: state.delete()
|
||||||
@@ -43,11 +46,13 @@ const CORE_PLUGIN = {
|
|||||||
|
|
||||||
case 'y': {
|
case 'y': {
|
||||||
if (!isCtrl(e) || !IS_WINDOWS) return
|
if (!isCtrl(e) || !IS_WINDOWS) return
|
||||||
|
e.preventDefault()
|
||||||
return state.redo()
|
return state.redo()
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'z': {
|
case 'z': {
|
||||||
if (!isCommand(e)) return
|
if (!isCommand(e)) return
|
||||||
|
e.preventDefault()
|
||||||
return IS_MAC && e.shiftKey
|
return IS_MAC && e.shiftKey
|
||||||
? state.redo()
|
? state.redo()
|
||||||
: state.undo()
|
: state.undo()
|
||||||
|
Reference in New Issue
Block a user