diff --git a/README.md b/README.md index 9fbdfe76056d..a5d926ac903e 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Collection of essential Vue Composition Utilities NPM version NPM Downloads Docs & Demos -Function Count +Function Count
GitHub stars

diff --git a/package.json b/package.json index 4953aef7e9ae..3a9b0c1c73c9 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "@vitest/ui": "^0.2.8", "@vue/compiler-sfc": "^3.2.30", "@vue/composition-api": "^1.4.6", - "@vue/test-utils": "^1.3.0", + "@vue/test-utils": "^2.0.0-rc.18", "@vue/theme": "^1.0.0", "axios": "^0.25.0", "bumpp": "^7.1.1", diff --git a/packages/components/index.ts b/packages/components/index.ts index 3d380cfb03a1..6a04b8390afd 100644 --- a/packages/components/index.ts +++ b/packages/components/index.ts @@ -1,5 +1,7 @@ export * from '../core/onClickOutside/component' export * from '../core/onClickOutside/directive' +export * from '../core/onLongPress/component' +export * from '../core/onLongPress/directive' export * from '../core/useActiveElement/component' export * from '../core/useBattery/component' export * from '../core/useBrowserLocation/component' diff --git a/packages/core/index.ts b/packages/core/index.ts index 8b5a049febf2..2471ee261956 100644 --- a/packages/core/index.ts +++ b/packages/core/index.ts @@ -4,6 +4,7 @@ export * from './computedInject' export * from './createUnrefFn' export * from './onClickOutside' export * from './onKeyStroke' +export * from './onLongPress' export * from './onStartTyping' export * from './templateRef' export * from './unrefElement' diff --git a/packages/core/onLongpress/component.ts b/packages/core/onLongpress/component.ts new file mode 100644 index 000000000000..01547ff27c8b --- /dev/null +++ b/packages/core/onLongpress/component.ts @@ -0,0 +1,28 @@ +import { defineComponent, h, ref } from 'vue-demi' +import type { RenderableComponent } from '../types' +import type { OnLongPressOptions } from '.' +import { onLongPress } from '.' + +export interface OnLongPressProps extends RenderableComponent { + options?: OnLongPressOptions +} + +export const OnLongPress = defineComponent({ + name: 'OnLongPress', + props: ['as', 'options'] as unknown as undefined, + emits: ['trigger'], + setup(props, { slots, emit }) { + const target = ref() + onLongPress( + target, + (e) => { + emit('trigger', e) + }, + props.options, + ) + return () => { + if (slots.default) + return h(props.as || 'div', { ref: target }, slots.default()) + } + }, +}) diff --git a/packages/core/onLongpress/demo.vue b/packages/core/onLongpress/demo.vue new file mode 100644 index 000000000000..d972c2f8ca19 --- /dev/null +++ b/packages/core/onLongpress/demo.vue @@ -0,0 +1,33 @@ + + + diff --git a/packages/core/onLongpress/directive.test.ts b/packages/core/onLongpress/directive.test.ts new file mode 100644 index 000000000000..6e4aff9e19b5 --- /dev/null +++ b/packages/core/onLongpress/directive.test.ts @@ -0,0 +1,91 @@ +import { defineComponent } from 'vue-demi' +import type { VueWrapper } from '@vue/test-utils' +import { mount } from '@vue/test-utils' +import { promiseTimeout } from '@vueuse/shared' + +import { VOnLongPress } from './directive' +import type { OnLongPressOptions } from '.' + +const App = defineComponent({ + props: { + onLongPress: { + type: Function, + required: true, + }, + options: { + type: Object, + required: false, + }, + }, + + template: ` + `, +}) + +describe('vOnLongPress', () => { + let onLongPress = vi.fn() + let wrapper: VueWrapper + + describe('given no options', () => { + beforeEach(() => { + onLongPress = vi.fn() + wrapper = mount(App, { + props: { + onLongPress, + }, + global: { + directives: { + 'on-longpress': VOnLongPress, + }, + }, + }) + }) + + it('should be defined', () => { + expect(wrapper).toBeDefined() + }) + + it('should trigger longpress after 500ms', async() => { + const element = wrapper.get('[data-test=element]') + await element.trigger('pointerdown') + await promiseTimeout(500) + expect(onLongPress).toHaveBeenCalledTimes(1) + }) + }) + + describe('given options', () => { + beforeEach(() => { + onLongPress = vi.fn() + const options: OnLongPressOptions = { + delay: 1000, + } + wrapper = mount(App, { + props: { + onLongPress, + options, + }, + global: { + directives: { + 'on-longpress': VOnLongPress, + }, + }, + }) + }) + + it('should be defined', () => { + expect(wrapper).toBeDefined() + }) + + it('should trigger longpress after 500ms', async() => { + const element = wrapper.get('[data-test=element]') + await element.trigger('pointerdown') + await promiseTimeout(500) + expect(onLongPress).toHaveBeenCalledTimes(0) + await promiseTimeout(500) + expect(onLongPress).toHaveBeenCalledTimes(1) + }) + }) +}) diff --git a/packages/core/onLongpress/directive.ts b/packages/core/onLongpress/directive.ts new file mode 100644 index 000000000000..165104024c13 --- /dev/null +++ b/packages/core/onLongpress/directive.ts @@ -0,0 +1,20 @@ +import type { FunctionDirective } from 'vue-demi' +import type { OnLongPressOptions } from '.' +import { onLongPress } from '.' + +type BindingValueFunction = (evt: PointerEvent) => void + +interface BindingValueObject { + handler: BindingValueFunction + options: OnLongPressOptions +} + +export const VOnLongPress: FunctionDirective< +HTMLElement, +BindingValueFunction | BindingValueObject +> = (el, binding) => { + if (typeof binding.value === 'function') + onLongPress(el, binding.value) + else + onLongPress(el, binding.value.handler, binding.value.options) +} diff --git a/packages/core/onLongpress/index.md b/packages/core/onLongpress/index.md new file mode 100644 index 000000000000..6888343a4ed0 --- /dev/null +++ b/packages/core/onLongpress/index.md @@ -0,0 +1,110 @@ +--- +category: Sensors +--- + +# onLongPress + +Listen for a long press on an element. + +## Usage + +### As a hook + +```html + + + +``` + +### As a component + + + +```html + + + +``` + +### As a directive + + + +```html + + + +``` diff --git a/packages/core/onLongpress/index.test.ts b/packages/core/onLongpress/index.test.ts new file mode 100644 index 000000000000..a1b5cd09a837 --- /dev/null +++ b/packages/core/onLongpress/index.test.ts @@ -0,0 +1,68 @@ +import { promiseTimeout } from '@vueuse/shared' +import type { Ref } from 'vue-demi' +import { ref } from 'vue-demi' +import { onLongPress } from '.' + +const pointerdownEvent = new PointerEvent('pointerdown') + +describe('onLongPress', () => { + let element: Ref + + beforeEach(() => { + element = ref(document.createElement('div')) + }) + + it('should be defined', () => { + expect(onLongPress).toBeDefined() + }) + + describe('given argument is ref', () => { + describe('given no options', () => { + it('should trigger longpress after 500ms', async() => { + const onLongPressCallback = vi.fn() + onLongPress(element, onLongPressCallback) + element.value.dispatchEvent(pointerdownEvent) + expect(onLongPressCallback).toHaveBeenCalledTimes(0) + await promiseTimeout(500) + expect(onLongPressCallback).toHaveBeenCalledTimes(1) + }) + }) + + describe('given options', () => { + it('should trigger longpress after options.delay ms', async() => { + const onLongPressCallback = vi.fn() + onLongPress(element, onLongPressCallback, { delay: 1000 }) + element.value.dispatchEvent(pointerdownEvent) + await promiseTimeout(500) + expect(onLongPressCallback).toHaveBeenCalledTimes(0) + await promiseTimeout(500) + expect(onLongPressCallback).toHaveBeenCalledTimes(1) + }) + }) + }) + + describe('given argument is no ref', () => { + describe('given no options', () => { + it('should trigger longpress after 500ms', async() => { + const onLongPressCallback = vi.fn() + onLongPress(element.value, onLongPressCallback) + element.value.dispatchEvent(pointerdownEvent) + expect(onLongPressCallback).toHaveBeenCalledTimes(0) + await promiseTimeout(500) + expect(onLongPressCallback).toHaveBeenCalledTimes(1) + }) + }) + + describe('given options', () => { + it('should trigger longpress after options.delay ms', async() => { + const onLongPressCallback = vi.fn() + onLongPress(element.value, onLongPressCallback, { delay: 1000 }) + element.value.dispatchEvent(pointerdownEvent) + await promiseTimeout(500) + expect(onLongPressCallback).toHaveBeenCalledTimes(0) + await promiseTimeout(500) + expect(onLongPressCallback).toHaveBeenCalledTimes(1) + }) + }) + }) +}) diff --git a/packages/core/onLongpress/index.ts b/packages/core/onLongpress/index.ts new file mode 100644 index 000000000000..1bfcba2d659f --- /dev/null +++ b/packages/core/onLongpress/index.ts @@ -0,0 +1,43 @@ +import type { MaybeElementRef } from '@vueuse/core' +import { unrefElement, useEventListener } from '@vueuse/core' +import { computed } from 'vue-demi' + +const DEFAULT_DELAY = 500 + +export interface OnLongPressOptions { + /** + * Time in ms till `longpress` gets called + * + * @default 500 + */ + delay?: number +} + +export function onLongPress( + target: MaybeElementRef, + handler: (evt: PointerEvent) => void, + options?: OnLongPressOptions, +) { + const elementRef = computed(() => unrefElement(target)) + + let timeout: number | null = null + + function clear() { + if (timeout != null) { + clearTimeout(timeout) + timeout = null + } + } + + function onDown(ev: PointerEvent) { + clear() + timeout = setTimeout( + () => handler(ev), + options?.delay ?? DEFAULT_DELAY, + ) as unknown as number + } + + useEventListener(elementRef, 'pointerdown', onDown) + useEventListener(elementRef, 'pointerup', clear) + useEventListener(elementRef, 'pointerleave', clear) +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1da6672a2df0..c3aca9eca2eb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,7 +23,7 @@ importers: '@vitest/ui': ^0.2.8 '@vue/compiler-sfc': ^3.2.30 '@vue/composition-api': ^1.4.6 - '@vue/test-utils': ^1.3.0 + '@vue/test-utils': ^2.0.0-rc.18 '@vue/theme': ^1.0.0 axios: ^0.25.0 bumpp: ^7.1.1 @@ -83,7 +83,7 @@ importers: '@vitest/ui': 0.2.8 '@vue/compiler-sfc': 3.2.30 '@vue/composition-api': 1.4.6_vue@3.2.30 - '@vue/test-utils': 1.3.0_vue@3.2.30 + '@vue/test-utils': 2.0.0-rc.18_vue@3.2.30 '@vue/theme': 1.0.0_b4ee0005b62e2a8821f93ec48f0b95ae axios: 0.25.0 bumpp: 7.1.1 @@ -2644,15 +2644,11 @@ packages: resolution: {integrity: sha512-B3HouBtUxcfu2w2d+VhdLcVBXKYYhXiFMAfQ+hoe8NUhKkPRkWDIqhpuehCZxVQ3S2dN1P1WfKGlxGC+pfmxGg==} dev: true - /@vue/test-utils/1.3.0_vue@3.2.30: - resolution: {integrity: sha512-Xk2Xiyj2k5dFb8eYUKkcN9PzqZSppTlx7LaQWBbdA8tqh3jHr/KHX2/YLhNFc/xwDrgeLybqd+4ZCPJSGPIqeA==} + /@vue/test-utils/2.0.0-rc.18_vue@3.2.30: + resolution: {integrity: sha512-aifolXjVdsogjaLmDoZ0FU8vN+R67aWmg9OuVeED4w5Ij5GFQLrlhM19uhWe/r5xXUL4fXMk3pX5wW6FJP1NcQ==} peerDependencies: - vue: 2.x - vue-template-compiler: ^2.x + vue: ^3.0.1 dependencies: - dom-event-types: 1.0.0 - lodash: 4.17.21 - pretty: 2.0.0 vue: 3.2.30 dev: true @@ -2740,10 +2736,6 @@ packages: resolution: {integrity: sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==} dev: true - /abbrev/1.1.1: - resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} - dev: true - /acorn-globals/6.0.0: resolution: {integrity: sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==} dependencies: @@ -3403,21 +3395,13 @@ packages: typedarray: 0.0.6 dev: true - /condense-newlines/0.2.1: - resolution: {integrity: sha1-PemFVTE5R10yUCyDsC9gaE0kxV8=} - engines: {node: '>=0.10.0'} - dependencies: - extend-shallow: 2.0.1 - is-whitespace: 0.3.0 - kind-of: 3.2.2 - dev: true - /config-chain/1.1.13: resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} dependencies: ini: 1.3.8 proto-list: 1.2.4 dev: true + optional: true /consola/2.15.3: resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==} @@ -3669,10 +3653,6 @@ packages: esutils: 2.0.3 dev: true - /dom-event-types/1.0.0: - resolution: {integrity: sha512-2G2Vwi2zXTHBGqXHsJ4+ak/iP0N8Ar+G8a7LiD2oup5o4sQWytwqqrZu/O6hIMV0KMID2PL69OhpshLO0n7UJQ==} - dev: true - /dom-serializer/1.3.2: resolution: {integrity: sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==} dependencies: @@ -3735,16 +3715,6 @@ packages: resolution: {integrity: sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=} dev: true - /editorconfig/0.15.3: - resolution: {integrity: sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==} - hasBin: true - dependencies: - commander: 2.20.3 - lru-cache: 4.1.5 - semver: 5.7.1 - sigmund: 1.0.1 - dev: true - /ejs/3.1.6: resolution: {integrity: sha512-9lt9Zse4hPucPkoP7FHDF0LQAlGyF9JVpnClFLFH3aSSbxmyoqINRpp/9wePWJTUl4KOQwRL72Iw3InHPDkoGw==} engines: {node: '>=0.10.0'} @@ -5279,6 +5249,7 @@ packages: /ini/1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} dev: true + optional: true /inquirer/8.2.0: resolution: {integrity: sha512-0crLweprevJ02tTuA6ThpoAERAGyVILC4sS74uib58Xf/zSr1/ZWtmm7D5CI+bSQEaA04f0K7idaHpQbSWgiVQ==} @@ -5500,11 +5471,6 @@ packages: resolution: {integrity: sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w==} dev: true - /is-whitespace/0.3.0: - resolution: {integrity: sha1-Fjnssb4DauxppUy7QBz77XEUq38=} - engines: {node: '>=0.10.0'} - dev: true - /is-word-character/1.0.4: resolution: {integrity: sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA==} dev: true @@ -5545,17 +5511,6 @@ packages: engines: {node: '>=10'} dev: true - /js-beautify/1.14.0: - resolution: {integrity: sha512-yuck9KirNSCAwyNJbqW+BxJqJ0NLJ4PwBUzQQACl5O3qHMBXVkXb/rD0ilh/Lat/tn88zSZ+CAHOlk0DsY7GuQ==} - engines: {node: '>=10'} - hasBin: true - dependencies: - config-chain: 1.1.13 - editorconfig: 0.15.3 - glob: 7.2.0 - nopt: 5.0.0 - dev: true - /js-levenshtein/1.1.6: resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} engines: {node: '>=0.10.0'} @@ -5729,13 +5684,6 @@ packages: json-buffer: 3.0.0 dev: true - /kind-of/3.2.2: - resolution: {integrity: sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=} - engines: {node: '>=0.10.0'} - dependencies: - is-buffer: 1.1.6 - dev: true - /kind-of/6.0.3: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} @@ -5939,13 +5887,6 @@ packages: engines: {node: '>=8'} dev: true - /lru-cache/4.1.5: - resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} - dependencies: - pseudomap: 1.0.2 - yallist: 2.1.2 - dev: true - /lru-cache/6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} @@ -6159,14 +6100,6 @@ packages: resolution: {integrity: sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==} dev: true - /nopt/5.0.0: - resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} - engines: {node: '>=6'} - hasBin: true - dependencies: - abbrev: 1.1.1 - dev: true - /normalize-package-data/2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} dependencies: @@ -6575,15 +6508,6 @@ packages: engines: {node: '>=6'} dev: true - /pretty/2.0.0: - resolution: {integrity: sha1-rbx5YLe7/iiaVX3F9zdhmiINBqU=} - engines: {node: '>=0.10.0'} - dependencies: - condense-newlines: 0.2.1 - extend-shallow: 2.0.1 - js-beautify: 1.14.0 - dev: true - /prism-theme-vars/0.2.2: resolution: {integrity: sha512-EL9ifuU/F8tEldoCa2sspiiLWysCL54xDbf2gN/ubwdtbuJROqOGopG5kSwunapwaioT+jLUQ/Ky+7jnv62xJA==} dev: true @@ -6624,6 +6548,7 @@ packages: /proto-list/1.2.4: resolution: {integrity: sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=} dev: true + optional: true /protobufjs/6.11.2: resolution: {integrity: sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw==} @@ -6645,10 +6570,6 @@ packages: long: 4.0.0 dev: true - /pseudomap/1.0.2: - resolution: {integrity: sha1-8FKijacOYYkX7wqKw0wa5aaChrM=} - dev: true - /psl/1.8.0: resolution: {integrity: sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==} dev: true @@ -7116,10 +7037,6 @@ packages: object-inspect: 1.12.0 dev: true - /sigmund/1.0.1: - resolution: {integrity: sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=} - dev: true - /signal-exit/3.0.6: resolution: {integrity: sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==} dev: true @@ -8491,10 +8408,6 @@ packages: engines: {node: '>=10'} dev: true - /yallist/2.1.2: - resolution: {integrity: sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=} - dev: true - /yallist/3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} diff --git a/scripts/changelog.ts b/scripts/changelog.ts index 716b300665f1..fb19db640d22 100644 --- a/scripts/changelog.ts +++ b/scripts/changelog.ts @@ -38,23 +38,31 @@ export async function getChangeLog(count = 200) { } export async function getContributorsAt(path: string) { - const list = (await git.raw(['log', '--pretty=format:"%an|%ae"', '--', path])) - .split('\n') - .map(i => i.slice(1, -1).split('|') as [string, string]) - const map: Record = {} + try { + const list = (await git.raw(['log', '--pretty=format:"%an|%ae"', '--', path])) + .split('\n') + .map(i => i.slice(1, -1).split('|') as [string, string]) + const map: Record = {} - list.forEach((i) => { - if (!map[i[1]]) { - map[i[1]] = { - name: i[0], - count: 0, - hash: md5(i[1]), - } - } - map[i[1]].count++ - }) + list + .filter(i => i[1]) + .forEach((i) => { + if (!map[i[1]]) { + map[i[1]] = { + name: i[0], + count: 0, + hash: md5(i[1]), + } + } + map[i[1]].count++ + }) - return Object.values(map).sort((a, b) => b.count - a.count) + return Object.values(map).sort((a, b) => b.count - a.count) + } + catch (e) { + console.error(e) + return [] + } } export async function getFunctionContributors() {