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:
File diff suppressed because one or more lines are too long
@@ -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 })
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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 })
|
||||||
}
|
}
|
||||||
|
@@ -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.
|
||||||
|
24
lib/utils/find-offset-key.js
Normal file
24
lib/utils/find-offset-key.js
Normal 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
|
||||||
|
}
|
35
lib/utils/find-selection.js
Normal file
35
lib/utils/find-selection.js
Normal 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
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user