mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-04-21 13:51:59 +02:00
adding selections to transforms
This commit is contained in:
parent
c639db25c4
commit
1c45670ff8
3
Makefile
3
Makefile
@ -11,6 +11,7 @@ watchify = $(bin)/watchify
|
||||
|
||||
# Flags.
|
||||
DEBUG ?=
|
||||
GREP ?=
|
||||
|
||||
# Config.
|
||||
ifeq ($(DEBUG),true)
|
||||
@ -93,6 +94,7 @@ test-browser: ./test/support/build.js
|
||||
@ $(mocha-phantomjs) \
|
||||
--reporter spec \
|
||||
--timeout 5000 \
|
||||
--fgrep "$(GREP)" \
|
||||
./test/support/browser.html
|
||||
|
||||
# Run the server-side tests.
|
||||
@ -102,6 +104,7 @@ test-server:
|
||||
--require source-map-support/register \
|
||||
--reporter spec \
|
||||
--timeout 5000 \
|
||||
--fgrep "$(GREP)" \
|
||||
./test/server.js
|
||||
|
||||
# Watch the auto-markdown example.
|
||||
|
@ -23,6 +23,29 @@ class App extends React.Component {
|
||||
state: Raw.deserialize(state)
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the block type for a series of auto-markdown shortcut `chars`.
|
||||
*
|
||||
* @param {String} chars
|
||||
* @return {String} block
|
||||
*/
|
||||
|
||||
getType(chars) {
|
||||
switch (chars) {
|
||||
case '*':
|
||||
case '-':
|
||||
case '+': return 'list-item'
|
||||
case '>': return 'block-quote'
|
||||
case '#': return 'heading-one'
|
||||
case '##': return 'heading-two'
|
||||
case '###': return 'heading-three'
|
||||
case '####': return 'heading-four'
|
||||
case '#####': return 'heading-five'
|
||||
case '######': return 'heading-six'
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Render the example.
|
||||
@ -120,57 +143,27 @@ class App extends React.Component {
|
||||
*/
|
||||
|
||||
onSpace(e, state) {
|
||||
if (state.isCurrentlyExpanded) return
|
||||
if (state.isExpanded) return
|
||||
let { selection } = state
|
||||
const { currentTextNodes, document } = state
|
||||
const { startOffset } = selection
|
||||
const node = currentTextNodes.first()
|
||||
const { text } = node
|
||||
const chars = text.slice(0, startOffset).replace(/\s*/g, '')
|
||||
let transform = state.transform()
|
||||
|
||||
switch (chars) {
|
||||
case '#':
|
||||
transform = transform.setType('heading-one')
|
||||
break
|
||||
case '##':
|
||||
transform = transform.setType('heading-two')
|
||||
break
|
||||
case '###':
|
||||
transform = transform.setType('heading-three')
|
||||
break
|
||||
case '####':
|
||||
transform = transform.setType('heading-four')
|
||||
break
|
||||
case '#####':
|
||||
transform = transform.setType('heading-five')
|
||||
break
|
||||
case '######':
|
||||
transform = transform.setType('heading-six')
|
||||
break
|
||||
case '>':
|
||||
transform = transform.setType('block-quote')
|
||||
break
|
||||
case '*':
|
||||
case '-':
|
||||
case '+':
|
||||
if (node.type == 'list-item') return
|
||||
transform = transform
|
||||
.setType('list-item')
|
||||
.wrapBlock('bulleted-list')
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
const { startText, startBlock, startOffset } = state
|
||||
const chars = startBlock.text.slice(0, startOffset).replace(/\s*/g, '')
|
||||
const type = this.getType(chars)
|
||||
|
||||
if (!type) return
|
||||
if (type == 'list-item' && startBlock.type == 'list-item') return
|
||||
e.preventDefault()
|
||||
|
||||
let transform = state
|
||||
.transform()
|
||||
.setBlock(type)
|
||||
|
||||
if (type == 'list-item') transform = transform.wrapBlock('bulleted-list')
|
||||
|
||||
state = transform
|
||||
.deleteAtRange(selection.extendBackwardToStartOf(node))
|
||||
.extendToStartOf(startBlock)
|
||||
.delete()
|
||||
.apply()
|
||||
|
||||
selection = selection.moveToStartOf(node)
|
||||
state = state.merge({ selection })
|
||||
return state
|
||||
}
|
||||
|
||||
@ -184,18 +177,18 @@ class App extends React.Component {
|
||||
*/
|
||||
|
||||
onBackspace(e, state) {
|
||||
if (state.isCurrentlyExpanded) return
|
||||
if (state.currentStartOffset != 0) return
|
||||
const node = state.currentBlockNodes.first()
|
||||
if (!node) debugger
|
||||
if (node.type == 'paragraph') return
|
||||
if (state.isExpanded) return
|
||||
if (state.startOffset != 0) return
|
||||
const { startBlock } = state
|
||||
|
||||
if (startBlock.type == 'paragraph') return
|
||||
e.preventDefault()
|
||||
|
||||
let transform = state
|
||||
.transform()
|
||||
.setType('paragraph')
|
||||
.setBlock('paragraph')
|
||||
|
||||
if (node.type == 'list-item') transform = transform.unwrapBlock('bulleted-list')
|
||||
if (startBlock.type == 'list-item') transform = transform.unwrapBlock('bulleted-list')
|
||||
|
||||
state = transform.apply()
|
||||
return state
|
||||
@ -211,20 +204,19 @@ class App extends React.Component {
|
||||
*/
|
||||
|
||||
onEnter(e, state) {
|
||||
if (state.isCurrentlyExpanded) return
|
||||
const node = state.currentBlockNodes.first()
|
||||
if (!node) debugger
|
||||
if (state.currentStartOffset == 0 && node.length == 0) return this.onBackspace(e, state)
|
||||
if (state.currentEndOffset != node.length) return
|
||||
if (state.isExpanded) return
|
||||
const { startBlock, startOffset, endOffset } = state
|
||||
if (startOffset == 0 && startBlock.length == 0) return this.onBackspace(e, state)
|
||||
if (endOffset != startBlock.length) return
|
||||
|
||||
if (
|
||||
node.type != 'heading-one' &&
|
||||
node.type != 'heading-two' &&
|
||||
node.type != 'heading-three' &&
|
||||
node.type != 'heading-four' &&
|
||||
node.type != 'heading-five' &&
|
||||
node.type != 'heading-six' &&
|
||||
node.type != 'block-quote'
|
||||
startBlock.type != 'heading-one' &&
|
||||
startBlock.type != 'heading-two' &&
|
||||
startBlock.type != 'heading-three' &&
|
||||
startBlock.type != 'heading-four' &&
|
||||
startBlock.type != 'heading-five' &&
|
||||
startBlock.type != 'heading-six' &&
|
||||
startBlock.type != 'block-quote'
|
||||
) {
|
||||
return
|
||||
}
|
||||
@ -232,8 +224,8 @@ class App extends React.Component {
|
||||
e.preventDefault()
|
||||
return state
|
||||
.transform()
|
||||
.split()
|
||||
.setType('paragraph')
|
||||
.splitBlock()
|
||||
.setBlock('paragraph')
|
||||
.apply()
|
||||
}
|
||||
|
||||
|
@ -153,7 +153,8 @@ const Node = {
|
||||
|
||||
if (range.isAtStartOf(startNode)) {
|
||||
const previous = node.getPreviousText(startNode)
|
||||
range = range.extendBackwardToEndOf(previous)
|
||||
range = range.extendToEndOf(previous)
|
||||
range = range.normalize(node)
|
||||
node = node.deleteAtRange(range)
|
||||
return node
|
||||
}
|
||||
@ -190,7 +191,8 @@ const Node = {
|
||||
|
||||
if (range.isAtEndOf(startNode)) {
|
||||
const next = node.getNextText(startNode)
|
||||
range = range.extendForwardToStartOf(next)
|
||||
range = range.extendToStartOf(next)
|
||||
range = range.normalize(node)
|
||||
node = node.deleteAtRange(range)
|
||||
return node
|
||||
}
|
||||
@ -491,27 +493,19 @@ const Node = {
|
||||
key = normalizeKey(key)
|
||||
this.assertHasDescendant(key)
|
||||
|
||||
const match = this.getDescendant(key)
|
||||
|
||||
// Find the shallow matching child.
|
||||
const child = this.nodes.find((node) => {
|
||||
if (node == match) return true
|
||||
return node.kind == 'text'
|
||||
? false
|
||||
: node.hasDescendant(match)
|
||||
})
|
||||
const isChild = this.hasChild(key)
|
||||
const child = isChild
|
||||
? this.getChild(key)
|
||||
: this.nodes.find(node => node.hasDescendant && node.hasDescendant(key))
|
||||
|
||||
// Get all of the nodes that come before the matching child.
|
||||
const befores = this.nodes.takeUntil(node => node.key == child.key)
|
||||
// Calculate the offset of the nodes before the child.
|
||||
const offset = this.nodes
|
||||
.takeUntil(node => node == child)
|
||||
.reduce((offset, child) => offset + child.length, 0)
|
||||
|
||||
// Calculate the offset of the nodes before the matching child.
|
||||
const offset = befores.reduce((offset, child) => {
|
||||
return offset + child.length
|
||||
}, 0)
|
||||
|
||||
// If the child's parent is this node, return the offset of all of the nodes
|
||||
// before it, otherwise recurse.
|
||||
return this.nodes.find(node => node.key == match.key)
|
||||
// Recurse if need be.
|
||||
return isChild
|
||||
? offset
|
||||
: offset + child.getOffset(key)
|
||||
},
|
||||
|
@ -10,7 +10,7 @@ const DEFAULTS = {
|
||||
anchorOffset: 0,
|
||||
focusKey: null,
|
||||
focusOffset: 0,
|
||||
isBackward: false,
|
||||
isBackward: null,
|
||||
isFocused: false
|
||||
}
|
||||
|
||||
@ -52,7 +52,7 @@ class Selection extends Record(DEFAULTS) {
|
||||
*/
|
||||
|
||||
get isExpanded() {
|
||||
return ! this.isCollapsed
|
||||
return !this.isCollapsed
|
||||
}
|
||||
|
||||
/**
|
||||
@ -72,7 +72,7 @@ class Selection extends Record(DEFAULTS) {
|
||||
*/
|
||||
|
||||
get isForward() {
|
||||
return ! this.isBackward
|
||||
return this.isBackward == null ? null : !this.isBackward
|
||||
}
|
||||
|
||||
/**
|
||||
@ -141,10 +141,10 @@ class Selection extends Record(DEFAULTS) {
|
||||
|
||||
normalize(node) {
|
||||
let selection = this
|
||||
let { anchorKey, anchorOffset, focusKey, focusOffset } = selection
|
||||
let { anchorKey, anchorOffset, focusKey, focusOffset, isBackward } = selection
|
||||
|
||||
// If the selection isn't formed yet, abort.
|
||||
if (anchorKey == null || focusKey == null) return selection
|
||||
if (this.isUnset) return this
|
||||
|
||||
// Asset that the anchor and focus nodes exist in the node tree.
|
||||
node.assertHasDescendant(anchorKey)
|
||||
@ -154,28 +154,37 @@ class Selection extends Record(DEFAULTS) {
|
||||
|
||||
// If the anchor node isn't a text node, match it to one.
|
||||
if (anchorNode.kind != 'text') {
|
||||
anchorNode = node.getTextAtOffset(anchorOffset)
|
||||
let parent = node.getParent(anchorNode)
|
||||
let offset = parent.getOffset(anchorNode)
|
||||
let anchorText = anchorNode.getTextAtOffset(anchorOffset)
|
||||
let offset = anchorNode.getOffset(anchorText)
|
||||
anchorOffset = anchorOffset - offset
|
||||
anchorKey = anchorNode.key
|
||||
anchorNode = anchorText
|
||||
}
|
||||
|
||||
// If the focus node isn't a text node, match it to one.
|
||||
if (focusNode.kind != 'text') {
|
||||
focusNode = node.getTextAtOffset(focusOffset)
|
||||
let parent = node.getParent(focusNode)
|
||||
let offset = parent.getOffset(focusNode)
|
||||
let focusText = focusNode.getTextAtOffset(focusOffset)
|
||||
let offset = focusNode.getOffset(focusText)
|
||||
focusOffset = focusOffset - offset
|
||||
focusKey = focusNode.key
|
||||
focusNode = focusText
|
||||
}
|
||||
|
||||
// If `isBackward` is not set, derive it.
|
||||
if (isBackward == null) {
|
||||
let texts = node.getTextNodes()
|
||||
let anchorIndex = texts.indexOf(anchorNode)
|
||||
let focusIndex = texts.indexOf(focusNode)
|
||||
isBackward = anchorIndex == focusIndex
|
||||
? anchorOffset > focusOffset
|
||||
: anchorIndex > focusIndex
|
||||
}
|
||||
|
||||
// Merge in any updated properties.
|
||||
return selection.merge({
|
||||
anchorKey,
|
||||
anchorKey: anchorNode.key,
|
||||
anchorOffset,
|
||||
focusKey,
|
||||
focusOffset
|
||||
focusKey: focusNode.key,
|
||||
focusOffset,
|
||||
isBackward
|
||||
})
|
||||
}
|
||||
|
||||
@ -303,10 +312,6 @@ class Selection extends Record(DEFAULTS) {
|
||||
*/
|
||||
|
||||
moveForward(n = 1) {
|
||||
if (!this.isCollapsed) {
|
||||
throw new Error('The selection must be collapsed to move forward.')
|
||||
}
|
||||
|
||||
return this.merge({
|
||||
anchorOffset: this.anchorOffset + n,
|
||||
focusOffset: this.focusOffset + n
|
||||
@ -321,10 +326,6 @@ class Selection extends Record(DEFAULTS) {
|
||||
*/
|
||||
|
||||
moveBackward(n = 1) {
|
||||
if (!this.isCollapsed) {
|
||||
throw new Error('The selection must be collapsed to move backward.')
|
||||
}
|
||||
|
||||
return this.merge({
|
||||
anchorOffset: this.anchorOffset - n,
|
||||
focusOffset: this.focusOffset - n
|
||||
@ -339,13 +340,9 @@ class Selection extends Record(DEFAULTS) {
|
||||
*/
|
||||
|
||||
extendForward(n = 1) {
|
||||
if (!this.isCollapsed) {
|
||||
throw new Error('The selection must be collapsed before extending.')
|
||||
}
|
||||
|
||||
return this.merge({
|
||||
focusOffset: this.focusOffset + n,
|
||||
isBackward: false
|
||||
isBackward: null
|
||||
})
|
||||
}
|
||||
|
||||
@ -357,89 +354,39 @@ class Selection extends Record(DEFAULTS) {
|
||||
*/
|
||||
|
||||
extendBackward(n = 1) {
|
||||
if (!this.isCollapsed) {
|
||||
throw new Error('The selection must be collapsed before extending.')
|
||||
}
|
||||
|
||||
return this.merge({
|
||||
focusOffset: this.focusOffset - n,
|
||||
isBackward: true
|
||||
isBackward: null
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the focus forward to the start of a `node`.
|
||||
* Extend the focus point to the start of a `node`.
|
||||
*
|
||||
* @param {Node} node
|
||||
* @return {Selection} selection
|
||||
*/
|
||||
|
||||
extendForwardToStartOf(node) {
|
||||
if (!this.isCollapsed) {
|
||||
throw new Error('The selection must be collapsed before extending.')
|
||||
}
|
||||
|
||||
extendToStartOf(node) {
|
||||
return this.merge({
|
||||
focusKey: node.key,
|
||||
focusOffset: 0,
|
||||
isBackward: false
|
||||
isBackward: null
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the focus backward to the start of a `node`.
|
||||
* Extend the focus point to the end of a `node`.
|
||||
*
|
||||
* @param {Node} node
|
||||
* @return {Selection} selection
|
||||
*/
|
||||
|
||||
extendBackwardToStartOf(node) {
|
||||
if (!this.isCollapsed) {
|
||||
throw new Error('The selection must be collapsed before extending.')
|
||||
}
|
||||
|
||||
return this.merge({
|
||||
focusKey: node.key,
|
||||
focusOffset: 0,
|
||||
isBackward: true
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the focus forward to the end of a `node`.
|
||||
*
|
||||
* @param {Node} node
|
||||
* @return {Selection} selection
|
||||
*/
|
||||
|
||||
extendForwardToEndOf(node) {
|
||||
if (!this.isCollapsed) {
|
||||
throw new Error('The selection must be collapsed before extending.')
|
||||
}
|
||||
|
||||
extendToEndOf(node) {
|
||||
return this.merge({
|
||||
focusKey: node.key,
|
||||
focusOffset: node.length,
|
||||
isBackward: false
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the focus backward to the end of a `node`.
|
||||
*
|
||||
* @param {Node} node
|
||||
* @return {Selection} selection
|
||||
*/
|
||||
|
||||
extendBackwardToEndOf(node) {
|
||||
if (!this.isCollapsed) {
|
||||
throw new Error('The selection must be collapsed before extending.')
|
||||
}
|
||||
|
||||
return this.merge({
|
||||
focusKey: node.key,
|
||||
focusOffset: node.length,
|
||||
isBackward: true
|
||||
isBackward: null
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -24,27 +24,6 @@ const DEFAULTS = {
|
||||
isNative: true
|
||||
}
|
||||
|
||||
/**
|
||||
* Node-like methods that should be mixed into the `State` prototype.
|
||||
*/
|
||||
|
||||
const NODE_LIKE_METHODS = [
|
||||
'deleteAtRange',
|
||||
'deleteBackwardAtRange',
|
||||
'deleteForwardAtRange',
|
||||
'insertTextAtRange',
|
||||
'markAtRange',
|
||||
'setBlockAtRange',
|
||||
'setInlineAtRange',
|
||||
'splitBlockAtRange',
|
||||
'splitInlineAtRange',
|
||||
'unmarkAtRange',
|
||||
'unwrapBlockAtRange',
|
||||
'unwrapInlineAtRange',
|
||||
'wrapBlockAtRange',
|
||||
'wrapInlineAtRange'
|
||||
]
|
||||
|
||||
/**
|
||||
* State.
|
||||
*/
|
||||
@ -71,7 +50,7 @@ class State extends Record(DEFAULTS) {
|
||||
* @return {Boolean} isCollapsed
|
||||
*/
|
||||
|
||||
get isCurrentlyCollapsed() {
|
||||
get isCollapsed() {
|
||||
return this.selection.isCollapsed
|
||||
}
|
||||
|
||||
@ -81,7 +60,7 @@ class State extends Record(DEFAULTS) {
|
||||
* @return {Boolean} isExpanded
|
||||
*/
|
||||
|
||||
get isCurrentlyExpanded() {
|
||||
get isExpanded() {
|
||||
return this.selection.isExpanded
|
||||
}
|
||||
|
||||
@ -91,7 +70,7 @@ class State extends Record(DEFAULTS) {
|
||||
* @return {Boolean} isBackward
|
||||
*/
|
||||
|
||||
get isCurrentlyBackward() {
|
||||
get isBackward() {
|
||||
return this.selection.isBackward
|
||||
}
|
||||
|
||||
@ -101,7 +80,7 @@ class State extends Record(DEFAULTS) {
|
||||
* @return {Boolean} isForward
|
||||
*/
|
||||
|
||||
get isCurrentlyForward() {
|
||||
get isForward() {
|
||||
return this.selection.isForward
|
||||
}
|
||||
|
||||
@ -111,7 +90,7 @@ class State extends Record(DEFAULTS) {
|
||||
* @return {String} startKey
|
||||
*/
|
||||
|
||||
get currentStartKey() {
|
||||
get startKey() {
|
||||
return this.selection.startKey
|
||||
}
|
||||
|
||||
@ -121,7 +100,7 @@ class State extends Record(DEFAULTS) {
|
||||
* @return {String} endKey
|
||||
*/
|
||||
|
||||
get currentEndKey() {
|
||||
get endKey() {
|
||||
return this.selection.endKey
|
||||
}
|
||||
|
||||
@ -131,7 +110,7 @@ class State extends Record(DEFAULTS) {
|
||||
* @return {String} startOffset
|
||||
*/
|
||||
|
||||
get currentStartOffset() {
|
||||
get startOffset() {
|
||||
return this.selection.startOffset
|
||||
}
|
||||
|
||||
@ -141,7 +120,7 @@ class State extends Record(DEFAULTS) {
|
||||
* @return {String} endOffset
|
||||
*/
|
||||
|
||||
get currentEndOffset() {
|
||||
get endOffset() {
|
||||
return this.selection.endOffset
|
||||
}
|
||||
|
||||
@ -151,7 +130,7 @@ class State extends Record(DEFAULTS) {
|
||||
* @return {String} anchorKey
|
||||
*/
|
||||
|
||||
get currentAnchorKey() {
|
||||
get anchorKey() {
|
||||
return this.selection.anchorKey
|
||||
}
|
||||
|
||||
@ -161,7 +140,7 @@ class State extends Record(DEFAULTS) {
|
||||
* @return {String} focusKey
|
||||
*/
|
||||
|
||||
get currentFocusKey() {
|
||||
get focusKey() {
|
||||
return this.selection.focusKey
|
||||
}
|
||||
|
||||
@ -171,7 +150,7 @@ class State extends Record(DEFAULTS) {
|
||||
* @return {String} anchorOffset
|
||||
*/
|
||||
|
||||
get currentAnchorOffset() {
|
||||
get anchorOffset() {
|
||||
return this.selection.anchorOffset
|
||||
}
|
||||
|
||||
@ -181,17 +160,97 @@ class State extends Record(DEFAULTS) {
|
||||
* @return {String} focusOffset
|
||||
*/
|
||||
|
||||
get currentFocusOffset() {
|
||||
get focusOffset() {
|
||||
return this.selection.focusOffset
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current start text node.
|
||||
*
|
||||
* @return {Text} text
|
||||
*/
|
||||
|
||||
get startText() {
|
||||
return this.document.getDescendant(this.selection.startKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current end node.
|
||||
*
|
||||
* @return {Text} text
|
||||
*/
|
||||
|
||||
get endText() {
|
||||
return this.document.getDescendant(this.selection.endKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current anchor node.
|
||||
*
|
||||
* @return {Text} text
|
||||
*/
|
||||
|
||||
get anchorText() {
|
||||
return this.document.getDescendant(this.selection.anchorKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current focus node.
|
||||
*
|
||||
* @return {Text} text
|
||||
*/
|
||||
|
||||
get focusText() {
|
||||
return this.document.getDescendant(this.selection.focusKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current start text node's closest block parent.
|
||||
*
|
||||
* @return {Block} block
|
||||
*/
|
||||
|
||||
get startBlock() {
|
||||
return this.document.getClosestBlock(this.selection.startKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current end text node's closest block parent.
|
||||
*
|
||||
* @return {Block} block
|
||||
*/
|
||||
|
||||
get endBlock() {
|
||||
return this.document.getClosestBlock(this.selection.endKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current anchor text node's closest block parent.
|
||||
*
|
||||
* @return {Block} block
|
||||
*/
|
||||
|
||||
get anchorBlock() {
|
||||
return this.document.getClosestBlock(this.selection.anchorKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current focus text node's closest block parent.
|
||||
*
|
||||
* @return {Block} block
|
||||
*/
|
||||
|
||||
get focusBlock() {
|
||||
return this.document.getClosestBlock(this.selection.focusKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the characters in the current selection.
|
||||
*
|
||||
* @return {List} characters
|
||||
*/
|
||||
|
||||
get currentCharacters() {
|
||||
get characters() {
|
||||
return this.document.getCharactersAtRange(this.selection)
|
||||
}
|
||||
|
||||
@ -201,7 +260,7 @@ class State extends Record(DEFAULTS) {
|
||||
* @return {Set} marks
|
||||
*/
|
||||
|
||||
get currentMarks() {
|
||||
get marks() {
|
||||
return this.document.getMarksAtRange(this.selection)
|
||||
}
|
||||
|
||||
@ -211,7 +270,7 @@ class State extends Record(DEFAULTS) {
|
||||
* @return {OrderedMap} nodes
|
||||
*/
|
||||
|
||||
get currentBlocks() {
|
||||
get blocks() {
|
||||
return this.document.getBlocksAtRange(this.selection)
|
||||
}
|
||||
|
||||
@ -221,7 +280,7 @@ class State extends Record(DEFAULTS) {
|
||||
* @return {OrderedMap} nodes
|
||||
*/
|
||||
|
||||
get currentInlines() {
|
||||
get inlines() {
|
||||
return this.document.getInlinesAtRange(this.selection)
|
||||
}
|
||||
|
||||
@ -231,7 +290,7 @@ class State extends Record(DEFAULTS) {
|
||||
* @return {OrderedMap} nodes
|
||||
*/
|
||||
|
||||
get currentTexts() {
|
||||
get texts() {
|
||||
return this.document.getTextsAtRange(this.selection)
|
||||
}
|
||||
|
||||
@ -292,7 +351,7 @@ class State extends Record(DEFAULTS) {
|
||||
|
||||
else if (selection.isAtStartOf(startNode)) {
|
||||
const parent = document.getParent(startNode)
|
||||
const previous = document.getPrevious(parent).nodes.first()
|
||||
const previous = document.getPreviousSibling(parent).nodes.first()
|
||||
after = selection.moveToEndOf(previous)
|
||||
}
|
||||
|
||||
@ -395,26 +454,50 @@ class State extends Record(DEFAULTS) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Split the node at the current selection.
|
||||
* Split the block node at the current selection.
|
||||
*
|
||||
* @return {State} state
|
||||
*/
|
||||
|
||||
split() {
|
||||
splitBlock() {
|
||||
let state = this
|
||||
let { document, selection } = state
|
||||
let after
|
||||
|
||||
// Split the document.
|
||||
document = document.splitAtRange(selection)
|
||||
document = document.splitBlockAtRange(selection)
|
||||
|
||||
// Determine what the selection should be after splitting.
|
||||
const { startKey } = selection
|
||||
const startNode = document.getDescendant(startKey)
|
||||
const parent = document.getParent(startNode)
|
||||
const next = document.getNext(parent)
|
||||
const text = next.nodes.first()
|
||||
selection = selection.moveToStartOf(text)
|
||||
const nextNode = document.getNextText(startNode)
|
||||
selection = selection.moveToStartOf(nextNode)
|
||||
|
||||
state = state.merge({ document, selection })
|
||||
return state
|
||||
}
|
||||
|
||||
/**
|
||||
* Split the inline nodes at the current selection.
|
||||
*
|
||||
* @return {State} state
|
||||
*/
|
||||
|
||||
splitInline() {
|
||||
let state = this
|
||||
let { document, selection } = state
|
||||
|
||||
// Split the document.
|
||||
document = document.splitInlineAtRange(selection)
|
||||
|
||||
// Determine what the selection should be after splitting.
|
||||
const { startKey } = selection
|
||||
const inlineParent = document.getClosestInline(startKey)
|
||||
|
||||
if (inlineParent) {
|
||||
const startNode = document.getDescendant(startKey)
|
||||
const nextNode = document.getNextText(startNode)
|
||||
selection = selection.moveToStartOf(nextNode)
|
||||
}
|
||||
|
||||
state = state.merge({ document, selection })
|
||||
return state
|
||||
@ -460,7 +543,6 @@ class State extends Record(DEFAULTS) {
|
||||
unwrapBlock(type) {
|
||||
let state = this
|
||||
let { document, selection } = state
|
||||
selection = selection.normalize(document)
|
||||
document = document.unwrapBlockAtRange(selection, type)
|
||||
state = state.merge({ document, selection })
|
||||
return state
|
||||
@ -492,7 +574,6 @@ class State extends Record(DEFAULTS) {
|
||||
unwrapInline(type) {
|
||||
let state = this
|
||||
let { document, selection } = state
|
||||
selection = selection.normalize(document)
|
||||
document = document.unwrapInlineAtRange(selection, type)
|
||||
state = state.merge({ document, selection })
|
||||
return state
|
||||
@ -500,18 +581,6 @@ class State extends Record(DEFAULTS) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Mix in node-like methods.
|
||||
*/
|
||||
|
||||
NODE_LIKE_METHODS.forEach((method) => {
|
||||
State.prototype[method] = function (...args) {
|
||||
let { document } = this
|
||||
document = document[method](...args)
|
||||
return this.merge({ document })
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Export.
|
||||
*/
|
||||
|
@ -22,6 +22,77 @@ const Step = Record({
|
||||
args: null
|
||||
})
|
||||
|
||||
/**
|
||||
* Document transforms.
|
||||
*/
|
||||
|
||||
const DOCUMENT_TRANSFORMS = [
|
||||
'deleteAtRange',
|
||||
'deleteBackwardAtRange',
|
||||
'deleteForwardAtRange',
|
||||
'insertTextAtRange',
|
||||
'markAtRange',
|
||||
'setBlockAtRange',
|
||||
'setInlineAtRange',
|
||||
'splitBlockAtRange',
|
||||
'splitInlineAtRange',
|
||||
'unmarkAtRange',
|
||||
'unwrapBlockAtRange',
|
||||
'unwrapInlineAtRange',
|
||||
'wrapBlockAtRange',
|
||||
'wrapInlineAtRange'
|
||||
]
|
||||
|
||||
/**
|
||||
* Selection transforms.
|
||||
*/
|
||||
|
||||
const SELECTION_TRANSFORMS = [
|
||||
'moveToAnchor',
|
||||
'moveToFocus',
|
||||
'moveToStart',
|
||||
'moveToEnd',
|
||||
'moveToStartOf',
|
||||
'moveToEndOf',
|
||||
'moveToRangeOf',
|
||||
'moveForward',
|
||||
'moveBackward',
|
||||
'extendForward',
|
||||
'extendBackward',
|
||||
'extendToStartOf',
|
||||
'extendToEndOf'
|
||||
]
|
||||
|
||||
/**
|
||||
* State transforms, that act on both the document and selection.
|
||||
*/
|
||||
|
||||
const STATE_TRANSFORMS = [
|
||||
'delete',
|
||||
'deleteBackward',
|
||||
'deleteForward',
|
||||
'insertText',
|
||||
'mark',
|
||||
'setBlock',
|
||||
'setInline',
|
||||
'splitBlock',
|
||||
'splitInline',
|
||||
'unmark',
|
||||
'unwrapBlock',
|
||||
'unwrapInline',
|
||||
'wrapBlock',
|
||||
'wrapInline'
|
||||
]
|
||||
|
||||
/**
|
||||
* All transforms.
|
||||
*/
|
||||
|
||||
const TRANSFORMS = []
|
||||
.concat(DOCUMENT_TRANSFORMS)
|
||||
.concat(SELECTION_TRANSFORMS)
|
||||
.concat(STATE_TRANSFORMS)
|
||||
|
||||
/**
|
||||
* Defaults.
|
||||
*/
|
||||
@ -31,41 +102,6 @@ const DEFAULT_PROPERTIES = {
|
||||
steps: new List()
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform types.
|
||||
*/
|
||||
|
||||
const TRANSFORM_TYPES = [
|
||||
'delete',
|
||||
'deleteAtRange',
|
||||
'deleteBackward',
|
||||
'deleteBackwardAtRange',
|
||||
'deleteForward',
|
||||
'deleteForwardAtRange',
|
||||
'insertText',
|
||||
'insertTextAtRange',
|
||||
'mark',
|
||||
'markAtRange',
|
||||
'setBlock',
|
||||
'setBlockAtRange',
|
||||
'setInline',
|
||||
'setInlineAtRange',
|
||||
'splitBlock',
|
||||
'splitBlockAtRange',
|
||||
'splitInline',
|
||||
'splitInlineAtRange',
|
||||
'unmark',
|
||||
'unmarkAtRange',
|
||||
'unwrapBlock',
|
||||
'unwrapBlockAtRange',
|
||||
'unwrapInline',
|
||||
'unwrapInlineAtRange',
|
||||
'wrapBlock',
|
||||
'wrapBlockAtRange',
|
||||
'wrapInline',
|
||||
'wrapInlineAtRange'
|
||||
]
|
||||
|
||||
/**
|
||||
* Transform.
|
||||
*/
|
||||
@ -134,10 +170,7 @@ class Transform extends Record(DEFAULT_PROPERTIES) {
|
||||
}
|
||||
|
||||
// Apply each of the steps in the transform, arriving at a new state.
|
||||
state = steps.reduce((state, step) => {
|
||||
const { type, args } = step
|
||||
return state[type](...args)
|
||||
}, state)
|
||||
state = steps.reduce((state, step) => this.applyStep(state, step), state)
|
||||
|
||||
// Apply the "isNative" flag, which is used to allow for natively-handled
|
||||
// content changes to skip rerendering the editor for performance.
|
||||
@ -148,6 +181,41 @@ class Transform extends Record(DEFAULT_PROPERTIES) {
|
||||
return state
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a single `step` to a `state`, differentiating between types.
|
||||
*
|
||||
* @param {State} state
|
||||
* @param {Step} step
|
||||
* @return {State} state
|
||||
*/
|
||||
|
||||
applyStep(state, step) {
|
||||
const { type, args } = step
|
||||
|
||||
if (DOCUMENT_TRANSFORMS.includes(type)) {
|
||||
let { document, selection } = state
|
||||
let [ range, ...rest ] = args
|
||||
range = range.normalize(document)
|
||||
document = document[type](range, ...rest)
|
||||
selection = selection.normalize(document)
|
||||
state = state.merge({ document, selection })
|
||||
return state
|
||||
}
|
||||
|
||||
else if (SELECTION_TRANSFORMS.includes(type)) {
|
||||
let { document, selection } = state
|
||||
selection = selection[type](...args)
|
||||
selection = selection.normalize(document)
|
||||
state = state.merge({ selection })
|
||||
return state
|
||||
}
|
||||
|
||||
else if (STATE_TRANSFORMS.includes(type)) {
|
||||
state = state[type](...args)
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Undo to the previous state in the history.
|
||||
*
|
||||
@ -211,10 +279,10 @@ class Transform extends Record(DEFAULT_PROPERTIES) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a step-creating method for each transform type.
|
||||
* Add a step-creating method for each of the transforms.
|
||||
*/
|
||||
|
||||
TRANSFORM_TYPES.forEach((type) => {
|
||||
TRANSFORMS.forEach((type) => {
|
||||
Transform.prototype[type] = function (...args) {
|
||||
let transform = this
|
||||
let { steps } = transform
|
||||
|
@ -1,7 +1,7 @@
|
||||
|
||||
import React from 'react'
|
||||
import keycode from 'keycode'
|
||||
import environment from '../utils/environment'
|
||||
import { IS_WINDOWS, IS_MAC } from '../utils/environment'
|
||||
|
||||
/**
|
||||
* Export.
|
||||
@ -20,14 +20,13 @@ export default {
|
||||
|
||||
onKeyDown(e, state, editor) {
|
||||
const key = keycode(e.which)
|
||||
const { IS_WINDOWS, IS_MAC } = environment()
|
||||
|
||||
switch (key) {
|
||||
case 'enter': {
|
||||
e.preventDefault()
|
||||
return state
|
||||
.transform()
|
||||
.split()
|
||||
.splitBlock()
|
||||
.apply()
|
||||
}
|
||||
|
||||
|
@ -4,27 +4,25 @@ import Parser from 'ua-parser-js'
|
||||
|
||||
/**
|
||||
* Read the environment.
|
||||
*
|
||||
* @return {Object} environment
|
||||
*/
|
||||
|
||||
function environment() {
|
||||
return {
|
||||
IS_ANDROID: browser.name === 'android',
|
||||
IS_CHROME: browser.name === 'chrome',
|
||||
IS_EDGE: browser.name === 'edge',
|
||||
IS_FIREFOX: browser.name === 'firefox',
|
||||
IS_IE: browser.name === 'ie',
|
||||
IS_IOS: browser.name === 'ios',
|
||||
IS_MAC: new Parser().getOS().name === 'Mac OS',
|
||||
IS_UBUNTU: new Parser().getOS().name === 'Ubuntu',
|
||||
IS_SAFARI: browser.name === 'safari',
|
||||
IS_WINDOWS: new Parser().getOS().name.includes('Windows')
|
||||
}
|
||||
}
|
||||
const ENVIRONMENT = process.browser
|
||||
? {
|
||||
IS_ANDROID: browser.name === 'android',
|
||||
IS_CHROME: browser.name === 'chrome',
|
||||
IS_EDGE: browser.name === 'edge',
|
||||
IS_FIREFOX: browser.name === 'firefox',
|
||||
IS_IE: browser.name === 'ie',
|
||||
IS_IOS: browser.name === 'ios',
|
||||
IS_MAC: new Parser().getOS().name === 'Mac OS',
|
||||
IS_UBUNTU: new Parser().getOS().name === 'Ubuntu',
|
||||
IS_SAFARI: browser.name === 'safari',
|
||||
IS_WINDOWS: new Parser().getOS().name.includes('Windows')
|
||||
}
|
||||
: {}
|
||||
|
||||
/**
|
||||
* Export.
|
||||
*/
|
||||
|
||||
export default environment
|
||||
export default ENVIRONMENT
|
||||
|
Loading…
x
Reference in New Issue
Block a user