├── .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 |
click away
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 | handle
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 |
338 | handle
339 |
340 |
341 | reset
342 |
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 |
80 | validate('name1')} />
81 |
82 |
83 |
84 |
85 |
86 |
93 |
94 |
95 |
96 | 🇨🇳
97 |
98 | China (中国)
99 |
100 |
101 |
102 |
103 |
104 | 🇺🇸
105 |
106 | USA (美国)
107 |
108 |
109 |
110 |
111 | submit
112 | reset
113 | reset with new updated Values
114 | reset
115 | clearValidateAll
116 | clearValidateName1('name1')}>clearValidateName1
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 | toggle "h1"
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 |
19 | Toggle
20 |
21 |
22 | Toggle False
23 |
24 |
25 | {' '}
26 | Toggle True
27 |
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 |
--------------------------------------------------------------------------------