UNPKG

@varlet/use

Version:
449 lines (448 loc) 11.8 kB
import { call, getScrollTop, inBrowser, isArray, isFunction, kebabCase, motion, removeItem } from "@varlet/shared"; import { computed, getCurrentInstance, inject, isRef, isVNode, nextTick, onActivated, onBeforeUnmount, onDeactivated, onMounted, onUnmounted, onUpdated, provide, reactive, ref, unref, watch } from "vue"; //#region src/onSmartMounted.ts function onSmartMounted(hook) { let isMounted = false; onMounted(() => { hook(); nextTick(() => { isMounted = true; }); }); onActivated(() => { if (!isMounted) return; hook(); }); } //#endregion //#region src/useEventListener.ts function useEventListener(target, type, listener, options = {}) { if (!inBrowser()) return; const { passive = false, capture = false } = options; let listening = false; let cleaned = false; const getElement = (target) => isFunction(target) ? target() : unref(target); const add = (target) => { if (listening || cleaned) return; const element = getElement(target); if (element) { element.addEventListener(type, listener, { passive, capture }); listening = true; } }; const remove = (target) => { if (!listening || cleaned) return; const element = getElement(target); if (element) { element.removeEventListener(type, listener, { capture }); listening = false; } }; let watchStopHandle; if (isRef(target)) watchStopHandle = watch(() => target.value, (newValue, oldValue) => { remove(oldValue); add(newValue); }); const cleanup = () => { watchStopHandle?.(); remove(target); cleaned = true; }; onSmartMounted(() => { add(target); }); onBeforeUnmount(() => { remove(target); }); onDeactivated(() => { remove(target); }); return cleanup; } //#endregion //#region src/useClickOutside.ts function useClickOutside(target, type, listener) { if (!inBrowser()) return; const handler = (event) => { const element = isFunction(target) ? target() : unref(target); if (element && !element.contains(event.target)) listener(event); }; useEventListener(document, type, handler); } //#endregion //#region src/onSmartUnmounted.ts function onSmartUnmounted(hook) { let keepalive = false; onDeactivated(() => { keepalive = true; hook(); }); onUnmounted(() => { if (keepalive) return; hook(); }); } //#endregion //#region src/useParent.ts function keyInProvides(key) { return key in getCurrentInstance().provides; } function useParent(key) { if (!keyInProvides(key)) return { index: null, parentProvider: null, bindParent: null }; const { childInstances, collect, clear, ...parentProvider } = inject(key); const childInstance = getCurrentInstance(); const index = computed(() => childInstances.indexOf(childInstance)); const bindParent = (childProvider) => { onMounted(() => { nextTick().then(() => { collect(childInstance, childProvider); }); }); onBeforeUnmount(() => { nextTick().then(() => { clear(childInstance, childProvider); }); }); }; return { index, parentProvider, bindParent }; } //#endregion //#region src/useChildren.ts function flatVNodes(subTree) { const vNodes = []; const flat = (subTree) => { if (subTree?.component) { flat(subTree?.component.subTree); return; } if (isArray(subTree?.children)) subTree.children.forEach((child) => { if (isVNode(child)) { vNodes.push(child); flat(child); } }); }; flat(subTree); return vNodes; } function useChildren(key) { const parentInstance = getCurrentInstance(); const childInstances = reactive([]); const childProviders = []; const length = computed(() => childInstances.length); const sortInstances = () => { const vNodes = flatVNodes(parentInstance.subTree); const pairs = childInstances.map((instance, index) => ({ instance, provider: childProviders[index] })); const getVNodeIndex = (instance) => { const index = vNodes.indexOf(instance.vnode); return index === -1 ? Number.MAX_SAFE_INTEGER : index; }; pairs.sort((a, b) => getVNodeIndex(a.instance) - getVNodeIndex(b.instance)); childInstances.splice(0, childInstances.length, ...pairs.map(({ instance }) => instance)); childProviders.splice(0, childProviders.length, ...pairs.map(({ provider }) => provider)); }; const collect = (childInstance, childProvider) => { childInstances.push(childInstance); childProviders.push(childProvider); sortInstances(); }; const clear = (childInstance, childProvider) => { removeItem(childInstances, childInstance); removeItem(childProviders, childProvider); }; const bindChildren = (parentProvider) => { provide(key, { childInstances, collect, clear, ...parentProvider }); }; onUpdated(sortInstances); return { length, childInstances, childProviders, bindChildren }; } //#endregion //#region src/onWindowResize.ts function onWindowResize(listener) { useEventListener(() => window, "resize", listener, { passive: true }); useEventListener(() => window, "orientationchange", listener, { passive: true }); } //#endregion //#region src/useInitialized.ts function useInitialized(source, value) { const initialized = ref(false); watch(source, (newValue) => { if (value === newValue) initialized.value = true; }, { immediate: true }); return initialized; } //#endregion //#region src/useTouch.ts function getDirection(x, y) { if (x > y) return "horizontal"; if (y > x) return "vertical"; } function useTouch() { const startX = ref(0); const startY = ref(0); const deltaX = ref(0); const deltaY = ref(0); const offsetX = ref(0); const offsetY = ref(0); const prevX = ref(0); const prevY = ref(0); const moveX = ref(0); const moveY = ref(0); const direction = ref(); const touching = ref(false); const dragging = ref(false); const startTime = ref(0); const distance = ref(0); let draggingAnimationFrame = null; const resetTouch = () => { startX.value = 0; startY.value = 0; deltaX.value = 0; deltaY.value = 0; offsetX.value = 0; offsetY.value = 0; prevX.value = 0; prevY.value = 0; moveX.value = 0; moveY.value = 0; direction.value = void 0; touching.value = false; dragging.value = false; startTime.value = 0; distance.value = 0; }; const startTouch = (event) => { resetTouch(); const { clientX: x, clientY: y } = event.touches[0]; startX.value = x; startY.value = y; prevX.value = x; prevY.value = y; touching.value = true; startTime.value = performance.now(); dragging.value = false; if (draggingAnimationFrame) window.cancelAnimationFrame(draggingAnimationFrame); }; const moveTouch = (event) => { const { clientX: x, clientY: y } = event.touches[0]; dragging.value = true; deltaX.value = x - startX.value; deltaY.value = y - startY.value; offsetX.value = Math.abs(deltaX.value); offsetY.value = Math.abs(deltaY.value); distance.value = Math.sqrt(offsetX.value ** 2 + offsetY.value ** 2); moveX.value = x - prevX.value; moveY.value = y - prevY.value; if (!direction.value) direction.value = getDirection(offsetX.value, offsetY.value); prevX.value = x; prevY.value = y; }; const endTouch = () => { touching.value = false; draggingAnimationFrame = window.requestAnimationFrame(() => { dragging.value = false; }); }; const isReachTop = (element) => { return getScrollTop(element) === 0 && deltaY.value > 0; }; const isReachBottom = (element, offset = 1) => { const { scrollHeight, clientHeight, scrollTop } = element; const offsetBottom = Math.abs(scrollHeight - scrollTop - clientHeight); return deltaY.value < 0 && offsetBottom <= offset; }; return { startX, startY, deltaX, deltaY, offsetX, offsetY, prevX, prevY, moveX, moveY, direction, touching, dragging, startTime, distance, resetTouch, startTouch, moveTouch, endTouch, isReachTop, isReachBottom }; } //#endregion //#region src/useId.ts function useId() { const id = ref(); const instance = getCurrentInstance(); const name = kebabCase(instance.type.name); id.value = process.env.NODE_ENV === "test" ? `${name}-mock-id` : `${name}-${instance.uid}`; return id; } //#endregion //#region src/useClientId.ts function useClientId() { const instance = getCurrentInstance(); const name = kebabCase(instance.type.name); const id = ref(process.env.NODE_ENV === "test" ? `${name}-mock-id` : void 0); onMounted(() => { if (process.env.NODE_ENV !== "test") id.value = `${name}-${instance.uid}`; }); return id; } //#endregion //#region src/useWindowSize.ts function useWindowSize(options = {}) { const { initialWidth = 0, initialHeight = 0 } = options; const width = ref(initialWidth); const height = ref(initialHeight); const update = () => { if (!inBrowser()) return; width.value = window.innerWidth; height.value = window.innerHeight; }; onSmartMounted(update); onWindowResize(update); return { width, height }; } //#endregion //#region src/useVModel.ts function useVModel(props, key, options = {}) { const { passive = true, eventName, defaultValue, emit } = options; const event = eventName ?? `onUpdate:${key.toString()}`; const getValue = () => props[key] ?? defaultValue; if (!passive) return computed({ get() { return getValue(); }, set(value) { emit ? emit(event, value) : call(props[event], value); } }); const proxy = ref(getValue()); let shouldEmit = true; watch(() => props[key], () => { shouldEmit = false; proxy.value = getValue(); nextTick(() => { shouldEmit = true; }); }); watch(() => proxy.value, (newValue) => { if (!shouldEmit) return; emit ? emit(event, newValue) : call(props[event], newValue); }); return proxy; } //#endregion //#region src/useMotion.ts function useMotion(options) { const value = ref(getter(options.from)); const state = ref("pending"); let ctx = createMotionContext(); function getter(value) { return isFunction(value) ? value() : value; } function reset() { ctx.reset(); value.value = getter(options.from); state.value = "pending"; ctx = createMotionContext(); } function start() { ctx.start(); } function pause() { ctx.pause(); } function createMotionContext() { return motion({ from: getter(options.from), to: getter(options.to), duration: options.duration ? getter(options.duration) : 300, timingFunction: options.timingFunction, onStateChange(newState) { state.value = newState; }, frame({ value: newValue, done }) { value.value = newValue; if (done) options.onFinished?.(value.value); } }); } return { value, state, start, pause, reset }; } //#endregion //#region src/useResizeObserver.ts function useResizeObserver(target, callback) { if (!inBrowser() || typeof ResizeObserver === "undefined") return; let observer; let observedElement; let cleaned = false; const getElement = (target) => isFunction(target) ? target() : unref(target); const observe = (target) => { if (cleaned) return; const element = getElement(target); if (!element || element === observedElement) return; unobserve(); observer = new ResizeObserver(callback); observer.observe(element); observedElement = element; }; const unobserve = () => { observer?.disconnect(); observer = void 0; observedElement = void 0; }; let watchStopHandle; if (isRef(target)) watchStopHandle = watch(() => target.value, () => observe(target)); const cleanup = () => { watchStopHandle?.(); unobserve(); cleaned = true; }; onSmartMounted(() => observe(target)); onBeforeUnmount(unobserve); onDeactivated(unobserve); return cleanup; } //#endregion export { keyInProvides, onSmartMounted, onSmartUnmounted, onWindowResize, useChildren, useClickOutside, useClientId, useEventListener, useId, useInitialized, useMotion, useParent, useResizeObserver, useTouch, useVModel, useWindowSize };