2016-06-15 12:07:12 -07:00
|
|
|
|
|
|
|
import { Record } from 'immutable'
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Record.
|
|
|
|
*/
|
|
|
|
|
|
|
|
const SelectionRecord = new Record({
|
|
|
|
anchorKey: null,
|
|
|
|
anchorOffset: 0,
|
|
|
|
focusKey: null,
|
|
|
|
focusOffset: 0,
|
|
|
|
isBackward: false,
|
2016-06-15 20:13:02 -07:00
|
|
|
isFocused: false
|
2016-06-15 12:07:12 -07:00
|
|
|
})
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Selection.
|
|
|
|
*/
|
|
|
|
|
|
|
|
class Selection extends SelectionRecord {
|
|
|
|
|
2016-06-17 00:52:15 -07:00
|
|
|
/**
|
2016-06-17 18:20:26 -07:00
|
|
|
* Create a new `Selection` with `properties`.
|
2016-06-17 00:52:15 -07:00
|
|
|
*
|
2016-06-17 18:20:26 -07:00
|
|
|
* @param {Object} properties
|
2016-06-17 00:52:15 -07:00
|
|
|
* @return {Selection} selection
|
|
|
|
*/
|
|
|
|
|
2016-06-17 18:20:26 -07:00
|
|
|
static create(properties = {}) {
|
|
|
|
return new Selection(properties)
|
2016-06-15 12:07:12 -07:00
|
|
|
}
|
|
|
|
|
2016-06-17 00:52:15 -07:00
|
|
|
/**
|
|
|
|
* Get whether the selection is collapsed.
|
|
|
|
*
|
|
|
|
* @return {Boolean} isCollapsed
|
|
|
|
*/
|
|
|
|
|
2016-06-15 12:07:12 -07:00
|
|
|
get isCollapsed() {
|
|
|
|
return (
|
|
|
|
this.anchorKey === this.focusKey &&
|
|
|
|
this.anchorOffset === this.focusOffset
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2016-06-17 00:52:15 -07:00
|
|
|
/**
|
|
|
|
* Get whether the selection is expanded.
|
|
|
|
*
|
|
|
|
* Aliased as `isExtended` since browser implementations refer to it as both.
|
|
|
|
*
|
|
|
|
* @return {Boolean} isExpanded
|
|
|
|
*/
|
|
|
|
|
|
|
|
get isExpanded() {
|
|
|
|
return ! this.isCollapsed
|
|
|
|
}
|
|
|
|
|
|
|
|
get isExtended() {
|
|
|
|
return this.isExpanded
|
|
|
|
}
|
|
|
|
|
2016-06-22 18:42:49 -07:00
|
|
|
/**
|
|
|
|
* Get whether the range's anchor of focus keys are not set yet.
|
|
|
|
*
|
|
|
|
* @return {Boolean} isUnset
|
|
|
|
*/
|
|
|
|
|
|
|
|
get isUnset() {
|
|
|
|
return this.anchorKey == null || this.focusKey == null
|
|
|
|
}
|
|
|
|
|
2016-06-17 00:52:15 -07:00
|
|
|
/**
|
|
|
|
* Get whether the selection is forward.
|
|
|
|
*
|
|
|
|
* @return {Boolean} isForward
|
|
|
|
*/
|
|
|
|
|
2016-06-15 19:46:53 -07:00
|
|
|
get isForward() {
|
|
|
|
return ! this.isBackward
|
|
|
|
}
|
|
|
|
|
2016-06-17 00:52:15 -07:00
|
|
|
/**
|
|
|
|
* Get the start key.
|
|
|
|
*
|
|
|
|
* @return {String} startKey
|
|
|
|
*/
|
|
|
|
|
2016-06-15 12:07:12 -07:00
|
|
|
get startKey() {
|
|
|
|
return this.isBackward
|
|
|
|
? this.focusKey
|
|
|
|
: this.anchorKey
|
|
|
|
}
|
|
|
|
|
|
|
|
get startOffset() {
|
|
|
|
return this.isBackward
|
|
|
|
? this.focusOffset
|
|
|
|
: this.anchorOffset
|
|
|
|
}
|
|
|
|
|
|
|
|
get endKey() {
|
|
|
|
return this.isBackward
|
|
|
|
? this.anchorKey
|
|
|
|
: this.focusKey
|
|
|
|
}
|
|
|
|
|
|
|
|
get endOffset() {
|
|
|
|
return this.isBackward
|
|
|
|
? this.anchorOffset
|
|
|
|
: this.focusOffset
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2016-06-17 00:09:54 -07:00
|
|
|
* Check whether the selection is at the start of a `node`.
|
2016-06-15 12:07:12 -07:00
|
|
|
*
|
2016-06-17 00:09:54 -07:00
|
|
|
* @param {Node} node
|
2016-06-15 12:07:12 -07:00
|
|
|
* @return {Boolean} isAtStart
|
|
|
|
*/
|
|
|
|
|
2016-06-17 00:09:54 -07:00
|
|
|
isAtStartOf(node) {
|
|
|
|
const { startKey, startOffset } = this
|
2016-06-22 18:42:49 -07:00
|
|
|
const first = node.kind == 'text' ? node : node.getFirstText()
|
2016-06-17 00:09:54 -07:00
|
|
|
return startKey == first.key && startOffset == 0
|
2016-06-15 12:07:12 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2016-06-17 00:09:54 -07:00
|
|
|
* Check whether the selection is at the end of a `node`.
|
2016-06-15 12:07:12 -07:00
|
|
|
*
|
2016-06-17 00:09:54 -07:00
|
|
|
* @param {Node} node
|
2016-06-15 12:07:12 -07:00
|
|
|
* @return {Boolean} isAtEnd
|
|
|
|
*/
|
|
|
|
|
2016-06-17 00:09:54 -07:00
|
|
|
isAtEndOf(node) {
|
|
|
|
const { endKey, endOffset } = this
|
2016-06-22 18:42:49 -07:00
|
|
|
const last = node.kind == 'text' ? node : node.getLastText()
|
2016-06-17 00:09:54 -07:00
|
|
|
return endKey == last.key && endOffset == last.length
|
|
|
|
}
|
|
|
|
|
2016-06-21 14:49:08 -07:00
|
|
|
/**
|
|
|
|
* Normalize the selection, relative to a `node`, ensuring that the anchor
|
|
|
|
* and focus nodes of the selection always refer to leaf text nodes.
|
|
|
|
*
|
|
|
|
* @param {Node} node
|
|
|
|
* @return {Selection} selection
|
|
|
|
*/
|
|
|
|
|
|
|
|
normalize(node) {
|
|
|
|
let selection = this
|
|
|
|
let { anchorKey, anchorOffset, focusKey, focusOffset } = selection
|
|
|
|
|
|
|
|
// If the selection isn't formed yet, abort.
|
|
|
|
if (anchorKey == null || focusKey == null) return selection
|
|
|
|
|
|
|
|
// Asset that the anchor and focus nodes exist in the node tree.
|
2016-06-22 18:42:49 -07:00
|
|
|
node.assertHasDeep(anchorKey)
|
|
|
|
node.assertHasDeep(focusKey)
|
|
|
|
let anchorNode = node.getDeep(anchorKey)
|
|
|
|
let focusNode = node.getDeep(focusKey)
|
2016-06-21 14:49:08 -07:00
|
|
|
|
|
|
|
// If the anchor node isn't a text node, match it to one.
|
2016-06-21 16:44:11 -07:00
|
|
|
if (anchorNode.kind != 'text') {
|
2016-06-22 18:42:49 -07:00
|
|
|
anchorNode = node.getTextAtOffset(anchorOffset)
|
|
|
|
let parent = node.getParent(anchorNode)
|
|
|
|
let offset = parent.getOffset(anchorNode)
|
2016-06-21 14:49:08 -07:00
|
|
|
anchorOffset = anchorOffset - offset
|
|
|
|
anchorKey = anchorNode.key
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the focus node isn't a text node, match it to one.
|
2016-06-21 16:44:11 -07:00
|
|
|
if (focusNode.kind != 'text') {
|
2016-06-22 18:42:49 -07:00
|
|
|
focusNode = node.getTextAtOffset(focusOffset)
|
|
|
|
let parent = node.getParent(focusNode)
|
|
|
|
let offset = parent.getOffset(focusNode)
|
2016-06-21 14:49:08 -07:00
|
|
|
focusOffset = focusOffset - offset
|
|
|
|
focusKey = focusNode.key
|
|
|
|
}
|
|
|
|
|
|
|
|
// Merge in any updated properties.
|
|
|
|
return selection.merge({
|
|
|
|
anchorKey,
|
|
|
|
anchorOffset,
|
|
|
|
focusKey,
|
|
|
|
focusOffset
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2016-06-17 00:09:54 -07:00
|
|
|
/**
|
|
|
|
* Move the selection to a set of `properties`.
|
|
|
|
*
|
|
|
|
* @param {Object} properties
|
|
|
|
* @return {State} state
|
|
|
|
*/
|
|
|
|
|
|
|
|
moveTo(properties) {
|
|
|
|
return this.merge(properties)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Move the focus point to the anchor point.
|
|
|
|
*
|
|
|
|
* @return {Selection} selection
|
|
|
|
*/
|
|
|
|
|
|
|
|
moveToAnchor() {
|
|
|
|
return this.merge({
|
|
|
|
focusKey: this.anchorKey,
|
|
|
|
focusOffset: this.anchorOffset
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Move the anchor point to the focus point.
|
|
|
|
*
|
|
|
|
* @return {Selection} selection
|
|
|
|
*/
|
|
|
|
|
|
|
|
moveToFocus() {
|
|
|
|
return this.merge({
|
|
|
|
anchorKey: this.focusKey,
|
|
|
|
anchorOffset: this.focusOffset
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Move the end point to the start point.
|
|
|
|
*
|
|
|
|
* @return {Selection} selection
|
|
|
|
*/
|
|
|
|
|
|
|
|
moveToStart() {
|
|
|
|
return this.isBackward
|
|
|
|
? this.merge({
|
|
|
|
anchorKey: this.focusKey,
|
|
|
|
anchorOffset: this.focusOffset,
|
|
|
|
isBackward: false
|
|
|
|
})
|
|
|
|
: this.merge({
|
|
|
|
focusKey: this.anchorKey,
|
|
|
|
focusOffset: this.anchorOffset,
|
|
|
|
isBackward: false
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Move the start point to the end point.
|
|
|
|
*
|
|
|
|
* @return {Selection} selection
|
|
|
|
*/
|
|
|
|
|
|
|
|
moveToEnd() {
|
|
|
|
return this.isBackward
|
|
|
|
? this.merge({
|
|
|
|
focusKey: this.anchorKey,
|
|
|
|
focusOffset: this.anchorOffset,
|
|
|
|
isBackward: false
|
|
|
|
})
|
|
|
|
: this.merge({
|
|
|
|
anchorKey: this.focusKey,
|
|
|
|
anchorOffset: this.focusOffset,
|
|
|
|
isBackward: false
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Move to the start of a `node`.
|
|
|
|
*
|
|
|
|
* @return {Selection} selection
|
|
|
|
*/
|
|
|
|
|
|
|
|
moveToStartOf(node) {
|
|
|
|
return this.merge({
|
|
|
|
anchorKey: node.key,
|
|
|
|
anchorOffset: 0,
|
|
|
|
focusKey: node.key,
|
|
|
|
focusOffset: 0,
|
|
|
|
isBackward: false
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Move to the end of a `node`.
|
|
|
|
*
|
|
|
|
* @return {Selection} selection
|
|
|
|
*/
|
|
|
|
|
|
|
|
moveToEndOf(node) {
|
|
|
|
return this.merge({
|
|
|
|
anchorKey: node.key,
|
|
|
|
anchorOffset: node.length,
|
|
|
|
focusKey: node.key,
|
|
|
|
focusOffset: node.length,
|
|
|
|
isBackward: false
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Move to the entire range of a `node`.
|
|
|
|
*
|
|
|
|
* @return {Selection} selection
|
|
|
|
*/
|
|
|
|
|
|
|
|
moveToRangeOf(node) {
|
|
|
|
return this.merge({
|
|
|
|
anchorKey: node.key,
|
|
|
|
anchorOffset: 0,
|
|
|
|
focusKey: node.key,
|
|
|
|
focusOffset: node.length,
|
|
|
|
isBackward: false
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Move the selection forward `n` characters.
|
|
|
|
*
|
2016-06-17 13:34:29 -07:00
|
|
|
* @param {Number} n (optional)
|
2016-06-17 00:09:54 -07:00
|
|
|
* @return {Selection} selection
|
|
|
|
*/
|
|
|
|
|
|
|
|
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
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Move the selection backward `n` characters.
|
|
|
|
*
|
2016-06-17 13:34:29 -07:00
|
|
|
* @param {Number} n (optional)
|
2016-06-17 00:09:54 -07:00
|
|
|
* @return {Selection} selection
|
|
|
|
*/
|
|
|
|
|
|
|
|
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
|
|
|
|
})
|
2016-06-15 12:07:12 -07:00
|
|
|
}
|
|
|
|
|
2016-06-17 13:34:29 -07:00
|
|
|
/**
|
|
|
|
* Extend the focus point forward `n` characters.
|
|
|
|
*
|
|
|
|
* @param {Number} n (optional)
|
|
|
|
* @return {Selection} selection
|
|
|
|
*/
|
|
|
|
|
|
|
|
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
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Extend the focus point backward `n` characters.
|
|
|
|
*
|
|
|
|
* @param {Number} n (optional)
|
|
|
|
* @return {Selection} selection
|
|
|
|
*/
|
|
|
|
|
|
|
|
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
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Extend the focus forward 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.')
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.merge({
|
|
|
|
focusKey: node.key,
|
|
|
|
focusOffset: 0,
|
|
|
|
isBackward: false
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Extend the focus backward to the start 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.')
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2016-06-15 12:07:12 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Export.
|
|
|
|
*/
|
|
|
|
|
|
|
|
export default Selection
|