iT邦幫忙

2024 iThome 鐵人賽

DAY 13
0
自我挑戰組

30 天 vueuse 原始碼閱讀與實作系列 第 13

[Day 13] useMouseInElement

  • 分享至 

  • xImage
  •  

官方 Demo:https://vueuse.org/core/useMouseInElement/#usemouseinelement

看到 Demo 中有 x、y、sourceType,完全就是用 Day 11 & Day 12 看到的 useMouse 所 return 出來的值。

完整程式碼

// src/compositions/useMouseInElement.js

import { ref, watch } from 'vue'
import { defaultWindow, unrefElement } from '@/helper'
import { useMouse } from '@/compositions/useMouse'
import { useEventListener } from '@/compositions/useEventListener'

export function useMouseInElement(target, options = {}) {
  const {
    handleOutside = true,
    window = defaultWindow,
  } = options
  const type = options.type || 'page'

  const { x, y, sourceType } = useMouse(options)

  const targetRef = ref(target ?? window?.document.body)
  const elementX = ref(0)
  const elementY = ref(0)
  const elementPositionX = ref(0)
  const elementPositionY = ref(0)
  const elementHeight = ref(0)
  const elementWidth = ref(0)
  const isOutside = ref(true)

  let stop = () => {}
  if (window) {
    stop = watch(
      [targetRef, x, y],
      () => {
        const el = unrefElement(targetRef)
        if (!el)
          return

        const {
          left,
          top,
          width,
          height,
        } = el.getBoundingClientRect()
        elementPositionX.value = left + (type === 'page' ? window.pageXOffset : 0)
        elementPositionY.value = top + (type === 'page' ? window.pageYOffset : 0)
        elementHeight.value = height
        elementWidth.value = width

        const elX = x.value - elementPositionX.value
        const elY = y.value - elementPositionY.value
        isOutside.value = width === 0 || height === 0
        || elX < 0 || elY < 0
        || elX > width || elY > height

        if (handleOutside || !isOutside.value) {
          elementX.value = elX
          elementY.value = elY
        }
      },
      { immediate: true },
    )

    useEventListener(document, 'mouseleave', () => {
      isOutside.value = true
    })
  }

  return {
    x,
    y,
    sourceType,
    elementX,
    elementY,
    elementPositionX,
    elementPositionY,
    elementHeight,
    elementWidth,
    isOutside,
    stop,
  }
}

useMouseInElement return value

  • xy:就是之前 useMouse 提到的那個 x、y,在 useMouseInElement 中也會到 useMouse 回傳的 xy 來進行計算。
  • sourceTypeuseMouse 回傳的東西,判斷當前觸發的事 mouse 或是 touch event。
  • elementX, elementY:以 element 左上角為原點(0, 0),計算出原點與當前鼠標位置之間的距離。
  • elementPositionX, elementPositionY:以 document 左上角(如果 type 是 page 的話)為原點(0, 0),計算出 element 與原點間的距離。
  • elementWidthelementHeight:element 的寬高。
  • isOutside:當前鼠標是否在 element 區域內。
  • stop:執行 stop 可以停止 useMouseInElement 內部 watch 的觀察。

useMouseInElement 參數 & 基本架構

// src/compositions/useMouseInElement.js

export function useMouseInElement(target, options = {}) {
    const {
        handleOutside = true,
        window = defaultWindow,
    } = options
    const type = options.type || 'page'

    const { x, y, sourceType } = useMouse(options)
    
    // ...略
    
    stop = watch(
        [targetRef, x, y],
        () => {
            // 相關計算都在這邊
        },
        { immediate: true },
    )
    
    useEventListener(document, 'mouseleave', () => {
        isOutside.value = true
    })
}
  • target:官網 Demo 那塊灰色區域,在這邊統一用 element 來代替。
  • options.handleOutside:option 跟 elementX、elementY 座標有關,如果是 true(預設值),鼠標移到 element 以外的地方,以會持續以 element 左上角為原點,計算並更新 elementX、elementY 的數值。
  • options.type: 'page' | 'client' | 'screen' | 'movement',預設為 page
  • options.window

options 的參數也會一起傳給 useMouse,這邊看一段 TS 的 code 會比較清楚

export function useMouseInElement(target, options = {}) {
    // ...略

    export interface MouseInElementOptions extends UseMouseOptions {
        handleOutside?: boolean
    }
    // ...略

}

除了 handleOutside,是 useMouseInElement 自己的,其他 option 都會是 useMouse 可以傳入的 option。而 useMouseInElement 實作有用到的 option 是我前面有提到的 targethandleOutsideoptions.typeoptions.window

elementPositionX, elementPositionY 的計算

// src/compositions/useMouseInElement.js

export function useMouseInElement(target, options = {}) {
    // ...略
    
    // 相關計算都在這邊
    // 這邊的 left, top 是透過 element.getBoundingClientRect() 取得
    elementPositionX.value = left + (type === 'page' ? window.pageXOffset : 0)
    elementPositionY.value = top + (type === 'page' ? window.pageYOffset : 0)
}

頁面卷軸滾動時,滾動距離必須加上 element 本身跟視窗左上角(0, 0)的距離,才能維持 element 跟 document 原點之間的距離。

window.pageXOffset 跟 window.scrollX 的差異基本上是沒有差異,mdn 說明 pageXOffset 是 scrollX 的別名,可以參考:https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollX#notes

關於 type 判斷式 type === 'page',假設 type 是 client 的話,那原點是在視窗中可見區域的左上角位置,所以不會受到卷軸滾動影響,就不需要額外加上 pageOffset。

elementX, elementY 的計算

// src/compositions/useMouseInElement.js

export function useMouseInElement(target, options = {}) {
    // ...略
    
    // 相關計算都在這邊
    const elX = x.value - elementPositionX.value
    const elY = y.value - elementPositionY.value
    
    if (handleOutside || !isOutside.value) {
        elementX.value = elX
        elementY.value = elY
    }
}

以 x 座標、type 為 page 的情境來說,假設 element 的 x 座標(距離 document 最左側)為 300,而鼠標 x 座標(距離 document 最左側)為 100,那鼠標跟 element 的距離就是 100 - 300 = -200

isOutside 的計算

// src/compositions/useMouseInElement.js

export function useMouseInElement(target, options = {}) {
    // ...略
    
        // 相關計算都在這邊
        isOutside.value = width === 0 || height === 0
            || elX < 0 || elY < 0
            || elX > width || elY > height
    
    useEventListener(document, 'mouseleave', () => {
      isOutside.value = true
    })
}

這裡有用到 Day 8 ~ Day 10 實作到的 useEventListener,
可以看到連對 document 的 mouseleave 事件監聽都有考慮進來,我來做的話大概會漏掉這個吧 XD

GitHub:https://github.com/RhinoLee/30days_vue/pull/3/files#diff-23cad8fa4e0daae675a12b2ea970f5cdd9405109a8d0dba0cb78de3acfe66202


useMouseInElement 到這邊告一段落,主要都是距離的計算,明天開始會從 useDeviceOrientation 接著講~


上一篇
[Day 12] useMouse - 加入其他參數 & 功能
系列文
30 天 vueuse 原始碼閱讀與實作13
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言