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:
3
Makefile
3
Makefile
@@ -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.
|
||||||
|
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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)
|
||||||
},
|
},
|
||||||
|
@@ -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
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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.
|
||||||
*/
|
*/
|
||||||
|
@@ -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
|
||||||
|
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
Reference in New Issue
Block a user