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

fix selecting behavior, got delete and backspace working

This commit is contained in:
Ian Storm Taylor 2016-06-16 12:12:50 -07:00
parent 8636242931
commit de6aeb9dfe
8 changed files with 230 additions and 55 deletions

View File

@ -114,7 +114,8 @@ class App extends React.Component {
renderMark={renderMark}
state={this.state.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 })
}}
/>

View File

@ -25,24 +25,6 @@ class Content extends React.Component {
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.
*
@ -60,11 +42,6 @@ class Content extends React.Component {
*/
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)
}
@ -75,10 +52,6 @@ class Content extends React.Component {
*/
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 { selection } = state
const native = window.getSelection()
@ -111,12 +84,15 @@ class Content extends React.Component {
(startAndEnd.size == 1 && anchor.offset > focus.offset)
)
selection = selection.set('anchorKey', anchor.key)
selection = selection.set('anchorOffset', anchor.offset)
selection = selection.set('focusKey', focus.key)
selection = selection.set('focusOffset', focus.offset)
selection = selection.set('isBackward', isBackward)
selection = selection.set('isFocused', true)
selection = selection.merge({
anchorKey: anchor.key,
anchorOffset: anchor.offset,
focusKey: focus.key,
focusOffset: focus.offset,
isBackward: isBackward,
isFocused: true
})
state = state.set('selection', selection)
this.onChange(state)
return
@ -136,13 +112,19 @@ class Content extends React.Component {
.toArray()
.map(node => this.renderNode(node))
const style = {
whiteSpace: 'pre-wrap' // preserve adjacent whitespace and new lines
}
return (
<div
contentEditable
suppressContentEditableWarning
spellCheck={false}
data-type='content'
onKeyDown={(e) => this.onKeyDown(e)}
onSelect={(e) => this.onSelect(e)}
style={style}
>
{children}
</div>

View File

@ -24,10 +24,8 @@ class Editor extends React.Component {
constructor(props) {
super(props)
this.state = {
plugins: this.resolvePlugins(props),
state: props.state
}
this.state = {}
this.state.plugins = this.resolvePlugins(props)
}
componentWillReceiveProps(props) {
@ -36,6 +34,8 @@ class Editor extends React.Component {
}
onChange(state) {
if (state == this.props.state) return
for (const plugin of this.state.plugins) {
if (!plugin.onChange) continue
const newState = plugin.onChange(state, this)
@ -53,7 +53,7 @@ class Editor extends React.Component {
*/
getState() {
return this.state.state
return this.props.state
}
/**

View File

@ -4,10 +4,10 @@ import ReactDOM from 'react-dom'
import createOffsetKey from '../utils/create-offset-key'
/**
* LeafNode.
* Leaf.
*/
class LeafNode extends React.Component {
class Leaf extends React.Component {
static propTypes = {
node: React.PropTypes.object.isRequired,
@ -16,11 +16,11 @@ class LeafNode extends React.Component {
state: React.PropTypes.object.isRequired,
};
componentDidUpdate() {
componentDidMount() {
this.updateSelection()
}
componentDidMount() {
componentDidUpdate() {
this.updateSelection()
}
@ -133,4 +133,4 @@ class LeafNode extends React.Component {
* Export.
*/
export default LeafNode
export default Leaf

View File

@ -5,10 +5,10 @@ import convertCharactersToRanges from '../utils/convert-characters-to-ranges'
import createOffsetKey from '../utils/create-offset-key'
/**
* TextNode.
* Text.
*/
class TextNode extends React.Component {
class Text extends React.Component {
static propTypes = {
node: React.PropTypes.object.isRequired,
@ -53,4 +53,4 @@ class TextNode extends React.Component {
* Export.
*/
export default TextNode
export default Text

View File

@ -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`.
*

View File

@ -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`.
*
@ -51,6 +102,81 @@ class State extends StateRecord {
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.
*
@ -60,7 +186,11 @@ class State extends StateRecord {
delete(selection = this.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
if (selection.isAtEndOf(this)) return this
@ -68,11 +198,28 @@ class State extends StateRecord {
// otherwise, remove one character ahead of the cursor
let { startKey, startOffset } = selection
let { nodes } = this
let node = nodes.get(startKey)
let node = this.findNode(node => node.key == startKey)
let endOffset = startOffset + 1
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.
*
@ -116,15 +263,14 @@ class State extends StateRecord {
removeText(node, startOffset, endOffset) {
let { nodes } = this
let { text } = node
let { characters } = node
text = text.filter((char, i) => {
return i > startOffset && i < endOffset
characters = characters.filterNot((char, i) => {
return startOffset <= i && i < endOffset
})
node = node.set('text', text)
nodes = nodes.set(node.key, node)
let state = this.set('nodes', nodes)
node = node.set('characters', characters)
let state = this.setNode(node.key, node)
return state
}

View File

@ -22,12 +22,14 @@ const CORE_PLUGIN = {
switch (key) {
case 'enter': {
e.preventDefault()
return state.split()
}
case 'backspace': {
// COMPAT: Windows has a special "cut" behavior for the shift key.
if (IS_WINDOWS && e.shiftKey) return
e.preventDefault()
return isWord(e)
? state.backspaceWord()
: state.backspace()
@ -36,6 +38,7 @@ const CORE_PLUGIN = {
case 'delete': {
// COMPAT: Windows has a special "cut" behavior for the shift key.
if (IS_WINDOWS && e.shiftKey) return
e.preventDefault()
return isWord(e)
? state.deleteWord()
: state.delete()
@ -43,11 +46,13 @@ const CORE_PLUGIN = {
case 'y': {
if (!isCtrl(e) || !IS_WINDOWS) return
e.preventDefault()
return state.redo()
}
case 'z': {
if (!isCommand(e)) return
e.preventDefault()
return IS_MAC && e.shiftKey
? state.redo()
: state.undo()