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

got basic selection working

This commit is contained in:
Ian Storm Taylor
2016-06-15 14:47:52 -07:00
parent 567884c9f2
commit 64574c4f64
7 changed files with 640 additions and 321 deletions

File diff suppressed because one or more lines are too long

View File

@@ -125,7 +125,7 @@ class App extends React.Component {
renderMark={renderMark} renderMark={renderMark}
state={this.state.state} state={this.state.state}
onChange={(state) => { onChange={(state) => {
console.log('State:', state) console.log('Change:', state.toJS())
this.setState({ state }) this.setState({ state })
}} }}
/> />

View File

@@ -1,7 +1,9 @@
import React from 'react' import React from 'react'
import ReactDOM from 'react-dom'
import TextNode from './text-node' import TextNode from './text-node'
import TextNodeModel from '../models/text-node' import TextNodeModel from '../models/text-node'
import findSelection from '../utils/find-selection'
import keycode from 'keycode' import keycode from 'keycode'
/** /**
@@ -10,33 +12,92 @@ import keycode from 'keycode'
class Content extends React.Component { class Content extends React.Component {
/**
* Props.
*/
static propTypes = { static propTypes = {
onChange: React.PropTypes.func, onChange: React.PropTypes.func,
onKeyDown: React.PropTypes.func, onKeyDown: React.PropTypes.func,
onSelect: React.PropTypes.func,
renderMark: React.PropTypes.func.isRequired, renderMark: React.PropTypes.func.isRequired,
renderNode: React.PropTypes.func.isRequired, renderNode: React.PropTypes.func.isRequired,
state: React.PropTypes.object.isRequired, state: React.PropTypes.object.isRequired,
}; };
/**
* On change, bubble up.
*
* @param {State} state
*/
onChange(state) { onChange(state) {
this.props.onChange(state) this.props.onChange(state)
} }
onKeyDown(e) { /**
const key = keycode(e.which) * On key down, bubble up.
*
* @param {Event} e
*/
onKeyDown(e) {
// COMPAT: Certain keys should never be handled by the browser's mechanism, // COMPAT: Certain keys should never be handled by the browser's mechanism,
// because using the native contenteditable behavior introduces quirks. // because using the native contenteditable behavior introduces quirks.
if (key === 'escape' || key === 'return') { const key = keycode(e.which)
e.preventDefault() if (key === 'escape' || key === 'return') e.preventDefault()
}
this.props.onKeyDown(e) this.props.onKeyDown(e)
} }
/**
* On select, update the current state's selection.
*
* @param {Event} e
*/
onSelect(e) {
let { state } = this.props
let { selection } = state
const native = window.getSelection()
// no selection is active, so unset `hasFocus`
if (!native.rangeCount && selection.hasFocus) {
selection = selection.set('hasFocus', false)
state = state.set('selection', selection)
this.onChange(state)
return
}
const el = ReactDOM.findDOMNode(this)
const { anchorNode, anchorOffset, focusNode, focusOffset } = native
const anchorIsText = anchorNode.nodeType == 3
const focusIsText = focusNode.nodeType == 3
// if both text nodes, find their parent's and create the selection
if (anchorIsText && focusIsText) {
const anchor = findSelection(anchorNode, anchorOffset)
const focus = findSelection(focusNode, focusOffset)
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('hasFocus', true)
state = state.set('selection', selection)
this.onChange(state)
return
}
}
/**
* Render the editor content.
*
* @return {Component} component
*/
render() { render() {
const { state } = this.props const { state } = this.props
const { nodes, selection } = state const { nodes } = state
const children = nodes const children = nodes
.toArray() .toArray()
.map(node => this.renderNode(node)) .map(node => this.renderNode(node))
@@ -47,12 +108,20 @@ class Content extends React.Component {
suppressContentEditableWarning suppressContentEditableWarning
data-type='content' data-type='content'
onKeyDown={(e) => this.onKeyDown(e)} onKeyDown={(e) => this.onKeyDown(e)}
onSelect={(e) => this.onSelect(e)}
> >
{children} {children}
</div> </div>
) )
} }
/**
* Render a single `node`.
*
* @param {Node} node
* @return {Component} component
*/
renderNode(node) { renderNode(node) {
const { renderMark, renderNode } = this.props const { renderMark, renderNode } = this.props
@@ -87,3 +156,4 @@ class Content extends React.Component {
*/ */
export default Content export default Content

View File

@@ -30,7 +30,7 @@ class Editor extends React.Component {
} }
} }
componentWillUpdate(props) { componentWillReceiveProps(props) {
const plugins = this.resolvePlugins(props) const plugins = this.resolvePlugins(props)
this.setState({ plugins }) this.setState({ plugins })
} }

View File

@@ -20,7 +20,7 @@ class TextNode extends React.Component {
const ranges = characters const ranges = characters
.toArray() .toArray()
.reduce((ranges, char, i) => { .reduce((ranges, char, i) => {
const previous = characters[i - 1] const previous = i == 0 ? null : characters.get(i - 1)
const { text } = char const { text } = char
const marks = char.marks.toArray().map(mark => mark.type) const marks = char.marks.toArray().map(mark => mark.type)
@@ -49,16 +49,25 @@ class TextNode extends React.Component {
}, {}) }, {})
return ( return (
<LeafNode <span
key={key} key={key}
styles={styles} data-key={key}
text={range.text} data-type='leaf'
/> style={styles}
>
{range.text}
</span>
) )
}) })
return ( return (
<span key={node.key} data-type='text'>{leaves}</span> <span
key={node.key}
data-key={node.key}
data-type='text'
>
{leaves}
</span>
) )
} }
@@ -109,21 +118,21 @@ class TextNode extends React.Component {
} }
/** // /**
* Find matching `marks` at `index`. // * Find matching `marks` at `index`.
* // *
* @param {Array} marks // * @param {Array} marks
* @param {Number} index // * @param {Number} index
* @return {Array} marks // * @return {Array} marks
*/ // */
function findMarks(marks, index) { // function findMarks(marks, index) {
return marks // return marks
.filter(mark => mark.start < index) // .filter(mark => mark.start < index)
.filter(mark => mark.end + 1 > index) // .filter(mark => mark.end + 1 > index)
.map(mark => mark.type) // .map(mark => mark.type)
.sort() // .sort()
} // }
/** /**
* Export. * Export.

View File

@@ -0,0 +1,24 @@
/**
* Find the nearest parent of a `node` and return their offset key.
*
* @param {Node} node
* @return {String} key
*/
export default function findOffsetKey(node) {
let match = node
while (match && match != document.documentElement) {
if (
match instanceof Element &&
match.hasAttribute('data-key')
) {
return match.getAttribute('data-key')
}
match = match.parentNode
}
return null
}

View File

@@ -0,0 +1,35 @@
import findOffsetKey from './find-offset-key'
/**
* Offset key splitter.
*/
const SPLITTER = /^(\d+)(?:\.(\d+)-(\d+))?$/
/**
* Find the selection anchor properties from a `node`.
*
* @param {Node} node
* @param {Number} nodeOffset
* @return {Object} anchor
* @property {String} anchorKey
* @property {Number} anchorOffset
*/
export default function findSelection(node, nodeOffset) {
const offsetKey = findOffsetKey(node)
if (!offsetKey) return null
const matches = SPLITTER.exec(offsetKey)
if (!matches) throw new Error(`Unknown offset key "${offsetKey}".`)
let [ all, key, offsetStart, offsetEnd ] = matches
offsetStart = parseInt(offsetStart, 10)
offsetEnd = parseInt(offsetEnd, 10)
return {
key: key,
offset: offsetStart + nodeOffset
}
}