mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-30 18:39:51 +02:00
fix firefox support
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
|
|
||||||
import { Editor, Mark, Raw } from '../..'
|
import { Editor, Mark, Raw, Utils } from '../..'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import initialState from './state.json'
|
import initialState from './state.json'
|
||||||
|
import keycode from 'keycode'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Node renderers.
|
* Node renderers.
|
||||||
@@ -55,42 +56,6 @@ class RichText extends React.Component {
|
|||||||
state: Raw.deserialize(initialState)
|
state: Raw.deserialize(initialState)
|
||||||
};
|
};
|
||||||
|
|
||||||
hasMark = (type) => {
|
|
||||||
const { state } = this.state
|
|
||||||
return state.marks.some(mark => mark.type == type)
|
|
||||||
}
|
|
||||||
|
|
||||||
hasBlock = (type) => {
|
|
||||||
const { state } = this.state
|
|
||||||
return state.blocks.some(node => node.type == type)
|
|
||||||
}
|
|
||||||
|
|
||||||
onClickMark = (e, type) => {
|
|
||||||
e.preventDefault()
|
|
||||||
const isActive = this.hasMark(type)
|
|
||||||
let { state } = this.state
|
|
||||||
|
|
||||||
state = state
|
|
||||||
.transform()
|
|
||||||
[isActive ? 'unmark' : 'mark'](type)
|
|
||||||
.apply()
|
|
||||||
|
|
||||||
this.setState({ state })
|
|
||||||
}
|
|
||||||
|
|
||||||
onClickBlock = (e, type) => {
|
|
||||||
e.preventDefault()
|
|
||||||
const isActive = this.hasBlock(type)
|
|
||||||
let { state } = this.state
|
|
||||||
|
|
||||||
state = state
|
|
||||||
.transform()
|
|
||||||
.setBlock(isActive ? 'paragraph' : type)
|
|
||||||
.apply()
|
|
||||||
|
|
||||||
this.setState({ state })
|
|
||||||
}
|
|
||||||
|
|
||||||
render = () => {
|
render = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -146,6 +111,7 @@ class RichText extends React.Component {
|
|||||||
renderNode={this.renderNode}
|
renderNode={this.renderNode}
|
||||||
renderMark={this.renderMark}
|
renderMark={this.renderMark}
|
||||||
onChange={this.onChange}
|
onChange={this.onChange}
|
||||||
|
onKeyDown={this.onKeyDown}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -159,6 +125,16 @@ class RichText extends React.Component {
|
|||||||
return MARKS[mark.type]
|
return MARKS[mark.type]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasMark = (type) => {
|
||||||
|
const { state } = this.state
|
||||||
|
return state.marks.some(mark => mark.type == type)
|
||||||
|
}
|
||||||
|
|
||||||
|
hasBlock = (type) => {
|
||||||
|
const { state } = this.state
|
||||||
|
return state.blocks.some(node => node.type == type)
|
||||||
|
}
|
||||||
|
|
||||||
onChange = (state) => {
|
onChange = (state) => {
|
||||||
console.groupCollapsed('Change!')
|
console.groupCollapsed('Change!')
|
||||||
console.log('Document:', state.document.toJS())
|
console.log('Document:', state.document.toJS())
|
||||||
@@ -168,6 +144,63 @@ class RichText extends React.Component {
|
|||||||
this.setState({ state })
|
this.setState({ state })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onKeyDown = (e, state) => {
|
||||||
|
if (!Utils.Key.isCommand(e)) return
|
||||||
|
const key = keycode(e.which)
|
||||||
|
let mark
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case 'b':
|
||||||
|
mark = 'bold'
|
||||||
|
break
|
||||||
|
case 'i':
|
||||||
|
mark = 'italic'
|
||||||
|
break
|
||||||
|
case 'u':
|
||||||
|
mark = 'underlined'
|
||||||
|
break
|
||||||
|
case '`':
|
||||||
|
mark = 'code'
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state = state
|
||||||
|
.transform()
|
||||||
|
[this.hasMark(mark) ? 'unmark' : 'mark'](mark)
|
||||||
|
.apply()
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
onClickMark = (e, type) => {
|
||||||
|
e.preventDefault()
|
||||||
|
const isActive = this.hasMark(type)
|
||||||
|
let { state } = this.state
|
||||||
|
|
||||||
|
state = state
|
||||||
|
.transform()
|
||||||
|
[isActive ? 'unmark' : 'mark'](type)
|
||||||
|
.apply()
|
||||||
|
|
||||||
|
this.setState({ state })
|
||||||
|
}
|
||||||
|
|
||||||
|
onClickBlock = (e, type) => {
|
||||||
|
e.preventDefault()
|
||||||
|
const isActive = this.hasBlock(type)
|
||||||
|
let { state } = this.state
|
||||||
|
|
||||||
|
state = state
|
||||||
|
.transform()
|
||||||
|
.setBlock(isActive ? 'paragraph' : type)
|
||||||
|
.apply()
|
||||||
|
|
||||||
|
this.setState({ state })
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -65,7 +65,7 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},{
|
},{
|
||||||
"text": ", or add a semanticlly rendered block quote in the middle of the page, like this:"
|
"text": ", or add a semantically rendered block quote in the middle of the page, like this:"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@@ -1,11 +1,18 @@
|
|||||||
|
|
||||||
|
import Key from '../utils/key'
|
||||||
import OffsetKey from '../utils/offset-key'
|
import OffsetKey from '../utils/offset-key'
|
||||||
|
import Raw from '../serializers/raw'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import Text from './text'
|
import Text from './text'
|
||||||
import Void from './void'
|
import Void from './void'
|
||||||
import keycode from 'keycode'
|
import keycode from 'keycode'
|
||||||
import { Raw } from '..'
|
import { IS_FIREFOX } from '../utils/environment'
|
||||||
import { isCommand, isWindowsCommand } from '../utils/event'
|
|
||||||
|
/**
|
||||||
|
* Noop.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function noop() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Content.
|
* Content.
|
||||||
@@ -63,20 +70,10 @@ class Content extends React.Component {
|
|||||||
* @param {Object} props
|
* @param {Object} props
|
||||||
*/
|
*/
|
||||||
|
|
||||||
componentWillMount = () => {
|
|
||||||
this.tmp.isRendering = true
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUpdate = (props, state) => {
|
componentWillUpdate = (props, state) => {
|
||||||
this.tmp.isRendering = true
|
this.tmp.isRendering = true
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount = () => {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.tmp.isRendering = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate = (props, state) => {
|
componentDidUpdate = (props, state) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.tmp.isRendering = false
|
this.tmp.isRendering = false
|
||||||
@@ -214,10 +211,10 @@ class Content extends React.Component {
|
|||||||
(key == 'enter') ||
|
(key == 'enter') ||
|
||||||
(key == 'backspace') ||
|
(key == 'backspace') ||
|
||||||
(key == 'delete') ||
|
(key == 'delete') ||
|
||||||
(key == 'b' && isCommand(e)) ||
|
(key == 'b' && Key.isCommand(e)) ||
|
||||||
(key == 'i' && isCommand(e)) ||
|
(key == 'i' && Key.isCommand(e)) ||
|
||||||
(key == 'y' && isWindowsCommand(e)) ||
|
(key == 'y' && Key.isWindowsCommand(e)) ||
|
||||||
(key == 'z' && isCommand(e))
|
(key == 'z' && Key.isCommand(e))
|
||||||
) {
|
) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
@@ -234,7 +231,7 @@ class Content extends React.Component {
|
|||||||
onPaste = (e) => {
|
onPaste = (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const data = e.clipboardData
|
const data = e.clipboardData
|
||||||
const { types } = data
|
const types = Array.from(data.types)
|
||||||
const paste = {}
|
const paste = {}
|
||||||
|
|
||||||
// Handle files.
|
// Handle files.
|
||||||
@@ -308,13 +305,13 @@ class Content extends React.Component {
|
|||||||
|
|
||||||
state = state
|
state = state
|
||||||
.transform()
|
.transform()
|
||||||
|
.focus()
|
||||||
.moveTo({
|
.moveTo({
|
||||||
anchorKey: anchor.key,
|
anchorKey: anchor.key,
|
||||||
anchorOffset: anchor.offset,
|
anchorOffset: anchor.offset,
|
||||||
focusKey: focus.key,
|
focusKey: focus.key,
|
||||||
focusOffset: focus.offset
|
focusOffset: focus.offset
|
||||||
})
|
})
|
||||||
.focus()
|
|
||||||
.apply({ isNative: true })
|
.apply({ isNative: true })
|
||||||
|
|
||||||
this.onChange(state)
|
this.onChange(state)
|
||||||
@@ -350,6 +347,7 @@ class Content extends React.Component {
|
|||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
onPaste={this.onPaste}
|
onPaste={this.onPaste}
|
||||||
onSelect={this.onSelect}
|
onSelect={this.onSelect}
|
||||||
|
onKeyUp={noop}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
@@ -27,17 +27,22 @@ class Leaf extends React.Component {
|
|||||||
* Should component update?
|
* Should component update?
|
||||||
*
|
*
|
||||||
* @param {Object} props
|
* @param {Object} props
|
||||||
* @param {Object} state
|
|
||||||
* @return {Boolean} shouldUpdate
|
* @return {Boolean} shouldUpdate
|
||||||
*/
|
*/
|
||||||
|
|
||||||
shouldComponentUpdate(props, state) {
|
shouldComponentUpdate(props) {
|
||||||
return (
|
const { start, end, node, state } = props
|
||||||
|
const { selection } = state
|
||||||
|
|
||||||
|
const should = (
|
||||||
|
selection.hasEdgeBetween(node, start, end) ||
|
||||||
props.start != this.props.start ||
|
props.start != this.props.start ||
|
||||||
props.end != this.props.end ||
|
props.end != this.props.end ||
|
||||||
props.text != this.props.text ||
|
props.text != this.props.text ||
|
||||||
props.marks != this.props.marks
|
props.marks != this.props.marks
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return should
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@@ -55,19 +60,19 @@ class Leaf extends React.Component {
|
|||||||
// If the selection is not focused we have nothing to do.
|
// If the selection is not focused we have nothing to do.
|
||||||
if (!selection.isFocused) return
|
if (!selection.isFocused) return
|
||||||
|
|
||||||
const { anchorKey, anchorOffset, focusKey, focusOffset } = selection
|
const { anchorOffset, focusOffset } = selection
|
||||||
const { node, start, end } = this.props
|
const { node, start, end } = this.props
|
||||||
const { key } = node
|
|
||||||
|
|
||||||
// If neither matches, the selection doesn't start or end here, so exit.
|
// If neither matches, the selection doesn't start or end here, so exit.
|
||||||
const hasStart = key == anchorKey && start <= anchorOffset && anchorOffset <= end
|
const hasStart = selection.hasStartBetween(node, start, end)
|
||||||
const hasEnd = key == focusKey && start <= focusOffset && focusOffset <= end
|
const hasEnd = selection.hasEndBetween(node, start, end)
|
||||||
if (!hasStart && !hasEnd) return
|
if (!hasStart && !hasEnd) return
|
||||||
|
|
||||||
// We have a selection to render, so prepare a few things...
|
// We have a selection to render, so prepare a few things...
|
||||||
const native = window.getSelection()
|
const native = window.getSelection()
|
||||||
const el = ReactDOM.findDOMNode(this).firstChild
|
const el = ReactDOM.findDOMNode(this).firstChild
|
||||||
|
|
||||||
|
|
||||||
// If both the start and end are here, set the selection all at once.
|
// If both the start and end are here, set the selection all at once.
|
||||||
if (hasStart && hasEnd) {
|
if (hasStart && hasEnd) {
|
||||||
native.removeAllRanges()
|
native.removeAllRanges()
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
|
|
||||||
import Leaf from './leaf'
|
import Leaf from './leaf'
|
||||||
import OffsetKey from '../utils/offset-key'
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import groupByMarks from '../utils/group-by-marks'
|
import groupByMarks from '../utils/group-by-marks'
|
||||||
import { List } from 'immutable'
|
import { List } from 'immutable'
|
||||||
@@ -32,6 +31,7 @@ class Text extends React.Component {
|
|||||||
|
|
||||||
shouldComponentUpdate(props, state) {
|
shouldComponentUpdate(props, state) {
|
||||||
return (
|
return (
|
||||||
|
props.state.selection.hasEdgeIn(props.node) ||
|
||||||
props.node.decorations != this.props.node.decorations ||
|
props.node.decorations != this.props.node.decorations ||
|
||||||
props.node.characters != this.props.node.characters
|
props.node.characters != this.props.node.characters
|
||||||
)
|
)
|
||||||
@@ -67,7 +67,7 @@ class Text extends React.Component {
|
|||||||
const offset = previous.size
|
const offset = previous.size
|
||||||
? previous.map(r => r.text).join('').length
|
? previous.map(r => r.text).join('').length
|
||||||
: 0
|
: 0
|
||||||
return this.renderLeaf(range, offset)
|
return this.renderLeaf(range, i, offset)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,25 +75,21 @@ class Text extends React.Component {
|
|||||||
* Render a single leaf node given a `range` and `offset`.
|
* Render a single leaf node given a `range` and `offset`.
|
||||||
*
|
*
|
||||||
* @param {Object} range
|
* @param {Object} range
|
||||||
|
* @param {Number} index
|
||||||
* @param {Number} offset
|
* @param {Number} offset
|
||||||
* @return {Element} leaf
|
* @return {Element} leaf
|
||||||
*/
|
*/
|
||||||
|
|
||||||
renderLeaf(range, offset) {
|
renderLeaf(range, index, offset) {
|
||||||
const { node, renderMark, state } = this.props
|
const { node, renderMark, state } = this.props
|
||||||
const text = range.text
|
const text = range.text
|
||||||
const marks = range.marks
|
const marks = range.marks
|
||||||
const start = offset
|
const start = offset
|
||||||
const end = offset + text.length
|
const end = offset + text.length
|
||||||
const offsetKey = OffsetKey.stringify({
|
|
||||||
key: node.key,
|
|
||||||
start,
|
|
||||||
end
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Leaf
|
<Leaf
|
||||||
key={offsetKey}
|
key={`${node.key}-${index}`}
|
||||||
state={state}
|
state={state}
|
||||||
node={node}
|
node={node}
|
||||||
start={start}
|
start={start}
|
||||||
|
68
lib/index.js
68
lib/index.js
@@ -3,25 +3,69 @@
|
|||||||
* Components.
|
* Components.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { default as Editor } from './components/editor'
|
import Editor from './components/editor'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Models.
|
* Models.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { default as Block } from './models/block'
|
import Block from './models/block'
|
||||||
export { default as Character } from './models/character'
|
import Character from './models/character'
|
||||||
export { default as Data } from './models/data'
|
import Data from './models/data'
|
||||||
export { default as Document } from './models/document'
|
import Document from './models/document'
|
||||||
export { default as Inline } from './models/inline'
|
import Inline from './models/inline'
|
||||||
export { default as Mark } from './models/mark'
|
import Mark from './models/mark'
|
||||||
export { default as Selection } from './models/selection'
|
import Selection from './models/selection'
|
||||||
export { default as State } from './models/state'
|
import State from './models/state'
|
||||||
export { default as Text } from './models/text'
|
import Text from './models/text'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serializers.
|
* Serializers.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { default as Html } from './serializers/html'
|
import Html from './serializers/html'
|
||||||
export { default as Raw } from './serializers/raw'
|
import Raw from './serializers/raw'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utils.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Key from './utils/key'
|
||||||
|
|
||||||
|
const Utils = { Key }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export {
|
||||||
|
Block,
|
||||||
|
Character,
|
||||||
|
Data,
|
||||||
|
Document,
|
||||||
|
Editor,
|
||||||
|
Html,
|
||||||
|
Inline,
|
||||||
|
Mark,
|
||||||
|
Raw,
|
||||||
|
Selection,
|
||||||
|
State,
|
||||||
|
Text,
|
||||||
|
Utils
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
Block,
|
||||||
|
Character,
|
||||||
|
Data,
|
||||||
|
Document,
|
||||||
|
Editor,
|
||||||
|
Html,
|
||||||
|
Inline,
|
||||||
|
Mark,
|
||||||
|
Raw,
|
||||||
|
Selection,
|
||||||
|
State,
|
||||||
|
Text,
|
||||||
|
Utils
|
||||||
|
}
|
||||||
|
@@ -2,7 +2,26 @@
|
|||||||
import { Record } from 'immutable'
|
import { Record } from 'immutable'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Record.
|
* Start-and-end convenience methods to auto-generate.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const START_END_METHODS = [
|
||||||
|
'moveTo%'
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start-end-and-edge convenience methods to auto-generate.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const EDGE_METHODS = [
|
||||||
|
'has%AtStartOf',
|
||||||
|
'has%AtEndOf',
|
||||||
|
'has%Between',
|
||||||
|
'has%In'
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default properties.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const DEFAULTS = {
|
const DEFAULTS = {
|
||||||
@@ -115,6 +134,114 @@ class Selection extends new Record(DEFAULTS) {
|
|||||||
: this.focusOffset
|
: this.focusOffset
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether anchor point of the selection is at the start of a `node`.
|
||||||
|
*
|
||||||
|
* @param {Node} node
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
|
||||||
|
hasAnchorAtStartOf(node) {
|
||||||
|
const first = node.kind == 'text' ? node : node.getTextNodes().first()
|
||||||
|
return this.anchorKey == first.key && this.anchorOffset == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether anchor point of the selection is at the end of a `node`.
|
||||||
|
*
|
||||||
|
* @param {Node} node
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
|
||||||
|
hasAnchorAtEndOf(node) {
|
||||||
|
const last = node.kind == 'text' ? node : node.getTextNodes().last()
|
||||||
|
return this.anchorKey == last.key && this.anchorOffset == last.length
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the anchor edge of a selection is in a `node` and at an
|
||||||
|
* offset between `start` and `end`.
|
||||||
|
*
|
||||||
|
* @param {Node} node
|
||||||
|
* @param {Number} start
|
||||||
|
* @param {Number} end
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
|
||||||
|
hasAnchorBetween(node, start, end) {
|
||||||
|
return (
|
||||||
|
this.hasAnchorIn(node) &&
|
||||||
|
start <= this.anchorOffset &&
|
||||||
|
this.anchorOffset <= end
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the anchor edge of a selection is in a `node`.
|
||||||
|
*
|
||||||
|
* @param {Node} node
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
|
||||||
|
hasAnchorIn(node) {
|
||||||
|
const nodes = node.kind == 'text' ? [node] : node.getTextNodes()
|
||||||
|
return nodes.some(n => n.key == this.anchorKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether focus point of the selection is at the end of a `node`.
|
||||||
|
*
|
||||||
|
* @param {Node} node
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
|
||||||
|
hasFocusAtEndOf(node) {
|
||||||
|
const last = node.kind == 'text' ? node : node.getTextNodes().last()
|
||||||
|
return this.focusKey == last.key && this.focusOffset == last.length
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether focus point of the selection is at the start of a `node`.
|
||||||
|
*
|
||||||
|
* @param {Node} node
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
|
||||||
|
hasFocusAtStartOf(node) {
|
||||||
|
const first = node.kind == 'text' ? node : node.getTextNodes().first()
|
||||||
|
return this.focusKey == first.key && this.focusOffset == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the focus edge of a selection is in a `node` and at an
|
||||||
|
* offset between `start` and `end`.
|
||||||
|
*
|
||||||
|
* @param {Node} node
|
||||||
|
* @param {Number} start
|
||||||
|
* @param {Number} end
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
|
||||||
|
hasFocusBetween(node, start, end) {
|
||||||
|
return (
|
||||||
|
this.hasFocusIn(node) &&
|
||||||
|
start <= this.focusOffset &&
|
||||||
|
this.focusOffset <= end
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the focus edge of a selection is in a `node`.
|
||||||
|
*
|
||||||
|
* @param {Node} node
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
|
||||||
|
hasFocusIn(node) {
|
||||||
|
const nodes = node.kind == 'text' ? [node] : node.getTextNodes()
|
||||||
|
return nodes.some(n => n.key == this.focusKey)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether the selection is at the start of a `node`.
|
* Check whether the selection is at the start of a `node`.
|
||||||
*
|
*
|
||||||
@@ -141,38 +268,6 @@ class Selection extends new Record(DEFAULTS) {
|
|||||||
return this.isCollapsed && endKey == last.key && endOffset == last.length
|
return this.isCollapsed && endKey == last.key && endOffset == last.length
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check whether the selection has an edge at the start of a `node`.
|
|
||||||
*
|
|
||||||
* @param {Node} node
|
|
||||||
* @return {Boolean} hasEdgeAtStart
|
|
||||||
*/
|
|
||||||
|
|
||||||
hasEdgeAtStartOf(node) {
|
|
||||||
const { startKey, startOffset, endKey, endOffset } = this
|
|
||||||
const first = node.kind == 'text' ? node : node.getTextNodes().first()
|
|
||||||
return (
|
|
||||||
(startKey == first.key && startOffset == 0) ||
|
|
||||||
(endKey == first.key && endOffset == 0)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check whether the selection has an edge at the end of a `node`.
|
|
||||||
*
|
|
||||||
* @param {Node} node
|
|
||||||
* @return {Boolean} hasEdgeAtEnd
|
|
||||||
*/
|
|
||||||
|
|
||||||
hasEdgeAtEndOf(node) {
|
|
||||||
const { startKey, startOffset, endKey, endOffset } = this
|
|
||||||
const last = node.kind == 'text' ? node : node.getTextNodes().last()
|
|
||||||
return (
|
|
||||||
(startKey == last.key && startOffset == last.length) ||
|
|
||||||
(endKey == last.key && endOffset == last.length)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalize the selection, relative to a `node`, ensuring that the anchor
|
* Normalize the selection, relative to a `node`, ensuring that the anchor
|
||||||
* and focus nodes of the selection always refer to leaf text nodes.
|
* and focus nodes of the selection always refer to leaf text nodes.
|
||||||
@@ -221,36 +316,6 @@ class Selection extends new Record(DEFAULTS) {
|
|||||||
: anchorIndex > focusIndex
|
: anchorIndex > focusIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the selection is expanded and has an edge on a void block, move it.
|
|
||||||
// if (isExpanded) {
|
|
||||||
// let anchorBlock = node.getClosestBlock(anchorNode)
|
|
||||||
// let focusBlock = node.getClosestBlock(focusNode)
|
|
||||||
|
|
||||||
// if (anchorBlock.isVoid) {
|
|
||||||
// while (anchorBanchorBlock.isVoid) {
|
|
||||||
// anchorBlock = isBackward
|
|
||||||
// ? node.getPreviousBlock(anchorBlock)
|
|
||||||
// : node.getNextBlock(anchorBlock)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// anchorNode = isBackward
|
|
||||||
// ? anchorBlock.getTextNodes().last()
|
|
||||||
// : anchorBlock.getTextNodes().first()
|
|
||||||
// anchorOffset = isBackward
|
|
||||||
// ? anchorNode.length
|
|
||||||
// : 0
|
|
||||||
// }
|
|
||||||
|
|
||||||
// else if (focusBlock.isVoid) {
|
|
||||||
// focusNode = isBackward
|
|
||||||
// ? node.getNextBlock(focusBlock).getTextNodes().first()
|
|
||||||
// : node.getPreviousBlock(focusBlock).getTextNodes().last()
|
|
||||||
// focusOffset = isBackward
|
|
||||||
// ? 0
|
|
||||||
// : focusNode.length
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Merge in any updated properties.
|
// Merge in any updated properties.
|
||||||
return selection.merge({
|
return selection.merge({
|
||||||
anchorKey: anchorNode.key,
|
anchorKey: anchorNode.key,
|
||||||
@@ -285,17 +350,6 @@ class Selection extends new Record(DEFAULTS) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Move the selection to a specific anchor and focus point.
|
|
||||||
*
|
|
||||||
* @param {Object} properties
|
|
||||||
* @return {Selection} selection
|
|
||||||
*/
|
|
||||||
|
|
||||||
moveTo(properties) {
|
|
||||||
return this.merge(properties)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move the focus point to the anchor point.
|
* Move the focus point to the anchor point.
|
||||||
*
|
*
|
||||||
@@ -324,46 +378,6 @@ class Selection extends new Record(DEFAULTS) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Move the end point to the start point.
|
|
||||||
*
|
|
||||||
* @return {Selection} selection
|
|
||||||
*/
|
|
||||||
|
|
||||||
moveToStart() {
|
|
||||||
return this.isBackward
|
|
||||||
? this.merge({
|
|
||||||
anchorKey: this.focusKey,
|
|
||||||
anchorOffset: this.focusOffset,
|
|
||||||
isBackward: false
|
|
||||||
})
|
|
||||||
: this.merge({
|
|
||||||
focusKey: this.anchorKey,
|
|
||||||
focusOffset: this.anchorOffset,
|
|
||||||
isBackward: false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Move the start point to the end point.
|
|
||||||
*
|
|
||||||
* @return {Selection} selection
|
|
||||||
*/
|
|
||||||
|
|
||||||
moveToEnd() {
|
|
||||||
return this.isBackward
|
|
||||||
? this.merge({
|
|
||||||
focusKey: this.anchorKey,
|
|
||||||
focusOffset: this.anchorOffset,
|
|
||||||
isBackward: false
|
|
||||||
})
|
|
||||||
: this.merge({
|
|
||||||
anchorKey: this.focusKey,
|
|
||||||
anchorOffset: this.focusOffset,
|
|
||||||
isBackward: false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move to the start of a `node`.
|
* Move to the start of a `node`.
|
||||||
*
|
*
|
||||||
@@ -502,6 +516,41 @@ class Selection extends new Record(DEFAULTS) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add start, end and edge convenience methods.
|
||||||
|
*/
|
||||||
|
|
||||||
|
START_END_METHODS.concat(EDGE_METHODS).forEach((pattern) => {
|
||||||
|
const [ p, s ] = pattern.split('%')
|
||||||
|
const anchor = `${p}Anchor${s}`
|
||||||
|
const edge = `${p}Edge${s}`
|
||||||
|
const end = `${p}End${s}`
|
||||||
|
const focus = `${p}Focus${s}`
|
||||||
|
const start = `${p}Start${s}`
|
||||||
|
|
||||||
|
Selection.prototype[start] = function (...args) {
|
||||||
|
return this.isBackward
|
||||||
|
? this[focus](...args)
|
||||||
|
: this[anchor](...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
Selection.prototype[end] = function (...args) {
|
||||||
|
return this.isBackward
|
||||||
|
? this[anchor](...args)
|
||||||
|
: this[focus](...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!EDGE_METHODS.includes(pattern)) return
|
||||||
|
|
||||||
|
Selection.prototype[edge] = function (...args) {
|
||||||
|
return this[anchor](...args) || this[focus](...args)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add edge methods.
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export.
|
* Export.
|
||||||
*/
|
*/
|
||||||
|
@@ -519,6 +519,31 @@ class State extends new Record(DEFAULTS) {
|
|||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the selection to a specific anchor and focus point.
|
||||||
|
*
|
||||||
|
* @param {Object} properties
|
||||||
|
* @return {State} state
|
||||||
|
*/
|
||||||
|
|
||||||
|
moveTo(properties) {
|
||||||
|
let state = this
|
||||||
|
let { document, selection } = state
|
||||||
|
|
||||||
|
// Pass in properties, and force `isBackward` to be re-resolved.
|
||||||
|
selection = selection.merge({
|
||||||
|
anchorKey: properties.anchorKey,
|
||||||
|
anchorOffset: properties.anchorOffset,
|
||||||
|
focusKey: properties.focusKey,
|
||||||
|
focusOffset: properties.focusOffset,
|
||||||
|
isBackward: null
|
||||||
|
})
|
||||||
|
|
||||||
|
selection = selection.normalize(document)
|
||||||
|
state = state.merge({ selection })
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move the selection to the start of the previous block.
|
* Move the selection to the start of the previous block.
|
||||||
*
|
*
|
||||||
|
@@ -57,7 +57,6 @@ const SELECTION_TRANSFORMS = [
|
|||||||
'focus',
|
'focus',
|
||||||
'moveBackward',
|
'moveBackward',
|
||||||
'moveForward',
|
'moveForward',
|
||||||
'moveTo',
|
|
||||||
'moveToAnchor',
|
'moveToAnchor',
|
||||||
'moveToEnd',
|
'moveToEnd',
|
||||||
'moveToEndOf',
|
'moveToEndOf',
|
||||||
@@ -78,6 +77,7 @@ const STATE_TRANSFORMS = [
|
|||||||
'insertFragment',
|
'insertFragment',
|
||||||
'insertText',
|
'insertText',
|
||||||
'mark',
|
'mark',
|
||||||
|
'moveTo',
|
||||||
'moveToStartOfPreviousBlock',
|
'moveToStartOfPreviousBlock',
|
||||||
'moveToEndOfPreviousBlock',
|
'moveToEndOfPreviousBlock',
|
||||||
'moveToStartOfNextBlock',
|
'moveToStartOfNextBlock',
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
|
import Key from '../utils/key'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import keycode from 'keycode'
|
import keycode from 'keycode'
|
||||||
import { isCommand, isCtrl, isWindowsCommand, isWord } from '../utils/event'
|
|
||||||
import { IS_WINDOWS, IS_MAC } from '../utils/environment'
|
import { IS_WINDOWS, IS_MAC } from '../utils/environment'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -93,13 +93,13 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'backspace': {
|
case 'backspace': {
|
||||||
return isWord(e)
|
return Key.isWord(e)
|
||||||
? transform.backspaceWord().apply()
|
? transform.backspaceWord().apply()
|
||||||
: transform.deleteBackward().apply()
|
: transform.deleteBackward().apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'delete': {
|
case 'delete': {
|
||||||
return isWord(e)
|
return Key.isWord(e)
|
||||||
? transform.deleteWord().apply()
|
? transform.deleteWord().apply()
|
||||||
: transform.deleteForward().apply()
|
: transform.deleteForward().apply()
|
||||||
}
|
}
|
||||||
@@ -137,13 +137,13 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'y': {
|
case 'y': {
|
||||||
if (!isWindowsCommand(e)) return
|
if (!Key.isWindowsCommand(e)) return
|
||||||
return transform.redo()
|
return transform.redo()
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'z': {
|
case 'z': {
|
||||||
if (!isCommand(e)) return
|
if (!Key.isCommand(e)) return
|
||||||
return IS_MAC && e.shiftKey
|
return IS_MAC && Key.isShift(e)
|
||||||
? transform.redo()
|
? transform.redo()
|
||||||
: transform.undo()
|
: transform.undo()
|
||||||
}
|
}
|
||||||
|
@@ -16,3 +16,16 @@ export const IS_MAC = process.browser && new Parser().getOS().name == 'Mac OS'
|
|||||||
export const IS_SAFARI = process.browser && browser.name == 'safari'
|
export const IS_SAFARI = process.browser && browser.name == 'safari'
|
||||||
export const IS_UBUNTU = process.browser && new Parser().getOS().name == 'Ubuntu'
|
export const IS_UBUNTU = process.browser && new Parser().getOS().name == 'Ubuntu'
|
||||||
export const IS_WINDOWS = process.browser && new Parser().getOS().name.includes('Windows')
|
export const IS_WINDOWS = process.browser && new Parser().getOS().name.includes('Windows')
|
||||||
|
|
||||||
|
export default {
|
||||||
|
IS_ANDROID,
|
||||||
|
IS_CHROME,
|
||||||
|
IS_EDGE,
|
||||||
|
IS_FIREFOX,
|
||||||
|
IS_IE,
|
||||||
|
IS_IOS,
|
||||||
|
IS_MAC,
|
||||||
|
IS_SAFARI,
|
||||||
|
IS_UBUNTU,
|
||||||
|
IS_WINDOWS
|
||||||
|
}
|
||||||
|
@@ -2,16 +2,27 @@
|
|||||||
import { IS_MAC, IS_WINDOWS } from './environment'
|
import { IS_MAC, IS_WINDOWS } from './environment'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Does an `e` have the word-level modifier?
|
* Does an `e` have the alt modifier?
|
||||||
*
|
*
|
||||||
* @param {Event} e
|
* @param {Event} e
|
||||||
* @return {Boolean}
|
* @return {Boolean}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function isWord(e) {
|
function isAlt(e) {
|
||||||
|
return e.altKey
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does an `e` have the command modifier?
|
||||||
|
*
|
||||||
|
* @param {Event} e
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
|
||||||
|
function isCommand(e) {
|
||||||
return IS_MAC
|
return IS_MAC
|
||||||
? e.altKey
|
? e.metaKey && !e.altKey
|
||||||
: e.ctrlKey
|
: e.ctrlKey && !e.altKey
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -21,10 +32,21 @@ export function isWord(e) {
|
|||||||
* @return {Boolean}
|
* @return {Boolean}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function isCtrl(e) {
|
function isCtrl(e) {
|
||||||
return e.ctrlKey && !e.altKey
|
return e.ctrlKey && !e.altKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does an `e` have the Mac command modifier?
|
||||||
|
*
|
||||||
|
* @param {Event} e
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
|
||||||
|
function isMacCommand(e) {
|
||||||
|
return IS_MAC && isCommand(e)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Does an `e` have the option modifier?
|
* Does an `e` have the option modifier?
|
||||||
*
|
*
|
||||||
@@ -32,7 +54,7 @@ export function isCtrl(e) {
|
|||||||
* @return {Boolean}
|
* @return {Boolean}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function isOption(e) {
|
function isOption(e) {
|
||||||
return IS_MAC && e.altKey
|
return IS_MAC && e.altKey
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,34 +65,10 @@ export function isOption(e) {
|
|||||||
* @return {Boolean}
|
* @return {Boolean}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function isShift(e) {
|
function isShift(e) {
|
||||||
return e.shiftKey
|
return e.shiftKey
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Does an `e` have the command modifier?
|
|
||||||
*
|
|
||||||
* @param {Event} e
|
|
||||||
* @return {Boolean}
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function isCommand(e) {
|
|
||||||
return IS_MAC
|
|
||||||
? e.metaKey && !e.altKey
|
|
||||||
: e.ctrlKey && !e.altKey
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Does an `e` have the Mac command modifier?
|
|
||||||
*
|
|
||||||
* @param {Event} e
|
|
||||||
* @return {Boolean}
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function isMacCommand(e) {
|
|
||||||
return IS_MAC && isCommand(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Does an `e` have the Windows command modifier?
|
* Does an `e` have the Windows command modifier?
|
||||||
*
|
*
|
||||||
@@ -78,6 +76,34 @@ export function isMacCommand(e) {
|
|||||||
* @return {Boolean}
|
* @return {Boolean}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function isWindowsCommand(e) {
|
function isWindowsCommand(e) {
|
||||||
return IS_WINDOWS && isCommand(e)
|
return IS_WINDOWS && isCommand(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does an `e` have the word-level modifier?
|
||||||
|
*
|
||||||
|
* @param {Event} e
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
|
||||||
|
function isWord(e) {
|
||||||
|
return IS_MAC
|
||||||
|
? e.altKey
|
||||||
|
: e.ctrlKey
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default {
|
||||||
|
isAlt,
|
||||||
|
isCommand,
|
||||||
|
isCtrl,
|
||||||
|
isMacCommand,
|
||||||
|
isOption,
|
||||||
|
isShift,
|
||||||
|
isWindowsCommand,
|
||||||
|
isWord
|
||||||
|
}
|
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": "git://github.com/ianstormtaylor/slate.git",
|
"repository": "git://github.com/ianstormtaylor/slate.git",
|
||||||
"main": "./dist/index.js",
|
"main": "./lib/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepublish": "make dist",
|
"prepublish": "make dist",
|
||||||
"test": "make check"
|
"test": "make check"
|
||||||
@@ -32,6 +32,7 @@
|
|||||||
"babelify": "^7.3.0",
|
"babelify": "^7.3.0",
|
||||||
"browserify": "^13.0.1",
|
"browserify": "^13.0.1",
|
||||||
"component-type": "^1.2.1",
|
"component-type": "^1.2.1",
|
||||||
|
"draft-js": "^0.7.0",
|
||||||
"eslint": "^3.0.1",
|
"eslint": "^3.0.1",
|
||||||
"eslint-plugin-import": "^1.10.2",
|
"eslint-plugin-import": "^1.10.2",
|
||||||
"eslint-plugin-react": "^5.2.2",
|
"eslint-plugin-react": "^5.2.2",
|
||||||
|
Reference in New Issue
Block a user