1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-09 16:56:36 +02:00

adding selections to transforms

This commit is contained in:
Ian Storm Taylor
2016-06-23 15:39:44 -07:00
parent c639db25c4
commit 1c45670ff8
8 changed files with 363 additions and 293 deletions

View File

@@ -11,6 +11,7 @@ watchify = $(bin)/watchify
# Flags. # Flags.
DEBUG ?= DEBUG ?=
GREP ?=
# Config. # Config.
ifeq ($(DEBUG),true) ifeq ($(DEBUG),true)
@@ -93,6 +94,7 @@ test-browser: ./test/support/build.js
@ $(mocha-phantomjs) \ @ $(mocha-phantomjs) \
--reporter spec \ --reporter spec \
--timeout 5000 \ --timeout 5000 \
--fgrep "$(GREP)" \
./test/support/browser.html ./test/support/browser.html
# Run the server-side tests. # Run the server-side tests.
@@ -102,6 +104,7 @@ test-server:
--require source-map-support/register \ --require source-map-support/register \
--reporter spec \ --reporter spec \
--timeout 5000 \ --timeout 5000 \
--fgrep "$(GREP)" \
./test/server.js ./test/server.js
# Watch the auto-markdown example. # Watch the auto-markdown example.

View File

@@ -23,6 +23,29 @@ class App extends React.Component {
state: Raw.deserialize(state) 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. * Render the example.
@@ -120,57 +143,27 @@ class App extends React.Component {
*/ */
onSpace(e, state) { onSpace(e, state) {
if (state.isCurrentlyExpanded) return if (state.isExpanded) return
let { selection } = state let { selection } = state
const { currentTextNodes, document } = state const { startText, startBlock, startOffset } = state
const { startOffset } = selection const chars = startBlock.text.slice(0, startOffset).replace(/\s*/g, '')
const node = currentTextNodes.first() const type = this.getType(chars)
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
}
if (!type) return
if (type == 'list-item' && startBlock.type == 'list-item') return
e.preventDefault() e.preventDefault()
let transform = state
.transform()
.setBlock(type)
if (type == 'list-item') transform = transform.wrapBlock('bulleted-list')
state = transform state = transform
.deleteAtRange(selection.extendBackwardToStartOf(node)) .extendToStartOf(startBlock)
.delete()
.apply() .apply()
selection = selection.moveToStartOf(node)
state = state.merge({ selection })
return state return state
} }
@@ -184,18 +177,18 @@ class App extends React.Component {
*/ */
onBackspace(e, state) { onBackspace(e, state) {
if (state.isCurrentlyExpanded) return if (state.isExpanded) return
if (state.currentStartOffset != 0) return if (state.startOffset != 0) return
const node = state.currentBlockNodes.first() const { startBlock } = state
if (!node) debugger
if (node.type == 'paragraph') return if (startBlock.type == 'paragraph') return
e.preventDefault() e.preventDefault()
let transform = state let transform = state
.transform() .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() state = transform.apply()
return state return state
@@ -211,20 +204,19 @@ class App extends React.Component {
*/ */
onEnter(e, state) { onEnter(e, state) {
if (state.isCurrentlyExpanded) return if (state.isExpanded) return
const node = state.currentBlockNodes.first() const { startBlock, startOffset, endOffset } = state
if (!node) debugger if (startOffset == 0 && startBlock.length == 0) return this.onBackspace(e, state)
if (state.currentStartOffset == 0 && node.length == 0) return this.onBackspace(e, state) if (endOffset != startBlock.length) return
if (state.currentEndOffset != node.length) return
if ( if (
node.type != 'heading-one' && startBlock.type != 'heading-one' &&
node.type != 'heading-two' && startBlock.type != 'heading-two' &&
node.type != 'heading-three' && startBlock.type != 'heading-three' &&
node.type != 'heading-four' && startBlock.type != 'heading-four' &&
node.type != 'heading-five' && startBlock.type != 'heading-five' &&
node.type != 'heading-six' && startBlock.type != 'heading-six' &&
node.type != 'block-quote' startBlock.type != 'block-quote'
) { ) {
return return
} }
@@ -232,8 +224,8 @@ class App extends React.Component {
e.preventDefault() e.preventDefault()
return state return state
.transform() .transform()
.split() .splitBlock()
.setType('paragraph') .setBlock('paragraph')
.apply() .apply()
} }

View File

@@ -153,7 +153,8 @@ const Node = {
if (range.isAtStartOf(startNode)) { if (range.isAtStartOf(startNode)) {
const previous = node.getPreviousText(startNode) const previous = node.getPreviousText(startNode)
range = range.extendBackwardToEndOf(previous) range = range.extendToEndOf(previous)
range = range.normalize(node)
node = node.deleteAtRange(range) node = node.deleteAtRange(range)
return node return node
} }
@@ -190,7 +191,8 @@ const Node = {
if (range.isAtEndOf(startNode)) { if (range.isAtEndOf(startNode)) {
const next = node.getNextText(startNode) const next = node.getNextText(startNode)
range = range.extendForwardToStartOf(next) range = range.extendToStartOf(next)
range = range.normalize(node)
node = node.deleteAtRange(range) node = node.deleteAtRange(range)
return node return node
} }
@@ -491,27 +493,19 @@ const Node = {
key = normalizeKey(key) key = normalizeKey(key)
this.assertHasDescendant(key) this.assertHasDescendant(key)
const match = this.getDescendant(key)
// Find the shallow matching child. // Find the shallow matching child.
const child = this.nodes.find((node) => { const isChild = this.hasChild(key)
if (node == match) return true const child = isChild
return node.kind == 'text' ? this.getChild(key)
? false : this.nodes.find(node => node.hasDescendant && node.hasDescendant(key))
: node.hasDescendant(match)
})
// Get all of the nodes that come before the matching child. // Calculate the offset of the nodes before the child.
const befores = this.nodes.takeUntil(node => node.key == child.key) 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. // Recurse if need be.
const offset = befores.reduce((offset, child) => { return isChild
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)
? offset ? offset
: offset + child.getOffset(key) : offset + child.getOffset(key)
}, },

View File

@@ -10,7 +10,7 @@ const DEFAULTS = {
anchorOffset: 0, anchorOffset: 0,
focusKey: null, focusKey: null,
focusOffset: 0, focusOffset: 0,
isBackward: false, isBackward: null,
isFocused: false isFocused: false
} }
@@ -52,7 +52,7 @@ class Selection extends Record(DEFAULTS) {
*/ */
get isExpanded() { get isExpanded() {
return ! this.isCollapsed return !this.isCollapsed
} }
/** /**
@@ -72,7 +72,7 @@ class Selection extends Record(DEFAULTS) {
*/ */
get isForward() { get isForward() {
return ! this.isBackward return this.isBackward == null ? null : !this.isBackward
} }
/** /**
@@ -141,10 +141,10 @@ class Selection extends Record(DEFAULTS) {
normalize(node) { normalize(node) {
let selection = this 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 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. // Asset that the anchor and focus nodes exist in the node tree.
node.assertHasDescendant(anchorKey) 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 the anchor node isn't a text node, match it to one.
if (anchorNode.kind != 'text') { if (anchorNode.kind != 'text') {
anchorNode = node.getTextAtOffset(anchorOffset) let anchorText = anchorNode.getTextAtOffset(anchorOffset)
let parent = node.getParent(anchorNode) let offset = anchorNode.getOffset(anchorText)
let offset = parent.getOffset(anchorNode)
anchorOffset = anchorOffset - offset anchorOffset = anchorOffset - offset
anchorKey = anchorNode.key anchorNode = anchorText
} }
// If the focus node isn't a text node, match it to one. // If the focus node isn't a text node, match it to one.
if (focusNode.kind != 'text') { if (focusNode.kind != 'text') {
focusNode = node.getTextAtOffset(focusOffset) let focusText = focusNode.getTextAtOffset(focusOffset)
let parent = node.getParent(focusNode) let offset = focusNode.getOffset(focusText)
let offset = parent.getOffset(focusNode)
focusOffset = focusOffset - offset 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. // Merge in any updated properties.
return selection.merge({ return selection.merge({
anchorKey, anchorKey: anchorNode.key,
anchorOffset, anchorOffset,
focusKey, focusKey: focusNode.key,
focusOffset focusOffset,
isBackward
}) })
} }
@@ -303,10 +312,6 @@ class Selection extends Record(DEFAULTS) {
*/ */
moveForward(n = 1) { moveForward(n = 1) {
if (!this.isCollapsed) {
throw new Error('The selection must be collapsed to move forward.')
}
return this.merge({ return this.merge({
anchorOffset: this.anchorOffset + n, anchorOffset: this.anchorOffset + n,
focusOffset: this.focusOffset + n focusOffset: this.focusOffset + n
@@ -321,10 +326,6 @@ class Selection extends Record(DEFAULTS) {
*/ */
moveBackward(n = 1) { moveBackward(n = 1) {
if (!this.isCollapsed) {
throw new Error('The selection must be collapsed to move backward.')
}
return this.merge({ return this.merge({
anchorOffset: this.anchorOffset - n, anchorOffset: this.anchorOffset - n,
focusOffset: this.focusOffset - n focusOffset: this.focusOffset - n
@@ -339,13 +340,9 @@ class Selection extends Record(DEFAULTS) {
*/ */
extendForward(n = 1) { extendForward(n = 1) {
if (!this.isCollapsed) {
throw new Error('The selection must be collapsed before extending.')
}
return this.merge({ return this.merge({
focusOffset: this.focusOffset + n, focusOffset: this.focusOffset + n,
isBackward: false isBackward: null
}) })
} }
@@ -357,89 +354,39 @@ class Selection extends Record(DEFAULTS) {
*/ */
extendBackward(n = 1) { extendBackward(n = 1) {
if (!this.isCollapsed) {
throw new Error('The selection must be collapsed before extending.')
}
return this.merge({ return this.merge({
focusOffset: this.focusOffset - n, 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 * @param {Node} node
* @return {Selection} selection * @return {Selection} selection
*/ */
extendForwardToStartOf(node) { extendToStartOf(node) {
if (!this.isCollapsed) {
throw new Error('The selection must be collapsed before extending.')
}
return this.merge({ return this.merge({
focusKey: node.key, focusKey: node.key,
focusOffset: 0, 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 * @param {Node} node
* @return {Selection} selection * @return {Selection} selection
*/ */
extendBackwardToStartOf(node) { extendToEndOf(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.')
}
return this.merge({ return this.merge({
focusKey: node.key, focusKey: node.key,
focusOffset: node.length, focusOffset: node.length,
isBackward: false isBackward: null
})
}
/**
* 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
}) })
} }

View File

@@ -24,27 +24,6 @@ const DEFAULTS = {
isNative: true 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. * State.
*/ */
@@ -71,7 +50,7 @@ class State extends Record(DEFAULTS) {
* @return {Boolean} isCollapsed * @return {Boolean} isCollapsed
*/ */
get isCurrentlyCollapsed() { get isCollapsed() {
return this.selection.isCollapsed return this.selection.isCollapsed
} }
@@ -81,7 +60,7 @@ class State extends Record(DEFAULTS) {
* @return {Boolean} isExpanded * @return {Boolean} isExpanded
*/ */
get isCurrentlyExpanded() { get isExpanded() {
return this.selection.isExpanded return this.selection.isExpanded
} }
@@ -91,7 +70,7 @@ class State extends Record(DEFAULTS) {
* @return {Boolean} isBackward * @return {Boolean} isBackward
*/ */
get isCurrentlyBackward() { get isBackward() {
return this.selection.isBackward return this.selection.isBackward
} }
@@ -101,7 +80,7 @@ class State extends Record(DEFAULTS) {
* @return {Boolean} isForward * @return {Boolean} isForward
*/ */
get isCurrentlyForward() { get isForward() {
return this.selection.isForward return this.selection.isForward
} }
@@ -111,7 +90,7 @@ class State extends Record(DEFAULTS) {
* @return {String} startKey * @return {String} startKey
*/ */
get currentStartKey() { get startKey() {
return this.selection.startKey return this.selection.startKey
} }
@@ -121,7 +100,7 @@ class State extends Record(DEFAULTS) {
* @return {String} endKey * @return {String} endKey
*/ */
get currentEndKey() { get endKey() {
return this.selection.endKey return this.selection.endKey
} }
@@ -131,7 +110,7 @@ class State extends Record(DEFAULTS) {
* @return {String} startOffset * @return {String} startOffset
*/ */
get currentStartOffset() { get startOffset() {
return this.selection.startOffset return this.selection.startOffset
} }
@@ -141,7 +120,7 @@ class State extends Record(DEFAULTS) {
* @return {String} endOffset * @return {String} endOffset
*/ */
get currentEndOffset() { get endOffset() {
return this.selection.endOffset return this.selection.endOffset
} }
@@ -151,7 +130,7 @@ class State extends Record(DEFAULTS) {
* @return {String} anchorKey * @return {String} anchorKey
*/ */
get currentAnchorKey() { get anchorKey() {
return this.selection.anchorKey return this.selection.anchorKey
} }
@@ -161,7 +140,7 @@ class State extends Record(DEFAULTS) {
* @return {String} focusKey * @return {String} focusKey
*/ */
get currentFocusKey() { get focusKey() {
return this.selection.focusKey return this.selection.focusKey
} }
@@ -171,7 +150,7 @@ class State extends Record(DEFAULTS) {
* @return {String} anchorOffset * @return {String} anchorOffset
*/ */
get currentAnchorOffset() { get anchorOffset() {
return this.selection.anchorOffset return this.selection.anchorOffset
} }
@@ -181,17 +160,97 @@ class State extends Record(DEFAULTS) {
* @return {String} focusOffset * @return {String} focusOffset
*/ */
get currentFocusOffset() { get focusOffset() {
return this.selection.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. * Get the characters in the current selection.
* *
* @return {List} characters * @return {List} characters
*/ */
get currentCharacters() { get characters() {
return this.document.getCharactersAtRange(this.selection) return this.document.getCharactersAtRange(this.selection)
} }
@@ -201,7 +260,7 @@ class State extends Record(DEFAULTS) {
* @return {Set} marks * @return {Set} marks
*/ */
get currentMarks() { get marks() {
return this.document.getMarksAtRange(this.selection) return this.document.getMarksAtRange(this.selection)
} }
@@ -211,7 +270,7 @@ class State extends Record(DEFAULTS) {
* @return {OrderedMap} nodes * @return {OrderedMap} nodes
*/ */
get currentBlocks() { get blocks() {
return this.document.getBlocksAtRange(this.selection) return this.document.getBlocksAtRange(this.selection)
} }
@@ -221,7 +280,7 @@ class State extends Record(DEFAULTS) {
* @return {OrderedMap} nodes * @return {OrderedMap} nodes
*/ */
get currentInlines() { get inlines() {
return this.document.getInlinesAtRange(this.selection) return this.document.getInlinesAtRange(this.selection)
} }
@@ -231,7 +290,7 @@ class State extends Record(DEFAULTS) {
* @return {OrderedMap} nodes * @return {OrderedMap} nodes
*/ */
get currentTexts() { get texts() {
return this.document.getTextsAtRange(this.selection) return this.document.getTextsAtRange(this.selection)
} }
@@ -292,7 +351,7 @@ class State extends Record(DEFAULTS) {
else if (selection.isAtStartOf(startNode)) { else if (selection.isAtStartOf(startNode)) {
const parent = document.getParent(startNode) const parent = document.getParent(startNode)
const previous = document.getPrevious(parent).nodes.first() const previous = document.getPreviousSibling(parent).nodes.first()
after = selection.moveToEndOf(previous) 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 * @return {State} state
*/ */
split() { splitBlock() {
let state = this let state = this
let { document, selection } = state let { document, selection } = state
let after
// Split the document. // Split the document.
document = document.splitAtRange(selection) document = document.splitBlockAtRange(selection)
// Determine what the selection should be after splitting. // Determine what the selection should be after splitting.
const { startKey } = selection const { startKey } = selection
const startNode = document.getDescendant(startKey) const startNode = document.getDescendant(startKey)
const parent = document.getParent(startNode) const nextNode = document.getNextText(startNode)
const next = document.getNext(parent) selection = selection.moveToStartOf(nextNode)
const text = next.nodes.first()
selection = selection.moveToStartOf(text) 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 }) state = state.merge({ document, selection })
return state return state
@@ -460,7 +543,6 @@ class State extends Record(DEFAULTS) {
unwrapBlock(type) { unwrapBlock(type) {
let state = this let state = this
let { document, selection } = state let { document, selection } = state
selection = selection.normalize(document)
document = document.unwrapBlockAtRange(selection, type) document = document.unwrapBlockAtRange(selection, type)
state = state.merge({ document, selection }) state = state.merge({ document, selection })
return state return state
@@ -492,7 +574,6 @@ class State extends Record(DEFAULTS) {
unwrapInline(type) { unwrapInline(type) {
let state = this let state = this
let { document, selection } = state let { document, selection } = state
selection = selection.normalize(document)
document = document.unwrapInlineAtRange(selection, type) document = document.unwrapInlineAtRange(selection, type)
state = state.merge({ document, selection }) state = state.merge({ document, selection })
return state 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. * Export.
*/ */

View File

@@ -22,6 +22,77 @@ const Step = Record({
args: null 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. * Defaults.
*/ */
@@ -31,41 +102,6 @@ const DEFAULT_PROPERTIES = {
steps: new List() 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. * Transform.
*/ */
@@ -134,10 +170,7 @@ class Transform extends Record(DEFAULT_PROPERTIES) {
} }
// Apply each of the steps in the transform, arriving at a new state. // Apply each of the steps in the transform, arriving at a new state.
state = steps.reduce((state, step) => { state = steps.reduce((state, step) => this.applyStep(state, step), state)
const { type, args } = step
return state[type](...args)
}, state)
// Apply the "isNative" flag, which is used to allow for natively-handled // Apply the "isNative" flag, which is used to allow for natively-handled
// content changes to skip rerendering the editor for performance. // content changes to skip rerendering the editor for performance.
@@ -148,6 +181,41 @@ class Transform extends Record(DEFAULT_PROPERTIES) {
return state 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. * 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) { Transform.prototype[type] = function (...args) {
let transform = this let transform = this
let { steps } = transform let { steps } = transform

View File

@@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import keycode from 'keycode' import keycode from 'keycode'
import environment from '../utils/environment' import { IS_WINDOWS, IS_MAC } from '../utils/environment'
/** /**
* Export. * Export.
@@ -20,14 +20,13 @@ export default {
onKeyDown(e, state, editor) { onKeyDown(e, state, editor) {
const key = keycode(e.which) const key = keycode(e.which)
const { IS_WINDOWS, IS_MAC } = environment()
switch (key) { switch (key) {
case 'enter': { case 'enter': {
e.preventDefault() e.preventDefault()
return state return state
.transform() .transform()
.split() .splitBlock()
.apply() .apply()
} }

View File

@@ -4,12 +4,10 @@ import Parser from 'ua-parser-js'
/** /**
* Read the environment. * Read the environment.
*
* @return {Object} environment
*/ */
function environment() { const ENVIRONMENT = process.browser
return { ? {
IS_ANDROID: browser.name === 'android', IS_ANDROID: browser.name === 'android',
IS_CHROME: browser.name === 'chrome', IS_CHROME: browser.name === 'chrome',
IS_EDGE: browser.name === 'edge', IS_EDGE: browser.name === 'edge',
@@ -21,10 +19,10 @@ function environment() {
IS_SAFARI: browser.name === 'safari', IS_SAFARI: browser.name === 'safari',
IS_WINDOWS: new Parser().getOS().name.includes('Windows') IS_WINDOWS: new Parser().getOS().name.includes('Windows')
} }
} : {}
/** /**
* Export. * Export.
*/ */
export default environment export default ENVIRONMENT