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:
parent
8636242931
commit
de6aeb9dfe
@ -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 })
|
||||
}}
|
||||
/>
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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`.
|
||||
*
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user