由于鼠标框选提及来大略,便是选择的内容,但是这包含很多中情形,比如:只选择文案、选择图片、选择输入框、输入框中的内容选择、iframe、等。
大略总结,分为以下几点:
选择文案时选择图片、svg、iframe、video、audio 等标签时选择 input、select、textarea 等标签时选择 input、textarea 标签内容时选择类似 字符时键盘全选时鼠标右键选择以上各模块结合时当包含标签的时候,返回 html 构造,只有文本时返回文本内容二、技能要点鼠标框选包含以下几点:
旧调重弹的技能点了,这里不能用节流,由于肯定不能你鼠标选择的时候,隔一段韶光返回一段内容,肯定是选择之后一起返回。
这里用 debounce 紧张也是用在事宜监听和事宜处理上。
【debounce 掘金】【debounce CSDN】2、addEventListener事宜监听,由于鼠标选择,不仅仅是鼠标按下到鼠标抬起,还包括双击、右键、全选。
须要利用事宜监听对事宜作处理。
【addEventListener MDN】3、RangeRange 接口表示一个包含节点与文本节点的一部分的文档片段。
Range 是浏览器原生的工具。
3.1. 创建 Range 实例,并设置起始位置
<body> <ul> <li>Vite</li> <li>Vue</li> <li>React</li> <li>VitePress</li> <li>NaiveUI</li> </ul></body><script> // 创建 Range 工具 const range = new Range() const liDoms = document.querySelectorAll("li"); // Range 起始位置在 li 2 range.setStartBefore(liDoms[1]); // Range 结束位置在 li 3 range.setEndAfter(liDoms[2]); // 获取 selection 工具 const selection = window.getSelection(); // 添加光标选择的范围 selection.addRange(range);</script>
可以看到,选择内容为第二行和第三行
3.1.1 浏览器兼容情形3.2. Range 属性startContainer:起始节点。startOffset:起始节点偏移量。endContainer:结束节点。endOffset:结束节点偏移量。collapsed:范围的开始和结束是否为同一点。commonAncestorContainer:返回完全包含 startContainer 和 endContainer 的最深一级的节点。3.2.1. 用我们上面创建的实例来看下 range 属性的值
3.2.2. 如果我们只选择文本内容时
只选择 li 中的 itePres
可以看出 range 属性对应的值
3.3. Range 方法cloneContents():复制范围内容,并将复制的内容作为 DocumentFragment 返回。cloneRange():创建一个具有相同出发点/终点的新范围, 非引用,可以随意改变,不会影响另一方。collapse(toStart):如果 toStart=true 则设置 end=start,否则设置 start=end,从而折叠范围。compareBoundaryPoints(how, sourceRange):两个范围边界点进行比较,返回一个数字 -1、0、1。comparePoint(referenceNode, offset):返回-1、0、1详细取决于 是 referenceNode 在 之前、相同还是之后。createContextualFragment(tagString):返回一个 DocumentFragment。deleteContents():删除框选的内容。extractContents():从文档中删除范围内容,并将删除的内容作为 DocumentFragment 返回。getBoundingClientRect():和 dom 一样,返回 DOMRect 工具。getClientRects():返回可迭代的工具序列 DOMRect。insertNode(node):在范围的起始处将 node 插入文档。intersectsNode(referenceNode):判断与给定的 node 是否相交。selectNode(node):设置范围以选择全体 node。selectNodeContents(node):设置范围以选择全体 node 的内容。setStart(startNode, startOffset):设置出发点。setEnd(endNode, endOffset):设置终点。setStartBefore(node):将出发点设置在 node 前面。setStartAfter(node):将出发点设置在 node 后面。setEndBefore(node):将终点设置为 node 前面。setEndAfter(node):将终点设置为 node 后面。surroundContents(node):利用 node 将所选范围内容包裹起来。3.4. 创建 Range 的方法3.4.1. Document.createRange
const range = document.createRange();
3.4.2. Selection 的 getRangeAt() 方法
const range = window.getSelection().getRangeAt(0)
3.4.3. caretRangeFromPoint() 方法
if (document.caretRangeFromPoint) { range = document.caretRangeFromPoint(e.clientX, e.clientY);}
3.4.4. Range() 布局函数
const range = new Range()
3.5. Range 兼容性
4、Selection
Selection 工具表示用户选择的文本范围或插入符号确当前位置。它代表页面中的文本选区,可能横跨多个元素。
4.1. 获取文本工具window.getSelection()
锚指的是一个选区的起始点(不同于 HTML 中的锚点链接)。当我们利用鼠标框选一个区域的时候,锚点便是我们鼠标按下瞬间的那个点。在用户拖动鼠标时,锚点是不会变的。
4.2.2. 焦点 (focus)选区的焦点是该选区的终点,当你用鼠标框选一个选区的时候,焦点是你的鼠标松开瞬间所记录的那个点。随着用户拖动鼠标,焦点的位置会随着改变。
4.2.3. 范围 (range)范围指的是文档中连续的一部分。一个范围包括全体节点,也可以包含节点的一部分,例如文本节点的一部分。用户常日下只能选择一个范围,但是有的时候用户也有可能选择多个范围。
4.2.4. 可编辑元素 (editing host)一个用户可编辑的元素(例如一个利用 contenteditable 的 HTML 元素,或是在启用了 designMode 的 Document 的子元素)。
4.3. Selection 的属性首先要清楚,选择的出发点称为锚点(anchor),终点称为焦点(focus)。
anchorNode:选择的起始节点。anchorOffset:选择开始的 anchorNode 中的偏移量。focusNode:选择的结束节点。focusOffset:选择开始处 focusNode 的偏移量。isCollapsed:如果未选择任何内容(空范围)或不存在,则为 true。rangeCount:选择中的范围数,之前说过,除 Firefox 外,其他浏览器最多为1。type:类型:None、Caret、Range4.4. Selection 方法addRange(range): 将一个 Range 工具添加到当前选区。collapse(node, offset): 将选区折叠到指定的节点和偏移位置。collapseToEnd(): 将选区折叠到当前选区的末端。collapseToStart(): 将选区折叠到当前选区的起始位置。containsNode(node, partlyContained): 判断选区是否包含指定的节点,可以选择是否部分包含。deleteFromDocument(): 从文档中删除选区内容。empty(): 从选区中移除所有范围(同 `removeAllRanges()``,已废弃)。extend(node, offset): 将选区的焦点节点扩展到指定的节点和偏移位置。getRangeAt(index): 返回选区中指定索引处的 Range 工具。removeAllRanges(): 移除所有选区中的范围。removeRange(range): 从选区中移除指定的 Range 工具。selectAllChildren(node): 选中指定节点的所有子节点。setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset): 设置选区的起始和结束节点及偏移位置。setPosition(node, offset):collapse 的别名4.5. Selection 兼容性三、项目实现1、实现思路先获取选择的内容,开拓 getSelectContent 函数对获取的内容进行判断,是否存在 selection 实例,没有直接返回 null判断 selection 实例的 isCollapsed 属性 没有选中,对 selection 进行 toString().trim() 操作,判断内容 有内容,直接返回 text 类型 无内容,返回 null 有选中,则判断内容判断选中的内容有没有节点 没有节点,则和没有选中一样处理,进行 toString().trim() 操作,判断内容 有内容,直接返回 text 类型 无内容,返回 null 有节点,进行 toString().trim() 操作,判断内容 没有内容,判断是否有分外节点 有 'iframe', 'svg', 'img', 'audio', 'video' 节点,返回 html 类型 有 'input', 'textarea', 'select',判断 value 值,是否存在 存在:返回 html 类型 不存在:返回 null 没有分外节点,返回 null 有内容,返回 html 类型对鼠标 mousedown、mouseup 事宜和 selectionchange、contextmenu、dblclick 事宜进行监听,触发 getSelectContent 函数在须要的地方进行 debounce 防抖处理2、大略单纯流程图
2、Debounce 方法实现2.1. JS
function debounce (fn, time = 500) { let timeout = null; // 创建一个标记用来存放定时器的返回值 return function () { clearTimeout(timeout) // 每当触发时,把前一个 定时器 clear 掉 timeout = setTimeout(() => { // 创建一个新的 定时器,并赋值给 timeout fn.apply(this, arguments) }, time) }}
2.2. TS
/ debounce 函数类型 /type DebouncedFunction<F extends (...args: any[]) => any> = (...args: Parameters<F>) => void/ debounce 防抖函数 @param {Function} func 函数 @param {number} wait 等待韶光 @param {false} immediate 是否立即实行 @returns {DebouncedFunction} /function debounce<F extends (...args: any[]) => any>( func: F, wait = 500, immediate = false): DebouncedFunction<F> { let timeout: ReturnType<typeof setTimeout> | null return function (this: ThisParameterType<F>, ...args: Parameters<F>) { // eslint-disable-next-line @typescript-eslint/no-this-alias const context = this const later = function () { timeout = null if (!immediate) { func.apply(context, args) } } const callNow = immediate && !timeout if (timeout) { clearTimeout(timeout) } timeout = setTimeout(later, wait) if (callNow) { func.apply(context, args) } }}
3、获取选择的文本/html 元素3.1. 获取文本/html 元素
nterface IGetSelectContentProps { type: 'html' | 'text' content: string}/ 获取选择的内容 @returns {null | IGetSelectContentProps} 返回选择的内容 /const getSelectContent = (): null | IGetSelectContentProps => { const selection = window.getSelection() if (selection) { // 1. 是焦点在 input 输入框 // 2. 没有选中 // 3. 选择的是输入框 if (selection.isCollapsed) { return selection.toString().trim().length ? { type: 'text', content: selection.toString().trim() } : null } // 获取选择范围 const range = selection.getRangeAt(0) // 获取选择内容 const rangeClone = range.cloneContents() // 判断选择内容里面有没有节点 if (rangeClone.childElementCount > 0) { // 创建 div 标签 const container = document.createElement('div') // div 标签 append 复制节点 container.appendChild(rangeClone) // 如果复制的内容长度为 0 if (!selection.toString().trim().length) { // 判断是否有选择分外节点 const isSpNode = hasSpNode(container) return isSpNode ? { type: 'html', content: container.innerHTML } : null } return { type: 'html', content: container.innerHTML } } else { return selection.toString().trim().length ? { type: 'text', content: selection.toString().trim() } : null } } else { return null }}/ 判断是否包含分外元素 @param {Element} parent 父元素 @returns {boolean} 是否包含分外元素 /const hasSpNode = (parent: Element): boolean => { const nodeNameList = ['iframe', 'svg', 'img', 'audio', 'video'] const inpList = ['input', 'textarea', 'select'] return Array.from(parent.children).some((node) => { if (nodeNameList.includes(node.nodeName.toLocaleLowerCase())) return true if ( inpList.includes(node.nodeName.toLocaleLowerCase()) && (node as HTMLInputElement).value.trim().length ) return true if (node.children) { return hasSpNode(node) } return false })}
3.2. 只须要文本
/ 获取框选的文案内容 @returns {string} 返回框选的内容 /const getSelectTextContent = (): string => { const selection = window.getSelection() return selection?.toString().trim() || ''}
4、添加事宜监听
// 是否时鼠标点击动作let selectionchangeMouseTrack: boolean = falseconst selectionChangeFun = debounce(() => { const selectContent = getSelectContent() console.log('selectContent', selectContent) // todo... 处理上报 selectionchangeMouseTrack = false})// 添加 mousedown 监听事宜document.addEventListener('mousedown', () => { selectionchangeMouseTrack = true})// 添加 mouseup 监听事宜document.addEventListener( 'mouseup', debounce(() => { selectionChangeFun() }, 100))// 添加 selectionchange 监听事宜document.addEventListener( 'selectionchange', debounce(() => { if (selectionchangeMouseTrack) return selectionChangeFun() }))// 添加 dblclick 监听事宜document.addEventListener('dblclick', () => { selectionChangeFun()})// 添加 contextmenu 监听事宜document.addEventListener( 'contextmenu', debounce(() => { selectionChangeFun() }))
也可以进行封装
/ addEventlistener function 类型 /export interface IEventHandlerProps { [eventName: string]: EventListenerOrEventListenerObject}let selectionchangeMouseTrack: boolean = falseconst eventHandlers: IEventHandlerProps = { // 鼠标 down 事宜 mousedown: () => { selectionchangeMouseTrack = true }, // 鼠标 up 事宜 mouseup: debounce(() => selectionChangeFun(), 100), // 选择事宜 selectionchange: debounce(() => { if (selectionchangeMouseTrack) return selectionChangeFun() }), // 双击事宜 dblclick: () => selectionChangeFun(), // 右键事宜 contextmenu: debounce(() => selectionChangeFun())}Object.keys(eventHandlers).forEach((event) => { document.addEventListener(event, eventHandlers[event])})
5、返回内容5.1. 纯文本内容
5.2. html 格式
6. 完全 JS 代码
function debounce (fn, time = 500) { let timeout = null; // 创建一个标记用来存放定时器的返回值 return function () { clearTimeout(timeout) // 每当触发时,把前一个 定时器 clear 掉 timeout = setTimeout(() => { // 创建一个新的 定时器,并赋值给 timeout fn.apply(this, arguments) }, time) }}let selectionchangeMouseTrack = falsedocument.addEventListener('mousedown', (e) => { selectionchangeMouseTrack = true console.log('mousedown', e)})document.addEventListener('mouseup', debounce((e) => { console.log('mouseup', e) selectionChangeFun()}, 100))document.addEventListener('selectionchange', debounce((e) => { console.log('selectionchange', e) if (selectionchangeMouseTrack) return selectionChangeFun()}))document.addEventListener('dblclick', (e) => { console.log('dblclick', e) selectionChangeFun()})document.addEventListener('contextmenu',debounce(() => { selectionChangeFun()}))const selectionChangeFun = debounce(() => { const selectContent = getSelectContent() selectionchangeMouseTrack = false console.log('selectContent', selectContent)})const getSelectContent = () => { const selection = window.getSelection(); if (selection) { // 1. 是焦点在 input 输入框 // 2. 没有选中 // 3. 选择的是输入框 if (selection.isCollapsed) { return selection.toString().trim().length ? { type: 'text', content: selection.toString().trim() } : null } // 获取选择范围 const range = selection.getRangeAt(0); // 获取选择内容 const rangeClone = range.cloneContents() // 判断选择内容里面有没有节点 if (rangeClone.childElementCount > 0) { const container = document.createElement('div'); container.appendChild(rangeClone); if (!selection.toString().trim().length) { const hasSpNode = getSpNode(container) return hasSpNode ? { type: 'html', content: container.innerHTML } : null } return { type: 'html', content: container.innerHTML } } else { return selection.toString().trim().length ? { type: 'text', content: selection.toString().trim() } : null } } else { return null }}const getSpNode = (parent) => { const nodeNameList = ['iframe', 'svg', 'img', 'audio', 'video'] const inpList = ['input', 'textarea', 'select'] return Array.from(parent.children).some((node) => { if (nodeNameList.includes(node.nodeName.toLocaleLowerCase())) return true if (inpList.includes(node.nodeName.toLocaleLowerCase()) && node.value.trim().length) return true if (node.children) { return getSpNode(node) } return false })}
四、总结鼠标框选上报能监控用户在页面的行为,能为后续的数据剖析等供应便利基于 JS 中的 Selection 和 Range 实现的,利用原生 JS涉及到的操作比较多,包含键盘、鼠标右键、全选等能对框选的内容进行分类,差异 html 和 text,更方便的看出用户选择了哪些内容