UNPKG

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 systems.

380 lines (328 loc) 10 kB
import { isNil } from '../util' import Coordinate from './Coordinate' import Selection from './Selection' import PropertySelection from './PropertySelection' /** A selection spanning multiple nodes. @class @extends PropertySelection @example ```js let containerSel = doc.createSelection({ type: 'container', containerId: 'body', startPath: ['p1', 'content'], startOffset: 5, endPath: ['p3', 'content'], endOffset: 4, }) ``` */ class ContainerSelection extends Selection { constructor(containerId, startPath, startOffset, endPath, endOffset, reverse, surfaceId) { super() if (arguments.length === 1) { let data = arguments[0] containerId = data.containerId startPath = data.startPath startOffset = data.startOffset endPath = data.endPath endOffset = data.endOffset reverse = data.reverse surfaceId = data.surfaceId } /** @type {String} */ this.containerId = containerId if (!this.containerId) throw new Error('Invalid arguments: `containerId` 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', containerId: this.containerId, 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) && this.containerId === other.containerId && (this.start.equals(other.start) && this.end.equals(other.end)) ) } toString() { /* istanbul ignore next */ return [ "ContainerSelection(", this.containerId, ", ", JSON.stringify(this.start.path), ", ", this.start.offset, " -> ", JSON.stringify(this.end.path), ", ", this.end.offset, (this.reverse?", reverse":""), (this.surfaceId?(", "+this.surfaceId):""), ")" ].join('') } /** @return {model/Container} The container node instance for this selection. */ getContainer() { if (!this._internal.container) { this._internal.container = this.getDocument().get(this.containerId) } return this._internal.container } isInsideOf(other, strict) { // Note: this gets called from PropertySelection.contains() // because this implementation can deal with mixed selection types. if (other.isNull()) return false strict = Boolean(strict) let r1 = this._range(this) let r2 = this._range(other) return (r2.start.isBefore(r1.start, strict) && r1.end.isBefore(r2.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 strict = Boolean(strict) let r1 = this._range(this) let r2 = this._range(other) return (r1.start.isBefore(r2.start, strict) && r2.end.isBefore(r1.end, strict)) } containsNode(nodeId, strict) { const container = this.getContainer() if (!container.contains(nodeId)) return false const coor = new Coordinate([nodeId], 0) const address = container.getAddress(coor) const r = this._range(this) // console.log('ContainerSelection.containsNode()', address, 'is within', r.start, '->', r.end, '?') let contained = r.start.isBefore(address, strict) if (contained) { address.offset = 1 contained = r.end.isAfter(address, strict) } return contained } overlaps(other) { let r1 = this._range(this) let r2 = this._range(other) // it overlaps if they are not disjunct return !(r1.end.isBefore(r2.start, false) || r2.end.isBefore(r1.start, false)) } isLeftAlignedWith(other) { let r1 = this._range(this) let r2 = this._range(other) return r1.start.isEqual(r2.start) } isRightAlignedWith(other) { let r1 = this._range(this) let r2 = this._range(other) return r1.end.isEqual(r2.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 r1 = this._range(this) let r2 = this._range(other) let start let end if (r1.start.isEqual(r2.start)) { start = new Coordinate(this.start.path, Math.min(this.start.offset, other.start.offset)) } else if (r1.start.isAfter(r2.start)) { start = new Coordinate(other.start.path, other.start.offset) } else { start = this.start } if (r1.end.isEqual(r2.end)) { end = new Coordinate(this.end.path, Math.max(this.end.offset, other.end.offset)) } else if (r1.end.isBefore(r2.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 r1 = this._range(this) let r2 = this._range(other) let start, end if (r2.start.isBefore(r1.start, 'strict') && r2.end.isBefore(r1.end, 'strict')) { start = other.end end = this.end } else if (r1.start.isBefore(r2.start, 'strict') && r1.end.isBefore(r2.end, 'strict')) { start = this.start end = other.start } else if (r1.start.isEqual(r2.start)) { if (r2.end.isBefore(r1.end, 'strict')) { start = other.end end = this.end } else { // the other selection is larger which eliminates this one return Selection.nullSelection } } else if (r1.end.isEqual(r2.end)) { if (r1.start.isBefore(r2.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) } /** Get the node ids covered by this selection. @returns {String[]} an array of ids */ getNodeIds() { const container = this.getContainer() const startPos = container.getPosition(this.start.path[0]) const endPos = container.getPosition(this.end.path[0]) return container.getContent().slice(startPos, endPos+1) } /** Splits a container selection into property selections. @returns {PropertySelection[]} */ splitIntoPropertySelections() { let sels = [] let fragments = this.getFragments() fragments.forEach(function(fragment) { if (fragment instanceof Selection.Fragment) { sels.push( new PropertySelection(fragment.path, fragment.startOffset, fragment.endOffset, false, this.containerId, this.surfaceId) ) } }.bind(this)) return sels } _clone() { return new ContainerSelection(this) } _range(sel) { // EXPERIMENTAL: caching the internal address based range // as we use it very often. // However, this is dangerous as this data can get invalid by a change if (sel._internal.addressRange) { return sel._internal.addressRange } let container = this.getContainer() let startAddress = container.getAddress(sel.start) let endAddress if (sel.isCollapsed()) { endAddress = startAddress } else { endAddress = container.getAddress(sel.end) } let addressRange = { start: startAddress, end: endAddress } if (sel._isContainerSelection) { sel._internal.addressRange = addressRange } return addressRange } get path() { throw new Error('ContainerSelection has no path property. Use startPath and endPath instead') } } ContainerSelection.prototype._isContainerSelection = true ContainerSelection.fromJSON = function(properties) { let 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, containerId: containerSel.containerId, surfaceId: containerSel.surfaceId }) } else { newSel = new ContainerSelection(containerSel.containerId, 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 } export default ContainerSelection