substance
Version:
Substance is a JavaScript library for web-based content editing. It provides building blocks for realizing custom text editors and web-based publishing system. It is developed to power our online editing platform [Substance](http://substance.io).
325 lines (284 loc) • 8.99 kB
JavaScript
import isArrayEqual from '../util/isArrayEqual'
import isNil from '../util/isNil'
import Coordinate from './Coordinate'
import Selection from './Selection'
import PropertySelection from './PropertySelection'
import compareCoordinates from './_compareCoordinates'
import isCoordinateBefore from './_isCoordinateBefore'
/**
* A selection spanning multiple nodes.
*
*
* @example
*
* ```js
* let containerSel = doc.createSelection({
* type: 'container',
* containerPath: 'body',
* startPath: ['p1', 'content'],
* startOffset: 5,
* endPath: ['p3', 'content'],
* endOffset: 4,
* })
* ```
*/
export default class ContainerSelection extends Selection {
constructor (containerPath, startPath, startOffset, endPath, endOffset, reverse, surfaceId) {
super()
if (arguments.length === 1) {
const data = arguments[0]
containerPath = data.containerPath
startPath = data.startPath
startOffset = data.startOffset
endPath = data.endPath
endOffset = data.endOffset
reverse = data.reverse
surfaceId = data.surfaceId
}
/**
@type {String}
*/
this.containerPath = containerPath
if (!this.containerPath) throw new Error('Invalid arguments: `containerPath` is mandatory')
this.start = new Coordinate(startPath, startOffset)
this.end = new Coordinate(isNil(endPath) ? startPath : endPath, isNil(endOffset) ? startOffset : endOffset)
this.reverse = Boolean(reverse)
this.surfaceId = surfaceId
}
/* istanbul ignore start */
get startPath () {
console.warn('DEPRECATED: use sel.start.path instead.')
return this.start.path
}
get startOffset () {
console.warn('DEPRECATED: use sel.start.offset instead.')
return this.start.offset
}
get endPath () {
console.warn('DEPRECATED: use sel.end.path instead.')
return this.end.path
}
get endOffset () {
console.warn('DEPRECATED: use sel.end.offset instead.')
return this.end.offset
}
/* istanbul ignore end */
toJSON () {
return {
type: 'container',
containerPath: this.containerPath,
startPath: this.start.path,
startOffset: this.start.offset,
endPath: this.end.path,
endOffset: this.end.offset,
reverse: this.reverse,
surfaceId: this.surfaceId
}
}
isContainerSelection () {
return true
}
getType () {
return 'container'
}
isNull () {
return false
}
isCollapsed () {
return this.start.equals(this.end)
}
isReverse () {
return this.reverse
}
equals (other) {
return (
Selection.prototype.equals.call(this, other) &&
isArrayEqual(this.containerPath, other.containerPath) &&
(this.start.equals(other.start) && this.end.equals(other.end))
)
}
toString () {
/* istanbul ignore next */
return [
'ContainerSelection(',
this.containerPath, ', ',
JSON.stringify(this.start.path), ', ', this.start.offset,
' -> ',
JSON.stringify(this.end.path), ', ', this.end.offset,
(this.reverse ? ', reverse' : ''),
(this.surfaceId ? (', ' + this.surfaceId) : ''),
')'
].join('')
}
isInsideOf (other, strict) {
// Note: this gets called from PropertySelection.contains()
// because this implementation can deal with mixed selection types.
if (other.isNull()) return false
return (
this._isCoordinateBefore(other.start, this.start, strict) &&
this._isCoordinateBefore(this.end, other.end, strict)
)
}
contains (other, strict) {
// Note: this gets called from PropertySelection.isInsideOf()
// because this implementation can deal with mixed selection types.
if (other.isNull()) return false
return (
this._isCoordinateBefore(this.start, other.start, strict) &&
this._isCoordinateBefore(other.end, this.end, strict)
)
}
containsNode (nodeId) {
const containerPath = this.containerPath
const doc = this.getDocument()
const nodeCoor = new Coordinate([nodeId], 0)
const cmpStart = compareCoordinates(doc, containerPath, nodeCoor, this.start)
const cmpEnd = compareCoordinates(doc, containerPath, nodeCoor, this.end)
// HACK: trying to get this working
// the coor created is always ([nodeId], 0)
// The node is considered inside the selection if this coor >= start and coor < end
return cmpStart >= 0 && cmpEnd < 0
}
overlaps (other) {
// it overlaps if they are not disjunct
return (
!this._isCoordinateBefore(this.end, other.start, false) ||
this._isCoordinateBefore(other.end, this.start, false)
)
}
isLeftAlignedWith (other) {
return this.start.isEqual(other.start)
}
isRightAlignedWith (other) {
return this.end.isEqual(other.end)
}
/**
* Collapse a selection to chosen direction.
*
* @param {String} direction either left of right
* @returns {PropertySelection}
*/
collapse (direction) {
let coor
if (direction === 'left') {
coor = this.start
} else {
coor = this.end
}
return _createNewSelection(this, coor, coor)
}
expand (other) {
let start
let end
if (this.start.isEqual(other.start)) {
start = new Coordinate(this.start.path, Math.min(this.start.offset, other.start.offset))
} else if (this._isCoordinateBefore(other.start, this.start, false)) {
start = new Coordinate(other.start.path, other.start.offset)
} else {
start = this.start
}
if (this.end.isEqual(other.end)) {
end = new Coordinate(this.end.path, Math.max(this.end.offset, other.end.offset))
} else if (this._isCoordinateBefore(this.end, other.end, false)) {
end = new Coordinate(other.end.path, other.end.offset)
} else {
end = this.end
}
return _createNewSelection(this, start, end)
}
truncateWith (other) {
if (other.isInsideOf(this, 'strict')) {
// the other selection should overlap only on one side
throw new Error('Can not truncate with a contained selections')
}
if (!this.overlaps(other)) {
return this
}
let start, end
if (this._isCoordinateBefore(other.start, this.start, 'strict') && this._isCoordinateBefore(other.end, this.end, 'strict')) {
start = other.end
end = this.end
} else if (this._isCoordinateBefore(this.start, other.start, 'strict') && this._isCoordinateBefore(this.end, other.end, 'strict')) {
start = this.start
end = other.start
} else if (this.start.isEqual(other.start)) {
if (this._isCoordinateBefore(other.end, this.end, 'strict')) {
start = other.end
end = this.end
} else {
// the other selection is larger which eliminates this one
return Selection.nullSelection
}
} else if (this.end.isEqual(other.end)) {
if (this._isCoordinateBefore(this.start, other.start, 'strict')) {
start = this.start
end = other.start
} else {
// the other selection is larger which eliminates this one
return Selection.nullSelection
}
} else if (this.isInsideOf(other)) {
return Selection.nullSelection
} else {
throw new Error('Could not determine coordinates for truncate. Check input')
}
return _createNewSelection(this, start, end)
}
/**
* Splits a container selection into property selections.
*
* @returns {PropertySelection[]}
*/
splitIntoPropertySelections () {
const fragments = this.getFragments()
return fragments.filter(f => f instanceof Selection.Fragment).map(f => {
return new PropertySelection(f.path, f.startOffset,
f.endOffset, false, this.containerPath, this.surfaceId)
})
}
/**
* @return {Array} an array of ids.
*/
_getContainerContent () {
return this.getDocument().get(this.containerPath)
}
_clone () {
return new ContainerSelection(this)
}
_isCoordinateBefore (coor1, coor2, strict) {
const doc = this.getDocument()
return isCoordinateBefore(doc, this.containerPath, coor1, coor2, strict)
}
get path () {
throw new Error('ContainerSelection has no path property. Use startPath and endPath instead')
}
get _isContainerSelection () { return true }
static fromJSON (properties) {
const sel = new ContainerSelection(properties)
return sel
}
}
function _createNewSelection (containerSel, start, end) {
let newSel
if (start === end) {
newSel = new PropertySelection({
path: start.path,
startOffset: start.offset,
endOffset: start.offset,
containerPath: containerSel.containerPath,
surfaceId: containerSel.surfaceId
})
} else {
newSel = new ContainerSelection(
containerSel.containerPath,
start.path, start.offset, end.path, end.offset,
false, containerSel.surfaceId
)
}
// we need to attach the new selection
const doc = containerSel._internal.doc
if (doc) {
newSel.attach(doc)
}
return newSel
}