@varlet/use
Version:
composable utils of varlet
449 lines (448 loc) • 11.8 kB
JavaScript
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 };