Skip to content

Commit

Permalink
Merge branch 'dev'
Browse files Browse the repository at this point in the history
  • Loading branch information
liuyunhe committed Oct 17, 2024
2 parents f5265de + cdcf547 commit 16fe2a6
Show file tree
Hide file tree
Showing 11 changed files with 243 additions and 2 deletions.
120 changes: 120 additions & 0 deletions src/directives/click-outside/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { isClient, isElement } from '@/utils'

import type {
ComponentPublicInstance,
DirectiveBinding,
ObjectDirective,
} from 'vue'

type DocumentHandler = <T extends MouseEvent>(mouseup: T, mousedown: T) => void
type FlushList = Map<
HTMLElement,
{
documentHandler: DocumentHandler
bindingFn: (...args: unknown[]) => unknown
}[]
>

const nodeList: FlushList = new Map()

if (isClient) {
let startClick: MouseEvent | undefined
document.addEventListener('mousedown', (e: MouseEvent) => (startClick = e))
document.addEventListener('mouseup', (e: MouseEvent) => {
if (startClick) {
for (const handlers of nodeList.values()) {
for (const { documentHandler } of handlers) {
documentHandler(e as MouseEvent, startClick)
}
}
startClick = undefined
}
})
}

function createDocumentHandler(
el: HTMLElement,
binding: DirectiveBinding
): DocumentHandler {
let excludes: HTMLElement[] = []
if (Array.isArray(binding.arg)) {
excludes = binding.arg
} else if (isElement(binding.arg)) {
// due to current implementation on binding type is wrong the type casting is necessary here
excludes.push(binding.arg as unknown as HTMLElement)
}
return function (mouseup, mousedown) {
const popperRef = (
binding.instance as ComponentPublicInstance<{
popperRef: HTMLElement
}>
).popperRef
const mouseUpTarget = mouseup.target as Node
const mouseDownTarget = mousedown?.target as Node
const isBound = !binding || !binding.instance
const isTargetExists = !mouseUpTarget || !mouseDownTarget
const isContainedByEl =
el.contains(mouseUpTarget) || el.contains(mouseDownTarget)
const isSelf = el === mouseUpTarget

const isTargetExcluded =
(excludes.length &&
excludes.some((item) => item?.contains(mouseUpTarget))) ||
(excludes.length && excludes.includes(mouseDownTarget as HTMLElement))
const isContainedByPopper =
popperRef &&
(popperRef.contains(mouseUpTarget) || popperRef.contains(mouseDownTarget))
if (
isBound ||
isTargetExists ||
isContainedByEl ||
isSelf ||
isTargetExcluded ||
isContainedByPopper
) {
return
}
binding.value(mouseup, mousedown)
}
}

const ClickOutside: ObjectDirective = {
beforeMount(el: HTMLElement, binding: DirectiveBinding) {
// there could be multiple handlers on the element
if (!nodeList.has(el)) {
nodeList.set(el, [])
}

nodeList.get(el)!.push({
documentHandler: createDocumentHandler(el, binding),
bindingFn: binding.value,
})
},
updated(el: HTMLElement, binding: DirectiveBinding) {
if (!nodeList.has(el)) {
nodeList.set(el, [])
}

const handlers = nodeList.get(el)!
const oldHandlerIndex = handlers.findIndex(
(item) => item.bindingFn === binding.oldValue
)
const newHandler = {
documentHandler: createDocumentHandler(el, binding),
bindingFn: binding.value,
}

if (oldHandlerIndex >= 0) {
// replace the old handler to the new handler
handlers.splice(oldHandlerIndex, 1, newHandler)
} else {
handlers.push(newHandler)
}
},
unmounted(el: HTMLElement) {
// remove all listeners when a component unmounted
nodeList.delete(el)
},
}

export default ClickOutside
1 change: 1 addition & 0 deletions src/directives/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { default as TrapFocus } from './trap-focus'
export { vRepeatClick } from './repeat-click'
export { default as ClickOutside } from './click-outside'
1 change: 1 addition & 0 deletions src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './useAria'
export * from './useAttrs'
export * from './useClickOutside'
export * from './useDeprecated'
export * from './useEventListener'
Expand Down
38 changes: 38 additions & 0 deletions src/hooks/useAttrs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { computed, getCurrentInstance } from 'vue'
import { fromPairs } from 'lodash-unified'
import { debugWarn } from '@/utils'

import type { ComputedRef } from 'vue'

interface Params {
excludeListeners?: boolean
excludeKeys?: ComputedRef<string[]>
}

const DEFAULT_EXCLUDE_KEYS = ['class', 'style']
const LISTENER_PREFIX = /^on[A-Z]/

export const useAttrs = (params: Params = {}): ComputedRef<Record<string, unknown>> => {
const { excludeListeners = false, excludeKeys } = params
const allExcludeKeys = computed<string[]>(() => {
return (excludeKeys?.value || []).concat(DEFAULT_EXCLUDE_KEYS)
})

const instance = getCurrentInstance()
if (!instance) {
debugWarn(
'use-attrs',
'getCurrentInstance() returned null. useAttrs() must be called at the top of a setup function'
)
return computed(() => ({}))
}

return computed(() =>
fromPairs(
Object.entries(instance.proxy?.$attrs!).filter(
([key]) =>
!allExcludeKeys.value.includes(key) && !(excludeListeners && LISTENER_PREFIX.test(key))
)
)
)
}
3 changes: 2 additions & 1 deletion src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ export * from './types'
export * from './typescript'
export * from './validator'
export * from './easings'
export * from './raf'
export * from './raf'
export * from './throttleByRaf'
14 changes: 14 additions & 0 deletions src/utils/rand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* @deprecated Use `useId` `useIdInjection` instead
* Generate random number in range [0, 1000]
* Maybe replace with [uuid](https://www.npmjs.com/package/uuid)
*/
export const generateId = (): number => Math.floor(Math.random() * 10000)

/**
* @deprecated
* Generating a random int in range (0, max - 1)
* @param max {number}
*/
export const getRandomInt = (max: number) =>
Math.floor(Math.random() * Math.floor(max))
22 changes: 22 additions & 0 deletions src/utils/throttleByRaf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { cAF, rAF } from './raf'

export function throttleByRaf(cb: (...args: any[]) => void) {
let timer = 0

const throttle = (...args: any[]): void => {
if (timer) {
cAF(timer)
}
timer = rAF(() => {
cb(...args)
timer = 0
})
}

throttle.cancel = () => {
cAF(timer)
timer = 0
}

return throttle
}
34 changes: 34 additions & 0 deletions src/utils/vue/global-node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { isClient } from '../browser'

const globalNodes: HTMLElement[] = []
let target: HTMLElement | undefined = !isClient ? undefined : document.body

export function createGlobalNode(id?: string) {
const el = document.createElement('div')
if (id !== undefined) {
el.setAttribute('id', id)
}

if (target) {
target.appendChild(el)
globalNodes.push(el)
}

return el
}

export function removeGlobalNode(el: HTMLElement) {
globalNodes.splice(globalNodes.indexOf(el), 1)
el.remove()
}

export function changeGlobalNodesTarget(el: HTMLElement) {
if (el === target) return

target = el
globalNodes.forEach((el) => {
if (target && !el.contains(target)) {
target.appendChild(el)
}
})
}
1 change: 1 addition & 0 deletions src/utils/vue/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './typescript'
export * from './vnode'
export * from './props'
export * from './refs'
export * from './size'
7 changes: 7 additions & 0 deletions src/utils/vue/size.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { componentSizeMap } from '@/constants'

import type { ComponentSize } from '@/constants'

export const getComponentSize = (size?: ComponentSize) => {
return componentSizeMap[size || 'default']
}
4 changes: 3 additions & 1 deletion src/utils/vue/typescript.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import type { AppContext, Plugin } from 'vue'
import type { AppContext, EmitsOptions, Plugin, SetupContext } from 'vue'

export type SFCWithInstall<T> = T & Plugin

export type SFCInstallWithContext<T> = SFCWithInstall<T> & {
_context: AppContext | null
}

export type EmitFn<E extends EmitsOptions> = SetupContext<E>['emit']

0 comments on commit 16fe2a6

Please sign in to comment.