Skip to content

Commit

Permalink
Merge branch 'main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
robinscholz committed Jan 2, 2023
2 parents 420e070 + 268a644 commit 3f6bf6b
Show file tree
Hide file tree
Showing 29 changed files with 215 additions and 133 deletions.
2 changes: 1 addition & 1 deletion package.json
@@ -1,6 +1,6 @@
{
"name": "@vueuse/monorepo",
"version": "9.7.0",
"version": "9.9.0",
"private": true,
"packageManager": "pnpm@7.6.0",
"description": "Collection of essential Vue Composition Utilities",
Expand Down
2 changes: 1 addition & 1 deletion packages/components/package.json
@@ -1,6 +1,6 @@
{
"name": "@vueuse/components",
"version": "9.7.0",
"version": "9.9.0",
"description": "Renderless components for VueUse",
"author": "Jacob Clevenger<https://github.com/wheatjs>",
"license": "MIT",
Expand Down
27 changes: 18 additions & 9 deletions packages/core/onClickOutside/index.ts
Expand Up @@ -9,7 +9,7 @@ export interface OnClickOutsideOptions extends ConfigurableWindow {
/**
* List of elements that should not trigger the event.
*/
ignore?: MaybeElementRef[]
ignore?: (MaybeElementRef | string)[]
/**
* Use capturing phase for internal event listener.
* @default true
Expand Down Expand Up @@ -37,7 +37,7 @@ export function onClickOutside<T extends OnClickOutsideOptions>(
handler: OnClickOutsideHandler<{ detectIframe: T['detectIframe'] }>,
options: T = {} as T,
) {
const { window = defaultWindow, ignore, capture = true, detectIframe = false } = options
const { window = defaultWindow, ignore = [], capture = true, detectIframe = false } = options

if (!window)
return
Expand All @@ -46,6 +46,19 @@ export function onClickOutside<T extends OnClickOutsideOptions>(

let fallback: number

const shouldIgnore = (event: PointerEvent) => {
return ignore.some((target) => {
if (typeof target === 'string') {
return Array.from(window.document.querySelectorAll(target))
.some(el => el === event.target || event.composedPath().includes(el))
}
else {
const el = unrefElement(target)
return el && (event.target === el || event.composedPath().includes(el))
}
})
}

const listener = (event: PointerEvent) => {
window.clearTimeout(fallback)

Expand All @@ -54,6 +67,9 @@ export function onClickOutside<T extends OnClickOutsideOptions>(
if (!el || el === event.target || event.composedPath().includes(el))
return

if (event.detail === 0)
shouldListen = !shouldIgnore(event)

if (!shouldListen) {
shouldListen = true
return
Expand All @@ -62,13 +78,6 @@ export function onClickOutside<T extends OnClickOutsideOptions>(
handler(event)
}

const shouldIgnore = (event: PointerEvent) => {
return ignore && ignore.some((target) => {
const el = unrefElement(target)
return el && (event.target === el || event.composedPath().includes(el))
})
}

const cleanup = [
useEventListener(window, 'click', listener, { passive: true, capture }),
useEventListener(window, 'pointerdown', (e) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
@@ -1,6 +1,6 @@
{
"name": "@vueuse/core",
"version": "9.7.0",
"version": "9.9.0",
"description": "Collection of essential Vue Composition Utilities",
"author": "Anthony Fu <https://github.com/antfu>",
"license": "MIT",
Expand Down
13 changes: 10 additions & 3 deletions packages/core/useAsyncState/index.ts
Expand Up @@ -10,7 +10,7 @@ export interface UseAsyncStateReturn<Data, Shallow extends boolean> {
execute: (delay?: number, ...args: any[]) => Promise<Data>
}

export interface UseAsyncStateOptions<Shallow extends boolean> {
export interface UseAsyncStateOptions<Shallow extends boolean, D = any> {
/**
* Delay for executing the promise. In milliseconds.
*
Expand All @@ -33,6 +33,12 @@ export interface UseAsyncStateOptions<Shallow extends boolean> {
*/
onError?: (e: unknown) => void

/**
* Callback when success is caught.
* @param {D} data
*/
onSuccess?: (data: D) => void

/**
* Sets the state to initialState before executing the promise.
*
Expand Down Expand Up @@ -71,17 +77,17 @@ export interface UseAsyncStateOptions<Shallow extends boolean> {
export function useAsyncState<Data, Shallow extends boolean = true>(
promise: Promise<Data> | ((...args: any[]) => Promise<Data>),
initialState: Data,
options?: UseAsyncStateOptions<Shallow>,
options?: UseAsyncStateOptions<Shallow, Data>,
): UseAsyncStateReturn<Data, Shallow> {
const {
immediate = true,
delay = 0,
onError = noop,
onSuccess = noop,
resetOnExecute = true,
shallow = true,
throwError,
} = options ?? {}

const state = shallow ? shallowRef(initialState) : ref(initialState)
const isReady = ref(false)
const isLoading = ref(false)
Expand All @@ -105,6 +111,7 @@ export function useAsyncState<Data, Shallow extends boolean = true>(
const data = await _promise
state.value = data
isReady.value = true
onSuccess(data)
}
catch (e) {
error.value = e
Expand Down
2 changes: 1 addition & 1 deletion packages/core/useElementBounding/index.ts
Expand Up @@ -93,7 +93,7 @@ export function useElementBounding(
watch(() => unrefElement(target), ele => !ele && update())

if (windowScroll)
useEventListener('scroll', update, { passive: true })
useEventListener('scroll', update, { capture: true, passive: true })
if (windowResize)
useEventListener('resize', update, { passive: true })

Expand Down
5 changes: 2 additions & 3 deletions packages/core/useFetch/index.ts
Expand Up @@ -95,7 +95,6 @@ type Combination = 'overwrite' | 'chain'
const payloadMapping: Record<string, string> = {
json: 'application/json',
text: 'text/plain',
formData: 'multipart/form-data',
}

export interface BeforeFetchContext {
Expand Down Expand Up @@ -532,9 +531,9 @@ export function useFetch<T>(url: MaybeComputedRef<string>, ...args: any[]): UseF
}

const rawPayload = resolveUnref(config.payload)
// Set the payload to json type only if it's not provided and a literal object is provided
// Set the payload to json type only if it's not provided and a literal object is provided and the object is not `formData`
// The only case we can deduce the content type and `fetch` can't
if (!payloadType && rawPayload && Object.getPrototypeOf(rawPayload) === Object.prototype)
if (!payloadType && rawPayload && Object.getPrototypeOf(rawPayload) === Object.prototype && !(rawPayload instanceof FormData))
config.payloadType = 'json'

return {
Expand Down
8 changes: 5 additions & 3 deletions packages/core/useStorage/demo.vue
Expand Up @@ -2,14 +2,16 @@
import { stringify } from '@vueuse/docs-utils'
import { useStorage } from '@vueuse/core'
const state = useStorage('vue-use-local-storage', {
const theDefault = {
name: 'Banana',
color: 'Yellow',
size: 'Medium',
count: 0,
})
}
const state = useStorage('vue-use-local-storage', theDefault)
const state2 = useStorage('vue-use-local-storage', theDefault)
const text = stringify(state)
const text = stringify(state2)
</script>

<template>
Expand Down
4 changes: 2 additions & 2 deletions packages/core/useStorage/index.md
Expand Up @@ -7,12 +7,12 @@ related: useLocalStorage, useSessionStorage, useStorageAsync

Reactive [LocalStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage)/[SessionStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage)

## Usage

::: tip
When using with Nuxt 3, this functions will **NOT** be auto imported in favor of Nitro's built-in [`useStorage()`](https://nitro.unjs.io/guide/introduction/storage). Use explicit import if you want to use the function from VueUse.
:::

## Usage

```js
import { useStorage } from '@vueuse/core'

Expand Down
8 changes: 5 additions & 3 deletions packages/core/useStorage/index.test.ts
Expand Up @@ -113,7 +113,7 @@ describe('useStorage', () => {
})

it('remove value', async () => {
storage.setItem(KEY, 'null')
storage.setItem(KEY, 'random')

const store = useStorage(KEY, null, storage)
store.value = null
Expand Down Expand Up @@ -433,10 +433,12 @@ describe('useStorage', () => {
expect(useStorage(KEY, 0).value).toBe(0)
expect(console.error).toHaveBeenCalledWith(new Error('getDefaultStorage error'))

expect(useStorage(KEY, 0, {
const ref = useStorage(KEY, 0, {
getItem: () => null,
setItem: () => { throw new Error('write item error') },
} as any).value).toBeUndefined()
} as any)
expect(ref.value).toBe(0)
ref.value = 1
expect(console.error).toHaveBeenCalledWith(new Error('write item error'))
})
})
90 changes: 55 additions & 35 deletions packages/core/useStorage/index.ts
@@ -1,6 +1,6 @@
import { nextTick, ref, shallowRef } from 'vue-demi'
import type { Awaitable, ConfigurableEventFilter, ConfigurableFlush, MaybeComputedRef, RemovableRef } from '@vueuse/shared'
import { isFunction, pausableWatch, resolveUnref } from '@vueuse/shared'
import { ref, shallowRef } from 'vue-demi'
import type { StorageLike } from '../ssr-handlers'
import { getSSRHandler } from '../ssr-handlers'
import { useEventListener } from '../useEventListener'
Expand Down Expand Up @@ -169,63 +169,83 @@ export function useStorage<T extends(string | number | boolean | object | null)>

function write(v: unknown) {
try {
if (v == null)
if (v == null) {
storage!.removeItem(key)
else
storage!.setItem(key, serializer.write(v))
}
else {
const serialized = serializer.write(v)
const oldValue = storage!.getItem(key)
if (oldValue !== serialized) {
storage!.setItem(key, serialized)

// send custom event to communicate within same page
if (window) {
window?.dispatchEvent(new StorageEvent('storage', {
key,
oldValue,
newValue: serialized,
storageArea: storage as any,
}))
}
}
}
}
catch (e) {
onError(e)
}
}

function read(event?: StorageEvent) {
pauseWatch()
try {
const rawValue = event
? event.newValue
: storage!.getItem(key)

if (rawValue == null) {
if (writeDefaults && rawInit !== null)
storage!.setItem(key, serializer.write(rawInit))
return rawInit
}
else if (!event && mergeDefaults) {
const value = serializer.read(rawValue)
if (isFunction(mergeDefaults))
return mergeDefaults(value, rawInit)
else if (type === 'object' && !Array.isArray(value))
return { ...rawInit as any, ...value }
return value
}
else if (typeof rawValue !== 'string') {
return rawValue
}
else {
return serializer.read(rawValue)
}
const rawValue = event
? event.newValue
: storage!.getItem(key)

if (rawValue == null) {
if (writeDefaults && rawInit !== null)
storage!.setItem(key, serializer.write(rawInit))
return rawInit
}
catch (e) {
onError(e)
else if (!event && mergeDefaults) {
const value = serializer.read(rawValue)
if (isFunction(mergeDefaults))
return mergeDefaults(value, rawInit)
else if (type === 'object' && !Array.isArray(value))
return { ...rawInit as any, ...value }
return value
}
finally {
resumeWatch()
else if (typeof rawValue !== 'string') {
return rawValue
}
else {
return serializer.read(rawValue)
}
}

function update(event?: StorageEvent) {
if (event && event.storageArea !== storage)
return

if (event && event.key === null) {
if (event && event.key == null) {
data.value = rawInit
return
}

if (event && event.key !== key)
return

data.value = read(event)
pauseWatch()
try {
data.value = read(event)
}
catch (e) {
onError(e)
}
finally {
// use nextTick to avoid infinite loop
if (event)
nextTick(resumeWatch)
else
resumeWatch()
}
}
}
12 changes: 11 additions & 1 deletion packages/core/useTimeAgo/index.md
Expand Up @@ -4,7 +4,7 @@ category: Time

# useTimeAgo

Reactive time ago.
Reactive time ago. Automatically update the time ago string when the time changes.

## Usage

Expand All @@ -21,3 +21,13 @@ const timeAgo = useTimeAgo(new Date(2021, 0, 1))
Time Ago: {{ timeAgo }}
</UseTimeAgo>
```

## Non-Reactivity Usage

In case you don't need the reactivity, you can use the `formatTimeAgo` function to get the formatted string instead of a Ref.

```js
import { formatTimeAgo } from '@vueuse/core'

const timeAgo = formatTimeAgo(new Date(2021, 0, 1)) // string
```
11 changes: 10 additions & 1 deletion packages/core/useTimeAgo/index.test.ts
Expand Up @@ -58,7 +58,7 @@ describe('useTimeAgo', () => {
})

test('get undefined when time is invalid', () => {
expect(useTimeAgo('invalid date').value).toBeUndefined()
expect(useTimeAgo('invalid date').value).toBe('')
})

describe('just now', () => {
Expand Down Expand Up @@ -249,6 +249,15 @@ describe('useTimeAgo', () => {
expect(useTimeAgo(changeTime, { rounding: 3 }).value).toBe('in 5.49 days')
})

test('rounding unit fallback', () => {
changeValue.value = getNeededTimeChange('month', 11.5)
expect(useTimeAgo(changeTime).value).toBe('next year')
expect(useTimeAgo(changeTime, { rounding: 'ceil' }).value).toBe('next year')
expect(useTimeAgo(changeTime, { rounding: 'floor' }).value).toBe('in 11 months')
expect(useTimeAgo(changeTime, { rounding: 1 }).value).toBe('in 0.9 year')
expect(useTimeAgo(changeTime, { rounding: 3 }).value).toBe('in 0.945 year')
})

test('custom units', () => {
changeValue.value = getNeededTimeChange('day', 14)
expect(useTimeAgo(changeTime).value).toBe('in 2 weeks')
Expand Down

0 comments on commit 3f6bf6b

Please sign in to comment.