feat: add affix
							parent
							
								
									bacb790bfb
								
							
						
					
					
						commit
						5cc4a63480
					
				| 
						 | 
				
			
			@ -0,0 +1,20 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <div class="flex h-[200vh] flex-col gap-2">
 | 
			
		||||
    <a-affix :offset-top="top" @change="onChange">
 | 
			
		||||
      <a-button type="primary" @click="top += 10">Affix top</a-button>
 | 
			
		||||
    </a-affix>
 | 
			
		||||
    <br />
 | 
			
		||||
    <a-affix :offset-bottom="bottom">
 | 
			
		||||
      <a-button type="primary" @click="bottom += 10">Affix bottom</a-button>
 | 
			
		||||
    </a-affix>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { ref } from 'vue'
 | 
			
		||||
const top = ref<number>(10)
 | 
			
		||||
const bottom = ref<number>(10)
 | 
			
		||||
const onChange = (lastAffix: boolean) => {
 | 
			
		||||
  console.log('onChange', lastAffix)
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -2,6 +2,7 @@
 | 
			
		|||
declare module 'vue' {
 | 
			
		||||
  export interface GlobalComponents {
 | 
			
		||||
    AButton: typeof import('@ant-design-vue/ui').Button
 | 
			
		||||
    AAffix: typeof import('@ant-design-vue/ui').Affix
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
export {}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -74,7 +74,8 @@
 | 
			
		|||
    "@floating-ui/vue": "^1.1.6",
 | 
			
		||||
    "lodash-es": "^4.17.21",
 | 
			
		||||
    "@ctrl/tinycolor": "^4.0.0",
 | 
			
		||||
    "resize-observer-polyfill": "^1.5.1"
 | 
			
		||||
    "resize-observer-polyfill": "^1.5.1",
 | 
			
		||||
    "@vueuse/core": "^13.6.0"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@ant-design-vue/eslint-config": "*",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,220 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <div ref="placeholderNode" :data-measure-status="state.status">
 | 
			
		||||
    <div v-if="enableFixed" :style="state.placeholderStyle" aria-hidden="true" />
 | 
			
		||||
    <div
 | 
			
		||||
      ref="fixedNode"
 | 
			
		||||
      :style="[state.affixStyle, { zIndex: props.zIndex }]"
 | 
			
		||||
      :class="enableFixed && 'ant-affix'"
 | 
			
		||||
    >
 | 
			
		||||
      <slot />
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import {
 | 
			
		||||
  getCurrentInstance,
 | 
			
		||||
  reactive,
 | 
			
		||||
  shallowRef,
 | 
			
		||||
  computed,
 | 
			
		||||
  watch,
 | 
			
		||||
  onMounted,
 | 
			
		||||
  onUpdated,
 | 
			
		||||
  onUnmounted,
 | 
			
		||||
} from 'vue'
 | 
			
		||||
import {
 | 
			
		||||
  AFFIX_STATUS_NONE,
 | 
			
		||||
  AFFIX_STATUS_PREPARE,
 | 
			
		||||
  AffixProps,
 | 
			
		||||
  AffixState,
 | 
			
		||||
  affixDefaultProps,
 | 
			
		||||
  AffixEmits,
 | 
			
		||||
} from './meta'
 | 
			
		||||
import {
 | 
			
		||||
  addObserveTarget,
 | 
			
		||||
  getFixedBottom,
 | 
			
		||||
  getFixedTop,
 | 
			
		||||
  getTargetRect,
 | 
			
		||||
  removeObserveTarget,
 | 
			
		||||
} from './utils'
 | 
			
		||||
import throttleByAnimationFrame from '@/utils/throttleByAnimationFrame'
 | 
			
		||||
import { useResizeObserver } from '@vueuse/core'
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<AffixProps>(), affixDefaultProps)
 | 
			
		||||
const emit = defineEmits<AffixEmits>()
 | 
			
		||||
const placeholderNode = shallowRef()
 | 
			
		||||
 | 
			
		||||
useResizeObserver(placeholderNode, () => {
 | 
			
		||||
  updatePosition()
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const fixedNode = shallowRef()
 | 
			
		||||
const state = reactive<AffixState>({
 | 
			
		||||
  affixStyle: undefined,
 | 
			
		||||
  placeholderStyle: undefined,
 | 
			
		||||
  status: AFFIX_STATUS_NONE,
 | 
			
		||||
  lastAffix: false,
 | 
			
		||||
})
 | 
			
		||||
const prevTarget = shallowRef<Window | HTMLElement | null>(null)
 | 
			
		||||
const timeout = shallowRef<any>(null)
 | 
			
		||||
const currentInstance = getCurrentInstance()
 | 
			
		||||
 | 
			
		||||
const offsetTop = computed(() => {
 | 
			
		||||
  return props.offsetBottom === undefined && props.offsetTop === undefined ? 0 : props.offsetTop
 | 
			
		||||
})
 | 
			
		||||
const offsetBottom = computed(() => props.offsetBottom)
 | 
			
		||||
const measure = () => {
 | 
			
		||||
  const { status, lastAffix } = state
 | 
			
		||||
  const { target } = props
 | 
			
		||||
  if (status !== AFFIX_STATUS_PREPARE || !fixedNode.value || !placeholderNode.value || !target) {
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const targetNode = target()
 | 
			
		||||
  if (!targetNode) {
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const newState = {
 | 
			
		||||
    status: AFFIX_STATUS_NONE,
 | 
			
		||||
  } as AffixState
 | 
			
		||||
  const placeholderRect = getTargetRect(placeholderNode.value as HTMLElement)
 | 
			
		||||
 | 
			
		||||
  if (
 | 
			
		||||
    placeholderRect.top === 0 &&
 | 
			
		||||
    placeholderRect.left === 0 &&
 | 
			
		||||
    placeholderRect.width === 0 &&
 | 
			
		||||
    placeholderRect.height === 0
 | 
			
		||||
  ) {
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const targetRect = getTargetRect(targetNode)
 | 
			
		||||
  const fixedTop = getFixedTop(placeholderRect, targetRect, offsetTop.value)
 | 
			
		||||
  const fixedBottom = getFixedBottom(placeholderRect, targetRect, offsetBottom.value)
 | 
			
		||||
  if (
 | 
			
		||||
    placeholderRect.top === 0 &&
 | 
			
		||||
    placeholderRect.left === 0 &&
 | 
			
		||||
    placeholderRect.width === 0 &&
 | 
			
		||||
    placeholderRect.height === 0
 | 
			
		||||
  ) {
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (fixedTop !== undefined) {
 | 
			
		||||
    const width = `${placeholderRect.width}px`
 | 
			
		||||
    const height = `${placeholderRect.height}px`
 | 
			
		||||
 | 
			
		||||
    newState.affixStyle = {
 | 
			
		||||
      position: 'fixed',
 | 
			
		||||
      top: fixedTop,
 | 
			
		||||
      width,
 | 
			
		||||
      height,
 | 
			
		||||
    }
 | 
			
		||||
    newState.placeholderStyle = {
 | 
			
		||||
      width,
 | 
			
		||||
      height,
 | 
			
		||||
    }
 | 
			
		||||
  } else if (fixedBottom !== undefined) {
 | 
			
		||||
    const width = `${placeholderRect.width}px`
 | 
			
		||||
    const height = `${placeholderRect.height}px`
 | 
			
		||||
 | 
			
		||||
    newState.affixStyle = {
 | 
			
		||||
      position: 'fixed',
 | 
			
		||||
      bottom: fixedBottom,
 | 
			
		||||
      width,
 | 
			
		||||
      height,
 | 
			
		||||
    }
 | 
			
		||||
    newState.placeholderStyle = {
 | 
			
		||||
      width,
 | 
			
		||||
      height,
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  newState.lastAffix = !!newState.affixStyle
 | 
			
		||||
  if (lastAffix !== newState.lastAffix) {
 | 
			
		||||
    emit('change', newState.lastAffix)
 | 
			
		||||
  }
 | 
			
		||||
  // update state
 | 
			
		||||
  Object.assign(state, newState)
 | 
			
		||||
}
 | 
			
		||||
const prepareMeasure = () => {
 | 
			
		||||
  Object.assign(state, {
 | 
			
		||||
    status: AFFIX_STATUS_PREPARE,
 | 
			
		||||
    affixStyle: undefined,
 | 
			
		||||
    placeholderStyle: undefined,
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const updatePosition = throttleByAnimationFrame(() => {
 | 
			
		||||
  prepareMeasure()
 | 
			
		||||
})
 | 
			
		||||
const lazyUpdatePosition = throttleByAnimationFrame(() => {
 | 
			
		||||
  const { target } = props
 | 
			
		||||
  const { affixStyle } = state
 | 
			
		||||
 | 
			
		||||
  // Check position change before measure to make Safari smooth
 | 
			
		||||
  if (target && affixStyle) {
 | 
			
		||||
    const targetNode = target()
 | 
			
		||||
    if (targetNode && placeholderNode.value) {
 | 
			
		||||
      const targetRect = getTargetRect(targetNode)
 | 
			
		||||
      const placeholderRect = getTargetRect(placeholderNode.value as HTMLElement)
 | 
			
		||||
      const fixedTop = getFixedTop(placeholderRect, targetRect, offsetTop.value)
 | 
			
		||||
      const fixedBottom = getFixedBottom(placeholderRect, targetRect, offsetBottom.value)
 | 
			
		||||
      if (
 | 
			
		||||
        (fixedTop !== undefined && affixStyle.top === fixedTop) ||
 | 
			
		||||
        (fixedBottom !== undefined && affixStyle.bottom === fixedBottom)
 | 
			
		||||
      ) {
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  // Directly call prepare measure since it's already throttled.
 | 
			
		||||
  prepareMeasure()
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
defineExpose({
 | 
			
		||||
  updatePosition,
 | 
			
		||||
  lazyUpdatePosition,
 | 
			
		||||
})
 | 
			
		||||
watch(
 | 
			
		||||
  () => props.target,
 | 
			
		||||
  val => {
 | 
			
		||||
    const newTarget = val?.() || null
 | 
			
		||||
    if (prevTarget.value !== newTarget) {
 | 
			
		||||
      removeObserveTarget(currentInstance)
 | 
			
		||||
      if (newTarget) {
 | 
			
		||||
        addObserveTarget(newTarget, currentInstance)
 | 
			
		||||
        // Mock Event object.
 | 
			
		||||
        updatePosition()
 | 
			
		||||
      }
 | 
			
		||||
      prevTarget.value = newTarget
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
)
 | 
			
		||||
watch(() => [props.offsetTop, props.offsetBottom], updatePosition)
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  const { target } = props
 | 
			
		||||
  if (target) {
 | 
			
		||||
    // [Legacy] Wait for parent component ref has its value.
 | 
			
		||||
    // We should use target as directly element instead of function which makes element check hard.
 | 
			
		||||
    timeout.value = setTimeout(() => {
 | 
			
		||||
      addObserveTarget(target(), currentInstance)
 | 
			
		||||
      // Mock Event object.
 | 
			
		||||
      updatePosition()
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
onUpdated(() => {
 | 
			
		||||
  measure()
 | 
			
		||||
})
 | 
			
		||||
onUnmounted(() => {
 | 
			
		||||
  clearTimeout(timeout.value)
 | 
			
		||||
  removeObserveTarget(currentInstance)
 | 
			
		||||
  ;(updatePosition as any).cancel()
 | 
			
		||||
  ;(lazyUpdatePosition as any).cancel()
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const enableFixed = computed(() => {
 | 
			
		||||
  return !!state.affixStyle
 | 
			
		||||
})
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,10 @@
 | 
			
		|||
import { describe, expect, it, vi } from 'vitest'
 | 
			
		||||
import { Affix, Button } from '@ant-design-vue/ui'
 | 
			
		||||
import { mount } from '@vue/test-utils'
 | 
			
		||||
 | 
			
		||||
describe('Affix', () => {
 | 
			
		||||
  it('should render correctly', () => {
 | 
			
		||||
    const wrapper = mount(Affix)
 | 
			
		||||
    expect(wrapper.html()).toMatchSnapshot()
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,14 @@
 | 
			
		|||
import { App, Plugin } from 'vue'
 | 
			
		||||
import Affix from './Affix.vue'
 | 
			
		||||
import './style/index.css'
 | 
			
		||||
 | 
			
		||||
export { default as Affix } from './Affix.vue'
 | 
			
		||||
export * from './meta'
 | 
			
		||||
 | 
			
		||||
/* istanbul ignore next */
 | 
			
		||||
Affix.install = function (app: App) {
 | 
			
		||||
  app.component('AAffix', Affix)
 | 
			
		||||
  return app
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Affix as typeof Affix & Plugin
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,48 @@
 | 
			
		|||
import { CSSProperties } from 'vue'
 | 
			
		||||
 | 
			
		||||
function getDefaultTarget() {
 | 
			
		||||
  return typeof window !== 'undefined' ? window : null
 | 
			
		||||
}
 | 
			
		||||
export const AFFIX_STATUS_NONE = 0
 | 
			
		||||
export const AFFIX_STATUS_PREPARE = 1
 | 
			
		||||
 | 
			
		||||
type AffixStatus = typeof AFFIX_STATUS_NONE | typeof AFFIX_STATUS_PREPARE
 | 
			
		||||
 | 
			
		||||
export interface AffixState {
 | 
			
		||||
  affixStyle?: CSSProperties
 | 
			
		||||
  placeholderStyle?: CSSProperties
 | 
			
		||||
  status: AffixStatus
 | 
			
		||||
  lastAffix: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type AffixProps = {
 | 
			
		||||
  /**
 | 
			
		||||
   * Specifies the offset top of the affix
 | 
			
		||||
   */
 | 
			
		||||
  offsetTop?: number
 | 
			
		||||
  /**
 | 
			
		||||
   * Specifies the offset bottom of the affix
 | 
			
		||||
   */
 | 
			
		||||
  offsetBottom?: number
 | 
			
		||||
  /**
 | 
			
		||||
   * Specifies the target of the affix
 | 
			
		||||
   */
 | 
			
		||||
  target?: () => Window | HTMLElement | null
 | 
			
		||||
  /**
 | 
			
		||||
   * Specifies the z-index of the affix
 | 
			
		||||
   */
 | 
			
		||||
  zIndex?: number
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const affixDefaultProps = {
 | 
			
		||||
  target: getDefaultTarget,
 | 
			
		||||
  zIndex: 10,
 | 
			
		||||
} as const
 | 
			
		||||
 | 
			
		||||
export type AffixEmits = {
 | 
			
		||||
  /**
 | 
			
		||||
   * Triggered when the affix status changes
 | 
			
		||||
   * @param lastAffix - The last affix status
 | 
			
		||||
   */
 | 
			
		||||
  (e: 'change', lastAffix: boolean): void
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,5 @@
 | 
			
		|||
@reference '../../../style/tailwind.css';
 | 
			
		||||
 | 
			
		||||
.ant-affix {
 | 
			
		||||
  @apply fixed z-10;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,113 @@
 | 
			
		|||
import addEventListener from '../../utils/addEventListener'
 | 
			
		||||
import supportsPassive from '../../utils/supportsPassive'
 | 
			
		||||
 | 
			
		||||
export type BindElement = HTMLElement | Window | null | undefined
 | 
			
		||||
 | 
			
		||||
export function getTargetRect(target: BindElement): DOMRect {
 | 
			
		||||
  return target !== window
 | 
			
		||||
    ? (target as HTMLElement).getBoundingClientRect()
 | 
			
		||||
    : ({ top: 0, bottom: window.innerHeight } as DOMRect)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getFixedTop(placeholderRect: DOMRect, targetRect: DOMRect, offsetTop?: number) {
 | 
			
		||||
  if (
 | 
			
		||||
    offsetTop !== undefined &&
 | 
			
		||||
    Math.round(targetRect.top) > Math.round(placeholderRect.top) - offsetTop
 | 
			
		||||
  ) {
 | 
			
		||||
    return `${offsetTop + targetRect.top}px`
 | 
			
		||||
  }
 | 
			
		||||
  return undefined
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getFixedBottom(
 | 
			
		||||
  placeholderRect: DOMRect,
 | 
			
		||||
  targetRect: DOMRect,
 | 
			
		||||
  offsetBottom?: number,
 | 
			
		||||
) {
 | 
			
		||||
  if (
 | 
			
		||||
    offsetBottom !== undefined &&
 | 
			
		||||
    Math.round(targetRect.bottom) < Math.round(placeholderRect.bottom) + offsetBottom
 | 
			
		||||
  ) {
 | 
			
		||||
    const targetBottomOffset = window.innerHeight - targetRect.bottom
 | 
			
		||||
    return `${offsetBottom + targetBottomOffset}px`
 | 
			
		||||
  }
 | 
			
		||||
  return undefined
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ======================== Observer ========================
 | 
			
		||||
const TRIGGER_EVENTS: (keyof WindowEventMap)[] = [
 | 
			
		||||
  'resize',
 | 
			
		||||
  'scroll',
 | 
			
		||||
  'touchstart',
 | 
			
		||||
  'touchmove',
 | 
			
		||||
  'touchend',
 | 
			
		||||
  'pageshow',
 | 
			
		||||
  'load',
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
interface ObserverEntity {
 | 
			
		||||
  target: HTMLElement | Window
 | 
			
		||||
  affixList: any[]
 | 
			
		||||
  eventHandlers: { [eventName: string]: any }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let observerEntities: ObserverEntity[] = []
 | 
			
		||||
 | 
			
		||||
export function getObserverEntities() {
 | 
			
		||||
  // Only used in test env. Can be removed if refactor.
 | 
			
		||||
  return observerEntities
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function addObserveTarget<T>(target: HTMLElement | Window | null, affix: T): void {
 | 
			
		||||
  if (!target) return
 | 
			
		||||
 | 
			
		||||
  let entity = observerEntities.find(item => item.target === target)
 | 
			
		||||
 | 
			
		||||
  if (entity) {
 | 
			
		||||
    entity.affixList.push(affix)
 | 
			
		||||
  } else {
 | 
			
		||||
    entity = {
 | 
			
		||||
      target,
 | 
			
		||||
      affixList: [affix],
 | 
			
		||||
      eventHandlers: {},
 | 
			
		||||
    }
 | 
			
		||||
    observerEntities.push(entity)
 | 
			
		||||
 | 
			
		||||
    // Add listener
 | 
			
		||||
    TRIGGER_EVENTS.forEach(eventName => {
 | 
			
		||||
      entity!.eventHandlers[eventName] = addEventListener(target, eventName, () => {
 | 
			
		||||
        entity!.affixList.forEach(
 | 
			
		||||
          targetAffix => {
 | 
			
		||||
            const { lazyUpdatePosition } = (targetAffix as any).exposed
 | 
			
		||||
            lazyUpdatePosition()
 | 
			
		||||
          },
 | 
			
		||||
          (eventName === 'touchstart' || eventName === 'touchmove') && supportsPassive
 | 
			
		||||
            ? ({ passive: true } as EventListenerOptions)
 | 
			
		||||
            : false,
 | 
			
		||||
        )
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function removeObserveTarget<T>(affix: T): void {
 | 
			
		||||
  const observerEntity = observerEntities.find(oriObserverEntity => {
 | 
			
		||||
    const hasAffix = oriObserverEntity.affixList.some(item => item === affix)
 | 
			
		||||
    if (hasAffix) {
 | 
			
		||||
      oriObserverEntity.affixList = oriObserverEntity.affixList.filter(item => item !== affix)
 | 
			
		||||
    }
 | 
			
		||||
    return hasAffix
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  if (observerEntity && observerEntity.affixList.length === 0) {
 | 
			
		||||
    observerEntities = observerEntities.filter(item => item !== observerEntity)
 | 
			
		||||
 | 
			
		||||
    // Remove listener
 | 
			
		||||
    TRIGGER_EVENTS.forEach(eventName => {
 | 
			
		||||
      const handler = observerEntity.eventHandlers[eventName]
 | 
			
		||||
      if (handler && handler.remove) {
 | 
			
		||||
        handler.remove()
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,3 +1,4 @@
 | 
			
		|||
export { default as Button } from './button'
 | 
			
		||||
export { default as Input } from './input'
 | 
			
		||||
export { default as Theme } from './theme'
 | 
			
		||||
export { default as Affix } from './affix'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,27 @@
 | 
			
		|||
import supportsPassive from './supportsPassive'
 | 
			
		||||
 | 
			
		||||
export default function addEventListenerWrap(
 | 
			
		||||
  target: HTMLElement | Window,
 | 
			
		||||
  eventType: string,
 | 
			
		||||
  cb: EventListener,
 | 
			
		||||
  option?: boolean | AddEventListenerOptions,
 | 
			
		||||
) {
 | 
			
		||||
  if (target && target.addEventListener) {
 | 
			
		||||
    let opt = option
 | 
			
		||||
    if (
 | 
			
		||||
      opt === undefined &&
 | 
			
		||||
      supportsPassive &&
 | 
			
		||||
      (eventType === 'touchstart' || eventType === 'touchmove' || eventType === 'wheel')
 | 
			
		||||
    ) {
 | 
			
		||||
      opt = { passive: false }
 | 
			
		||||
    }
 | 
			
		||||
    target.addEventListener(eventType, cb, opt)
 | 
			
		||||
  }
 | 
			
		||||
  return {
 | 
			
		||||
    remove: () => {
 | 
			
		||||
      if (target && target.removeEventListener) {
 | 
			
		||||
        target.removeEventListener(eventType, cb)
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,13 @@
 | 
			
		|||
// Test via a getter in the options object to see if the passive property is accessed
 | 
			
		||||
let supportsPassive = false
 | 
			
		||||
try {
 | 
			
		||||
  const opts = Object.defineProperty({}, 'passive', {
 | 
			
		||||
    get() {
 | 
			
		||||
      supportsPassive = true
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
  window.addEventListener('testPassive', null, opts)
 | 
			
		||||
  window.removeEventListener('testPassive', null, opts)
 | 
			
		||||
} catch (e) {}
 | 
			
		||||
 | 
			
		||||
export default supportsPassive
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,29 @@
 | 
			
		|||
import raf from './raf'
 | 
			
		||||
 | 
			
		||||
type throttledFn = (...args: any[]) => void
 | 
			
		||||
 | 
			
		||||
type throttledCancelFn = { cancel: () => void }
 | 
			
		||||
 | 
			
		||||
function throttleByAnimationFrame<T extends any[]>(fn: (...args: T) => void) {
 | 
			
		||||
  let requestId: number | null
 | 
			
		||||
 | 
			
		||||
  const later = (args: T) => () => {
 | 
			
		||||
    requestId = null
 | 
			
		||||
    fn(...args)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const throttled: throttledFn & throttledCancelFn = (...args: T) => {
 | 
			
		||||
    if (requestId == null) {
 | 
			
		||||
      requestId = raf(later(args))
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  throttled.cancel = () => {
 | 
			
		||||
    raf.cancel(requestId!)
 | 
			
		||||
    requestId = null
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return throttled
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default throttleByAnimationFrame
 | 
			
		||||
		Loading…
	
		Reference in New Issue