From d409558a705965a905dfdaca55bc108d937531ba Mon Sep 17 00:00:00 2001 From: Craig Riley <68274157+craigrileyuk@users.noreply.github.com> Date: Thu, 9 Nov 2023 14:36:29 +0000 Subject: [PATCH 01/22] chore: typo (#3521) --- packages/core/useScriptTag/index.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core/useScriptTag/index.md b/packages/core/useScriptTag/index.md index 5ec732908c3..42cae28e090 100644 --- a/packages/core/useScriptTag/index.md +++ b/packages/core/useScriptTag/index.md @@ -4,9 +4,9 @@ category: Browser # useScriptTag -Creates a script tag, with support for automaticly unloading (deleting) the script tag on unmount. +Creates a script tag, with support for automatically unloading (deleting) the script tag on unmount. -If a script tag already exists for the given URL, `useScriptTag()` will not create another script tag, but keep in mind that, depending on how you use it, `useScriptTag()` it might have already loaded then unloaded that particular JS file from a previous call of `useScriptTag()`. +If a script tag already exists for the given URL, `useScriptTag()` will not create another script tag, but keep in mind that depending on how you use it, `useScriptTag()` might have already loaded then unloaded that particular JS file from a previous call of `useScriptTag()`. ## Usage @@ -22,9 +22,9 @@ useScriptTag( ) ``` -The script will be automatically loaded on the component mounted and removed when the component on unmounting. +The script will be automatically loaded when the component is mounted and removed when the component is unmounted. -## Configurations +## Configuration Set `manual: true` to have manual control over the timing to load the script. From c2f92c4c4587149cc506d4ae1aef2e31fcf40d46 Mon Sep 17 00:00:00 2001 From: Doctorwu <44631608+Doctor-wu@users.noreply.github.com> Date: Thu, 9 Nov 2023 22:40:21 +0800 Subject: [PATCH 02/22] docs(reactiveOmit): correct parameters order (#3519) --- packages/shared/reactiveOmit/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/reactiveOmit/index.md b/packages/shared/reactiveOmit/index.md index 60ef8f18199..50e3e06ebfc 100644 --- a/packages/shared/reactiveOmit/index.md +++ b/packages/shared/reactiveOmit/index.md @@ -35,7 +35,7 @@ const obj = reactive({ qux: true, }) -const picked = reactiveOmit(obj, (key, value) => key === 'baz' || value === true) +const picked = reactiveOmit(obj, (value, key) => key === 'baz' || value === true) // { bar: string, foo: string } ``` From fb9f3ba3975b7420e1469f7762ce3813b2ef9d10 Mon Sep 17 00:00:00 2001 From: Sma11X <540351143@qq.com> Date: Thu, 9 Nov 2023 22:42:08 +0800 Subject: [PATCH 03/22] docs(useIntersectionObserver): add type for example (#3516) --- packages/core/useIntersectionObserver/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/useIntersectionObserver/index.md b/packages/core/useIntersectionObserver/index.md index 29239bdeda9..74902c19f5d 100644 --- a/packages/core/useIntersectionObserver/index.md +++ b/packages/core/useIntersectionObserver/index.md @@ -49,7 +49,7 @@ const root = ref(null) const isVisible = ref(false) -function onIntersectionObserver([{ isIntersecting }]) { +function onIntersectionObserver([{ isIntersecting }]: IntersectionObserverEntry[]) { isVisible.value = isIntersecting } From 000337ef9586b5f9c469c01e171c3c32af37b1b5 Mon Sep 17 00:00:00 2001 From: Toni Engelhardt Date: Thu, 9 Nov 2023 14:42:26 +0000 Subject: [PATCH 04/22] docs: fix template ref type for infinite scroll (#3509) --- packages/core/useInfiniteScroll/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/useInfiniteScroll/index.md b/packages/core/useInfiniteScroll/index.md index d531cca1c16..54ac0a84c28 100644 --- a/packages/core/useInfiniteScroll/index.md +++ b/packages/core/useInfiniteScroll/index.md @@ -13,7 +13,7 @@ Infinite scrolling of the element. import { ref } from 'vue' import { useInfiniteScroll } from '@vueuse/core' -const el = ref(null) +const el = ref(null) const data = ref([1, 2, 3, 4, 5, 6]) useInfiniteScroll( From b6d2bd3577545209383abb8c531745a4dbc3642d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=B6=E8=BF=9C=E6=96=B9?= Date: Thu, 9 Nov 2023 22:35:33 +0800 Subject: [PATCH 05/22] fix(useStorage): fix defaults not unwrapped (#3534) --- packages/core/useStorage/index.test.ts | 15 +++++++++++++++ packages/core/useStorage/index.ts | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/core/useStorage/index.test.ts b/packages/core/useStorage/index.test.ts index d797854866c..7211c8a98e4 100644 --- a/packages/core/useStorage/index.test.ts +++ b/packages/core/useStorage/index.test.ts @@ -467,4 +467,19 @@ describe('useStorage', () => { ref.value = 1 expect(console.error).toHaveBeenCalledWith(new Error('write item error')) }) + + it.each([ + 1, + 'a', + [1, 2], + { a: 1 }, + new Map([[1, 2]]), + new Set([1, 2]), + ])('should work in conjunction with defaults', (value) => { + const basicRef = useStorage(KEY, () => value, storage) + expect(basicRef.value).toEqual(value) + storage.removeItem(KEY) + const objectRef = useStorage(KEY, value, storage) + expect(objectRef.value).toEqual(value) + }) }) diff --git a/packages/core/useStorage/index.ts b/packages/core/useStorage/index.ts index 82dd5a240ea..4d718c0a11b 100644 --- a/packages/core/useStorage/index.ts +++ b/packages/core/useStorage/index.ts @@ -145,7 +145,7 @@ export function useStorage }, } = options - const data = (shallow ? shallowRef : ref)(defaults) as RemovableRef + const data = (shallow ? shallowRef : ref)(typeof defaults === 'function' ? defaults() : defaults) as RemovableRef if (!storage) { try { From cefca9ab51e7a7c3ac562a5787770e496fdd5b31 Mon Sep 17 00:00:00 2001 From: huiliangShen Date: Thu, 9 Nov 2023 22:36:01 +0800 Subject: [PATCH 06/22] feat(useFileDialog): add directory parameters (#3513) Co-authored-by: banruo --- packages/core/useFileDialog/index.md | 1 + packages/core/useFileDialog/index.ts | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/packages/core/useFileDialog/index.md b/packages/core/useFileDialog/index.md index e340c280767..30db2a07a2f 100644 --- a/packages/core/useFileDialog/index.md +++ b/packages/core/useFileDialog/index.md @@ -13,6 +13,7 @@ import { useFileDialog } from '@vueuse/core' const { files, open, reset, onChange } = useFileDialog({ accept: 'image/*', // Set to accept only image files + directory: true, // Select directories instead of files if set true }) onChange((files) => { diff --git a/packages/core/useFileDialog/index.ts b/packages/core/useFileDialog/index.ts index 298c459584d..5c2b0498ad7 100644 --- a/packages/core/useFileDialog/index.ts +++ b/packages/core/useFileDialog/index.ts @@ -23,12 +23,19 @@ export interface UseFileDialogOptions extends ConfigurableDocument { * @default false */ reset?: boolean + /** + * Select directories instead of files. + * @see [HTMLInputElement webkitdirectory](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/webkitdirectory) + * @default false + */ + directory?: boolean } const DEFAULT_OPTIONS: UseFileDialogOptions = { multiple: true, accept: '*', reset: false, + directory: false, } export interface UseFileDialogReturn { @@ -79,6 +86,8 @@ export function useFileDialog(options: UseFileDialogOptions = {}): UseFileDialog } input.multiple = _options.multiple! input.accept = _options.accept! + // webkitdirectory key is not stabled, maybe replaced in the future. + input.webkitdirectory = _options.directory! if (hasOwn(_options, 'capture')) input.capture = _options.capture! if (_options.reset) From e024235981dbce172e2bdb60c7ca917b233709ff Mon Sep 17 00:00:00 2001 From: yn <95486647+yn-lgting@users.noreply.github.com> Date: Thu, 9 Nov 2023 22:37:02 +0800 Subject: [PATCH 07/22] refactor(onKeyStroke): remove duplicate code (#3495) --- packages/core/onKeyStroke/index.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/core/onKeyStroke/index.ts b/packages/core/onKeyStroke/index.ts index 986a84de6cc..4133217ef7c 100644 --- a/packages/core/onKeyStroke/index.ts +++ b/packages/core/onKeyStroke/index.ts @@ -31,9 +31,6 @@ function createKeyPredicate(keyFilter: KeyFilter): KeyPredicate { return () => true } -export function onKeyStroke(key: KeyFilter, handler: (event: KeyboardEvent) => void, options?: OnKeyStrokeOptions): () => void -export function onKeyStroke(handler: (event: KeyboardEvent) => void, options?: OnKeyStrokeOptions): () => void - /** * Listen for keyboard keystrokes. * From d98468d140eddee4be66270981902668dce46c83 Mon Sep 17 00:00:00 2001 From: Doctorwu <44631608+Doctor-wu@users.noreply.github.com> Date: Thu, 9 Nov 2023 22:41:21 +0800 Subject: [PATCH 08/22] fix(useSortable): prevent from creating multi instances (#3501) --- packages/integrations/useSortable/index.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/integrations/useSortable/index.ts b/packages/integrations/useSortable/index.ts index a9f9e6bb0b2..bc47ed9dda0 100644 --- a/packages/integrations/useSortable/index.ts +++ b/packages/integrations/useSortable/index.ts @@ -39,7 +39,7 @@ export function useSortable( list: MaybeRefOrGetter, options: UseSortableOptions = {}, ): UseSortableReturn { - let sortable: Sortable + let sortable: Sortable | undefined const { document = defaultDocument, ...resetOptions } = options @@ -51,12 +51,15 @@ export function useSortable( const start = () => { const target = (typeof el === 'string' ? document?.querySelector(el) : unrefElement(el)) - if (!target) + if (!target || sortable !== undefined) return sortable = new Sortable(target as HTMLElement, { ...defaultOptions, ...resetOptions }) } - const stop = () => sortable?.destroy() + const stop = () => { + sortable?.destroy() + sortable = undefined + } const option = (name: K, value?: Options[K]) => { if (value !== undefined) From 151f9b002d56ac68f9cf520b7d895ff89b2a20a3 Mon Sep 17 00:00:00 2001 From: jahnli Date: Thu, 9 Nov 2023 22:44:06 +0800 Subject: [PATCH 09/22] fix(useAxios): bail out on request abort (#3394) --- packages/integrations/useAxios/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/integrations/useAxios/index.ts b/packages/integrations/useAxios/index.ts index 848b6fa0d05..f7044373fb3 100644 --- a/packages/integrations/useAxios/index.ts +++ b/packages/integrations/useAxios/index.ts @@ -225,6 +225,8 @@ export function useAxios, D = any>(...args: any[]) instance(_url, { ...defaultConfig, ...typeof executeUrl === 'object' ? executeUrl : config, cancelToken: cancelToken.token }) .then((r: any) => { + if (isAborted.value) + return response.value = r const result = r.data data.value = result From 6040e1cceb2f002868e2f7f74dba54aba45ece1c Mon Sep 17 00:00:00 2001 From: Curt Grimes Date: Thu, 9 Nov 2023 08:54:53 -0600 Subject: [PATCH 10/22] feat(createEventHook): allow trigger to optionally have no parameters (#3507) --- packages/shared/createEventHook/index.test.ts | 12 ++++++++++++ packages/shared/createEventHook/index.ts | 17 +++++++++-------- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/packages/shared/createEventHook/index.test.ts b/packages/shared/createEventHook/index.test.ts index ec7ccc86e5b..366164ec620 100644 --- a/packages/shared/createEventHook/index.test.ts +++ b/packages/shared/createEventHook/index.test.ts @@ -27,6 +27,18 @@ describe('createEventHook', () => { expect(message).toBe('Hello World') }) + it('should trigger event with no params', () => { + let timesFired = 0 + + const { on: onResult, trigger } = createEventHook() + + onResult(() => timesFired++) + trigger() + trigger() + + expect(timesFired).toBe(2) + }) + it('should add and remove event listener', () => { const listener = vi.fn() const { on, off, trigger } = createEventHook() diff --git a/packages/shared/createEventHook/index.ts b/packages/shared/createEventHook/index.ts index d9a4f535227..9f99ee7d339 100644 --- a/packages/shared/createEventHook/index.ts +++ b/packages/shared/createEventHook/index.ts @@ -4,9 +4,10 @@ */ import { tryOnScopeDispose } from '../tryOnScopeDispose' -export type EventHookOn = (fn: (param: T) => void) => { off: () => void } -export type EventHookOff = (fn: (param: T) => void) => void -export type EventHookTrigger = (param: T) => Promise +type Callback = T extends void ? () => void : (param: T) => void +export type EventHookOn = (fn: Callback) => { off: () => void } +export type EventHookOff = (fn: Callback) => void +export type EventHookTrigger = (param?: T) => Promise export interface EventHook { on: EventHookOn @@ -20,13 +21,13 @@ export interface EventHook { * @see https://vueuse.org/createEventHook */ export function createEventHook(): EventHook { - const fns: Set<(param: T) => void> = new Set() + const fns: Set> = new Set() - const off = (fn: (param: T) => void) => { + const off = (fn: Callback) => { fns.delete(fn) } - const on = (fn: (param: T) => void) => { + const on = (fn: Callback) => { fns.add(fn) const offFn = () => off(fn) @@ -37,8 +38,8 @@ export function createEventHook(): EventHook { } } - const trigger = (param: T) => { - return Promise.all(Array.from(fns).map(fn => fn(param))) + const trigger: EventHookTrigger = (param?: T) => { + return Promise.all(Array.from(fns).map(fn => param ? fn(param) : (fn as Callback)())) } return { From 6985152d5c1a66e88dd24f38803fe3684d7bdb63 Mon Sep 17 00:00:00 2001 From: Doctorwu <44631608+Doctor-wu@users.noreply.github.com> Date: Thu, 9 Nov 2023 23:02:23 +0800 Subject: [PATCH 11/22] fix(onClickOutside): adjust shouldListen handling timing (#3503) --- packages/core/onClickOutside/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/core/onClickOutside/index.ts b/packages/core/onClickOutside/index.ts index 7374daad940..efa5f851e00 100644 --- a/packages/core/onClickOutside/index.ts +++ b/packages/core/onClickOutside/index.ts @@ -90,8 +90,7 @@ export function onClickOutside( useEventListener(window, 'click', listener, { passive: true, capture }), useEventListener(window, 'pointerdown', (e) => { const el = unrefElement(target) - if (el) - shouldListen = !e.composedPath().includes(el) && !shouldIgnore(e) + shouldListen = !shouldIgnore(e) && !!(el && !e.composedPath().includes(el)) }, { passive: true }), detectIframe && useEventListener(window, 'blur', (event) => { setTimeout(() => { From 9b0141ca32e4d2a4dee4d1186c70e4f40cb7119e Mon Sep 17 00:00:00 2001 From: Evan Date: Thu, 9 Nov 2023 16:06:59 +0100 Subject: [PATCH 12/22] fix(useWebSocket): webworker support (#3469) Co-authored-by: Anthony Fu --- packages/core/useWebSocket/index.ts | 7 ++++--- packages/shared/utils/is.ts | 1 + tsconfig.json | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/core/useWebSocket/index.ts b/packages/core/useWebSocket/index.ts index bbbfc2a90a4..85dbf2bc77f 100644 --- a/packages/core/useWebSocket/index.ts +++ b/packages/core/useWebSocket/index.ts @@ -1,7 +1,7 @@ import type { Ref } from 'vue-demi' import { ref, watch } from 'vue-demi' import type { Fn, MaybeRefOrGetter } from '@vueuse/shared' -import { isClient, toRef, tryOnScopeDispose, useIntervalFn } from '@vueuse/shared' +import { isClient, isWorker, toRef, tryOnScopeDispose, useIntervalFn } from '@vueuse/shared' import { useEventListener } from '../useEventListener' export type WebSocketStatus = 'OPEN' | 'CONNECTING' | 'CLOSED' @@ -288,12 +288,13 @@ export function useWebSocket( } if (autoClose) { - useEventListener('beforeunload', () => close()) + if (isClient) + useEventListener('beforeunload', () => close()) tryOnScopeDispose(close) } const open = () => { - if (!isClient) + if (!isClient && !isWorker) return close() explicitlyClosed = false diff --git a/packages/shared/utils/is.ts b/packages/shared/utils/is.ts index 977d2fa17f2..24fc7b3eabf 100644 --- a/packages/shared/utils/is.ts +++ b/packages/shared/utils/is.ts @@ -1,5 +1,6 @@ /* eslint-disable antfu/top-level-function */ export const isClient = typeof window !== 'undefined' && typeof document !== 'undefined' +export const isWorker = typeof WorkerGlobalScope !== 'undefined' && globalThis instanceof WorkerGlobalScope export const isDef = (val?: T): val is T => typeof val !== 'undefined' export const notNullish = (val?: T | null | undefined): val is T => val != null export const assert = (condition: boolean, ...infos: any[]) => { diff --git a/tsconfig.json b/tsconfig.json index 5f727dcb430..4a690cc04b0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "target": "es2020", "jsx": "preserve", - "lib": ["ESNext", "DOM", "DOM.Iterable"], + "lib": ["ESNext", "DOM", "DOM.Iterable", "webworker"], "baseUrl": ".", "rootDir": ".", "module": "esnext", From 8eb0b2d7bb254150c8498afe747a7c1a5179b747 Mon Sep 17 00:00:00 2001 From: Lee Crosby Date: Thu, 9 Nov 2023 15:07:53 +0000 Subject: [PATCH 13/22] feat(onLongClick): return stop function (#3506) Co-authored-by: lee --- packages/core/onLongPress/index.test.ts | 24 ++++++++++++++++++++++++ packages/core/onLongPress/index.ts | 11 +++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/packages/core/onLongPress/index.test.ts b/packages/core/onLongPress/index.test.ts index 8e51a919249..86d6a7f9107 100644 --- a/packages/core/onLongPress/index.test.ts +++ b/packages/core/onLongPress/index.test.ts @@ -87,6 +87,29 @@ describe('onLongPress', () => { expect(onParentLongPressCallback).toHaveBeenCalledTimes(0) } + async function stopEventListeners(isRef: boolean) { + const onLongPressCallback = vi.fn() + const stop = onLongPress(isRef ? element : element.value, onLongPressCallback, { modifiers: { stop: true } }) + + // before calling stop, the callback should be called + element.value.dispatchEvent(pointerdownEvent) + + await promiseTimeout(500) + + expect(onLongPressCallback).toHaveBeenCalledTimes(1) + + stop() + + // before calling stop, the callback should no longer be called + onLongPressCallback.mockClear() + + element.value.dispatchEvent(pointerdownEvent) + + await promiseTimeout(500) + + expect(onLongPressCallback).toHaveBeenCalledTimes(0) + } + function suites(isRef: boolean) { describe('given no options', () => { it('should trigger longpress after 500ms', () => triggerCallback(isRef)) @@ -97,6 +120,7 @@ describe('onLongPress', () => { it('should not tirgger longpress when child element on longpress', () => notTriggerCallbackOnChildLongPress(isRef)) it('should work with once and prevent modifiers', () => workOnceAndPreventModifiers(isRef)) it('should stop propagation', () => stopPropagation(isRef)) + it('should remove event listeners after being stopped', () => stopEventListeners(isRef)) }) } diff --git a/packages/core/onLongPress/index.ts b/packages/core/onLongPress/index.ts index eae6cdce882..86227fb31b7 100644 --- a/packages/core/onLongPress/index.ts +++ b/packages/core/onLongPress/index.ts @@ -1,3 +1,4 @@ +import type { Fn } from '@vueuse/shared' import { computed } from 'vue-demi' import type { MaybeElementRef } from '../unrefElement' import { unrefElement } from '../unrefElement' @@ -63,6 +64,12 @@ export function onLongPress( once: options?.modifiers?.once, } - useEventListener(elementRef, 'pointerdown', onDown, listenerOptions) - useEventListener(elementRef, ['pointerup', 'pointerleave'], clear, listenerOptions) + const cleanup = [ + useEventListener(elementRef, 'pointerdown', onDown, listenerOptions), + useEventListener(elementRef, ['pointerup', 'pointerleave'], clear, listenerOptions), + ].filter(Boolean) as Fn[] + + const stop = () => cleanup.forEach(fn => fn()) + + return stop } From 892666b708648b9e18e9999b0f725075d4417757 Mon Sep 17 00:00:00 2001 From: Doctorwu <44631608+Doctor-wu@users.noreply.github.com> Date: Thu, 9 Nov 2023 23:11:32 +0800 Subject: [PATCH 14/22] feat(syncRef): enhance syncRef type restrict (#3515) --- packages/shared/syncRef/index.test.ts | 153 ++++++++++++++++++++++++++ packages/shared/syncRef/index.ts | 133 +++++++++++++++++++--- 2 files changed, 270 insertions(+), 16 deletions(-) diff --git a/packages/shared/syncRef/index.test.ts b/packages/shared/syncRef/index.test.ts index b982630caed..bff72eeb557 100644 --- a/packages/shared/syncRef/index.test.ts +++ b/packages/shared/syncRef/index.test.ts @@ -97,4 +97,157 @@ describe('syncRef', () => { expect(right.value).toBe(10) expect(left.value).toBe(5) }) + + it('ts works', () => { + const ref0 = ref(0) + const ref1 = ref(1) + const refString = ref('1') + const refNumString = ref(1) + const refNumBoolean = ref(1) + // L = A && direction === 'both' + syncRef(ref0, ref1)() + syncRef(ref0, ref1, { + direction: 'both', + })() + syncRef(ref0, ref1, { + direction: 'both', + transform: {}, + })() + syncRef(ref0, ref1, { + direction: 'both', + transform: { + ltr: v => v, + }, + })() + syncRef(ref0, ref1, { + direction: 'both', + transform: { + rtl: v => v, + }, + })() + syncRef(ref0, ref1, { + direction: 'both', + transform: { + ltr: v => v, + rtl: v => v, + }, + })() + syncRef(ref0, ref1, { + direction: 'both', + transform: { + // @ts-expect-error wrong type, should be (left: L) => R + ltr: v => v.toString(), + rtl: v => v, + }, + })() + // L = A && direction === 'ltr' + syncRef(ref0, ref1, { + direction: 'ltr', + })() + syncRef(ref0, ref1, { + direction: 'ltr', + transform: {}, + })() + syncRef(ref0, ref1, { + direction: 'ltr', + transform: { + ltr: v => v, + }, + })() + syncRef(ref0, ref1, { + direction: 'ltr', + transform: { + // @ts-expect-error wrong transform type, should be ltr + rtl: v => v, + }, + })() + // L = A && direction === 'rtl' + syncRef(ref0, ref1, { + direction: 'rtl', + })() + syncRef(ref0, ref1, { + direction: 'rtl', + transform: {}, + })() + syncRef(ref0, ref1, { + direction: 'rtl', + transform: { + rtl: v => v, + }, + })() + // L ⊆ R && direction === 'both' + // @ts-expect-error wrong type, should provide transform + syncRef(ref0, refNumString, { + direction: 'both', + })() + syncRef(ref0, refNumString, { + direction: 'both', + transform: { + ltr: v => v.toString(), + rtl: v => Number(v), + }, + })() + // L ⊆ R && direction === 'ltr' + syncRef(ref0, refNumString, { + direction: 'ltr', + transform: { + ltr: v => v.toString(), + }, + })() + // L ⊆ R && direction === 'rtl' + syncRef(ref0, refNumString, { + direction: 'ltr', + transform: { + ltr: v => Number(v), + }, + })() + // L ∩ R = ∅ && direction === 'both' + syncRef(ref0, refString, { + direction: 'both', + transform: { + ltr: v => v.toString(), + rtl: v => Number(v), + }, + })() + // L ∩ R = ∅ && direction === 'ltr' + syncRef(ref0, refString, { + direction: 'ltr', + transform: { + ltr: v => v.toString(), + }, + })() + // L ∩ R = ∅ && direction === 'rtl' + syncRef(ref0, refString, { + direction: 'rtl', + transform: { + rtl: v => Number(v), + }, + })() + // L ∩ R = ∅ && direction === 'both' + syncRef(ref0, refString, { + direction: 'both', + // @ts-expect-error wrong type, should provide ltr + transform: { + rtl: v => Number(v), + }, + })() + // L ∩ R ≠ ∅ + syncRef(refNumString, refNumBoolean, { + transform: { + ltr: v => Number(v), + rtl: v => Number(v), + }, + }) + + // @ts-expect-error lack of options + syncRef(ref0, refString)() + + syncRef(ref0, refNumBoolean, { + direction: 'ltr', + })() + + syncRef(refNumBoolean, ref0, { + direction: 'rtl', + }) + }) }) diff --git a/packages/shared/syncRef/index.ts b/packages/shared/syncRef/index.ts index c0fe3391bcc..00d441b1dd0 100644 --- a/packages/shared/syncRef/index.ts +++ b/packages/shared/syncRef/index.ts @@ -1,9 +1,105 @@ -import type { Ref } from 'vue-demi' +import { type Ref } from 'vue-demi' import type { ConfigurableFlushSync } from '../utils' import type { WatchPausableReturn } from '../watchPausable' import { pausableWatch } from '../watchPausable' -export interface SyncRefOptions extends ConfigurableFlushSync { +type Direction = 'ltr' | 'rtl' | 'both' +type SpecificFieldPartial = Partial> & Omit +/** + * A = B + */ +type Equal = A extends B ? (B extends A ? true : false) : false + +/** + * A ∩ B ≠ ∅ + */ +type IntersectButNotEqual = Equal extends true + ? false + : A & B extends never + ? false + : true + +/** + * A ⊆ B + */ +type IncludeButNotEqual = Equal extends true + ? false + : A extends B + ? true + : false + +/** + * A ∩ B = ∅ + */ +type NotIntersect = Equal extends true + ? false + : A & B extends never + ? true + : false + +// L = R +interface EqualType< + D extends Direction, + L, + R, + O extends keyof Transform = D extends 'both' ? 'ltr' | 'rtl' : D, +> { + transform?: SpecificFieldPartial, O>, O> +} + +type StrictIncludeMap, L, R> = (Equal<[IncludeType, D], ['LR', 'ltr']> +& Equal<[IncludeType, D], ['RL', 'rtl']>) extends true + ? { + transform?: SpecificFieldPartial, D>, D> + } : { + transform: Pick, D> + } + +// L ⊆ R +type StrictIncludeType = D extends 'both' + ? { + transform: SpecificFieldPartial, IncludeType extends 'LR' ? 'ltr' : 'rtl'> + } + : D extends Exclude + ? StrictIncludeMap + : never + +// L ∩ R ≠ ∅ +type IntersectButNotEqualType = D extends 'both' + ? { + transform: Transform + } + : D extends Exclude + ? { + transform: Pick, D> + } + : never + +// L ∩ R = ∅ +type NotIntersectType = IntersectButNotEqualType +interface Transform { + ltr: (left: L) => R + rtl: (right: R) => L +} + +type TransformType = Equal extends true + // L = R + ? EqualType + : IncludeButNotEqual extends true + // L ⊆ R + ? StrictIncludeType<'LR', D, L, R> + : IncludeButNotEqual extends true + // R ⊆ L + ? StrictIncludeType<'RL', D, L, R> + : IntersectButNotEqual extends true + // L ∩ R ≠ ∅ + ? IntersectButNotEqualType + : NotIntersect extends true + // L ∩ R = ∅ + ? NotIntersectType + : never + +export type SyncRefOptions = ConfigurableFlushSync & { /** * Watch deeply * @@ -22,36 +118,41 @@ export interface SyncRefOptions extends ConfigurableFlushSync { * * @default 'both' */ - direction?: 'ltr' | 'rtl' | 'both' + direction?: D - /** - * Custom transform function - */ - transform?: { - ltr?: (left: L) => R - rtl?: (right: R) => L - } -} +} & TransformType /** * Two-way refs synchronization. - * + * From the set theory perspective to restrict the option's type + * Check in the following order: + * 1. L = R + * 2. L ∩ R ≠ ∅ + * 3. L ⊆ R + * 4. L ∩ R = ∅ * @param left * @param right + * @param [options?] */ -export function syncRef(left: Ref, right: Ref, options: SyncRefOptions = {}) { +export function syncRef( + left: Ref, + right: Ref, + ...[options]: Equal extends true + ? [options?: SyncRefOptions] + : [options: SyncRefOptions] +) { const { flush = 'sync', deep = false, immediate = true, direction = 'both', transform = {}, - } = options + } = options || {} const watchers: WatchPausableReturn[] = [] - const transformLTR = transform.ltr ?? (v => v) - const transformRTL = transform.rtl ?? (v => v) + const transformLTR = ('ltr' in transform && transform.ltr) || (v => v) + const transformRTL = ('rtl' in transform && transform.rtl) || (v => v) if (direction === 'both' || direction === 'ltr') { watchers.push(pausableWatch( From 931b279798fcdd89d737eed42ca1d94fab27b071 Mon Sep 17 00:00:00 2001 From: Chen <30278419+nothing-sy@users.noreply.github.com> Date: Thu, 9 Nov 2023 23:16:49 +0800 Subject: [PATCH 15/22] feat(useWindowScroll): allow rewriting back to scroll (#3500) Co-authored-by: chensiyuan <250758092@qq.com> Co-authored-by: Anthony Fu --- packages/core/useWindowScroll/demo.vue | 8 +++++- packages/core/useWindowScroll/index.md | 2 ++ packages/core/useWindowScroll/index.ts | 36 +++++++++++++++++++++----- 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/packages/core/useWindowScroll/demo.vue b/packages/core/useWindowScroll/demo.vue index c71da862ca8..28d59a0dea2 100644 --- a/packages/core/useWindowScroll/demo.vue +++ b/packages/core/useWindowScroll/demo.vue @@ -1,7 +1,7 @@