/**
* @description selection range API
* @author wangfupeng
*/
import $, { DomElement } from '../utils/dom-core'
import { UA } from '../utils/util'
import Editor from './index'
import SelectionRangeTopNodes from './selection-range-top-nodes/index'
class SelectionAndRange {
public editor: Editor
private _currentRange: Range | null | undefined = null
constructor(editor: Editor) {
this.editor = editor
}
/**
* 获取当前 range
*/
public getRange(): Range | null | undefined {
return this._currentRange
}
/**
* 保存选区范围
* @param _range 选区范围
*/
public saveRange(_range?: Range): void {
if (_range) {
// 保存已有选区
this._currentRange = _range
return
}
// 获取当前的选区
const selection = window.getSelection() as Selection
if (selection.rangeCount === 0) {
return
}
const range = selection.getRangeAt(0)
// 获取选区范围的 DOM 元素
const $containerElem = this.getSelectionContainerElem(range)
if (!$containerElem) {
// 当 选区范围内没有 DOM元素 则抛出
return
}
if (
$containerElem.attr('contenteditable') === 'false' ||
$containerElem.parentUntil('[contenteditable=false]')
) {
// 这里大体意义上就是个保险
// 确保 编辑区域 的 contenteditable属性 的值为 true
return
}
const editor = this.editor
const $textElem = editor.$textElem
if ($textElem.isContain($containerElem)) {
if ($textElem.elems[0] === $containerElem.elems[0]) {
if ($textElem.html() === '
') {
const $children = $textElem.children()
const $last = $children?.last()
editor.selection.createRangeByElem($last as DomElement, true, true)
editor.selection.restoreSelection()
}
}
// 是编辑内容之内的
this._currentRange = range
}
}
/**
* 折叠选区范围
* @param toStart true 开始位置,false 结束位置
*/
public collapseRange(toStart: boolean = false): void {
const range = this._currentRange
if (range) {
range.collapse(toStart)
}
}
/**
* 获取选区范围内的文字
*/
public getSelectionText(): string {
const range = this._currentRange
if (range) {
return range.toString()
} else {
return ''
}
}
/**
* 获取选区范围的 DOM 元素
* @param range 选区范围
*/
public getSelectionContainerElem(range?: Range): DomElement | undefined {
let r: Range | null | undefined
r = range || this._currentRange
let elem: Node
if (r) {
elem = r.commonAncestorContainer
return $(elem.nodeType === 1 ? elem : elem.parentNode)
}
}
/**
* 选区范围开始的 DOM 元素
* @param range 选区范围
*/
public getSelectionStartElem(range?: Range): DomElement | undefined {
let r: Range | null | undefined
r = range || this._currentRange
let elem: Node
if (r) {
elem = r.startContainer
return $(elem.nodeType === 1 ? elem : elem.parentNode)
}
}
/**
* 选区范围结束的 DOM 元素
* @param range 选区范围
*/
public getSelectionEndElem(range?: Range): DomElement | undefined {
let r: Range | null | undefined
r = range || this._currentRange
let elem: Node
if (r) {
elem = r.endContainer
return $(elem.nodeType === 1 ? elem : elem.parentNode)
}
}
/**
* 选区是否为空(没有选择文字)
*/
public isSelectionEmpty(): boolean {
const range = this._currentRange
if (range && range.startContainer) {
if (range.startContainer === range.endContainer) {
if (range.startOffset === range.endOffset) {
return true
}
}
}
return false
}
/**
* 恢复选区范围
*/
public restoreSelection(): void {
const selection = window.getSelection()
const r = this._currentRange
if (selection && r) {
selection.removeAllRanges()
selection.addRange(r)
}
}
/**
* 创建一个空白(即 字符)选区
*/
public createEmptyRange(): void {
const editor = this.editor
const range = this.getRange()
let $elem: DomElement
if (!range) {
// 当前无 range
return
}
if (!this.isSelectionEmpty()) {
// 当前选区必须没有内容才可以,有内容就直接 return
return
}
try {
// 目前只支持 webkit 内核
if (UA.isWebkit()) {
// 插入
editor.cmd.do('insertHTML', '')
// 修改 offset 位置
range.setEnd(range.endContainer, range.endOffset + 1)
// 存储
this.saveRange(range)
} else {
$elem = $('')
editor.cmd.do('insertElem', $elem)
this.createRangeByElem($elem, true)
}
} catch (ex) {
// 部分情况下会报错,兼容一下
}
}
/**
* 根据 DOM 元素设置选区
* @param $elem DOM 元素
* @param toStart true 开始位置,false 结束位置
* @param isContent 是否选中 $elem 的内容
*/
public createRangeByElem($elem: DomElement, toStart?: boolean, isContent?: boolean): void {
if (!$elem.length) {
return
}
const elem = $elem.elems[0]
const range = document.createRange()
if (isContent) {
range.selectNodeContents(elem)
} else {
// 如果用户没有传入 isContent 参数,那就默认为 false
range.selectNode(elem)
}
if (toStart != null) {
// 传入了 toStart 参数,折叠选区。如果没传入 toStart 参数,则忽略这一步
range.collapse(toStart)
}
// 存储 range
this.saveRange(range)
}
/**
* 获取 当前 选取范围的 顶级(段落) 元素
* @param $editor
*/
public getSelectionRangeTopNodes(editor: Editor): DomElement[] {
const item = new SelectionRangeTopNodes(editor)
item.init()
return item.getSelectionNodes()
}
/**
* 移动光标位置
* @param {Node} node 元素节点
* @param {Boolean} toStart 为true光标在开始位置 为false在结束位置 默认在结束位置
*/
public moveCursor(node: Node, toStart: boolean = false) {
const range = this.getRange()
const pos = toStart ? 0 : node.childNodes.length
if (!range) {
return
}
if (node) {
range.setStart(node, pos)
range.setEnd(node, pos)
this.restoreSelection()
}
}
}
export default SelectionAndRange