├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── babel.config.js ├── demo ├── App.tsx ├── componentList.ts ├── main.ts └── router.ts ├── index.html ├── jest.config.js ├── lerna.json ├── package.json ├── scripts └── tsconfig.json ├── src ├── index.ts ├── useClickAway │ ├── __demo__ │ │ └── index.tsx │ ├── __test__ │ │ └── index.spec.tsx │ └── index.ts ├── useDraggable │ ├── __demo__ │ │ └── index.tsx │ ├── __test__ │ │ └── index.spec.tsx │ └── index.ts ├── useEventListener │ ├── __demo__ │ │ └── index.tsx │ ├── __test__ │ │ └── index.spec.tsx │ └── index.ts ├── useForm │ ├── __demo__ │ │ └── index.tsx │ └── index.ts ├── useHover │ ├── __demo__ │ │ └── index.tsx │ ├── __tests__ │ │ └── index.spec.tsx │ └── index.ts ├── useInViewport │ ├── __demo__ │ │ └── index.tsx │ ├── __test__ │ │ └── index.spec.tsx │ └── index.ts ├── useReactiveRef │ ├── __demo__ │ │ └── index.tsx │ ├── __test__ │ │ └── index.spec.tsx │ └── index.ts ├── useScroll │ ├── __demo__ │ │ └── index.tsx │ ├── __test__ │ │ └── index.spec.tsx │ └── index.ts ├── useSize │ ├── __demo__ │ │ └── index.tsx │ └── index.ts ├── useToggle │ ├── __demo__ │ │ └── index.tsx │ ├── __test__ │ │ └── index.spec.ts │ └── index.ts └── utils │ ├── dom.ts │ ├── testingHelpers.ts │ └── typeHelpers.ts ├── tsconfig.json ├── typings └── some.d.ts └── webpack.config.js /.eslintignore: -------------------------------------------------------------------------------- 1 | *.js -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: [ 7 | 'plugin:vue/vue3-essential', 8 | 'eslint:recommended', 9 | '@vue/typescript/recommended', 10 | '@vue/prettier', 11 | '@vue/prettier/@typescript-eslint', 12 | ], 13 | parserOptions: { 14 | ecmaVersion: 2020, 15 | }, 16 | rules: { 17 | '@typescript-eslint/no-explicit-any': 0, 18 | '@typescript-eslint/no-inferrable-types': 0, 19 | 'vue/experimental-script-setup-vars': 0, 20 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 21 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript cache 45 | *.tsbuildinfo 46 | 47 | # Optional npm cache directory 48 | .npm 49 | 50 | # Optional eslint cache 51 | .eslintcache 52 | 53 | # Microbundle cache 54 | .rpt2_cache/ 55 | .rts2_cache_cjs/ 56 | .rts2_cache_es/ 57 | .rts2_cache_umd/ 58 | 59 | # Optional REPL history 60 | .node_repl_history 61 | 62 | # Output of 'npm pack' 63 | *.tgz 64 | 65 | # Yarn Integrity file 66 | .yarn-integrity 67 | 68 | # dotenv environment variables file 69 | .env 70 | .env.test 71 | 72 | # parcel-bundler cache (https://parceljs.org/) 73 | .cache 74 | 75 | # Next.js build output 76 | .next 77 | 78 | # Nuxt.js build / generate output 79 | .nuxt 80 | dist 81 | 82 | # Gatsby files 83 | .cache/ 84 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 85 | # https://nextjs.org/blog/next-9-1#public-directory-support 86 | # public 87 | 88 | # vuepress build output 89 | .vuepress/dist 90 | 91 | # Serverless directories 92 | .serverless/ 93 | 94 | # FuseBox cache 95 | .fusebox/ 96 | 97 | # DynamoDB Local files 98 | .dynamodb/ 99 | 100 | # TernJS port file 101 | .tern-port 102 | 103 | es 104 | lib 105 | .idea 106 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 100, 5 | "proseWrap": "never", 6 | "overrides": [ 7 | { 8 | "files": ".prettierrc", 9 | "options": { 10 | "parser": "json" 11 | } 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 vueComponent 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # @ant-design-vue/use 4 | 5 | ## 该项目已废弃, useForm 已集成进入 ant-design-vue 中,其它功能推荐使用 [vueuse](https://github.com/vueuse/vueuse) 6 | 7 | ## The project has been abandoned, useForm has been integrated into `ant-design-vue`,other functions recommended to use [vueuse](https://github.com/vueuse/vueuse) 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | presets: [ 3 | ['@babel/preset-env', { targets: { node: 'current' } }], 4 | [ 5 | '@babel/preset-typescript', 6 | { 7 | allExtensions: true, 8 | isTSX: true, 9 | }, 10 | ], 11 | ], 12 | plugins: [ 13 | '@vue/babel-plugin-jsx', 14 | '@babel/plugin-proposal-optional-chaining', 15 | ['@babel/plugin-transform-typescript', { allowNamespaces: true }], 16 | ], 17 | }; 18 | module.exports = { 19 | plugins: ['@babel/plugin-proposal-optional-chaining'], 20 | env: { 21 | dev: config, 22 | test: config, 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /demo/App.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'vue'; 2 | import list from './componentList'; 3 | export default { 4 | setup() { 5 | return () => ( 6 | <> 7 |
8 | {list.map(item => ( 9 | 10 | /{item} 11 | 12 | ))} 13 |
14 |
15 | 16 | 17 | ); 18 | }, 19 | } as Component; 20 | -------------------------------------------------------------------------------- /demo/componentList.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | 'useForm', 3 | 'useSize', 4 | 'useHover', 5 | 'useToggle', 6 | 'useClickAway', 7 | 'useEventListener', 8 | 'useReactiveRef', 9 | 'useScroll', 10 | 'useInViewport', 11 | 'useDraggable', 12 | ]; 13 | -------------------------------------------------------------------------------- /demo/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import App from './App'; 3 | import router from './router'; 4 | const app = createApp(App); 5 | app.use(router); 6 | app.mount('#app'); 7 | -------------------------------------------------------------------------------- /demo/router.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from 'vue-router'; 2 | import list from './componentList'; 3 | export default createRouter({ 4 | history: createWebHashHistory(), 5 | strict: true, 6 | routes: [ 7 | { path: '/home', redirect: '/' }, 8 | ...list.map(componentName => { 9 | return { 10 | path: `/${componentName}`, 11 | component: () => import(`../src/${componentName}/__demo__/index.tsx`), 12 | }; 13 | }), 14 | ], 15 | }); 16 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testURL: 'http://localhost/', 3 | testRegex: '.*\\.spec\\.tsx?$', 4 | collectCoverageFrom: [ 5 | '**/*.{ts,tsx}', 6 | '!**/node_modules/**', 7 | '!**/__demo__/**', 8 | '!**/__test__/**', 9 | '!**/demo/**', 10 | ], 11 | }; 12 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "version": "0.0.0" 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ant-design-vue/use", 3 | "version": "0.0.1-alpha.10", 4 | "repository": { 5 | "type": "git", 6 | "url": "git+https://github.com/vueComponent/use.git" 7 | }, 8 | "main": "./lib/index.js", 9 | "module": "./es/index.js", 10 | "files": [ 11 | "dist", 12 | "lib", 13 | "es", 14 | "typings" 15 | ], 16 | "scripts": { 17 | "dev": "NODE_ENV=dev webpack-dev-server", 18 | "test": "jest", 19 | "lint": "eslint src/ --fix --ext .tsx,.ts", 20 | "compile": "vc-tools run compile", 21 | "test-coverage": "jest --coverage", 22 | "prepublishOnly": "npm run lint && npm run compile && npm run test" 23 | }, 24 | "dependencies": { 25 | "async-validator": "^3.4.0", 26 | "lodash-es": "^4.17.15", 27 | "resize-observer-polyfill": "^1.5.1", 28 | "vue": "^3.0.0" 29 | }, 30 | "devDependencies": { 31 | "@babel/plugin-proposal-optional-chaining": "^7.11.0", 32 | "@babel/plugin-transform-typescript": "^7.10.3", 33 | "@babel/preset-env": "^7.10.4", 34 | "@babel/preset-typescript": "^7.10.4", 35 | "@babel/runtime": "^7.10.4", 36 | "@types/jest": "^26.0.3", 37 | "@types/lodash-es": "^4.17.3", 38 | "@typescript-eslint/eslint-plugin": "^4.13.0", 39 | "@typescript-eslint/parser": "^4.1.1", 40 | "@vue/babel-plugin-jsx": "^1.0.0", 41 | "@vue/cli-plugin-eslint": "^4.5.4", 42 | "@vue/cli-plugin-typescript": "^4.5.4", 43 | "@vue/compiler-sfc": "^3.0.0", 44 | "@vue/eslint-config-prettier": "^6.0.0", 45 | "@vue/eslint-config-typescript": "^7.0.0", 46 | "@vue/test-utils": "^2.0.0-beta.3", 47 | "ant-design-vue": "^2.0.0", 48 | "babel-jest": "^26.1.0", 49 | "babel-loader": "^8.1.0", 50 | "css-loader": "^3.4.2", 51 | "eslint": "^7.3.1", 52 | "eslint-config-prettier": "^6.11.0", 53 | "eslint-plugin-prettier": "^3.1.4", 54 | "eslint-plugin-vue": "^7.4.1", 55 | "jest": "^26.1.0", 56 | "lerna": "^3.22.1", 57 | "mini-css-extract-plugin": "^0.9.0", 58 | "typescript": "^4.1.3", 59 | "vc-tools": "^3.0.0", 60 | "vue": "^3.0.0", 61 | "vue-loader": "^16.0.0-0", 62 | "vue-router": "^4.0.0-0", 63 | "webpack": "^4.42.1", 64 | "webpack-cli": "^3.3.11", 65 | "webpack-dev-server": "^3.10.3" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonJS", 4 | } 5 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as useForm } from './useForm'; 2 | export { default as useSize } from './useSize'; 3 | export { default as useHover } from './useHover'; 4 | export { default as useToggle } from './useToggle'; 5 | export { default as useClickAway } from './useClickAway'; 6 | export { default as useEventListener } from './useEventListener'; 7 | export { default as useReactiveRef } from './useReactiveRef'; 8 | export { default as useScroll } from './useScroll'; 9 | export { default as useInViewport } from './useInViewport'; 10 | export { default as useDraggable } from './useDraggable'; 11 | -------------------------------------------------------------------------------- /src/useClickAway/__demo__/index.tsx: -------------------------------------------------------------------------------- 1 | import { Component, ref, defineComponent } from 'vue'; 2 | import useClickAway from '../index'; 3 | export default defineComponent({ 4 | setup() { 5 | const count = ref(0); 6 | const eleRef = ref(null); 7 | useClickAway(eleRef, () => { 8 | count.value++; 9 | }); 10 | return { eleRef, count }; 11 | }, 12 | render(_ctx) { 13 | return ( 14 |
15 | 16 |
count:{_ctx.count}
17 |
18 | ); 19 | }, 20 | }) as Component; 21 | -------------------------------------------------------------------------------- /src/useClickAway/__test__/index.spec.tsx: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | 3 | import useClickAway from '../index'; 4 | import { ref } from 'vue'; 5 | 6 | describe('useClickAway', () => { 7 | test('should work with custom funtion', async () => { 8 | // eslint-disable-next-line @typescript-eslint/no-empty-function 9 | const fn = jest.fn(() => {}); 10 | const eleRef = ref(null); 11 | const wrapRef = ref(null); 12 | let removeListener!: () => void; 13 | const wrapper = mount({ 14 | setup() { 15 | removeListener = useClickAway(eleRef, fn, 'click', wrapRef); 16 | return { eleRef, wrapRef }; 17 | }, 18 | render() { 19 | return ( 20 |
21 |

22 |

h2

23 |
24 | ); 25 | }, 26 | }); 27 | await wrapper.vm.$nextTick(); 28 | wrapper.find('h1').trigger('click'); 29 | expect(fn).toHaveBeenCalledTimes(0); 30 | wrapper.find('h2').trigger('click'); 31 | expect(fn).toHaveBeenCalledTimes(1); 32 | removeListener(); 33 | wrapper.find('h2').trigger('click'); 34 | expect(fn).toHaveBeenCalledTimes(1); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/useClickAway/index.ts: -------------------------------------------------------------------------------- 1 | import { Ref, isRef } from 'vue'; 2 | import useEventListener from '../useEventListener'; 3 | // 鼠标点击事件,click 不会监听右键 4 | const defaultEvent = 'click'; 5 | 6 | type EventType = MouseEvent | TouchEvent; 7 | 8 | export default function useClickAway( 9 | ele: Ref, 10 | onClickAway: (event: EventType) => void, 11 | eventName: string = defaultEvent, 12 | container: Document | HTMLElement | Ref = document, 13 | ): () => void { 14 | function onClickAwayFn(e: any) { 15 | const dom = ele.value; 16 | if (!dom || dom.contains(e.target)) { 17 | return; 18 | } 19 | onClickAway(e); 20 | } 21 | let removeListener!: (...args: any) => any; 22 | if (isRef(container)) { 23 | removeListener = useEventListener(container, { 24 | type: eventName, 25 | listener: onClickAwayFn, 26 | }); 27 | } else { 28 | container.addEventListener(eventName, onClickAwayFn); 29 | removeListener = () => { 30 | container.removeEventListener(eventName, onClickAwayFn); 31 | }; 32 | } 33 | return removeListener; 34 | } 35 | -------------------------------------------------------------------------------- /src/useDraggable/__demo__/index.tsx: -------------------------------------------------------------------------------- 1 | import { Component, defineComponent } from 'vue'; 2 | import useDraggable from '../index'; 3 | export default defineComponent({ 4 | setup() { 5 | const [targetRef, handleRef, { delta }] = useDraggable({ controlStyle: true }); 6 | return () => { 7 | return ( 8 | <> 9 |
10 | 11 |
12 |

{delta.value.x}

13 |

{delta.value.y}

14 | 15 | ); 16 | }; 17 | }, 18 | }) as Component; 19 | -------------------------------------------------------------------------------- /src/useDraggable/__test__/index.spec.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-empty-function */ 2 | import { mount } from '@vue/test-utils'; 3 | 4 | import useDraggable from '../index'; 5 | import { defineComponent, CSSProperties } from 'vue'; 6 | 7 | describe('drag', () => { 8 | let utils!: ReturnType; 9 | beforeEach(() => { 10 | utils = setup(); 11 | }); 12 | 13 | describe('starting drag', async () => { 14 | it('should supply proper props to target', async () => { 15 | const { wrapper } = await utils; 16 | wrapper.find('#handle').trigger('mousedown'); 17 | await wrapper.vm.$nextTick(); 18 | expect(wrapper.find('[aria-grabbed]')).toBeTruthy; 19 | }); 20 | 21 | it('should return a delta position', async () => { 22 | const startAt = { clientX: 10, clientY: 10 }; 23 | const delta = { x: 5, y: 5 }; 24 | const { drag, wrapper } = await utils; 25 | await drag({ start: startAt, delta }); 26 | await wrapper.vm.$nextTick(); 27 | expect(wrapper.find('#output').text()).toEqual(`${delta.x}, ${delta.y}`); 28 | }); 29 | it('should return a correct delta position after more drag', async () => { 30 | const startAt = { clientX: 10, clientY: 10 }; 31 | const delta = { x: 5, y: 5 }; 32 | const secondStart = { 33 | clientX: startAt.clientX + delta.x, 34 | clientY: startAt.clientY + delta.y, 35 | }; 36 | const { drag, wrapper } = await utils; 37 | await drag({ start: startAt, delta }); 38 | await drag({ 39 | start: secondStart, 40 | delta, 41 | }); 42 | await wrapper.vm.$nextTick(); 43 | expect(wrapper.find('#output').text()).toEqual(`${2 * delta.x}, ${2 * delta.y}`); 44 | }); 45 | }); 46 | }); 47 | 48 | describe('decorate with styles', async () => { 49 | let utils!: ReturnType; 50 | beforeEach(() => { 51 | utils = setup({ useDraggableOption: { controlStyle: true } }); 52 | }); 53 | 54 | it("should change target's style when dragging", async () => { 55 | const { wrapper, drag } = await utils; 56 | 57 | await drag({ 58 | start: { clientX: 3, clientY: 3 }, 59 | delta: { x: 10, y: 15 }, 60 | }); 61 | 62 | expect((wrapper.find('#main').element as HTMLElement).style.transform).toEqual( 63 | `translate(10px, 15px)`, 64 | ); 65 | }); 66 | 67 | it("should change target's style when dragging (touches)", async () => { 68 | const { wrapper, drag } = await utils; 69 | 70 | await drag({ 71 | start: { clientX: 3, clientY: 3 }, 72 | delta: { x: 10, y: 15 }, 73 | touch: true, 74 | }); 75 | await wrapper.vm.$nextTick(); 76 | expect((wrapper.find('#main').element as HTMLElement).style.transform).toEqual( 77 | `translate(10px, 15px)`, 78 | ); 79 | }); 80 | 81 | it('should set proper cursor', async () => { 82 | const { wrapper } = await utils; 83 | await wrapper.vm.$nextTick(); 84 | expect((wrapper.find('#handle').element as HTMLElement).style.cursor).toEqual('grab'); 85 | }); 86 | 87 | it('should change cursor while dragging', async () => { 88 | const { wrapper } = await utils; 89 | wrapper.find('#handle').trigger('mousedown'); 90 | await wrapper.vm.$nextTick(); 91 | expect((wrapper.find('#handle').element as HTMLElement).style.cursor).toEqual('grabbing'); 92 | }); 93 | 94 | it('should add `will-change: transform` to target', async () => { 95 | const { wrapper } = await utils; 96 | wrapper.find('#handle').trigger('mousedown'); 97 | expect((wrapper.find('#main').element as HTMLElement).style.willChange).toEqual('transform'); 98 | }); 99 | 100 | it('should remove `will-change: transform` from target', async () => { 101 | const { wrapper, drag } = await utils; 102 | await drag({ 103 | start: { clientX: 3, clientY: 3 }, 104 | delta: { x: 10, y: 15 }, 105 | }); 106 | expect((wrapper.find('#main').element as HTMLElement).style.willChange).toEqual(''); 107 | }); 108 | }); 109 | 110 | describe('ending drag', () => { 111 | let utils!: ReturnType; 112 | beforeEach(() => { 113 | utils = setup({ useDraggableOption: { controlStyle: true }, style: {} }); 114 | }); 115 | it('should supply proper props to target', async () => { 116 | const { drag, wrapper } = await utils; 117 | 118 | await drag(); 119 | 120 | expect(wrapper.find('[aria-grabbed]')).toBeNull; 121 | }); 122 | }); 123 | 124 | describe('limit in viewport', async () => { 125 | let utils!: ReturnType; 126 | beforeEach(() => { 127 | utils = setup({ 128 | useDraggableOption: { 129 | controlStyle: true, 130 | viewport: true, 131 | }, 132 | style: { 133 | ...defaultStyle, 134 | width: '180px', 135 | left: 'auto', 136 | right: '0', 137 | }, 138 | }); 139 | }); 140 | 141 | it('should not change transition beyond given rect', async () => { 142 | const { drag, wrapper } = await utils; 143 | const targetElement = wrapper.find('#main').element as HTMLElement; 144 | const rect = targetElement.getBoundingClientRect(); 145 | const startAt = { clientX: rect.left + 5, clientY: rect.top + 5 }; 146 | const delta = { x: 5, y: 5 }; 147 | 148 | await drag({ start: startAt, delta }); 149 | await wrapper.vm.$nextTick(); 150 | expect(targetElement.style.transform).toContain(`translate(5px, 5px)`); 151 | }); 152 | 153 | it('should leave transition as it was before limit', async () => { 154 | const { wrapper, beginDrag, move } = await utils; 155 | const targetElement = wrapper.find('#main').element as HTMLElement; 156 | 157 | const startAt = { clientX: 0 + 5, clientY: 0 + 5 }; 158 | const delta = { x: 5, y: 1 }; 159 | 160 | await beginDrag(startAt); 161 | await move({ 162 | clientX: startAt.clientX + delta.x, 163 | clientY: startAt.clientY + delta.y, 164 | }); 165 | await move({ 166 | clientX: startAt.clientX + delta.x + 10, 167 | clientY: startAt.clientY + delta.y, 168 | }); 169 | await move({ 170 | clientX: startAt.clientX + delta.x + 25, 171 | clientY: startAt.clientY + delta.y, 172 | }); 173 | await move({ 174 | clientX: startAt.clientX + delta.x + 50, 175 | clientY: startAt.clientY + delta.y, 176 | }); 177 | 178 | await wrapper.vm.$nextTick(); 179 | expect(targetElement.style.transform).toContain(`translate(55px, 1px)`); 180 | }); 181 | 182 | it('should keep limits when dragging more than once', async () => { 183 | const { drag, wrapper } = await utils; 184 | const targetElement = wrapper.find('#main').element as HTMLElement; 185 | targetElement.style.right = '50px'; 186 | const startAt = { clientX: 5, clientY: 5 }; 187 | const delta = { x: 15, y: 1 }; 188 | 189 | await drag({ start: startAt, delta }); 190 | await drag({ 191 | start: { 192 | clientX: startAt.clientX + delta.x, 193 | clientY: startAt.clientY + delta.y, 194 | }, 195 | delta: { x: 50, y: 0 }, 196 | }); 197 | 198 | await wrapper.vm.$nextTick(); 199 | expect(targetElement.style.transform).toContain(`translate(65px, 1px)`); 200 | }); 201 | }); 202 | 203 | // describe('limit in rect', async () => { 204 | // const limits = { 205 | // left: 11, 206 | // right: window.innerWidth - 11, 207 | // top: 5, 208 | // bottom: window.innerHeight - 13 209 | // }; 210 | 211 | // let utils!: ReturnType 212 | // beforeEach(() => { 213 | // utils = setup({ 214 | // useDraggableOption: { 215 | // controlStyle: true, 216 | // rectLimits: limits, 217 | // }, 218 | // style: { 219 | // ...defaultStyle, 220 | // width: '180px', 221 | // left: '20px' 222 | // } 223 | // }); 224 | // }); 225 | 226 | // it('should not change transition beyond given rect', async () => { 227 | // const { drag, wrapper } = await utils; 228 | // const targetElement = wrapper.find('#main').element as HTMLElement; 229 | // const rect = targetElement.getBoundingClientRect(); 230 | // const startAt = { clientX: rect.left + 5, clientY: rect.top + 5 }; 231 | // const delta = { x: -50, y: -90 }; 232 | 233 | // await drag({ start: startAt, delta }); 234 | // await wrapper.vm.$nextTick() 235 | // expect(targetElement.style.transform).toContain({ 236 | // left: limits.left, 237 | // top: limits.top 238 | // }); 239 | // }); 240 | 241 | // it('should keep limits when dragging more than once', async () => { 242 | // const { drag, wrapper } = await utils; 243 | // const targetElement = wrapper.find('#main').element as HTMLElement; 244 | // targetElement.style.right = '50px'; 245 | // targetElement.style.left = 'auto'; 246 | // const rect = targetElement.getBoundingClientRect(); 247 | 248 | // const startAt = { clientX: rect.left + 5, clientY: rect.top + 5 }; 249 | // const delta = { x: 15, y: 1 }; 250 | 251 | // drag({ start: startAt, delta }); 252 | // drag({ 253 | // start: { 254 | // clientX: startAt.clientX + delta.x, 255 | // clientY: startAt.clientY + delta.y 256 | // }, 257 | // delta: { x: 50, y: 0 } 258 | // }); 259 | 260 | // const { left, width } = targetElement.getBoundingClientRect(); 261 | // expect(left).toEqual(limits.right - width); 262 | // }); 263 | // }); 264 | 265 | describe('reset drags', () => { 266 | let utils!: ReturnType; 267 | beforeEach(() => { 268 | utils = setup({ useDraggableOption: { controlStyle: true } }); 269 | }); 270 | 271 | it('should start dragging from the original position', async () => { 272 | const { wrapper, drag } = await utils; 273 | await drag({ start: { clientX: 3, clientY: 5 }, delta: { x: 15, y: 20 } }); 274 | wrapper.find('#reset').trigger('click'); 275 | await wrapper.vm.$nextTick(); 276 | expect((wrapper.find('#main').element as HTMLElement).style.transform).toEqual( 277 | 'translate(0px, 0px)', 278 | ); 279 | }); 280 | 281 | describe('after reset', () => { 282 | it('should start dragging from the original position', async () => { 283 | const { wrapper, drag } = await utils; 284 | await drag({ start: { clientX: 3, clientY: 5 }, delta: { x: 15, y: 20 } }); 285 | wrapper.find('#reset').trigger('click'); 286 | await drag({ start: { clientX: 3, clientY: 5 }, delta: { x: 15, y: 20 } }); 287 | expect((wrapper.find('#main').element as HTMLElement).style.transform).toEqual( 288 | 'translate(15px, 20px)', 289 | ); 290 | }); 291 | }); 292 | }); 293 | 294 | describe('useDraggable', () => { 295 | test('starting drag', async () => { 296 | const [targetRef] = useDraggable({ controlStyle: true }); 297 | mount({ 298 | setup() { 299 | targetRef; 300 | return { targetRef }; 301 | }, 302 | render() { 303 | return ( 304 | <> 305 |

{ 307 | targetRef.value = ele; 308 | }} 309 | > 310 | {' '} 311 | click{' '} 312 |

313 | 314 | ); 315 | }, 316 | }); 317 | }); 318 | }); 319 | const Consumer = defineComponent({ 320 | props: ['useDraggableOption', 'style'], 321 | setup(props: { useDraggableOption: Parameters[0]; style: CSSProperties }) { 322 | const [targetRef, handleRef, { getTargetProps, resetState, delta, dragging }] = useDraggable( 323 | props.useDraggableOption, 324 | ); 325 | return () => ( 326 |
333 | {dragging && Dragging to:} 334 | 335 | {delta.value.x}, {delta.value.y} 336 | 337 | 340 | 343 |
344 | ); 345 | }, 346 | }); 347 | 348 | const defaultStyle = { position: 'fixed', top: '11px', left: '11px' } as CSSProperties; 349 | async function setup( 350 | props: { useDraggableOption: Parameters[0]; style?: CSSProperties } = { 351 | style: {}, 352 | useDraggableOption: {}, 353 | }, 354 | ) { 355 | const wrapper = mount( 356 | , 357 | ); 358 | async function drag({ 359 | start = { clientX: 0, clientY: 0 }, 360 | delta = { x: 0, y: 0 }, 361 | touch = false, 362 | } = {}) { 363 | beginDrag(start, touch); 364 | await wrapper.vm.$nextTick(); 365 | move( 366 | { 367 | clientX: start.clientX + delta.x, 368 | clientY: start.clientY + delta.y, 369 | }, 370 | touch, 371 | ); 372 | await wrapper.vm.$nextTick(); 373 | endDrag( 374 | { 375 | clientX: start.clientX + delta.x, 376 | clientY: start.clientY + delta.y, 377 | }, 378 | touch, 379 | ); 380 | } 381 | 382 | async function beginDrag(start, touch = false) { 383 | const target = wrapper.find('#handle'); 384 | if (touch) { 385 | const ev = new TouchEvent('touchstart', { 386 | bubbles: true, 387 | cancelable: true, 388 | touches: [createTouch({ target, ...start })], 389 | }); 390 | target.element.dispatchEvent(ev); 391 | } else { 392 | target.trigger('mousedown', start); 393 | } 394 | } 395 | 396 | async function move(to, touch = false) { 397 | const target = wrapper.find('#handle'); 398 | if (touch) { 399 | const ev = new TouchEvent('touchmove', { 400 | bubbles: true, 401 | cancelable: true, 402 | touches: [ 403 | createTouch({ 404 | target, 405 | ...to, 406 | }), 407 | ], 408 | }); 409 | document.dispatchEvent(ev); 410 | } else { 411 | const ev = new MouseEvent('mousemove', { 412 | view: window, 413 | bubbles: true, 414 | cancelable: true, 415 | ...to, 416 | }); 417 | document.dispatchEvent(ev); 418 | } 419 | } 420 | 421 | async function endDrag(end, touch = false) { 422 | const target = wrapper.find('#handle'); 423 | if (touch) { 424 | const ev = new TouchEvent('touchend', { 425 | bubbles: true, 426 | cancelable: true, 427 | touches: [ 428 | createTouch({ 429 | target, 430 | ...end, 431 | }), 432 | ], 433 | }); 434 | document.dispatchEvent(ev); 435 | } else { 436 | const ev = new MouseEvent('mouseup', { 437 | view: window, 438 | bubbles: true, 439 | cancelable: true, 440 | ...end, 441 | }); 442 | document.dispatchEvent(ev); 443 | } 444 | } 445 | 446 | return { 447 | wrapper, 448 | beginDrag, 449 | move, 450 | drag, 451 | }; 452 | } 453 | 454 | class Touch { 455 | constructor(touchInit: any) { 456 | this.altitudeAngle = touchInit.altitudeAngle; 457 | this.azimuthAngle = touchInit.azimuthAngle; 458 | this.clientX = touchInit.clientX; 459 | this.clientY = touchInit.clientY; 460 | this.force = touchInit.force; 461 | this.identifier = touchInit.identifier; 462 | this.pageX = touchInit.pageX; 463 | this.pageY = touchInit.pageY; 464 | this.radiusX = touchInit.radiusX; 465 | this.radiusY = touchInit.radiusY; 466 | this.rotationAngle = touchInit.rotationAngle; 467 | this.screenX = touchInit.screenX; 468 | this.screenY = touchInit.screenY; 469 | this.target = touchInit.target; 470 | this.touchType = touchInit.touchType; 471 | } 472 | readonly altitudeAngle: number; 473 | readonly azimuthAngle: number; 474 | readonly clientX: number; 475 | readonly clientY: number; 476 | readonly force: number; 477 | readonly identifier: number; 478 | readonly pageX: number; 479 | readonly pageY: number; 480 | readonly radiusX: number; 481 | readonly radiusY: number; 482 | readonly rotationAngle: number; 483 | readonly screenX: number; 484 | readonly screenY: number; 485 | readonly target: EventTarget; 486 | readonly touchType: TouchType; 487 | } 488 | function createTouch({ target, ...rest }) { 489 | return new Touch({ identifier: Date.now(), target, ...rest }); 490 | } 491 | -------------------------------------------------------------------------------- /src/useDraggable/index.ts: -------------------------------------------------------------------------------- 1 | import { Ref, ref, watchEffect, HTMLAttributes } from 'vue'; 2 | import { isComponentPublicInstance, ElementType } from '../utils/typeHelpers'; 3 | 4 | export default function useDraggable( 5 | config: { 6 | target?: Ref; 7 | handle?: Ref; 8 | controlStyle?: boolean; 9 | viewport?: boolean; 10 | rectLimits?: { 11 | left?: number; 12 | right?: number; 13 | top?: number; 14 | bottom?: number; 15 | }; 16 | } = { controlStyle: true }, 17 | ): [ 18 | Ref, 19 | Ref, 20 | { 21 | getTargetProps: () => HTMLAttributes; 22 | dragging: Ref; 23 | delta: Ref<{ x: number; y: number }>; 24 | resetState: () => void; 25 | }, 26 | ] { 27 | const { target: configTarget, handle, controlStyle, viewport, rectLimits } = config; 28 | const targetRef = configTarget || ref(null); 29 | const handleRef = handle || ref(null); 30 | const dragging = ref(null); 31 | const prev = ref({ x: 0, y: 0 }); 32 | const delta = ref({ x: 0, y: 0 }); 33 | const initial = ref({ x: 0, y: 0 }); 34 | const limits: Ref<{ 35 | minX: number; 36 | maxX: number; 37 | minY: number; 38 | maxY: number; 39 | }> = ref(null); 40 | const targetEleRef = ref(null); 41 | const handleEleRef = ref(null); 42 | 43 | watchEffect( 44 | () => { 45 | if (!targetRef.value) { 46 | targetEleRef.value = null; 47 | } else { 48 | targetEleRef.value = isComponentPublicInstance(targetRef.value) 49 | ? targetRef.value.$el 50 | : targetRef.value; 51 | } 52 | if (!handleRef.value) { 53 | handleEleRef.value = null; 54 | } else { 55 | handleEleRef.value = isComponentPublicInstance(handleRef.value) 56 | ? handleRef.value.$el 57 | : handleRef.value; 58 | } 59 | }, 60 | { flush: 'post' }, 61 | ); 62 | watchEffect( 63 | () => { 64 | const handle = handleEleRef.value || targetEleRef.value; 65 | if (!targetEleRef.value) return; 66 | handle.addEventListener('mousedown', startDragging); 67 | handle.addEventListener('touchstart', startDragging); 68 | return () => { 69 | handle.removeEventListener('mousedown', startDragging); 70 | handle.removeEventListener('touchstart', startDragging); 71 | }; 72 | 73 | function startDragging(event) { 74 | event.preventDefault(); 75 | dragging.value = true; 76 | const source = (event.touches && event.touches[0]) || event; 77 | const { clientX, clientY } = source; 78 | initial.value = { x: clientX, y: clientY }; 79 | if (controlStyle) { 80 | targetEleRef.value.style.willChange = 'transform'; 81 | } 82 | if (viewport || rectLimits) { 83 | const { left, top, width, height } = targetEleRef.value.getBoundingClientRect(); 84 | 85 | if (viewport) { 86 | limits.value = { 87 | minX: -left + delta.value.x, 88 | maxX: window.innerWidth - width - left + delta.value.x, 89 | minY: -top + delta.value.y, 90 | maxY: window.innerHeight - height - top + delta.value.y, 91 | }; 92 | } else { 93 | limits.value = { 94 | minX: rectLimits.left - left + delta.value.x, 95 | maxX: rectLimits.right - width - left + delta.value.x, 96 | minY: rectLimits.top - top + delta.value.y, 97 | maxY: rectLimits.bottom - height - top + delta.value.y, 98 | }; 99 | } 100 | } 101 | } 102 | }, 103 | { flush: 'post' }, 104 | ); 105 | 106 | watchEffect( 107 | () => { 108 | const handle = handleEleRef.value || targetEleRef.value; 109 | if (!targetEleRef.value) return; 110 | const reposition = function (event) { 111 | const source = 112 | (event.changedTouches && event.changedTouches[0]) || 113 | (event.touches && event.touches[0]) || 114 | event; 115 | const { clientX, clientY } = source; 116 | const x = clientX - initial.value.x + prev.value.x; 117 | const y = clientY - initial.value.y + prev.value.y; 118 | 119 | const newDelta = calcDelta({ 120 | x, 121 | y, 122 | limits: limits.value, 123 | }); 124 | delta.value = newDelta; 125 | 126 | return newDelta; 127 | }; 128 | if (dragging.value) { 129 | document.addEventListener('mousemove', reposition, { passive: true }); 130 | document.addEventListener('touchmove', reposition, { passive: true }); 131 | document.addEventListener('mouseup', stopDragging); 132 | document.addEventListener('touchend', stopDragging); 133 | } 134 | 135 | if (controlStyle) { 136 | handle.style.cursor = dragging.value ? 'grabbing' : 'grab'; 137 | } 138 | 139 | return () => { 140 | if (controlStyle) { 141 | handle.style.cursor = 'unset'; 142 | } 143 | document.removeEventListener('mousemove', reposition); 144 | document.removeEventListener('touchmove', reposition); 145 | document.removeEventListener('mouseup', stopDragging); 146 | document.removeEventListener('touchend', stopDragging); 147 | }; 148 | 149 | function stopDragging(event) { 150 | event.preventDefault(); 151 | dragging.value = false; 152 | document.removeEventListener('mousemove', reposition); 153 | document.removeEventListener('touchmove', reposition); 154 | document.removeEventListener('mouseup', stopDragging); 155 | document.removeEventListener('touchend', stopDragging); 156 | const newDelta = reposition(event); 157 | prev.value = newDelta; 158 | if (controlStyle) { 159 | targetEleRef.value.style.willChange = ''; 160 | } 161 | } 162 | }, 163 | { flush: 'post' }, 164 | ); 165 | 166 | watchEffect( 167 | () => { 168 | targetEleRef.value && 169 | (targetEleRef.value.style.transform = `translate(${delta.value.x}px, ${delta.value.y}px)`); 170 | }, 171 | { flush: 'post' }, 172 | ); 173 | 174 | const getTargetProps = () => ({ 175 | 'aria-grabbed': dragging.value || null, 176 | }); 177 | 178 | const resetState = () => { 179 | delta.value = { x: 0, y: 0 }; 180 | prev.value = { x: 0, y: 0 }; 181 | }; 182 | 183 | return [targetRef, handleRef, { getTargetProps, dragging, delta, resetState }]; 184 | } 185 | 186 | function calcDelta({ x, y, limits }) { 187 | if (!limits) { 188 | return { x, y }; 189 | } 190 | 191 | const { minX, maxX, minY, maxY } = limits; 192 | 193 | return { 194 | x: Math.min(Math.max(x, minX), maxX), 195 | y: Math.min(Math.max(y, minY), maxY), 196 | }; 197 | } 198 | -------------------------------------------------------------------------------- /src/useEventListener/__demo__/index.tsx: -------------------------------------------------------------------------------- 1 | import { Component, ref } from 'vue'; 2 | import useEventListener from '../index'; 3 | export default { 4 | setup() { 5 | const count = ref(0); 6 | const eleRef = ref(null); 7 | useEventListener(eleRef, { 8 | type: 'click', 9 | listener: () => { 10 | count.value++; 11 | }, 12 | }); 13 | return { eleRef, count }; 14 | }, 15 | render(_ctx) { 16 | return ( 17 | <> 18 |
click me
19 |
{_ctx.count}
20 | 21 | ); 22 | }, 23 | } as Component; 24 | -------------------------------------------------------------------------------- /src/useEventListener/__test__/index.spec.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-empty-function */ 2 | import { shallowMount } from '@vue/test-utils'; 3 | 4 | import useEventListener from '../index'; 5 | import { ref } from 'vue'; 6 | 7 | describe('useEventListener', () => { 8 | test('should work with Ref parameter', async () => { 9 | const clickFn = jest.fn(() => {}); 10 | const eleRef = ref(null); 11 | let removeListener!: () => void; 12 | const wrapper = shallowMount({ 13 | setup() { 14 | removeListener = useEventListener(eleRef, { 15 | type: 'click', 16 | listener: clickFn, 17 | }); 18 | return { eleRef }; 19 | }, 20 | render() { 21 | return ( 22 | <> 23 |

click

24 | 25 | ); 26 | }, 27 | }); 28 | await wrapper.vm.$nextTick(); 29 | expect(clickFn).toHaveBeenCalledTimes(0); 30 | wrapper.find('h1').trigger('click'); 31 | expect(clickFn).toHaveBeenCalledTimes(1); 32 | removeListener(); 33 | wrapper.find('h1').trigger('click'); 34 | expect(clickFn).toHaveBeenCalledTimes(1); 35 | }); 36 | test('should work with HTMLElement parameter', async () => { 37 | const clickFn = jest.fn(() => {}); 38 | const eleRef = ref(null); 39 | let removeListener!: () => void; 40 | const wrapper = shallowMount({ 41 | setup() { 42 | removeListener = useEventListener(eleRef, { 43 | type: 'click', 44 | listener: clickFn, 45 | }); 46 | return { eleRef }; 47 | }, 48 | render() { 49 | return ( 50 | <> 51 |

click

52 | 53 | ); 54 | }, 55 | }); 56 | await wrapper.vm.$nextTick(); 57 | expect(clickFn).toHaveBeenCalledTimes(0); 58 | wrapper.find('h1').trigger('click'); 59 | expect(clickFn).toHaveBeenCalledTimes(1); 60 | removeListener(); 61 | wrapper.find('h1').trigger('click'); 62 | expect(clickFn).toHaveBeenCalledTimes(1); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /src/useEventListener/index.ts: -------------------------------------------------------------------------------- 1 | import { watchEffect, Ref, isRef, onBeforeUnmount } from 'vue'; 2 | function useEventListener( 3 | target: Ref | HTMLElement | Document | Window, 4 | option: { 5 | type: string; 6 | listener: EventListenerOrEventListenerObject; 7 | options?: boolean | AddEventListenerOptions; 8 | }, 9 | ): () => void { 10 | const { type, listener, options } = option; 11 | const eleIsRef = isRef(target); 12 | if (eleIsRef) { 13 | const bindEle = target as Ref; 14 | let prevEle = null; 15 | const destroyWatcher = watchEffect( 16 | () => { 17 | bindEle.value?.addEventListener(type, listener, options); 18 | if (prevEle) { 19 | prevEle.removeEventListener(type, listener); 20 | } 21 | prevEle = bindEle?.value; 22 | }, 23 | { flush: 'post' }, 24 | ); 25 | const removeListener = (isDestroyWatcher = true) => { 26 | bindEle.value.removeEventListener(type, listener); 27 | if (isDestroyWatcher) { 28 | destroyWatcher(); 29 | } 30 | }; 31 | onBeforeUnmount(() => { 32 | removeListener(true); 33 | }); 34 | return removeListener; 35 | } else { 36 | const bindEle = target as HTMLElement | Document | Window; 37 | bindEle.addEventListener(type, listener, options); 38 | const removeListener = () => { 39 | bindEle.removeEventListener(type, listener); 40 | }; 41 | onBeforeUnmount(() => { 42 | removeListener(); 43 | }); 44 | return removeListener; 45 | } 46 | } 47 | export default useEventListener; 48 | -------------------------------------------------------------------------------- /src/useForm/__demo__/index.tsx: -------------------------------------------------------------------------------- 1 | import { Component, reactive, toRaw } from 'vue'; 2 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 3 | import { Form, Input, Select } from 'ant-design-vue'; 4 | import 'ant-design-vue/dist/antd.min.css'; 5 | import useForm from '../index'; 6 | export default { 7 | setup() { 8 | const modelRef = reactive({ 9 | name1: '', 10 | name2: '111', 11 | obj: { 12 | //嵌套数据 13 | test: [], 14 | }, 15 | }); 16 | const rulesRef = reactive({ 17 | name1: [ 18 | { 19 | required: true, 20 | message: 'Please input Activity name', 21 | }, 22 | { 23 | min: 3, 24 | max: 5, 25 | message: 'Length should be 3 to 5', 26 | trigger: 'blur', 27 | }, 28 | ], 29 | name2: [ 30 | { 31 | required: true, 32 | message: 'Please input name2', 33 | }, 34 | ], 35 | 'obj.test': [ 36 | { 37 | required: true, 38 | message: 'Please select', 39 | type: 'array', 40 | }, 41 | ], 42 | }); 43 | const { resetFields, validate, validateInfos, mergeValidateInfo, clearValidate } = useForm( 44 | modelRef, 45 | rulesRef, 46 | { debounce: { wait: 300 } }, 47 | ); 48 | const handleClick = (e) => { 49 | e.preventDefault(); 50 | validate() 51 | .then(() => { 52 | console.log(toRaw(modelRef)); 53 | }) 54 | .catch((err) => { 55 | console.log('error', err); 56 | }); 57 | }; 58 | const handleReset = (e) => { 59 | e.preventDefault(); 60 | resetFields(); 61 | }; 62 | const clearValidateAll = () => { 63 | clearValidate(); 64 | }; 65 | const clearValidateName1 = (name) => { 66 | clearValidate(name); 67 | }; 68 | const handleResetWithValues = (e) => { 69 | e.preventDefault(); 70 | resetFields({ 71 | name2: 'updated values', 72 | }); 73 | }; 74 | return () => ( 75 |
76 | 80 | validate('name1')} /> 81 | 82 | 83 | 84 | 85 | 86 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 |
118 | ); 119 | }, 120 | } as Component; 121 | -------------------------------------------------------------------------------- /src/useForm/index.ts: -------------------------------------------------------------------------------- 1 | import { reactive, watch, nextTick } from 'vue'; 2 | import cloneDeep from 'lodash-es/cloneDeep'; 3 | import intersection from 'lodash-es/intersection'; 4 | import isEqual from 'lodash-es/isEqual'; 5 | import debounce from 'lodash-es/debounce'; 6 | import omit from 'lodash-es/omit'; 7 | import { validateRules } from 'ant-design-vue/es/form/utils/validateUtil'; 8 | import { defaultValidateMessages } from 'ant-design-vue/es/form/utils/messages'; 9 | import { allPromiseFinish } from 'ant-design-vue/es/form/utils/asyncUtil'; 10 | import { tuple } from 'ant-design-vue/es/_util/type'; 11 | 12 | interface DebounceSettings { 13 | leading?: boolean; 14 | 15 | wait?: number; 16 | 17 | trailing?: boolean; 18 | } 19 | 20 | function isRequired(rules: any[]) { 21 | let isRequired = false; 22 | if (rules && rules.length) { 23 | rules.every((rule: { required: any }) => { 24 | if (rule.required) { 25 | isRequired = true; 26 | return false; 27 | } 28 | return true; 29 | }); 30 | } 31 | return isRequired; 32 | } 33 | 34 | function toArray(value) { 35 | if (value === undefined || value === null) { 36 | return []; 37 | } 38 | 39 | return Array.isArray(value) ? value : [value]; 40 | } 41 | 42 | type ValidateMessage = string | (() => string); 43 | export interface ValidateMessages { 44 | default?: ValidateMessage; 45 | required?: ValidateMessage; 46 | enum?: ValidateMessage; 47 | whitespace?: ValidateMessage; 48 | date?: { 49 | format?: ValidateMessage; 50 | parse?: ValidateMessage; 51 | invalid?: ValidateMessage; 52 | }; 53 | types?: { 54 | string?: ValidateMessage; 55 | method?: ValidateMessage; 56 | array?: ValidateMessage; 57 | object?: ValidateMessage; 58 | number?: ValidateMessage; 59 | date?: ValidateMessage; 60 | boolean?: ValidateMessage; 61 | integer?: ValidateMessage; 62 | float?: ValidateMessage; 63 | regexp?: ValidateMessage; 64 | email?: ValidateMessage; 65 | url?: ValidateMessage; 66 | hex?: ValidateMessage; 67 | }; 68 | string?: { 69 | len?: ValidateMessage; 70 | min?: ValidateMessage; 71 | max?: ValidateMessage; 72 | range?: ValidateMessage; 73 | }; 74 | number?: { 75 | len?: ValidateMessage; 76 | min?: ValidateMessage; 77 | max?: ValidateMessage; 78 | range?: ValidateMessage; 79 | }; 80 | array?: { 81 | len?: ValidateMessage; 82 | min?: ValidateMessage; 83 | max?: ValidateMessage; 84 | range?: ValidateMessage; 85 | }; 86 | pattern?: { 87 | mismatch?: ValidateMessage; 88 | }; 89 | } 90 | 91 | export interface Props { 92 | [key: string]: any; 93 | } 94 | 95 | export interface validateOptions { 96 | validateFirst?: boolean; 97 | validateMessages?: ValidateMessages; 98 | trigger?: 'change' | 'blur' | string | string[]; 99 | } 100 | 101 | const validateStatus = tuple('', 'success', 'warning', 'error', 'validating'); 102 | export type ValidateStatus = typeof validateStatus[number]; 103 | 104 | type namesType = string | string[]; 105 | export interface validateInfo { 106 | autoLink?: boolean; 107 | required?: boolean; 108 | validateStatus?: ValidateStatus; 109 | help?: string; 110 | } 111 | 112 | export interface validateInfos { 113 | [key: string]: validateInfo; 114 | } 115 | 116 | function getPropByPath(obj: Props, path: string, strict: boolean) { 117 | let tempObj = obj; 118 | path = path.replace(/\[(\w+)\]/g, '.$1'); 119 | path = path.replace(/^\./, ''); 120 | 121 | const keyArr = path.split('.'); 122 | let i = 0; 123 | for (let len = keyArr.length; i < len - 1; ++i) { 124 | if (!tempObj && !strict) break; 125 | const key = keyArr[i]; 126 | if (key in tempObj) { 127 | tempObj = tempObj[key]; 128 | } else { 129 | if (strict) { 130 | throw new Error('please transfer a valid name path to validate!'); 131 | } 132 | break; 133 | } 134 | } 135 | return { 136 | o: tempObj, 137 | k: keyArr[i], 138 | v: tempObj ? tempObj[keyArr[i]] : null, 139 | isValid: tempObj && keyArr[i] in tempObj, 140 | }; 141 | } 142 | 143 | function useForm( 144 | modelRef: Props, 145 | rulesRef?: Props, 146 | options?: { 147 | immediate?: boolean; 148 | deep?: boolean; 149 | validateOnRuleChange?: boolean; 150 | debounce?: DebounceSettings; 151 | }, 152 | ): { 153 | modelRef: Props; 154 | rulesRef: Props; 155 | initialModel: Props; 156 | validateInfos: validateInfos; 157 | resetFields: (newValues?: Props) => void; 158 | validate: (names?: namesType, option?: validateOptions) => Promise; 159 | validateField: ( 160 | name?: string, 161 | value?: any, 162 | rules?: [Record], 163 | option?: validateOptions, 164 | ) => Promise; 165 | mergeValidateInfo: (items: validateInfo | validateInfo[]) => validateInfo; 166 | clearValidate: (names?: namesType) => void; 167 | } { 168 | const initialModel = cloneDeep(modelRef); 169 | let validateInfos: validateInfos = {}; 170 | 171 | Object.keys(rulesRef).forEach((key) => { 172 | validateInfos[key] = { 173 | autoLink: false, 174 | required: isRequired(rulesRef[key]), 175 | }; 176 | }); 177 | validateInfos = reactive(validateInfos); 178 | const resetFields = (newValues: Props) => { 179 | Object.assign(modelRef, { 180 | ...cloneDeep(initialModel), 181 | ...newValues, 182 | }); 183 | //modelRef = resetReactiveValue(initialModel, modelRef); 184 | nextTick(() => { 185 | Object.keys(validateInfos).forEach((key) => { 186 | validateInfos[key] = { 187 | autoLink: false, 188 | required: isRequired(rulesRef[key]), 189 | }; 190 | }); 191 | }); 192 | }; 193 | const filterRules = (rules = [], trigger: string[]) => { 194 | if (!trigger.length) { 195 | return rules; 196 | } else { 197 | return rules.filter((rule) => { 198 | const triggerList = toArray(rule.trigger || 'change'); 199 | return intersection(triggerList, trigger).length; 200 | }); 201 | } 202 | }; 203 | let lastValidatePromise = null; 204 | const validateFields = (names: string[], option: validateOptions = {}, strict: boolean) => { 205 | const promiseList = []; 206 | const values = {}; 207 | for (let i = 0; i < names.length; i++) { 208 | const name = names[i]; 209 | const prop = getPropByPath(modelRef, name, strict); 210 | if (!prop.isValid) continue; 211 | values[name] = prop.v; 212 | const rules = filterRules(rulesRef[name], toArray(option && option.trigger)); 213 | if (rules.length) { 214 | promiseList.push( 215 | validateField(name, prop.v, rules, option || {}) 216 | .then(() => ({ 217 | name, 218 | errors: [], 219 | })) 220 | .catch((errors: any) => 221 | Promise.reject({ 222 | name, 223 | errors, 224 | }), 225 | ), 226 | ); 227 | } 228 | } 229 | 230 | const summaryPromise = allPromiseFinish(promiseList); 231 | lastValidatePromise = summaryPromise; 232 | 233 | const returnPromise = summaryPromise 234 | .then(() => { 235 | if (lastValidatePromise === summaryPromise) { 236 | return Promise.resolve(values); 237 | } 238 | return Promise.reject([]); 239 | }) 240 | .catch((results: any[]) => { 241 | const errorList = results.filter( 242 | (result: { errors: string | any[] }) => result && result.errors.length, 243 | ); 244 | return Promise.reject({ 245 | values: values, 246 | errorFields: errorList, 247 | outOfDate: lastValidatePromise !== summaryPromise, 248 | }); 249 | }); 250 | 251 | // Do not throw in console 252 | returnPromise.catch((e: any) => e); 253 | 254 | return returnPromise; 255 | }; 256 | const validateField = ( 257 | name: string, 258 | value: any, 259 | rules: any, 260 | option: validateOptions, 261 | ): Promise => { 262 | const promise = validateRules( 263 | [name], 264 | value, 265 | rules, 266 | { 267 | validateMessages: defaultValidateMessages, 268 | ...option, 269 | }, 270 | !!option.validateFirst, 271 | ); 272 | validateInfos[name].validateStatus = 'validating'; 273 | promise 274 | .catch((e: any) => e) 275 | .then((errors = []) => { 276 | if (validateInfos[name].validateStatus === 'validating') { 277 | validateInfos[name].validateStatus = errors.length ? 'error' : 'success'; 278 | validateInfos[name].help = errors[0]; 279 | } 280 | }); 281 | return promise; 282 | }; 283 | 284 | const validate = ( 285 | names?: namesType, 286 | option?: validateOptions, 287 | ): Promise => { 288 | let keys = []; 289 | let strict = true; 290 | if (!names) { 291 | strict = false; 292 | keys = Object.keys(rulesRef); 293 | } else if (Array.isArray(names)) { 294 | keys = names; 295 | } else { 296 | keys = [names]; 297 | } 298 | const promises = validateFields(keys, option || {}, strict); 299 | // Do not throw in console 300 | promises.catch((e: any) => e); 301 | return promises; 302 | }; 303 | 304 | const clearValidate = (names?: namesType) => { 305 | let keys = []; 306 | if (!names) { 307 | keys = Object.keys(rulesRef); 308 | } else if (Array.isArray(names)) { 309 | keys = names; 310 | } else { 311 | keys = [names]; 312 | } 313 | keys.forEach((key) => { 314 | validateInfos[key] && 315 | Object.assign(validateInfos[key], { 316 | validateStatus: '', 317 | help: '', 318 | }); 319 | }); 320 | }; 321 | 322 | const mergeValidateInfo = (items = []) => { 323 | const info = { autoLink: false } as validateInfo; 324 | const help = []; 325 | const infos = Array.isArray(items) ? items : [items]; 326 | for (let i = 0; i < infos.length; i++) { 327 | const arg = infos[i] as validateInfo; 328 | if (arg?.validateStatus === 'error') { 329 | info.validateStatus = 'error'; 330 | arg.help && help.push(arg.help); 331 | } 332 | info.required = info.required || arg?.required; 333 | } 334 | info.help = help.join('\n'); 335 | return info; 336 | }; 337 | let oldModel = initialModel; 338 | const modelFn = (model: { [x: string]: any }) => { 339 | const names = []; 340 | Object.keys(rulesRef).forEach((key) => { 341 | const prop = getPropByPath(model, key, false); 342 | const oldProp = getPropByPath(oldModel, key, false); 343 | if (!isEqual(prop.v, oldProp.v)) { 344 | names.push(key); 345 | } 346 | }); 347 | validate(names, { trigger: 'change' }); 348 | oldModel = cloneDeep(model); 349 | }; 350 | const debounceOptions = options?.debounce; 351 | watch( 352 | modelRef, 353 | debounceOptions && debounceOptions.wait 354 | ? debounce(modelFn, debounceOptions.wait, omit(debounceOptions, ['wait'])) 355 | : modelFn, 356 | { immediate: options && !!options.immediate, deep: true }, 357 | ); 358 | 359 | watch( 360 | rulesRef, 361 | () => { 362 | if (options && options.validateOnRuleChange) { 363 | validate(); 364 | } 365 | }, 366 | { deep: true }, 367 | ); 368 | 369 | return { 370 | modelRef, 371 | rulesRef, 372 | initialModel, 373 | validateInfos, 374 | resetFields, 375 | validate, 376 | validateField, 377 | mergeValidateInfo, 378 | clearValidate, 379 | }; 380 | } 381 | 382 | export default useForm; 383 | -------------------------------------------------------------------------------- /src/useHover/__demo__/index.tsx: -------------------------------------------------------------------------------- 1 | import { ref, Component } from 'vue'; 2 | import useHover from '../index'; 3 | export default { 4 | setup() { 5 | const eleRef = ref(null); 6 | const [isHover] = useHover(eleRef, { 7 | onEnter: () => { 8 | console.log('enter'); 9 | }, 10 | }); 11 | return { 12 | eleRef, 13 | isHover, 14 | }; 15 | }, 16 | render(_ctx) { 17 | return ( 18 | <> 19 |

move your mouse

20 |

{_ctx.isHover ? 'enter' : 'leave'}

21 | 22 | ); 23 | }, 24 | } as Component; 25 | -------------------------------------------------------------------------------- /src/useHover/__tests__/index.spec.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-empty-function */ 2 | import { shallowMount } from '@vue/test-utils'; 3 | import useHover from '../index'; 4 | import { ref } from 'vue'; 5 | 6 | describe('useHover', () => { 7 | test('should work with custom event', async () => { 8 | const onEnter = jest.fn(() => {}); 9 | const onLeave = jest.fn(() => {}); 10 | const wrapper = shallowMount({ 11 | setup() { 12 | const eleRef = ref(null); 13 | const [isHover] = useHover(eleRef, { 14 | onEnter, 15 | onLeave, 16 | }); 17 | return { 18 | eleRef, 19 | isHover, 20 | }; 21 | }, 22 | render() { 23 | return

move your mouse

; 24 | }, 25 | }); 26 | await wrapper.vm.$nextTick(); 27 | wrapper.find('h1').trigger('mouseenter'); 28 | expect(onEnter).toHaveBeenCalled(); 29 | expect(wrapper.vm.isHover).toBe(true); 30 | 31 | wrapper.find('h1').trigger('mouseleave'); 32 | expect(onLeave).toHaveBeenCalled(); 33 | expect(wrapper.vm.isHover).toBe(false); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/useHover/index.ts: -------------------------------------------------------------------------------- 1 | import { ref, Ref, watch } from 'vue'; 2 | export interface Options { 3 | onEnter?: (e: MouseEvent) => void; 4 | onLeave?: (e: MouseEvent) => void; 5 | } 6 | 7 | type Action = { 8 | actions: { 9 | removelistener: () => void; 10 | }; 11 | }; 12 | 13 | /** 14 | * useHover 15 | * 16 | * @param {Ref)} ele 17 | * @param {Options} [options] 18 | * @returns 19 | */ 20 | function useHover(target: Ref, options?: Options): [Ref, Action] { 21 | if (!target) { 22 | console.warn( 23 | `fucntiuon useHover first parameter expect HTMLElement | Ref,bug got ${target}`, 24 | ); 25 | return; 26 | } 27 | const isHovering = ref(null); 28 | const { onEnter, onLeave } = options || {}; 29 | const onMouseEnter = (e: MouseEvent) => { 30 | if (onEnter) { 31 | onEnter(e); 32 | } 33 | isHovering.value = true; 34 | }; 35 | const onMouseLeave = (e: MouseEvent) => { 36 | if (onLeave) { 37 | onLeave(e); 38 | } 39 | isHovering.value = false; 40 | }; 41 | 42 | const _addListeners = (ele: HTMLElement) => { 43 | if (ele) { 44 | ele.addEventListener('mouseenter', onMouseEnter); 45 | ele.addEventListener('mouseleave', onMouseLeave); 46 | } 47 | }; 48 | const _removeListeners = (ele: HTMLElement) => { 49 | if (ele) { 50 | ele.removeEventListener('mouseenter', onMouseEnter); 51 | ele.removeEventListener('mouseleave', onMouseLeave); 52 | } 53 | }; 54 | const removelistener = () => { 55 | _removeListeners((target as Ref).value); 56 | destoryWatcher(); 57 | }; 58 | const destoryWatcher = watch( 59 | target as Ref, 60 | (newValue, oldValue) => { 61 | if (newValue) { 62 | _addListeners(newValue); 63 | } 64 | if (oldValue) { 65 | _removeListeners(oldValue); 66 | } 67 | }, 68 | { flush: 'post' }, 69 | ); 70 | return [isHovering, { actions: { removelistener } }]; 71 | } 72 | 73 | export default useHover; 74 | -------------------------------------------------------------------------------- /src/useInViewport/__demo__/index.tsx: -------------------------------------------------------------------------------- 1 | import { Component, ref } from 'vue'; 2 | import useInViewport from '../index'; 3 | export default { 4 | setup() { 5 | const ele = ref(null); 6 | const inViewPort = useInViewport(ele); 7 | return { ele, inViewPort }; 8 | }, 9 | render: (_ctx) => ( 10 |
11 |
observer dom
12 |
13 | {_ctx.inViewPort === null ? '' : _ctx.inViewPort ? 'visible' : 'hidden'} 14 |
15 |
16 | ), 17 | } as Component; 18 | -------------------------------------------------------------------------------- /src/useInViewport/__test__/index.spec.tsx: -------------------------------------------------------------------------------- 1 | import useInViewport from '../index'; 2 | 3 | describe('useInViewport', () => { 4 | it('should be defined', () => { 5 | expect(useInViewport).toBeDefined(); 6 | }); 7 | // it('with argument', async () => { 8 | // const eleRef = ref(null) 9 | // let inViewPort!: Ref | Ref 10 | // const wrapper = shallowMount({ 11 | // setup() { 12 | // inViewPort = useInViewport(eleRef); 13 | // return { eleRef }; 14 | // }, 15 | // render(_ctx) { 16 | // return ( 17 | //

18 | //

19 | // ) 20 | // } 21 | // }); 22 | // await wrapper.vm.$nextTick() 23 | // expect(inViewPort.value).toBe(true) 24 | // }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/useInViewport/index.ts: -------------------------------------------------------------------------------- 1 | import { ref, Ref, watchEffect } from 'vue'; 2 | 3 | function useInViewport(target: Ref): Ref | Ref { 4 | const inViewPort = ref(null); 5 | let prevEl = null; 6 | const observer = new IntersectionObserver((entries) => { 7 | for (const entry of entries) { 8 | if (entry.isIntersecting) { 9 | inViewPort.value = true; 10 | } else { 11 | inViewPort.value = false; 12 | } 13 | } 14 | }); 15 | watchEffect(() => { 16 | if (prevEl) { 17 | observer.disconnect(); 18 | } 19 | if (target.value) { 20 | observer.observe((target as Ref).value); 21 | } 22 | prevEl = target.value; 23 | }); 24 | return inViewPort; 25 | } 26 | 27 | export default useInViewport; 28 | -------------------------------------------------------------------------------- /src/useReactiveRef/__demo__/index.tsx: -------------------------------------------------------------------------------- 1 | import { Component, ref, Ref, watchEffect } from 'vue'; 2 | import useReactiveRef from '../index'; 3 | export default { 4 | setup() { 5 | const [eleRef, setEle] = useReactiveRef(); 6 | const flag = ref(true); 7 | const toggle = () => { 8 | flag.value = !flag.value; 9 | }; 10 | watchEffect(() => { 11 | console.log((eleRef as Ref).value?.innerHTML); 12 | }); 13 | return () => ( 14 | <> 15 | 16 | {flag.value ?

h1

: null} 17 |

{(eleRef as Ref).value?.innerHTML}

18 | 19 | ); 20 | }, 21 | } as Component; 22 | -------------------------------------------------------------------------------- /src/useReactiveRef/__test__/index.spec.tsx: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils'; 2 | 3 | import useReactiveRef from '../index'; 4 | import { ref } from 'vue'; 5 | 6 | describe('useReactiveRef', () => { 7 | test('should work with custom event', async () => { 8 | const [eleRef, setEle] = useReactiveRef(); 9 | const showH1 = ref(false); 10 | const wrapper = shallowMount({ 11 | setup() { 12 | return () => ( 13 | <> 14 | {showH1.value ?

: null} 15 |

16 | 17 | ); 18 | }, 19 | }); 20 | expect(eleRef.value).toEqual(null); 21 | showH1.value = true; 22 | await wrapper.vm.$nextTick(); 23 | expect(eleRef.value).toBe(wrapper.find('h1').element); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/useReactiveRef/index.ts: -------------------------------------------------------------------------------- 1 | import { ref, ComponentPublicInstance, Ref } from 'vue'; 2 | type ElementType = HTMLElement | ComponentPublicInstance; 3 | function useReactiveRef(): [Ref, (...args: any) => void] { 4 | let prevEle = null as ElementType | null; 5 | const eleRef = ref(prevEle); 6 | function setEle(ele: HTMLElement) { 7 | if (prevEle === ele) return; 8 | prevEle = ele; 9 | eleRef.value = prevEle; 10 | } 11 | return [eleRef, setEle]; 12 | } 13 | export default useReactiveRef; 14 | -------------------------------------------------------------------------------- /src/useScroll/__demo__/index.tsx: -------------------------------------------------------------------------------- 1 | import { Component, ref } from 'vue'; 2 | import useScroll from '../index'; 3 | export default { 4 | setup() { 5 | const eleRef = ref(null); 6 | const scroll = useScroll(eleRef); 7 | return { eleRef, scroll }; 8 | }, 9 | render(_ctx) { 10 | return ( 11 | <> 12 |
{JSON.stringify(_ctx.scroll)}
13 |
24 |
25 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. A aspernatur atque, debitis ex 26 | excepturi explicabo iste iure labore molestiae neque optio perspiciatis 27 |
28 |
29 | Aspernatur cupiditate, deleniti id incidunt mollitia omnis! A aspernatur assumenda 30 | consequuntur culpa cumque dignissimos enim eos, et fugit natus nemo nesciunt 31 |
32 |
33 | Alias aut deserunt expedita, inventore maiores minima officia porro rem. Accusamus 34 | ducimus magni modi mollitia nihil nisi provident 35 |
36 |
37 | Alias aut autem consequuntur doloremque esse facilis id molestiae neque officia placeat, 38 | quia quisquam repellendus reprehenderit. 39 |
40 |
41 | Adipisci blanditiis facere nam perspiciatis sit soluta ullam! Architecto aut blanditiis, 42 | consectetur corporis cum deserunt distinctio dolore eius est exercitationem 43 |
44 |
Ab aliquid asperiores assumenda corporis cumque dolorum expedita
45 |
46 | Culpa cumque eveniet natus totam! Adipisci, animi at commodi delectus distinctio dolore 47 | earum, eum expedita facilis 48 |
49 |
50 | Quod sit, temporibus! Amet animi fugit officiis perspiciatis, quis unde. Cumque 51 | dignissimos distinctio, dolor eaque est fugit nisi non pariatur porro possimus, quas 52 | quasi 53 |
54 |
55 | 56 | ); 57 | }, 58 | } as Component; 59 | -------------------------------------------------------------------------------- /src/useScroll/__test__/index.spec.tsx: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import { ref } from 'vue'; 3 | import useScroll from '../index'; 4 | 5 | describe('useScroll', () => { 6 | it('define', async () => { 7 | const elem = document.createElement('div'); 8 | elem.style.height = '100px'; 9 | if (document.body) { 10 | document.body.appendChild(elem); 11 | } 12 | let scroll; 13 | const wrapper = mount( 14 | { 15 | setup() { 16 | scroll = useScroll(ref(elem)); 17 | return {}; 18 | }, 19 | render() { 20 | return

; 21 | }, 22 | }, 23 | { 24 | attachTo: elem, 25 | }, 26 | ); 27 | await wrapper.vm.$nextTick(); 28 | // elem did not trigger scroll 29 | elem.scrollTop = 120; 30 | await wrapper.vm.$nextTick(); 31 | expect(scroll.value.left).toBe(0); 32 | expect(scroll.value.top).toBe(0); 33 | }); 34 | // it('define2', async () => { 35 | // const el = ref(null) 36 | // const scroll = useScroll(el) 37 | // const wrapper = mount({ 38 | // setup() { 39 | // return { el } 40 | // }, 41 | // render() { 42 | // return ( 43 | //
44 | //

sad

45 | //
46 | // ) 47 | // } 48 | // }) 49 | // await wrapper.vm.$nextTick() 50 | // el.value.scrollTop = 100 51 | // // elem did not trigger scroll 52 | // await wrapper.vm.$nextTick() 53 | // expect(scroll.value.left).toBe(0); 54 | // expect(scroll.value.top).toBe(0); 55 | // }); 56 | }); 57 | -------------------------------------------------------------------------------- /src/useScroll/index.ts: -------------------------------------------------------------------------------- 1 | import { Ref, ref } from 'vue'; 2 | import useEventListener from '../useEventListener'; 3 | interface Position { 4 | left: number; 5 | top: number; 6 | } 7 | export default function useScroll(target: Ref): Ref { 8 | const position = ref({ left: 0, top: 0 } as Position); 9 | const updatePosition = (currentTarget: HTMLElement | Document) => { 10 | let newPosition; 11 | if (currentTarget === document) { 12 | if (!document.scrollingElement) return; 13 | newPosition = { 14 | left: document.scrollingElement.scrollLeft, 15 | top: document.scrollingElement.scrollTop, 16 | }; 17 | } else { 18 | newPosition = { 19 | left: (currentTarget as HTMLElement).scrollLeft, 20 | top: (currentTarget as HTMLElement).scrollTop, 21 | }; 22 | } 23 | position.value = newPosition; 24 | }; 25 | const listener = (event: Event) => { 26 | if (!event.target) return; 27 | updatePosition(event.target as HTMLElement | Document); 28 | }; 29 | useEventListener(target as Ref, { 30 | type: 'scroll', 31 | listener, 32 | }); 33 | 34 | return position; 35 | } 36 | -------------------------------------------------------------------------------- /src/useSize/__demo__/index.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'vue'; 2 | import useSize from '../index'; 3 | export default { 4 | setup() { 5 | const [size, elRef] = useSize(); 6 | return () => ( 7 |
8 | try to resize the preview window
9 | dimensions -- width: {size.width} px, height: {size.height} px 10 |
11 | ); 12 | }, 13 | } as Component; 14 | -------------------------------------------------------------------------------- /src/useSize/index.ts: -------------------------------------------------------------------------------- 1 | import { Ref, reactive, onMounted, watch, ref } from 'vue'; 2 | import ResizeObserver from 'resize-observer-polyfill'; 3 | 4 | type Size = { width?: number; height?: number }; 5 | 6 | function useSize(target?: Ref): [Size, Ref] { 7 | const elRef = target || ref(null); 8 | const state = reactive({ 9 | width: ((elRef || {}) as HTMLElement).clientWidth, 10 | height: ((elRef || {}) as HTMLElement).clientHeight, 11 | }); 12 | onMounted(() => { 13 | let resizeObserver = null; 14 | watch( 15 | elRef, 16 | (el, preElm, onInvalidate) => { 17 | if (!el) return; 18 | resizeObserver && resizeObserver.disconnect(); 19 | resizeObserver = new ResizeObserver((entries) => { 20 | entries.forEach((entry) => { 21 | state.width = entry.target.clientWidth; 22 | state.height = entry.target.clientHeight; 23 | }); 24 | }); 25 | resizeObserver.observe(el as HTMLElement); 26 | onInvalidate(() => { 27 | resizeObserver && resizeObserver.disconnect(); 28 | }); 29 | }, 30 | { immediate: true }, 31 | ); 32 | }); 33 | 34 | return [state, elRef]; 35 | } 36 | 37 | export default useSize; 38 | -------------------------------------------------------------------------------- /src/useToggle/__demo__/index.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'vue'; 2 | import useToggle from '../index'; 3 | export default { 4 | setup() { 5 | const [toggleRef, { toggle, setLeft, setRight }] = useToggle(); 6 | return { 7 | toggleRef, 8 | toggle, 9 | setLeft, 10 | setRight, 11 | }; 12 | }, 13 | render: (_ctx) => { 14 | return ( 15 |
16 |

Effects:{_ctx.toggleRef.toString()}

17 |

18 | 21 | 24 | 28 |

29 |
30 | ); 31 | }, 32 | } as Component; 33 | -------------------------------------------------------------------------------- /src/useToggle/__test__/index.spec.ts: -------------------------------------------------------------------------------- 1 | import useToggle from '../index'; 2 | 3 | describe('useToggle', () => { 4 | test('default', async () => { 5 | const [state, { toggle, setLeft, setRight }] = useToggle(); 6 | expect(state.value).toBe(false); 7 | toggle(); 8 | expect(state.value).toBe(true); 9 | setLeft(); 10 | expect(state.value).toBe(false); 11 | setRight(); 12 | expect(state.value).toBe(true); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/useToggle/index.ts: -------------------------------------------------------------------------------- 1 | import { ref, Ref } from 'vue'; 2 | type IState = string | number | boolean | undefined; 3 | 4 | export interface Actions { 5 | setLeft: () => void; 6 | setRight: () => void; 7 | toggle: (value?: T) => void; 8 | } 9 | 10 | function useToggle(): [Ref, Actions]; 11 | 12 | function useToggle(defaultValue: T): [Ref, Actions]; 13 | 14 | function useToggle( 15 | defaultValue: T, 16 | reverseValue: U, 17 | ): [T | U, Actions]; 18 | 19 | function useToggle( 20 | defaultValue: D = false as D, 21 | reverseValue?: R | boolean, 22 | ): [Ref, Actions] { 23 | reverseValue = reverseValue === undefined ? !defaultValue : reverseValue; 24 | const stateRef = ref(defaultValue) as Ref; 25 | function toggle(value?: D | R) { 26 | if (value === undefined) { 27 | stateRef.value = stateRef.value === defaultValue ? reverseValue : defaultValue; 28 | return; 29 | } 30 | if (value === defaultValue || value === reverseValue) { 31 | stateRef.value = value; 32 | } else { 33 | stateRef.value = stateRef.value === defaultValue ? reverseValue : defaultValue; 34 | console.warn(`Function toggle parameter must be ${defaultValue} or ${reverseValue}`); 35 | } 36 | return; 37 | } 38 | function setLeft() { 39 | stateRef.value = defaultValue; 40 | } 41 | function setRight() { 42 | stateRef.value = reverseValue; 43 | } 44 | const actions = { 45 | toggle, 46 | setLeft, 47 | setRight, 48 | }; 49 | return [stateRef, actions]; 50 | } 51 | 52 | export default useToggle; 53 | -------------------------------------------------------------------------------- /src/utils/dom.ts: -------------------------------------------------------------------------------- 1 | import { Ref } from 'vue'; 2 | export type BasicTarget = 3 | | (() => T | null) 4 | | T 5 | | null 6 | | Ref 7 | | Ref; 8 | 9 | type TargetElement = HTMLElement | Document | Window | Ref; 10 | 11 | export function getTargetElement( 12 | target?: BasicTarget, 13 | defaultElement?: TargetElement, 14 | ): TargetElement | undefined | null { 15 | if (!target) { 16 | return defaultElement; 17 | } 18 | 19 | let targetElement: TargetElement | undefined | null; 20 | 21 | if (typeof target === 'function') { 22 | targetElement = target(); 23 | } else { 24 | targetElement = target; 25 | } 26 | 27 | return targetElement; 28 | } 29 | -------------------------------------------------------------------------------- /src/utils/testingHelpers.ts: -------------------------------------------------------------------------------- 1 | export function sleep(time: number): Promise { 2 | return new Promise((resolve) => { 3 | setTimeout(() => { 4 | resolve(true); 5 | }, time); 6 | }); 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/typeHelpers.ts: -------------------------------------------------------------------------------- 1 | import { ComponentPublicInstance } from 'vue'; 2 | export type ElementType = Element | ComponentPublicInstance; 3 | export function isComponentPublicInstance( 4 | instance: ElementType, 5 | ): instance is ComponentPublicInstance { 6 | return (instance as ComponentPublicInstance).$ !== undefined; 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "declaration": true, 5 | "module": "esnext", 6 | "target": "esnext", 7 | "moduleResolution": "node", 8 | "jsx": "preserve", 9 | "esModuleInterop": true, 10 | "skipLibCheck": true 11 | }, 12 | "include": ["./src", "./typings/"], 13 | "typings": "./typings/index.d.ts", 14 | "exclude": [ 15 | "node_modules", 16 | "build", 17 | "scripts", 18 | "acceptance-tests", 19 | "webpack", 20 | "jest", 21 | "src/setupTests.ts", 22 | "tslint:latest", 23 | "tslint-config-prettier" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /typings/some.d.ts: -------------------------------------------------------------------------------- 1 | interface validateUtil { 2 | [key: string]: any; 3 | } 4 | declare module 'ant-design-vue/es/form/utils/validateUtil' { 5 | const validateRules: any; 6 | export { validateRules }; 7 | } 8 | 9 | declare module 'ant-design-vue/es/form/utils/messages' { 10 | const defaultValidateMessages: any; 11 | export { defaultValidateMessages }; 12 | } 13 | 14 | declare module 'ant-design-vue/es/form/utils/asyncUtil' { 15 | const allPromiseFinish: any; 16 | export { allPromiseFinish }; 17 | } 18 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { VueLoaderPlugin } = require('vue-loader') 3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 4 | module.exports = (env = {}) => ({ 5 | mode: env.prod ? 'production' : 'development', 6 | devtool: env.prod ? 'source-map' : 'cheap-module-eval-source-map', 7 | entry: path.resolve(__dirname, './demo/main.ts'), 8 | output: { 9 | path: path.resolve(__dirname, './dist'), 10 | publicPath: '/dist/' 11 | }, 12 | resolve: { 13 | extensions:['.js','.ts','.vue','.tsx','jsx'] 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.vue$/, 19 | use: 'vue-loader' 20 | }, 21 | { 22 | test: /\.css$/, 23 | use: [ 24 | { 25 | loader: MiniCssExtractPlugin.loader, 26 | options: { hmr: !env.prod } 27 | }, 28 | 'css-loader' 29 | ] 30 | }, 31 | { 32 | test: /\.jsx?$|\.tsx?$/, 33 | use: { 34 | loader: 'babel-loader', 35 | } 36 | }, 37 | ] 38 | }, 39 | plugins: [ 40 | new VueLoaderPlugin(), 41 | new MiniCssExtractPlugin({ 42 | filename: '[name].css' 43 | }) 44 | ], 45 | devServer: { 46 | inline: true, 47 | hot: true, 48 | stats: 'minimal', 49 | contentBase: __dirname, 50 | overlay: true 51 | } 52 | }) 53 | --------------------------------------------------------------------------------