1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-02-25 01:33:37 +01:00
slate/lib/models/selection.js

453 lines
9.0 KiB
JavaScript
Raw Normal View History

2016-06-15 12:07:12 -07:00
import { Record } from 'immutable'
/**
* Record.
*/
2016-06-23 10:43:36 -07:00
const DEFAULTS = {
2016-06-15 12:07:12 -07:00
anchorKey: null,
anchorOffset: 0,
focusKey: null,
focusOffset: 0,
isBackward: false,
2016-06-15 20:13:02 -07:00
isFocused: false
2016-06-23 10:43:36 -07:00
}
2016-06-15 12:07:12 -07:00
/**
* Selection.
*/
2016-06-23 10:43:36 -07:00
class Selection extends Record(DEFAULTS) {
2016-06-15 12:07:12 -07:00
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 = {}) {
2016-06-23 10:43:36 -07:00
if (properties instanceof Selection) return properties
2016-06-17 18:20:26 -07:00
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.
*
* @return {Boolean} isExpanded
*/
get isExpanded() {
return ! this.isCollapsed
}
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
}
/**
* 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)
// If the anchor node isn't a text node, match it to one.
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)
anchorOffset = anchorOffset - offset
anchorKey = anchorNode.key
}
// If the focus node isn't a text node, match it to one.
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)
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 focus point to the anchor point.
*
* @return {Selection} selection
*/
moveToAnchor() {
return this.merge({
focusKey: this.anchorKey,
2016-06-22 18:59:19 -07:00
focusOffset: this.anchorOffset,
isBackward: false
2016-06-17 00:09:54 -07:00
})
}
/**
* Move the anchor point to the focus point.
*
* @return {Selection} selection
*/
moveToFocus() {
return this.merge({
anchorKey: this.focusKey,
2016-06-22 18:59:19 -07:00
anchorOffset: this.focusOffset,
isBackward: false
2016-06-17 00:09:54 -07:00
})
}
/**
* 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