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.

368 lines (317 loc) 9.94 kB
import { isArrayEqual, isNumber } from '../util' import Selection from './Selection' import Coordinate from './Coordinate' /** A selection which is bound to a property. Implements {@link model/Selection}. @example ```js var propSel = doc.createSelection({ type: 'property', path: ['p1', 'content'], startOffset: 3, endOffset: 6 }) */ class PropertySelection extends Selection { /** @param {array} path @param {int} startOffset @param {int} endOffset @param {bool} reverse @param {string} [containerId] @param {string} [surfaceId] */ constructor(path, startOffset, endOffset, reverse, containerId, surfaceId) { super() if (arguments.length === 1) { let data = arguments[0] path = data.path startOffset = data.startOffset endOffset = data.endOffset reverse = data.reverse containerId = data.containerId surfaceId = data.surfaceId } if (!path || !isNumber(startOffset)) { throw new Error('Invalid arguments: `path` and `startOffset` are mandatory'); } this.start = new Coordinate(path, startOffset) this.end = new Coordinate(path, isNumber(endOffset) ? endOffset : startOffset) /** Selection direction. @type {Boolean} */ this.reverse = Boolean(reverse) this.containerId = containerId /** Identifier of the surface this selection should be active in. @type {String} */ this.surfaceId = surfaceId; } get path() { return this.start.path } get startOffset() { console.warn('DEPRECATED: Use sel.start.offset instead') return this.start.offset } get endOffset() { console.warn('DEPRECATED: Use sel.end.offset instead') return this.end.offset } /** Convert container selection to JSON. @returns {Object} */ toJSON() { return { type: 'property', path: this.start.path, startOffset: this.start.offset, endOffset: this.end.offset, reverse: this.reverse, containerId: this.containerId, surfaceId: this.surfaceId } } isPropertySelection() { return true } getType() { return 'property' } isNull() { return false } isCollapsed() { return this.start.offset === this.end.offset; } isReverse() { return this.reverse } equals(other) { return ( Selection.prototype.equals.call(this, other) && (this.start.equals(other.start) && this.end.equals(other.end)) ) } toString() { /* istanbul ignore next */ return [ "PropertySelection(", JSON.stringify(this.path), ", ", this.start.offset, " -> ", this.end.offset, (this.reverse?", reverse":""), (this.surfaceId?(", "+this.surfaceId):""), ")" ].join('') } /** Collapse a selection to chosen direction. @param {String} direction either left of right @returns {PropertySelection} */ collapse(direction) { var offset if (direction === 'left') { offset = this.start.offset; } else { offset = this.end.offset; } return this.createWithNewRange(offset, offset) } // Helper Methods // ---------------------- /** Get path of a selection, e.g. target property where selected data is stored. @returns {String[]} path */ getPath() { return this.start.path; } getNodeId() { return this.start.path[0]; } /** Checks if this selection is inside another one. @param {Selection} other @param {Boolean} [strict] true if should check that it is strictly inside the other @returns {Boolean} */ isInsideOf(other, strict) { if (other.isNull()) return false if (other.isContainerSelection()) { return other.contains(this, strict) } if (strict) { return (isArrayEqual(this.path, other.path) && this.start.offset > other.start.offset && this.end.offset < other.end.offset); } else { return (isArrayEqual(this.path, other.path) && this.start.offset >= other.start.offset && this.end.offset <= other.end.offset); } } /** Checks if this selection contains another one. @param {Selection} other @param {Boolean} [strict] true if should check that it is strictly contains the other @returns {Boolean} */ contains(other, strict) { if (other.isNull()) return false return other.isInsideOf(this, strict) } /** Checks if this selection overlaps another one. @param {Selection} other @param {Boolean} [strict] true if should check that it is strictly overlaps the other @returns {Boolean} */ overlaps(other, strict) { if (other.isNull()) return false if (other.isContainerSelection()) { // console.log('PropertySelection.overlaps: delegating to ContainerSelection.overlaps...') return other.overlaps(this) } if (!isArrayEqual(this.path, other.path)) return false if (strict) { return (! (this.start.offset>=other.end.offset||this.end.offset<=other.start.offset) ); } else { return (! (this.start.offset>other.end.offset||this.end.offset<other.start.offset) ); } } /** Checks if this selection has the right boundary in common with another one. @param {Selection} other @returns {Boolean} */ isRightAlignedWith(other) { if (other.isNull()) return false if (other.isContainerSelection()) { // console.log('PropertySelection.isRightAlignedWith: delegating to ContainerSelection.isRightAlignedWith...') return other.isRightAlignedWith(this) } return (isArrayEqual(this.path, other.path) && this.end.offset === other.end.offset); } /** Checks if this selection has the left boundary in common with another one. @param {Selection} other @returns {Boolean} */ isLeftAlignedWith(other) { if (other.isNull()) return false if (other.isContainerSelection()) { // console.log('PropertySelection.isLeftAlignedWith: delegating to ContainerSelection.isLeftAlignedWith...') return other.isLeftAlignedWith(this) } return (isArrayEqual(this.path, other.path) && this.start.offset === other.start.offset); } /** Expands selection to include another selection. @param {Selection} other @returns {Selection} a new selection */ expand(other) { if (other.isNull()) return this // if the other is a ContainerSelection // we delegate to that implementation as it is more complex // and can deal with PropertySelections, too if (other.isContainerSelection()) { return other.expand(this) } if (!isArrayEqual(this.path, other.path)) { throw new Error('Can not expand PropertySelection to a different property.') } var newStartOffset = Math.min(this.start.offset, other.start.offset); var newEndOffset = Math.max(this.end.offset, other.end.offset); return this.createWithNewRange(newStartOffset, newEndOffset); } /** Creates a new selection by truncating this one by another selection. @param {Selection} other @returns {Selection} a new selection */ truncateWith(other) { if (other.isNull()) return this 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 } var otherStartOffset, otherEndOffset if (other.isPropertySelection()) { otherStartOffset = other.start.offset; otherEndOffset = other.end.offset; } else if (other.isContainerSelection()) { // either the startPath or the endPath must be the same if (isArrayEqual(other.start.path, this.start.path)) { otherStartOffset = other.start.offset; } else { otherStartOffset = this.start.offset; } if (isArrayEqual(other.end.path, this.start.path)) { otherEndOffset = other.end.offset; } else { otherEndOffset = this.end.offset; } } else { return this } var newStartOffset; var newEndOffset; if (this.start.offset > otherStartOffset && this.end.offset > otherEndOffset) { newStartOffset = otherEndOffset; newEndOffset = this.end.offset; } else if (this.start.offset < otherStartOffset && this.end.offset < otherEndOffset) { newStartOffset = this.start.offset; newEndOffset = otherStartOffset; } else if (this.start.offset === otherStartOffset) { if (this.end.offset <= otherEndOffset) { return Selection.nullSelection; } else { newStartOffset = otherEndOffset; newEndOffset = this.end.offset; } } else if (this.end.offset === otherEndOffset) { if (this.start.offset >= otherStartOffset) { return Selection.nullSelection; } else { newStartOffset = this.start.offset; newEndOffset = otherStartOffset; } } else if (other.contains(this)) { return Selection.nullSelection } else { // FIXME: if this happens, we have a bug somewhere above throw new Error('Illegal state.') } return this.createWithNewRange(newStartOffset, newEndOffset) } /** Creates a new selection with given range and same path. @param {Number} startOffset @param {Number} endOffset @returns {Selection} a new selection */ createWithNewRange(startOffset, endOffset) { var sel = new PropertySelection(this.path, startOffset, endOffset, false, this.containerId, this.surfaceId) var doc = this._internal.doc if (doc) { sel.attach(doc) } return sel } _clone() { return new PropertySelection(this.start.path, this.start.offset, this.end.offset, this.reverse, this.containerId, this.surfaceId); } } PropertySelection.fromJSON = function(json) { return new PropertySelection(json) } export default PropertySelection