@typex-core_selection_index.js
import pluginContext from '../pluginContext'
import Range from './range'
/**
* @description 选区类
* @export
* @class Selection
*/
export default class Selection {
ranges = []
nativeSelection = pluginContext.platform.nativeSelection
constructor(editor) {
this.editor = editor
}
/**
* @description 选区是否折叠
* @readonly
* @memberof Selection
* @instance
*/
get collapsed () {
return this.ranges.every((range) => range.collapsed)
}
/**
* @description 选区范围数量
* @readonly
* @memberof Selection
* @instance
*/
get rangeCount () {
return this.ranges.length
}
/**
* @description 选区端点list
* @readonly
* @memberof Selection
* @instance
*/
get rangePoints () {
const points = []
this.ranges.forEach((range) => {
points.push(
{
container: range.startContainer,
offset: range.startOffset,
range,
pointName: 'start',
},
{
container: range.endContainer,
offset: range.endOffset,
range,
pointName: 'end',
}
)
})
return points
}
get rangesSnapshot () {
return this.ranges.map((range) => range.snapshot)
}
/**
* @description 清除范围选区
* @memberof Selection
* @instance
*/
clearRanges () {
while (this.ranges.length) {
this.ranges.pop().caret.remove()
}
}
/**
* @description 创建range
* @param {*} ops
* @returns {*}
* @memberof Selection
* @instance
*/
createRange (ops) {
return new Range(ops, this.editor)
}
/**
* @description 从原生range创建range
* @param {*} nativeRange
* @returns {*}
* @memberof Selection
* @instance
*/
createRangeFromNativeRange (nativeRange) {
const { startContainer, endContainer, startOffset, endOffset, collapsed } = nativeRange
const { focusNode, focusOffset } = this.nativeSelection
let d = 0
if (collapsed) {
d = 0
} else if (focusNode === endContainer && focusOffset === endOffset) {
d = 1
} else {
d = -1
}
return this.createRange({
startContainer: this.amendPathOffset(startContainer).path,
endContainer: this.amendPathOffset(endContainer).path,
startOffset,
endOffset,
d,
})
}
/**
* @description 增加range
* @param {*} range
* @memberof Selection
* @instance
*/
addRange (range) {
this.ranges.push(range)
}
/**
* @description 折叠选区
* @param {*} parentNode
* @param {*} offset
* @memberof Selection
* @instance
*/
collapse (parentNode, offset) {
this.nativeSelection.collapse(parentNode, offset)
this._resetRangesFromNative()
}
/**
* @description 路径查询
* @param {*} elm
* @returns {*}
* @memberof Selection
* @ignore
* @instance
*/
amendPathOffset (container, offset) {
const path = this.editor.queryPath(container)
if (path) {
if (path.isLeaf && path.dataType !== 'string') {
return this.amendPathOffset(path.elm.parentNode, path.index + 1)
}
return { path, offset }
} else {
return this.amendPathOffset(container.parentNode, offset)
}
}
/**
* @description 选区转化,修正鼠标点击的落点
* @param {*} nativeRange
* @returns {*}
* @memberof Selection
* @ignore
* @instance
*/
amendRange (nativeRange) {
const { startContainer, endContainer, startOffset, endOffset } = nativeRange
const startPathOffset = this.amendPathOffset(startContainer, startOffset)
const endPathOffset = this.amendPathOffset(endContainer, endOffset)
nativeRange.setStart(startPathOffset.path.elm, startPathOffset.offset)
nativeRange.setEnd(endPathOffset.path.elm, endPathOffset.offset)
return nativeRange
}
/**
* @description 从native重新设置选区
* @memberof Selection
* @ignore
* @private
* @instance
*/
_resetRangesFromNative () {
this.clearRanges()
const count = this.nativeSelection.rangeCount
for (let i = 0; i < count; i++) {
const nativeRange = this.amendRange(this.nativeSelection.getRangeAt(i))
if (!nativeRange) return
this.addRange(this.createRangeFromNativeRange(nativeRange))
}
}
/**
* @description 从native选区扩增,多选区支持
* @memberof Selection
* @ignore
* @instance
*/
_extendRangesFromNative () {
const count = this.nativeSelection.rangeCount
if (count > 0) {
const nativeRange = this.amendRange(this.nativeSelection.getRangeAt(count - 1))
if (!nativeRange) return
let flag = false
this.ranges.forEach((i) => {
if (
i.endContainer === nativeRange.endContainer &&
i.startOffset === nativeRange.startOffset
) {
flag = true
i.remove()
}
})
if (flag) return
this.addRange(this.createRangeFromNativeRange(nativeRange))
}
}
/**
* @description 获取第index个range
* @param {number} [index=0]
* @returns {*}
* @memberof Selection
* @instance
*/
getRangeAt (index = 0) {
return this.ranges[index]
}
/**
* @description 移除range并且清除原生range
* @memberof Selection
* @instance
*/
removeAllRanges () {
this.nativeSelection.removeAllRanges()
this.clearRanges()
}
/**
* @description 创建原生range
* @param {*} { startContainer, startOffset, endContainer, endOffset }
* @returns {*}
* @memberof Selection
* @instance
*/
createNativeRange ({ startContainer, startOffset, endContainer, endOffset }) {
const range = document.createRange()
range.setStart(startContainer, startOffset)
range.setEnd(endContainer, endOffset)
return range
}
/**
* @description 在指定容器指定位置发生内容平移,该位置右侧的range锚点需要跟随平移
* @param {*} container 目标容器
* @param {*} position 位置
* @param {*} distance 平移距离,负左正右
* @param {*} newContainer 设置新容器
* @memberof Selection
* @instance
*/
updatePoints (container, position, distance, newContainer) {
this.rangePoints.forEach((point) => {
if (point.container === container && position <= point.offset) {
point.range[point.pointName + 'Offset'] += distance
if (newContainer) point.range[point.pointName + 'Container'] = newContainer
}
})
}
/**
* @description range更新 追加ranges或者重新设置ranges
* @param {*} multiple
* @memberof Selection
* @instance
*/
updateRangesFromNative (multiple) {
// 选区的创建结果需要在宏任务中获取.
setTimeout(() => {
if (multiple) {
// 不清除ranges,从nativeSelection增加ranges
this._extendRangesFromNative()
} else {
// 清除ranges,再从nativeSelection同步ranges
this._resetRangesFromNative()
}
this.updateCaret()
})
}
/**
* @description 光标视图更新
* @param {boolean} [drawCaret=true]
* @memberof Selection
* @instance
*/
updateCaret (drawCaret = true) {
this.ranges.forEach((range) => range.updateCaret(drawCaret))
this.rangeCount > 1 && this._distinct()
drawCaret && this.drawRangeBg()
}
/**
* @description 检查光标是否重叠
* @param {*} rectA
* @param {*} rectB
* @returns {*} {boolean}
* @memberof Selection
* @instance
* @ignore
*/
_isCoverd (rectA, rectB) {
return rectA.y < rectB.y
? rectA.y + rectA.height >= rectB.y + rectB.height
: rectB.y + rectB.height >= rectA.y + rectA.height
}
/**
* @description 光标高性能去重
* @memberof Selection
* @ignore
* @instance
*/
_distinct () {
let tempObj = {}
let length = this.ranges.length
if (length < 2) return
for (let index = 0; index < length; index++) {
const range = this.ranges[index]
const path = this.editor.queryPath(range.startContainer)
const key = `${path.position}-${range.caret.rect.x}-${range.caret.rect.y}`
if (!tempObj[key]) {
// 这里解决当两个光标在同一行又不在同一个节点上却又重合的情况,通常在跨行内节点会出现,这时应该当作重复光标去重
const covereds = Object.entries(tempObj).filter(
(item) => range.caret.rect.x === item[1].caret.rect.x
)
if (covereds.length === 0) {
tempObj[key] = range
} else if (this._isCoverd(range.caret.rect, covereds[0][1].caret.rect)) {
range.caret.remove()
this.ranges.splice(index, 1)
length--
index--
} else {
tempObj[key] = range
}
} else {
range.caret.remove()
this.ranges.splice(index, 1)
length--
index--
}
}
tempObj = null
}
/**
* @description 默认以第一个range同步到native来绘制拖蓝
* @param {*} range
* @memberof Selection
* @instance
*/
drawRangeBg (range) {
const currRange = range || this.getRangeAt(0)
if (!currRange) return
const { startContainer, startOffset, endContainer, endOffset } = currRange
this.nativeSelection.removeAllRanges()
const createNativeRangeOps = {
startContainer: startContainer.elm,
endContainer: endContainer.elm,
startOffset,
endOffset,
}
this.nativeSelection.addRange(this.createNativeRange(createNativeRangeOps))
}
/**
* @description 获取选中的叶子节点迭代器
* @returns {Iterator} 迭代器
* @memberof Selection
* @instance
*/
getLeafPaths (splitPath = true) {
if (this.collapsed) return []
const range = this.ranges[0]
let start,
end,
value,
done = false
if (range.collapsed) {
done = true
} else {
if (range.startOffset === 0) {
start = range.startContainer
} else if (range.startOffset === range.startContainer.length) {
start = range.startContainer.nextLeaf
} else if (!splitPath) {
start = range.startContainer
} else {
const startSplits = range.startContainer.split(range.startOffset)
this.updatePoints(
range.startContainer,
range.startOffset,
-range.startOffset,
startSplits[1]
)
start = startSplits[1]
}
if (range.endOffset === 0) {
end = range.endContainer.prevLeaf
} else if (range.endOffset === range.endContainer.length) {
end = range.endContainer
} else if (!splitPath) {
end = range.endContainer
} else {
const endSplits = range.endContainer.split(range.endOffset)
this.updatePoints(range.endContainer, range.endOffset + 1, -range.endOffset, endSplits[1])
end = endSplits[0]
}
}
value = start
return {
length: 0,
next: function () {
if (!done) {
const res = { value, done }
done = value === end
value = value.nextLeaf
this.length++
return res
} else {
return { value: undefined, done }
}
},
[Symbol.iterator]: function () {
return this
},
}
}
recoverRangesFromSnapshot (rangesSnapshot) {
this.removeAllRanges()
this.ranges = rangesSnapshot.map((jsonRange) =>
this.createRange({
startContainer: this.editor.queryPath(jsonRange.startContainer),
endContainer: this.editor.queryPath(jsonRange.endContainer),
startOffset: jsonRange.startOffset,
endOffset: jsonRange.endOffset,
d: jsonRange.d,
})
)
this.updateCaret()
}
}