<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      JS 實(shí)現(xiàn)鼠標(biāo)框選(頁面選擇)時返回對應(yīng)的代碼或文本內(nèi)容

      JS 實(shí)現(xiàn)鼠標(biāo)框選(頁面選擇)時返回對應(yīng)的代碼或文案內(nèi)容

      一、需求背景

      1、項(xiàng)目需求

      當(dāng)用戶進(jìn)行鼠標(biāo)框選選擇了頁面上的內(nèi)容時,把選擇的內(nèi)容進(jìn)行上報。

      2、需求解析

      雖然這需求就一句話的事,但是很顯然,沒那么簡單...

      因?yàn)槭髽?biāo)框選說起來簡單,就是選擇的內(nèi)容,但是這包含很多中情況,比如:只選擇文案、選擇圖片、選擇輸入框、輸入框中的內(nèi)容選擇、iframe、等。

      簡單總結(jié),分為以下幾點(diǎn):

      1. 選擇文案時

      2. 選擇圖片、svgiframevideoaudio 等標(biāo)簽時

      3. 選擇 input、select、textarea 等標(biāo)簽時

      4. 選擇 input、textarea 標(biāo)簽內(nèi)容時

      5. 選擇類似 &nbsp; 字符時

      6. 鍵盤全選時

      7. 鼠標(biāo)右鍵選擇

      8. 以上各模塊結(jié)合時

      9. 當(dāng)包含標(biāo)簽的時候,返回 html 結(jié)構(gòu),只有文本時返回文本內(nèi)容

      二、技術(shù)要點(diǎn)

      鼠標(biāo)框選包含以下幾點(diǎn):

      1. debounce 防抖

      2. addEventListener 事件監(jiān)聽

      3. Range 對象

      4. Selection 對象

      1、debounce

      老生常談的技術(shù)點(diǎn)了,這里不能用節(jié)流,因?yàn)榭隙ú荒苣闶髽?biāo)選擇的時候,隔一段時間返回一段內(nèi)容,肯定是選擇之后一起返回。

      這里用 debounce 主要也是用在事件監(jiān)聽和事件處理上。

      2、addEventListener

      事件監(jiān)聽,因?yàn)槭髽?biāo)選擇,不僅僅是鼠標(biāo)按下到鼠標(biāo)抬起,還包括雙擊、右鍵、全選。

      需要使用事件監(jiān)聽對事件作處理。

      3、Range

      Range 接口表示一個包含節(jié)點(diǎn)與文本節(jié)點(diǎn)的一部分的文檔片段。

      Range 是瀏覽器原生的對象。

      image

      3.1. 創(chuàng)建 Range 實(shí)例,并設(shè)置起始位置

      <body>
        <ul>
          <li>Vite</li>
          <li>Vue</li>
          <li>React</li>
          <li>VitePress</li>
          <li>NaiveUI</li>
        </ul>
      </body>
      <script>
        // 創(chuàng)建 Range 對象
        const range = new Range()
        const liDoms = document.querySelectorAll("li");
        // Range 起始位置在 li 2
        range.setStartBefore(liDoms[1]);
        // Range 結(jié)束位置在 li 3
        range.setEndAfter(liDoms[2]);
        // 獲取 selection 對象
        const selection = window.getSelection();
        // 添加光標(biāo)選擇的范圍
        selection.addRange(range);
      </script>
      

      image

      可以看到,選擇內(nèi)容為第二行和第三行

      3.1.1 瀏覽器兼容情況

      image

      3.2. Range 屬性

      1. startContainer:起始節(jié)點(diǎn)。

      2. startOffset:起始節(jié)點(diǎn)偏移量。

      3. endContainer:結(jié)束節(jié)點(diǎn)。

      4. endOffset:結(jié)束節(jié)點(diǎn)偏移量。

      5. collapsed:范圍的開始和結(jié)束是否為同一點(diǎn)。

      6. commonAncestorContainer:返回完整包含 startContainerendContainer 的最深一級的節(jié)點(diǎn)。

      3.2.1. 用我們上面創(chuàng)建的實(shí)例來看下 range 屬性的值

      image

      3.2.2. 如果我們只選擇文本內(nèi)容時

      只選擇 li 中的 itePres

      image

      可以看出 range 屬性對應(yīng)的值

      image

      3.3. Range 方法

      1. cloneContents():復(fù)制范圍內(nèi)容,并將復(fù)制的內(nèi)容作為 DocumentFragment 返回。

      2. cloneRange():創(chuàng)建一個具有相同起點(diǎn)/終點(diǎn)的新范圍, 非引用,可以隨意改變,不會影響另一方。

      3. collapse(toStart):如果 toStart=true 則設(shè)置 end=start,否則設(shè)置 start=end,從而折疊范圍。

      4. compareBoundaryPoints(how, sourceRange):兩個范圍邊界點(diǎn)進(jìn)行比較,返回一個數(shù)字 -1、0、1。

      5. comparePoint(referenceNode, offset):返回-1、0、1具體取決于 是 referenceNode 在 之前、相同還是之后。

      6. createContextualFragment(tagString):返回一個 DocumentFragment

      7. deleteContents():刪除框選的內(nèi)容。

      8. extractContents():從文檔中刪除范圍內(nèi)容,并將刪除的內(nèi)容作為 DocumentFragment 返回。

      9. getBoundingClientRect():和 dom 一樣,返回 DOMRect 對象。

      10. getClientRects():返回可迭代的對象序列 DOMRect

      11. insertNode(node):在范圍的起始處將 node 插入文檔。

      12. intersectsNode(referenceNode):判斷與給定的 node 是否相交。

      13. selectNode(node):設(shè)置范圍以選擇整個 node。

      14. selectNodeContents(node):設(shè)置范圍以選擇整個 node 的內(nèi)容。

      15. setStart(startNode, startOffset):設(shè)置起點(diǎn)。

      16. setEnd(endNode, endOffset):設(shè)置終點(diǎn)。

      17. setStartBefore(node):將起點(diǎn)設(shè)置在 node 前面。

      18. setStartAfter(node):將起點(diǎn)設(shè)置在 node 后面。

      19. setEndBefore(node):將終點(diǎn)設(shè)置為 node 前面。

      20. setEndAfter(node):將終點(diǎn)設(shè)置為 node 后面。

      21. surroundContents(node):使用 node 將所選范圍內(nèi)容包裹起來。

      3.4. 創(chuàng)建 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() 構(gòu)造函數(shù)
      const range = new Range()
      

      3.5. Range 兼容性

      image

      4、Selection

      Selection 對象表示用戶選擇的文本范圍或插入符號的當(dāng)前位置。它代表頁面中的文本選區(qū),可能橫跨多個元素。

      4.1. 獲取文本對象

      window.getSelection()
      

      image

      image

      4.2. Selection 術(shù)語

      4.2.1. 錨點(diǎn) (anchor)

      錨指的是一個選區(qū)的起始點(diǎn)(不同于 HTML 中的錨點(diǎn)鏈接)。當(dāng)我們使用鼠標(biāo)框選一個區(qū)域的時候,錨點(diǎn)就是我們鼠標(biāo)按下瞬間的那個點(diǎn)。在用戶拖動鼠標(biāo)時,錨點(diǎn)是不會變的。

      4.2.2. 焦點(diǎn) (focus)

      選區(qū)的焦點(diǎn)是該選區(qū)的終點(diǎn),當(dāng)你用鼠標(biāo)框選一個選區(qū)的時候,焦點(diǎn)是你的鼠標(biāo)松開瞬間所記錄的那個點(diǎn)。隨著用戶拖動鼠標(biāo),焦點(diǎn)的位置會隨著改變。

      4.2.3. 范圍 (range)

      范圍指的是文檔中連續(xù)的一部分。一個范圍包括整個節(jié)點(diǎn),也可以包含節(jié)點(diǎn)的一部分,例如文本節(jié)點(diǎn)的一部分。用戶通常下只能選擇一個范圍,但是有的時候用戶也有可能選擇多個范圍。

      4.2.4. 可編輯元素 (editing host)

      一個用戶可編輯的元素(例如一個使用 contenteditableHTML 元素,或是在啟用了 designModeDocument 的子元素)。

      4.3. Selection 的屬性

      首先要清楚,選擇的起點(diǎn)稱為錨點(diǎn)(anchor),終點(diǎn)稱為焦點(diǎn)(focus)。

      1. anchorNode:選擇的起始節(jié)點(diǎn)。

      2. anchorOffset:選擇開始的 anchorNode 中的偏移量。

      3. focusNode:選擇的結(jié)束節(jié)點(diǎn)。

      4. focusOffset:選擇開始處 focusNode 的偏移量。

      5. isCollapsed:如果未選擇任何內(nèi)容(空范圍)或不存在,則為 true。

      6. rangeCount:選擇中的范圍數(shù),之前說過,除 Firefox 外,其他瀏覽器最多為1。

      7. type:類型:NoneCaret、Range

      4.4. Selection 方法

      1. addRange(range): 將一個 Range 對象添加到當(dāng)前選區(qū)。

      2. collapse(node, offset): 將選區(qū)折疊到指定的節(jié)點(diǎn)和偏移位置。

      3. collapseToEnd(): 將選區(qū)折疊到當(dāng)前選區(qū)的末尾。

      4. collapseToStart(): 將選區(qū)折疊到當(dāng)前選區(qū)的起始位置。

      5. containsNode(node, partlyContained): 判斷選區(qū)是否包含指定的節(jié)點(diǎn),可以選擇是否部分包含。

      6. deleteFromDocument(): 從文檔中刪除選區(qū)內(nèi)容。

      7. empty(): 從選區(qū)中移除所有范圍(同 `removeAllRanges()``,已廢棄)。

      8. extend(node, offset): 將選區(qū)的焦點(diǎn)節(jié)點(diǎn)擴(kuò)展到指定的節(jié)點(diǎn)和偏移位置。

      9. getRangeAt(index): 返回選區(qū)中指定索引處的 Range 對象。

      10. removeAllRanges(): 移除所有選區(qū)中的范圍。

      11. removeRange(range): 從選區(qū)中移除指定的 Range 對象。

      12. selectAllChildren(node): 選中指定節(jié)點(diǎn)的所有子節(jié)點(diǎn)。

      13. setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset): 設(shè)置選區(qū)的起始和結(jié)束節(jié)點(diǎn)及偏移位置。

      14. setPosition(node, offset)collapse 的別名

      4.5. Selection 兼容性

      image

      三、項(xiàng)目實(shí)現(xiàn)

      1、實(shí)現(xiàn)思路

      1. 先獲取選擇的內(nèi)容,開發(fā) getSelectContent 函數(shù)
      2. 對獲取的內(nèi)容進(jìn)行判斷,是否存在 selection 實(shí)例,沒有直接返回 null
      3. 判斷 selection 實(shí)例的 isCollapsed 屬性
        • 沒有選中,對 selection 進(jìn)行 toString().trim() 操作,判斷內(nèi)容
          • 有內(nèi)容,直接返回 text 類型
          • 無內(nèi)容,返回 null
        • 有選中,則判斷內(nèi)容
      4. 判斷選中的內(nèi)容有沒有節(jié)點(diǎn)
        • 沒有節(jié)點(diǎn),則和沒有選中一樣處理,進(jìn)行 toString().trim() 操作,判斷內(nèi)容
          • 有內(nèi)容,直接返回 text 類型
          • 無內(nèi)容,返回 null
        • 有節(jié)點(diǎn),進(jìn)行 toString().trim() 操作,判斷內(nèi)容
          • 沒有內(nèi)容,判斷是否有特殊節(jié)點(diǎn)
            • 'iframe', 'svg', 'img', 'audio', 'video' 節(jié)點(diǎn),返回 html 類型
            • 'input', 'textarea', 'select',判斷 value 值,是否存在
              • 存在:返回 html 類型
              • 不存在:返回 null
            • 沒有特殊節(jié)點(diǎn),返回 null
          • 有內(nèi)容,返回 html 類型
      5. 對鼠標(biāo) mousedown、mouseup 事件和 selectionchange、contextmenu、dblclick 事件進(jìn)行監(jiān)聽,觸發(fā) getSelectContent 函數(shù)
      6. 在需要的地方進(jìn)行 debounce 防抖處理

      2、簡易流程圖

      image

      2、Debounce 方法實(shí)現(xiàn)

      2.1. JS

      function debounce (fn, time = 500) {
        let timeout = null; // 創(chuàng)建一個標(biāo)記用來存放定時器的返回值
        return function () {
          clearTimeout(timeout) // 每當(dāng)觸發(fā)時,把前一個 定時器 clear 掉
          timeout = setTimeout(() => { // 創(chuàng)建一個新的 定時器,并賦值給 timeout
            fn.apply(this, arguments)
          }, time)
        }
      }
      

      2.2. TS

      /**
       * debounce 函數(shù)類型
       */
      type DebouncedFunction<F extends (...args: any[]) => any> = (...args: Parameters<F>) => void
      /**
       * debounce 防抖函數(shù)
       * @param {Function} func 函數(shù)
       * @param {number} wait 等待時間
       * @param {false} immediate 是否立即執(zhí)行
       * @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
      }
      /**
       * 獲取選擇的內(nèi)容
       * @returns {null | IGetSelectContentProps} 返回選擇的內(nèi)容
       */
      const getSelectContent = (): null | IGetSelectContentProps => {
        const selection = window.getSelection()
        if (selection) {
          // 1. 是焦點(diǎn)在 input 輸入框
          // 2. 沒有選中
          // 3. 選擇的是輸入框
          if (selection.isCollapsed) {
            return selection.toString().trim().length
              ? {
                  type: 'text',
                  content: selection.toString().trim()
                }
              : null
          }
          // 獲取選擇范圍
          const range = selection.getRangeAt(0)
          // 獲取選擇內(nèi)容
          const rangeClone = range.cloneContents()
          // 判斷選擇內(nèi)容里面有沒有節(jié)點(diǎn)
          if (rangeClone.childElementCount > 0) {
            // 創(chuàng)建 div 標(biāo)簽
            const container = document.createElement('div')
            // div 標(biāo)簽 append 復(fù)制節(jié)點(diǎn)
            container.appendChild(rangeClone)
            // 如果復(fù)制的內(nèi)容長度為 0
            if (!selection.toString().trim().length) {
              // 判斷是否有選擇特殊節(jié)點(diǎn)
              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. 只需要文本

      /**
       * 獲取框選的文案內(nèi)容
       * @returns {string} 返回框選的內(nèi)容
       */
      const getSelectTextContent = (): string => {
        const selection = window.getSelection()
        return selection?.toString().trim() || ''
      }
      

      4、添加事件監(jiān)聽

      // 是否時鼠標(biāo)點(diǎn)擊動作
      let selectionchangeMouseTrack: boolean = false
      const selectionChangeFun = debounce(() => {
        const selectContent = getSelectContent()
        console.log('selectContent', selectContent)
        // todo... 處理上報
        selectionchangeMouseTrack = false
      })
      // 添加 mousedown 監(jiān)聽事件
      document.addEventListener('mousedown', () => {
        selectionchangeMouseTrack = true
      })
      // 添加 mouseup 監(jiān)聽事件
      document.addEventListener(
        'mouseup',
        debounce(() => {
          selectionChangeFun()
        }, 100)
      )
      // 添加 selectionchange 監(jiān)聽事件
      document.addEventListener(
        'selectionchange',
        debounce(() => {
          if (selectionchangeMouseTrack) return
          selectionChangeFun()
        })
      )
      // 添加 dblclick 監(jiān)聽事件
      document.addEventListener('dblclick', () => {
        selectionChangeFun()
      })
      // 添加 contextmenu 監(jiān)聽事件
      document.addEventListener(
        'contextmenu',
        debounce(() => {
          selectionChangeFun()
        })
      )
      

      也可以進(jìn)行封裝

      /**
       * addEventlistener function 類型
       */
      export interface IEventHandlerProps {
        [eventName: string]: EventListenerOrEventListenerObject
      }
      
      let selectionchangeMouseTrack: boolean = false
      const eventHandlers: IEventHandlerProps = {
        // 鼠標(biāo) down 事件
        mousedown: () => {
          selectionchangeMouseTrack = true
        },
        // 鼠標(biāo) 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、返回內(nèi)容

      5.1. 純文本內(nèi)容

      image

      5.2. html 格式

      image

      6. 完整 JS 代碼

      function debounce (fn, time = 500) {
        let timeout = null; // 創(chuàng)建一個標(biāo)記用來存放定時器的返回值
        return function () {
          clearTimeout(timeout) // 每當(dāng)觸發(fā)時,把前一個 定時器 clear 掉
          timeout = setTimeout(() => { // 創(chuàng)建一個新的 定時器,并賦值給 timeout
            fn.apply(this, arguments)
          }, time)
        }
      }
      
      let selectionchangeMouseTrack = false
      document.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. 是焦點(diǎn)在 input 輸入框
          // 2. 沒有選中
          // 3. 選擇的是輸入框
          if (selection.isCollapsed) {
            return selection.toString().trim().length ? {
              type: 'text',
              content: selection.toString().trim()
            } : null
          }
          // 獲取選擇范圍
          const range = selection.getRangeAt(0);
          // 獲取選擇內(nèi)容
          const rangeClone = range.cloneContents()
          // 判斷選擇內(nèi)容里面有沒有節(jié)點(diǎn)
          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
        })
      }
      

      7. 線上預(yù)覽

      四、總結(jié)

      1. 鼠標(biāo)框選上報能監(jiān)控用戶在頁面的行為,能為后續(xù)的數(shù)據(jù)分析等提供便利

      2. 基于 JS 中的 SelectionRange 實(shí)現(xiàn)的,使用原生 JS

      3. 涉及到的操作比較多,包含鍵盤、鼠標(biāo)右鍵、全選等

      4. 能對框選的內(nèi)容進(jìn)行分類,區(qū)別 htmltext,更方便的看出用戶選擇了哪些內(nèi)容

      引用

      posted @ 2024-05-23 10:09  日升_rs  閱讀(635)  評論(2)    收藏  舉報
      關(guān)于
      CSDN
      掘金
      我的博客
      Chrome 瀏覽器插件開發(fā)實(shí)踐指南 netlify 版
      Chrome 瀏覽器插件開發(fā)實(shí)踐指南 github 版
      主站蜘蛛池模板: 国内精品久久久久影院网站| 鸡西市| 全椒县| 欧美做受视频播放| 又粗又硬又黄a级毛片| 欧美日韩v| 国产精品中文av专线| 国产精品美腿一区在线看| 国模肉肉视频一区二区三区| 国产91小视频在线观看| 亚洲精品二区在线播放| 亚洲综合久久精品国产高清| 亚洲欧美人成网站在线观看看| 久久国产精品久久精品国产| 色综合久久精品中文字幕| 国产一卡2卡三卡4卡免费网站| 色欲av蜜桃一区二区三| 亚洲精品中文综合第一页| 四虎影视www在线播放| 日韩不卡无码精品一区高清视频| 好深好湿好硬顶到了好爽| 无码人妻日韩一区日韩二区| 91亚洲国产成人久久蜜臀| 成人免费看片又大又黄| 亚洲一区二区中文av| 国产99在线 | 欧美| 极品人妻少妇一区二区| 宜丰县| 国产精品人成视频免费国产| 办公室强奷漂亮少妇视频| 中文国产不卡一区二区| 成人午夜无人区一区二区| 国内精品久久久久影视| 九九热久久只有精品2| 国产精品天天看天天狠| 91中文字幕一区在线| 久久日产一线二线三线| 日韩精品福利视频在线观看| 中文字幕日韩精品人妻| 樱花草视频www日本韩国| 精品熟女少妇av免费久久|