feat: add vueuse feature

This commit is contained in:
tangjinzhou 2022-11-07 13:42:51 +08:00
parent ac9d3e40c1
commit 7fa1fe428d
11 changed files with 462 additions and 0 deletions

22
LICENSE
View File

@ -44,3 +44,25 @@ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
MIT License
Copyright (c) 2019-PRESENT Anthony Fu<https://github.com/antfu>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,34 @@
import { isClient } from './is';
export interface ConfigurableWindow {
/*
* Specify a custom `window` instance, e.g. working with iframes or in testing environments.
*/
window?: Window;
}
export interface ConfigurableDocument {
/*
* Specify a custom `document` instance, e.g. working with iframes or in testing environments.
*/
document?: Document;
}
export interface ConfigurableNavigator {
/*
* Specify a custom `navigator` instance, e.g. working with iframes or in testing environments.
*/
navigator?: Navigator;
}
export interface ConfigurableLocation {
/*
* Specify a custom `location` instance, e.g. working with iframes or in testing environments.
*/
location?: Location;
}
export const defaultWindow = /* #__PURE__ */ isClient ? window : undefined;
export const defaultDocument = /* #__PURE__ */ isClient ? window.document : undefined;
export const defaultNavigator = /* #__PURE__ */ isClient ? window.navigator : undefined;
export const defaultLocation = /* #__PURE__ */ isClient ? window.location : undefined;

View File

@ -0,0 +1,28 @@
export const isClient = typeof window !== 'undefined';
export const isDef = <T = any>(val?: T): val is T => typeof val !== 'undefined';
export const assert = (condition: boolean, ...infos: any[]) => {
if (!condition) console.warn(...infos);
};
const toString = Object.prototype.toString;
export const isBoolean = (val: any): val is boolean => typeof val === 'boolean';
export const isFunction = <T extends Function>(val: any): val is T => typeof val === 'function';
export const isNumber = (val: any): val is number => typeof val === 'number';
export const isString = (val: unknown): val is string => typeof val === 'string';
export const isObject = (val: any): val is object => toString.call(val) === '[object Object]';
export const isWindow = (val: any): val is Window =>
typeof window !== 'undefined' && toString.call(val) === '[object Window]';
export const now = () => Date.now();
export const timestamp = () => +Date.now();
export const clamp = (n: number, min: number, max: number) => Math.min(max, Math.max(min, n));
export const noop = () => {};
export const rand = (min: number, max: number) => {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
};
export const isIOS =
/* #__PURE__ */ isClient &&
window?.navigator?.userAgent &&
/iP(ad|hone|od)/.test(window.navigator.userAgent);
export const hasOwn = <T extends object, K extends keyof T>(val: T, key: K): key is K =>
Object.prototype.hasOwnProperty.call(val, key);

View File

@ -0,0 +1,9 @@
import { unref } from 'vue';
import type { MaybeComputedRef } from './types';
/**
* Get the value of value/ref/getter.
*/
export function resolveUnref<T>(r: MaybeComputedRef<T>): T {
return typeof r === 'function' ? (r as any)() : unref(r);
}

View File

@ -0,0 +1,15 @@
// eslint-disable-next-line no-restricted-imports
import { getCurrentInstance, nextTick, onMounted } from 'vue';
import type { Fn } from './types';
/**
* Call onMounted() if it's inside a component lifecycle, if not, just call the function
*
* @param fn
* @param sync if set to false, it will run in the nextTick() of Vue
*/
export function tryOnMounted(fn: Fn, sync = true) {
if (getCurrentInstance()) onMounted(fn);
else if (sync) fn();
else nextTick(fn);
}

View File

@ -0,0 +1,15 @@
import { getCurrentScope, onScopeDispose } from 'vue';
import type { Fn } from './types';
/**
* Call onScopeDispose() if it's inside a effect scope lifecycle, if not, do nothing
*
* @param fn
*/
export function tryOnScopeDispose(fn: Fn) {
if (getCurrentScope()) {
onScopeDispose(fn);
return true;
}
return false;
}

View File

@ -0,0 +1,142 @@
import type { ComputedRef, Ref, WatchOptions, WatchSource } from 'vue';
/**
* Any function
*/
export type Fn = () => void;
/**
* A ref that allow to set null or undefined
*/
export type RemovableRef<T> = Omit<Ref<T>, 'value'> & {
get value(): T;
set value(value: T | null | undefined);
};
/**
* @deprecated Use `RemovableRef`
*/
export type RemoveableRef<T> = RemovableRef<T>;
/**
* Maybe it's a ref, or a plain value
*
* ```ts
* type MaybeRef<T> = T | Ref<T>
* ```
*/
export type MaybeRef<T> = T | Ref<T>;
/**
* Maybe it's a ref, or a plain value, or a getter function
*
* ```ts
* type MaybeComputedRef<T> = (() => T) | T | Ref<T> | ComputedRef<T>
* ```
*/
export type MaybeComputedRef<T> = MaybeReadonlyRef<T> | MaybeRef<T>;
/**
* Maybe it's a computed ref, or a getter function
*
* ```ts
* type MaybeReadonlyRef<T> = (() => T) | ComputedRef<T>
* ```
*/
export type MaybeReadonlyRef<T> = (() => T) | ComputedRef<T>;
/**
* Make all the nested attributes of an object or array to MaybeRef<T>
*
* Good for accepting options that will be wrapped with `reactive` or `ref`
*
* ```ts
* UnwrapRef<DeepMaybeRef<T>> === T
* ```
*/
export type DeepMaybeRef<T> = T extends Ref<infer V>
? MaybeRef<V>
: T extends Array<any> | object
? { [K in keyof T]: DeepMaybeRef<T[K]> }
: MaybeRef<T>;
/**
* Infers the element type of an array
*/
export type ElementOf<T> = T extends (infer E)[] ? E : never;
export type ShallowUnwrapRef<T> = T extends Ref<infer P> ? P : T;
export type Awaitable<T> = Promise<T> | T;
export type ArgumentsType<T> = T extends (...args: infer U) => any ? U : never;
export interface Pausable {
/**
* A ref indicate whether a pausable instance is active
*/
isActive: Ref<boolean>;
/**
* Temporary pause the effect from executing
*/
pause: Fn;
/**
* Resume the effects
*/
resume: Fn;
}
export interface Stoppable {
/**
* A ref indicate whether a stoppable instance is executing
*/
isPending: Ref<boolean>;
/**
* Stop the effect from executing
*/
stop: Fn;
/**
* Start the effects
*/
start: Fn;
}
/**
* @deprecated Use `Stoppable`
*/
export type Stopable = Stoppable;
export interface ConfigurableFlush {
/**
* Timing for monitoring changes, refer to WatchOptions for more details
*
* @default 'pre'
*/
flush?: WatchOptions['flush'];
}
export interface ConfigurableFlushSync {
/**
* Timing for monitoring changes, refer to WatchOptions for more details.
* Unlike `watch()`, the default is set to `sync`
*
* @default 'sync'
*/
flush?: WatchOptions['flush'];
}
// Internal Types
export type MapSources<T> = {
[K in keyof T]: T[K] extends WatchSource<infer V> ? V : never;
};
export type MapOldSources<T, Immediate> = {
[K in keyof T]: T[K] extends WatchSource<infer V>
? Immediate extends true
? V | undefined
: V
: never;
};

View File

@ -0,0 +1,24 @@
import type { ComponentPublicInstance } from 'vue';
import type { MaybeComputedRef, MaybeRef } from './types';
import { resolveUnref } from './resolveUnref';
export type VueInstance = ComponentPublicInstance;
export type MaybeElementRef<T extends MaybeElement = MaybeElement> = MaybeRef<T>;
export type MaybeComputedElementRef<T extends MaybeElement = MaybeElement> = MaybeComputedRef<T>;
export type MaybeElement = HTMLElement | SVGElement | VueInstance | undefined | null;
export type UnRefElementReturn<T extends MaybeElement = MaybeElement> = T extends VueInstance
? Exclude<MaybeElement, VueInstance>
: T | undefined;
/**
* Get the dom element of a ref of element or Vue component instance
*
* @param elRef
*/
export function unrefElement<T extends MaybeElement>(
elRef: MaybeComputedElementRef<T>,
): UnRefElementReturn<T> {
const plain = resolveUnref(elRef);
return (plain as VueInstance)?.$el ?? plain;
}

View File

@ -0,0 +1,65 @@
import { ref, watch } from 'vue';
import type { MaybeComputedElementRef } from './unrefElement';
import type { UseResizeObserverOptions } from './useResizeObserver';
import { useResizeObserver } from './useResizeObserver';
import { unrefElement } from './unrefElement';
export interface ElementSize {
width: number;
height: number;
}
/**
* Reactive size of an HTML element.
*
* @see https://vueuse.org/useElementSize
* @param target
* @param callback
* @param options
*/
export function useElementSize(
target: MaybeComputedElementRef,
initialSize: ElementSize = { width: 0, height: 0 },
options: UseResizeObserverOptions = {},
) {
const { box = 'content-box' } = options;
const width = ref(initialSize.width);
const height = ref(initialSize.height);
useResizeObserver(
target,
([entry]) => {
const boxSize =
box === 'border-box'
? entry.borderBoxSize
: box === 'content-box'
? entry.contentBoxSize
: entry.devicePixelContentBoxSize;
if (boxSize) {
width.value = boxSize.reduce((acc, { inlineSize }) => acc + inlineSize, 0);
height.value = boxSize.reduce((acc, { blockSize }) => acc + blockSize, 0);
} else {
// fallback
width.value = entry.contentRect.width;
height.value = entry.contentRect.height;
}
},
options,
);
watch(
() => unrefElement(target),
ele => {
width.value = ele ? initialSize.width : 0;
height.value = ele ? initialSize.height : 0;
},
);
return {
width,
height,
};
}
export type UseElementSizeReturn = ReturnType<typeof useElementSize>;

View File

@ -0,0 +1,94 @@
import { tryOnScopeDispose } from './tryOnScopeDispose';
import { watch } from 'vue';
import type { MaybeComputedElementRef } from './unrefElement';
import { unrefElement } from './unrefElement';
import { useSupported } from './useSupported';
import type { ConfigurableWindow } from './_configurable';
import { defaultWindow } from './_configurable';
export interface ResizeObserverSize {
readonly inlineSize: number;
readonly blockSize: number;
}
export interface ResizeObserverEntry {
readonly target: Element;
readonly contentRect: DOMRectReadOnly;
readonly borderBoxSize?: ReadonlyArray<ResizeObserverSize>;
readonly contentBoxSize?: ReadonlyArray<ResizeObserverSize>;
readonly devicePixelContentBoxSize?: ReadonlyArray<ResizeObserverSize>;
}
export type ResizeObserverCallback = (
entries: ReadonlyArray<ResizeObserverEntry>,
observer: ResizeObserver,
) => void;
export interface UseResizeObserverOptions extends ConfigurableWindow {
/**
* Sets which box model the observer will observe changes to. Possible values
* are `content-box` (the default), `border-box` and `device-pixel-content-box`.
*
* @default 'content-box'
*/
box?: ResizeObserverBoxOptions;
}
declare class ResizeObserver {
constructor(callback: ResizeObserverCallback);
disconnect(): void;
observe(target: Element, options?: UseResizeObserverOptions): void;
unobserve(target: Element): void;
}
/**
* Reports changes to the dimensions of an Element's content or the border-box
*
* @see https://vueuse.org/useResizeObserver
* @param target
* @param callback
* @param options
*/
export function useResizeObserver(
target: MaybeComputedElementRef,
callback: ResizeObserverCallback,
options: UseResizeObserverOptions = {},
) {
const { window = defaultWindow, ...observerOptions } = options;
let observer: ResizeObserver | undefined;
const isSupported = useSupported(() => window && 'ResizeObserver' in window);
const cleanup = () => {
if (observer) {
observer.disconnect();
observer = undefined;
}
};
const stopWatch = watch(
() => unrefElement(target),
el => {
cleanup();
if (isSupported.value && window && el) {
observer = new ResizeObserver(callback);
observer!.observe(el, observerOptions);
}
},
{ immediate: true, flush: 'post' },
);
const stop = () => {
cleanup();
stopWatch();
};
tryOnScopeDispose(stop);
return {
isSupported,
stop,
};
}
export type UseResizeObserverReturn = ReturnType<typeof useResizeObserver>;

View File

@ -0,0 +1,14 @@
import { tryOnMounted } from './tryOnMounted';
import type { Ref } from 'vue';
import { ref } from 'vue';
export function useSupported(callback: () => unknown, sync = false) {
const isSupported = ref() as Ref<boolean>;
const update = () => (isSupported.value = Boolean(callback()));
update();
tryOnMounted(update, sync);
return isSupported;
}