vuse-directive 0.0.3 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -60
- package/dist/directives/DebounceClick.d.ts +11 -0
- package/dist/index.d.ts +7 -1
- package/dist/utils/index.d.ts +6 -0
- package/dist/vue-directive.js +130 -36
- package/dist/vue-directive.umd.cjs +1 -1
- package/docs/debounce-click.md +208 -0
- package/docs/throttle-click.md +177 -0
- package/package.json +9 -12
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# vuse-directive
|
|
2
2
|
|
|
3
|
-
A collection of Vue 3 custom directives
|
|
3
|
+
A collection of Vue 3 custom directives.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -14,101 +14,104 @@ yarn add vuse-directive
|
|
|
14
14
|
|
|
15
15
|
## Getting Started
|
|
16
16
|
|
|
17
|
-
###
|
|
17
|
+
### Plugin (register all directives globally)
|
|
18
18
|
|
|
19
19
|
```ts
|
|
20
20
|
import { createApp } from 'vue'
|
|
21
21
|
import App from './App.vue'
|
|
22
|
-
import
|
|
22
|
+
import VuseDirective from 'vuse-directive'
|
|
23
|
+
|
|
24
|
+
const app = createApp(App)
|
|
25
|
+
app.use(VuseDirective)
|
|
26
|
+
app.mount('#app')
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Single directive
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { createApp } from 'vue'
|
|
33
|
+
import App from './App.vue'
|
|
34
|
+
import { throttleClick, debounceClick } from 'vuse-directive'
|
|
23
35
|
|
|
24
36
|
const app = createApp(App)
|
|
25
37
|
app.directive('throttle-click', throttleClick)
|
|
38
|
+
app.directive('debounce-click', debounceClick)
|
|
26
39
|
app.mount('#app')
|
|
27
40
|
```
|
|
28
41
|
|
|
29
|
-
### On-demand
|
|
42
|
+
### On-demand import (local registration)
|
|
30
43
|
|
|
31
44
|
```ts
|
|
32
|
-
import { throttleClick } from 'vuse-directive'
|
|
45
|
+
import { throttleClick, debounceClick } from 'vuse-directive'
|
|
33
46
|
```
|
|
34
47
|
|
|
35
48
|
---
|
|
36
49
|
|
|
37
50
|
## Directives
|
|
38
51
|
|
|
39
|
-
### `v-throttle-click` — Throttled Click
|
|
40
52
|
|
|
41
|
-
|
|
53
|
+
| Directive | Description | Docs |
|
|
54
|
+
| ------------------ | ------------------------------------------------------------------------------ | ---- |
|
|
55
|
+
| `v-throttle-click` | Throttle click events — fires immediately, then locks for a cooldown period | |
|
|
56
|
+
| `v-debounce-click` | Debounce click events — fires after the user stops clicking for a set duration | |
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## `v-throttle-click`
|
|
42
62
|
|
|
43
|
-
|
|
63
|
+
Fires immediately on click, then ignores subsequent clicks until the cooldown ends.
|
|
44
64
|
|
|
45
65
|
```vue
|
|
46
|
-
<!-- Default
|
|
66
|
+
<!-- Default: 300ms cooldown -->
|
|
47
67
|
<button v-throttle-click="handleClick">Submit</button>
|
|
48
68
|
|
|
49
|
-
<!-- Custom
|
|
50
|
-
<button v-throttle-click:
|
|
69
|
+
<!-- Custom cooldown -->
|
|
70
|
+
<button v-throttle-click:1000="handleClick">Submit</button>
|
|
51
71
|
|
|
52
|
-
<!--
|
|
53
|
-
<button v-throttle-click
|
|
72
|
+
<!-- Async lock: blocks until the returned Promise settles -->
|
|
73
|
+
<button v-throttle-click.async="submitForm">Submit</button>
|
|
74
|
+
|
|
75
|
+
<!-- Async + trailing: re-fires the last blocked click after the Promise settles -->
|
|
76
|
+
<button v-throttle-click.async.trailing="submitForm">Submit</button>
|
|
54
77
|
```
|
|
55
78
|
|
|
56
|
-
|
|
79
|
+
**Key modifiers:** `.once` · `.trailing` · `.async` · `.right` · `.stop` · `.prevent`
|
|
80
|
+
|
|
81
|
+
---
|
|
57
82
|
|
|
58
|
-
|
|
59
|
-
|----------|-------------|
|
|
60
|
-
| `.once` | Fire only once; all subsequent clicks are ignored |
|
|
61
|
-
| `.trailing` | If a click is blocked during cooldown, it fires once after the cooldown ends |
|
|
62
|
-
| `.async` | Async mode: new clicks are blocked until the previous callback's Promise resolves/rejects |
|
|
63
|
-
| `.right` | Listen to `contextmenu` (right-click) instead of `click` |
|
|
64
|
-
| `.capture` | Use capture phase for the event listener |
|
|
65
|
-
| `.passive` | Use passive listener mode for better scroll performance |
|
|
66
|
-
| `.stop` | Stop event propagation (passed to Vue's `withModifiers`) |
|
|
67
|
-
| `.prevent` | Prevent default behavior (passed to Vue's `withModifiers`) |
|
|
68
|
-
| `.self` | Only trigger when the event target is the element itself |
|
|
83
|
+
## `v-debounce-click`
|
|
69
84
|
|
|
70
|
-
|
|
85
|
+
Fires after the user stops clicking for the specified delay (trailing by default).
|
|
71
86
|
|
|
72
87
|
```vue
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
await fetch('/api/submit', { method: 'POST' })
|
|
76
|
-
}
|
|
77
|
-
</script>
|
|
78
|
-
|
|
79
|
-
<template>
|
|
80
|
-
<!-- Async + trailing: blocks repeated clicks during async call,
|
|
81
|
-
fires the last blocked click after the call completes -->
|
|
82
|
-
<button v-throttle-click:1000.async.trailing="submitForm">
|
|
83
|
-
Submit
|
|
84
|
-
</button>
|
|
85
|
-
|
|
86
|
-
<!-- Fire once only -->
|
|
87
|
-
<button v-throttle-click.once="handleClick">Click once</button>
|
|
88
|
-
|
|
89
|
-
<!-- Throttle right-click -->
|
|
90
|
-
<div v-throttle-click.right="openMenu">Right-click area</div>
|
|
91
|
-
</template>
|
|
92
|
-
```
|
|
88
|
+
<!-- Default: fires 300ms after the last click -->
|
|
89
|
+
<button v-debounce-click="handleClick">Search</button>
|
|
93
90
|
|
|
94
|
-
|
|
91
|
+
<!-- Custom delay -->
|
|
92
|
+
<button v-debounce-click:500="handleClick">Search</button>
|
|
95
93
|
|
|
96
|
-
|
|
97
|
-
|
|
94
|
+
<!-- Leading: fires immediately on first click, trailing if more clicks follow -->
|
|
95
|
+
<button v-debounce-click.leading="handleClick">Search</button>
|
|
98
96
|
```
|
|
99
97
|
|
|
100
|
-
|
|
98
|
+
**Key modifiers:** `.once` · `.leading` · `.right` · `.stop` · `.prevent`
|
|
101
99
|
|
|
102
|
-
|
|
103
|
-
function handleClick(e: MouseEvent) {
|
|
104
|
-
console.log('clicked', e)
|
|
105
|
-
}
|
|
106
|
-
```
|
|
100
|
+
### Max-wait cap
|
|
107
101
|
|
|
108
|
-
|
|
102
|
+
By default there is no max-wait limit. Use `makeDebounceClick` to guarantee the handler fires within a fixed window even during continuous clicks:
|
|
109
103
|
|
|
110
104
|
```ts
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
105
|
+
import { makeDebounceClick } from 'vuse-directive'
|
|
106
|
+
|
|
107
|
+
// Fires at most every 3000ms even if clicks never stop
|
|
108
|
+
app.directive('debounce-click', makeDebounceClick(3000))
|
|
114
109
|
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Issues
|
|
114
|
+
|
|
115
|
+
Found a bug or have a suggestion? Feel free to send an email to:
|
|
116
|
+
|
|
117
|
+
📮 stafanhulk@gmail.com
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Directive } from 'vue';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 创建防抖点击指令
|
|
5
|
+
* @param maxWait - 防抖最长等待时间,超过则强制触发一次;小于等于0时禁用该特性
|
|
6
|
+
* @returns
|
|
7
|
+
*/
|
|
8
|
+
declare const makeDebounceClick: (maxWait: number) => Directive;
|
|
9
|
+
export { makeDebounceClick };
|
|
10
|
+
declare const debounceClickDirective: Directive;
|
|
11
|
+
export default debounceClickDirective;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import { App } from 'vue';
|
|
2
|
+
import { default as debounceClick, makeDebounceClick } from './directives/DebounceClick';
|
|
1
3
|
import { default as throttleClick } from './directives/ThrottleClick';
|
|
2
4
|
|
|
3
|
-
export { throttleClick };
|
|
5
|
+
export { throttleClick, debounceClick, makeDebounceClick };
|
|
6
|
+
declare const _default: {
|
|
7
|
+
install: (app: App) => void;
|
|
8
|
+
};
|
|
9
|
+
export default _default;
|
package/dist/vue-directive.js
CHANGED
|
@@ -1,63 +1,157 @@
|
|
|
1
|
-
import { withModifiers as
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import { withModifiers as f } from "vue";
|
|
2
|
+
const v = (r) => typeof Symbol == "function" ? Symbol(r) : `__${r}__`, d = v("debounceClick"), g = (r) => {
|
|
3
|
+
const n = r;
|
|
4
|
+
return {
|
|
5
|
+
mounted(s, i) {
|
|
6
|
+
s[d] = {
|
|
7
|
+
_latestBinding: i,
|
|
8
|
+
_debounceTimer: null,
|
|
9
|
+
_maxWaitTimer: null,
|
|
10
|
+
_hasCalledOnce: !1,
|
|
11
|
+
_leadingFired: !1,
|
|
12
|
+
_waiting: !1,
|
|
13
|
+
_lastEvent: null,
|
|
14
|
+
_eventName: "click",
|
|
15
|
+
_listenerOptions: {},
|
|
16
|
+
_debounceHandler: () => {
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const e = s[d], a = Object.getOwnPropertyNames(
|
|
20
|
+
i.modifiers
|
|
21
|
+
);
|
|
22
|
+
e._debounceHandler = f((l) => {
|
|
23
|
+
const { modifiers: o, value: _ } = e._latestBinding;
|
|
24
|
+
if (o.once && e._hasCalledOnce)
|
|
25
|
+
return;
|
|
26
|
+
const u = typeof e._latestBinding.arg > "u" ? 300 : Number(e._latestBinding.arg);
|
|
27
|
+
e._lastEvent = l;
|
|
28
|
+
const c = (p) => {
|
|
29
|
+
e._hasCalledOnce = !0, _ == null || _(p);
|
|
30
|
+
};
|
|
31
|
+
if (u === 0) {
|
|
32
|
+
c(l);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
o.leading && !e._leadingFired ? (e._leadingFired = !0, c(l)) : e._waiting = !0, e._debounceTimer && clearTimeout(e._debounceTimer), n > 0 && e._maxWaitTimer === null && (e._maxWaitTimer = setTimeout(() => {
|
|
36
|
+
e._maxWaitTimer = null, e._debounceTimer && (clearTimeout(e._debounceTimer), e._debounceTimer = null), e._waiting && e._lastEvent && (e._waiting = !1, c(e._lastEvent)), e._leadingFired = !1;
|
|
37
|
+
}, n)), e._debounceTimer = setTimeout(() => {
|
|
38
|
+
e._debounceTimer = null, e._maxWaitTimer && (clearTimeout(e._maxWaitTimer), e._maxWaitTimer = null), e._waiting && (e._waiting = !1, c(l)), e._leadingFired = !1;
|
|
39
|
+
}, u);
|
|
40
|
+
}, a), i.modifiers.right && (e._eventName = "contextmenu"), e._listenerOptions = {
|
|
41
|
+
capture: i.modifiers.capture ?? !1,
|
|
42
|
+
passive: i.modifiers.passive ?? !1
|
|
43
|
+
}, s.addEventListener(
|
|
44
|
+
e._eventName,
|
|
45
|
+
e._debounceHandler,
|
|
46
|
+
e._listenerOptions
|
|
47
|
+
);
|
|
48
|
+
},
|
|
49
|
+
updated(s, i) {
|
|
50
|
+
const e = s[d], a = i.modifiers.capture ?? !1, l = i.modifiers.passive ?? !1, o = e._listenerOptions;
|
|
51
|
+
(a !== o.capture || l !== o.passive) && (s.removeEventListener(
|
|
52
|
+
e._eventName,
|
|
53
|
+
e._debounceHandler,
|
|
54
|
+
o
|
|
55
|
+
), e._listenerOptions = {
|
|
56
|
+
capture: a,
|
|
57
|
+
passive: l
|
|
58
|
+
}, s.addEventListener(
|
|
59
|
+
e._eventName,
|
|
60
|
+
e._debounceHandler,
|
|
61
|
+
e._listenerOptions
|
|
62
|
+
)), e._latestBinding = i;
|
|
63
|
+
},
|
|
64
|
+
unmounted(s) {
|
|
65
|
+
const i = s[d];
|
|
66
|
+
s.removeEventListener(
|
|
67
|
+
i._eventName,
|
|
68
|
+
i._debounceHandler,
|
|
69
|
+
i._listenerOptions
|
|
70
|
+
), i._debounceTimer && clearTimeout(i._debounceTimer), i._maxWaitTimer && clearTimeout(i._maxWaitTimer);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}, T = g(-1), m = v("throttleClick"), h = {
|
|
74
|
+
mounted(r, n) {
|
|
75
|
+
r[m] = {
|
|
76
|
+
_latestBinding: n,
|
|
77
|
+
_throttleTimer: null,
|
|
78
|
+
_hasCalledOnce: !1,
|
|
79
|
+
_trailingEvent: null,
|
|
80
|
+
_asyncPending: !1,
|
|
81
|
+
_unmounted: !1,
|
|
82
|
+
_eventName: "click",
|
|
83
|
+
_listenerOptions: {},
|
|
84
|
+
_throttleHandler: () => {
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const t = r[m], s = Object.getOwnPropertyNames(
|
|
88
|
+
n.modifiers
|
|
7
89
|
);
|
|
8
|
-
t._throttleHandler =
|
|
9
|
-
const { modifiers:
|
|
10
|
-
if (
|
|
90
|
+
t._throttleHandler = f((i) => {
|
|
91
|
+
const { modifiers: e, value: a } = t._latestBinding;
|
|
92
|
+
if (e.once && t._hasCalledOnce)
|
|
11
93
|
return;
|
|
12
|
-
if (
|
|
13
|
-
|
|
94
|
+
if (e.async && t._asyncPending) {
|
|
95
|
+
e.trailing && (t._trailingEvent = i);
|
|
14
96
|
return;
|
|
15
97
|
}
|
|
16
|
-
const
|
|
17
|
-
if (
|
|
18
|
-
|
|
98
|
+
const l = typeof t._latestBinding.arg > "u" ? 300 : Number(t._latestBinding.arg);
|
|
99
|
+
if (l > 0 && t._throttleTimer) {
|
|
100
|
+
e.trailing && (t._trailingEvent = i);
|
|
19
101
|
return;
|
|
20
102
|
}
|
|
21
103
|
const o = (_) => {
|
|
22
|
-
t._hasCalledOnce = !0, t._trailingEvent = null,
|
|
23
|
-
t.
|
|
24
|
-
})) :
|
|
104
|
+
t._hasCalledOnce = !0, t._trailingEvent = null, e.async ? (t._asyncPending = !0, Promise.resolve(a == null ? void 0 : a(_)).finally(() => {
|
|
105
|
+
t._asyncPending = !1, !t._unmounted && e.trailing && t._trailingEvent && t._throttleHandler(t._trailingEvent);
|
|
106
|
+
})) : a == null || a(_);
|
|
25
107
|
};
|
|
26
|
-
if (
|
|
108
|
+
if (l === 0) {
|
|
27
109
|
o(i);
|
|
28
110
|
return;
|
|
29
111
|
}
|
|
30
112
|
o(i), t._throttleTimer = setTimeout(() => {
|
|
31
|
-
t._throttleTimer = null, !
|
|
32
|
-
},
|
|
33
|
-
}, s), t._eventName =
|
|
34
|
-
capture:
|
|
35
|
-
passive:
|
|
36
|
-
},
|
|
113
|
+
t._throttleTimer = null, !e.async && e.trailing && t._trailingEvent && (t._throttleHandler(t._trailingEvent), t._trailingEvent = null);
|
|
114
|
+
}, l);
|
|
115
|
+
}, s), t._eventName = n.modifiers.right ? "contextmenu" : "click", t._listenerOptions = {
|
|
116
|
+
capture: n.modifiers.capture ?? !1,
|
|
117
|
+
passive: n.modifiers.passive ?? !1
|
|
118
|
+
}, r.addEventListener(
|
|
37
119
|
t._eventName,
|
|
38
120
|
t._throttleHandler,
|
|
39
121
|
t._listenerOptions
|
|
40
122
|
);
|
|
41
123
|
},
|
|
42
|
-
updated(
|
|
43
|
-
const s =
|
|
44
|
-
(s !==
|
|
124
|
+
updated(r, n) {
|
|
125
|
+
const t = r[m], s = n.modifiers.capture ?? !1, i = n.modifiers.passive ?? !1, e = t._listenerOptions;
|
|
126
|
+
(s !== e.capture || i !== e.passive) && (r.removeEventListener(
|
|
127
|
+
t._eventName,
|
|
128
|
+
t._throttleHandler,
|
|
129
|
+
e
|
|
130
|
+
), t._listenerOptions = {
|
|
45
131
|
capture: s,
|
|
46
132
|
passive: i
|
|
47
|
-
},
|
|
133
|
+
}, r.addEventListener(
|
|
48
134
|
t._eventName,
|
|
49
135
|
t._throttleHandler,
|
|
50
136
|
t._listenerOptions
|
|
51
|
-
)), t._latestBinding =
|
|
137
|
+
)), t._latestBinding = n;
|
|
52
138
|
},
|
|
53
|
-
unmounted(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
139
|
+
unmounted(r) {
|
|
140
|
+
const n = r[m];
|
|
141
|
+
r.removeEventListener(
|
|
142
|
+
n._eventName,
|
|
143
|
+
n._throttleHandler,
|
|
144
|
+
n._listenerOptions
|
|
145
|
+
), n._throttleTimer && clearTimeout(n._throttleTimer), n._unmounted = !0, n._trailingEvent = null;
|
|
146
|
+
}
|
|
147
|
+
}, E = {
|
|
148
|
+
install: (r) => {
|
|
149
|
+
r.directive("throttle-click", h), r.directive("debounce-click", T);
|
|
59
150
|
}
|
|
60
151
|
};
|
|
61
152
|
export {
|
|
62
|
-
|
|
153
|
+
T as debounceClick,
|
|
154
|
+
E as default,
|
|
155
|
+
g as makeDebounceClick,
|
|
156
|
+
h as throttleClick
|
|
63
157
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(
|
|
1
|
+
(function(o,d){typeof exports=="object"&&typeof module<"u"?d(exports,require("vue")):typeof define=="function"&&define.amd?define(["exports","vue"],d):(o=typeof globalThis<"u"?globalThis:o||self,d(o.VueDirective={},o.Vue))})(this,function(o,d){"use strict";const v=r=>typeof Symbol=="function"?Symbol(r):`__${r}__`,u=v("debounceClick"),p=r=>{const n=r;return{mounted(s,i){s[u]={_latestBinding:i,_debounceTimer:null,_maxWaitTimer:null,_hasCalledOnce:!1,_leadingFired:!1,_waiting:!1,_lastEvent:null,_eventName:"click",_listenerOptions:{},_debounceHandler:()=>{}};const e=s[u],a=Object.getOwnPropertyNames(i.modifiers);e._debounceHandler=d.withModifiers(l=>{const{modifiers:c,value:_}=e._latestBinding;if(c.once&&e._hasCalledOnce)return;const h=typeof e._latestBinding.arg>"u"?300:Number(e._latestBinding.arg);e._lastEvent=l;const f=y=>{e._hasCalledOnce=!0,_==null||_(y)};if(h===0){f(l);return}c.leading&&!e._leadingFired?(e._leadingFired=!0,f(l)):e._waiting=!0,e._debounceTimer&&clearTimeout(e._debounceTimer),n>0&&e._maxWaitTimer===null&&(e._maxWaitTimer=setTimeout(()=>{e._maxWaitTimer=null,e._debounceTimer&&(clearTimeout(e._debounceTimer),e._debounceTimer=null),e._waiting&&e._lastEvent&&(e._waiting=!1,f(e._lastEvent)),e._leadingFired=!1},n)),e._debounceTimer=setTimeout(()=>{e._debounceTimer=null,e._maxWaitTimer&&(clearTimeout(e._maxWaitTimer),e._maxWaitTimer=null),e._waiting&&(e._waiting=!1,f(l)),e._leadingFired=!1},h)},a),i.modifiers.right&&(e._eventName="contextmenu"),e._listenerOptions={capture:i.modifiers.capture??!1,passive:i.modifiers.passive??!1},s.addEventListener(e._eventName,e._debounceHandler,e._listenerOptions)},updated(s,i){const e=s[u],a=i.modifiers.capture??!1,l=i.modifiers.passive??!1,c=e._listenerOptions;(a!==c.capture||l!==c.passive)&&(s.removeEventListener(e._eventName,e._debounceHandler,c),e._listenerOptions={capture:a,passive:l},s.addEventListener(e._eventName,e._debounceHandler,e._listenerOptions)),e._latestBinding=i},unmounted(s){const i=s[u];s.removeEventListener(i._eventName,i._debounceHandler,i._listenerOptions),i._debounceTimer&&clearTimeout(i._debounceTimer),i._maxWaitTimer&&clearTimeout(i._maxWaitTimer)}}},g=p(-1),m=v("throttleClick"),T={mounted(r,n){r[m]={_latestBinding:n,_throttleTimer:null,_hasCalledOnce:!1,_trailingEvent:null,_asyncPending:!1,_unmounted:!1,_eventName:"click",_listenerOptions:{},_throttleHandler:()=>{}};const t=r[m],s=Object.getOwnPropertyNames(n.modifiers);t._throttleHandler=d.withModifiers(i=>{const{modifiers:e,value:a}=t._latestBinding;if(e.once&&t._hasCalledOnce)return;if(e.async&&t._asyncPending){e.trailing&&(t._trailingEvent=i);return}const l=typeof t._latestBinding.arg>"u"?300:Number(t._latestBinding.arg);if(l>0&&t._throttleTimer){e.trailing&&(t._trailingEvent=i);return}const c=_=>{t._hasCalledOnce=!0,t._trailingEvent=null,e.async?(t._asyncPending=!0,Promise.resolve(a==null?void 0:a(_)).finally(()=>{t._asyncPending=!1,!t._unmounted&&e.trailing&&t._trailingEvent&&t._throttleHandler(t._trailingEvent)})):a==null||a(_)};if(l===0){c(i);return}c(i),t._throttleTimer=setTimeout(()=>{t._throttleTimer=null,!e.async&&e.trailing&&t._trailingEvent&&(t._throttleHandler(t._trailingEvent),t._trailingEvent=null)},l)},s),t._eventName=n.modifiers.right?"contextmenu":"click",t._listenerOptions={capture:n.modifiers.capture??!1,passive:n.modifiers.passive??!1},r.addEventListener(t._eventName,t._throttleHandler,t._listenerOptions)},updated(r,n){const t=r[m],s=n.modifiers.capture??!1,i=n.modifiers.passive??!1,e=t._listenerOptions;(s!==e.capture||i!==e.passive)&&(r.removeEventListener(t._eventName,t._throttleHandler,e),t._listenerOptions={capture:s,passive:i},r.addEventListener(t._eventName,t._throttleHandler,t._listenerOptions)),t._latestBinding=n},unmounted(r){const n=r[m];r.removeEventListener(n._eventName,n._throttleHandler,n._listenerOptions),n._throttleTimer&&clearTimeout(n._throttleTimer),n._unmounted=!0,n._trailingEvent=null}},O={install:r=>{r.directive("throttle-click",T),r.directive("debounce-click",g)}};o.debounceClick=g,o.default=O,o.makeDebounceClick=p,o.throttleClick=T,Object.defineProperties(o,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# v-debounce-click
|
|
2
|
+
|
|
3
|
+
Debounces click events so the handler fires only after the user stops clicking for a specified duration. Supports custom delay, leading mode, max-wait cap, and Vue modifier pass-through.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
// Global registration (all directives)
|
|
9
|
+
import { createApp } from 'vue'
|
|
10
|
+
import VuseDirective from 'vuse-directive'
|
|
11
|
+
|
|
12
|
+
const app = createApp(App)
|
|
13
|
+
app.use(VuseDirective)
|
|
14
|
+
app.mount('#app')
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
// Single directive registration
|
|
19
|
+
import { createApp } from 'vue'
|
|
20
|
+
import { debounceClick } from 'vuse-directive'
|
|
21
|
+
|
|
22
|
+
const app = createApp(App)
|
|
23
|
+
app.directive('debounce-click', debounceClick)
|
|
24
|
+
app.mount('#app')
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
// On-demand import (local registration)
|
|
29
|
+
import { debounceClick } from 'vuse-directive'
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Basic Usage
|
|
33
|
+
|
|
34
|
+
```vue
|
|
35
|
+
<!-- Default debounce delay: 300ms (trailing) -->
|
|
36
|
+
<button v-debounce-click="handleClick">Search</button>
|
|
37
|
+
|
|
38
|
+
<!-- Custom delay (in ms) -->
|
|
39
|
+
<button v-debounce-click:500="handleClick">Search</button>
|
|
40
|
+
|
|
41
|
+
<!-- Delay 0: no debounce, fires on every click -->
|
|
42
|
+
<button v-debounce-click:0="handleClick">Search</button>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Arg
|
|
46
|
+
|
|
47
|
+
| Arg | Description |
|
|
48
|
+
|-------------|----------------------------------------------------------------------------------|
|
|
49
|
+
| _(omitted)_ | Default debounce delay: `300` ms |
|
|
50
|
+
| `0` | Disable debounce; callback fires on every click, modifiers still apply |
|
|
51
|
+
| _number_ | Custom debounce delay in milliseconds (e.g. `:500` → 500 ms) |
|
|
52
|
+
|
|
53
|
+
## Modifiers
|
|
54
|
+
|
|
55
|
+
### Debounce-specific
|
|
56
|
+
|
|
57
|
+
| Modifier | Description |
|
|
58
|
+
|------------|----------------------------------------------------------------------------------------------------------------------|
|
|
59
|
+
| `.leading` | Fire immediately on the first click of a debounce period; also fire trailing if more clicks happen during the delay |
|
|
60
|
+
| `.once` | Fire only once for the lifetime of the element; all subsequent clicks are ignored |
|
|
61
|
+
|
|
62
|
+
### Event listener
|
|
63
|
+
|
|
64
|
+
| Modifier | Description |
|
|
65
|
+
|------------|---------------------------------------------------------------|
|
|
66
|
+
| `.right` | Listen to `contextmenu` (right-click) instead of `click` |
|
|
67
|
+
| `.capture` | Use capture phase for the event listener |
|
|
68
|
+
| `.passive` | Mark the listener as passive for better scroll performance |
|
|
69
|
+
|
|
70
|
+
### Vue `withModifiers` pass-through
|
|
71
|
+
|
|
72
|
+
| Modifier | Description |
|
|
73
|
+
|-----------|---------------------------------------------------------------------------|
|
|
74
|
+
| `.stop` | Call `event.stopPropagation()` |
|
|
75
|
+
| `.prevent`| Call `event.preventDefault()` |
|
|
76
|
+
| `.self` | Only trigger when `event.target` is the element itself |
|
|
77
|
+
| `.ctrl` | Only trigger when the Ctrl key is held |
|
|
78
|
+
| `.shift` | Only trigger when the Shift key is held |
|
|
79
|
+
| `.alt` | Only trigger when the Alt / Option key is held |
|
|
80
|
+
| `.meta` | Only trigger when the Meta / Command key is held |
|
|
81
|
+
| `.left` | Only trigger on left mouse button clicks |
|
|
82
|
+
| `.middle` | Only trigger on middle mouse button clicks |
|
|
83
|
+
| `.exact` | Only trigger when exactly the specified modifier keys are pressed |
|
|
84
|
+
|
|
85
|
+
## Callback Signature
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
type ClickHandler = (event: MouseEvent, ...args: unknown[]) => void
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The binding value receives a `MouseEvent` as the first argument:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
function handleClick(e: MouseEvent) {
|
|
95
|
+
console.log('clicked', e)
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Behavior Details
|
|
100
|
+
|
|
101
|
+
### Default (trailing)
|
|
102
|
+
|
|
103
|
+
The handler fires once after the user stops clicking for the full delay duration. Every new click resets the timer.
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
click ── click ── click ── [silence 300ms]
|
|
107
|
+
↓ fire
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### `.leading`
|
|
111
|
+
|
|
112
|
+
The handler fires immediately on the **first** click of a debounce period. If more clicks occur during the delay, a trailing call is also made when the delay ends. If no clicks happen during the delay, no trailing call is made.
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
// With subsequent clicks:
|
|
116
|
+
click ── click ── [delay ends]
|
|
117
|
+
↓ fire (wait) ↓ fire (trailing)
|
|
118
|
+
|
|
119
|
+
// Without subsequent clicks:
|
|
120
|
+
click ── [delay ends]
|
|
121
|
+
↓ fire (no trailing)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### `.once`
|
|
125
|
+
|
|
126
|
+
The handler fires at most once for the lifetime of the element. All later clicks are silently ignored.
|
|
127
|
+
|
|
128
|
+
## Max-Wait Cap (`makeDebounceClick`)
|
|
129
|
+
|
|
130
|
+
The default `v-debounce-click` directive has no max-wait limit. If the user never stops clicking, the handler will never fire.
|
|
131
|
+
|
|
132
|
+
To set a max-wait cap — guaranteeing the handler fires at least once even during continuous clicks — use `makeDebounceClick`:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { makeDebounceClick } from 'vuse-directive'
|
|
136
|
+
|
|
137
|
+
// Creates a directive that fires at most every 3000ms even if clicks never stop
|
|
138
|
+
const debounceClickWithMaxWait = makeDebounceClick(3000)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Register and use it like any custom directive:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
// Local registration
|
|
145
|
+
app.directive('debounce-click-capped', debounceClickWithMaxWait)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
```vue
|
|
149
|
+
<button v-debounce-click-capped:500="handleClick">Search</button>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### `makeDebounceClick(maxWait)` parameter
|
|
153
|
+
|
|
154
|
+
| Value | Description |
|
|
155
|
+
|------------|----------------------------------------------------|
|
|
156
|
+
| `> 0` | Enable max-wait: force-fire after `maxWait` ms |
|
|
157
|
+
| `<= 0` | Disable max-wait (same as the default directive) |
|
|
158
|
+
|
|
159
|
+
The default exported `debounceClick` is equivalent to `makeDebounceClick(-1)`.
|
|
160
|
+
|
|
161
|
+
## Examples
|
|
162
|
+
|
|
163
|
+
```vue
|
|
164
|
+
<script setup lang="ts">
|
|
165
|
+
function search(e: MouseEvent) {
|
|
166
|
+
console.log('search triggered', e)
|
|
167
|
+
}
|
|
168
|
+
</script>
|
|
169
|
+
|
|
170
|
+
<template>
|
|
171
|
+
<!-- Basic: fires 300ms after the last click -->
|
|
172
|
+
<button v-debounce-click="search">Search</button>
|
|
173
|
+
|
|
174
|
+
<!-- Custom delay: fires 1000ms after the last click -->
|
|
175
|
+
<button v-debounce-click:1000="search">Search (1s)</button>
|
|
176
|
+
|
|
177
|
+
<!-- Leading: fires immediately on first click, trailing if more clicks follow -->
|
|
178
|
+
<button v-debounce-click.leading="search">Search (leading)</button>
|
|
179
|
+
|
|
180
|
+
<!-- Once: fires only the first time ever -->
|
|
181
|
+
<button v-debounce-click.once="search">Search (once)</button>
|
|
182
|
+
|
|
183
|
+
<!-- Debounce right-click -->
|
|
184
|
+
<div v-debounce-click.right="openMenu">Right-click area</div>
|
|
185
|
+
|
|
186
|
+
<!-- Stop propagation -->
|
|
187
|
+
<button v-debounce-click.stop="search">No bubble</button>
|
|
188
|
+
</template>
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### With max-wait cap
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
// main.ts
|
|
195
|
+
import { createApp } from 'vue'
|
|
196
|
+
import App from './App.vue'
|
|
197
|
+
import { makeDebounceClick } from 'vuse-directive'
|
|
198
|
+
|
|
199
|
+
const app = createApp(App)
|
|
200
|
+
app.directive('debounce-click', makeDebounceClick(3000))
|
|
201
|
+
app.mount('#app')
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
```vue
|
|
205
|
+
<!-- Fires after 500ms of silence, but guaranteed to fire within 3000ms
|
|
206
|
+
even if the user keeps clicking without stopping -->
|
|
207
|
+
<button v-debounce-click:500="search">Search</button>
|
|
208
|
+
```
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# v-throttle-click
|
|
2
|
+
|
|
3
|
+
Throttles click events to prevent repeated triggers within a short period. Supports custom delay, async lock, trailing call, and Vue modifier pass-through.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
// Global registration (all directives)
|
|
9
|
+
import { createApp } from 'vue'
|
|
10
|
+
import VuseDirective from 'vuse-directive'
|
|
11
|
+
|
|
12
|
+
const app = createApp(App)
|
|
13
|
+
app.use(VuseDirective)
|
|
14
|
+
app.mount('#app')
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
// Single directive registration
|
|
19
|
+
import { createApp } from 'vue'
|
|
20
|
+
import { throttleClick } from 'vuse-directive'
|
|
21
|
+
|
|
22
|
+
const app = createApp(App)
|
|
23
|
+
app.directive('throttle-click', throttleClick)
|
|
24
|
+
app.mount('#app')
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
// On-demand import (local registration)
|
|
29
|
+
import { throttleClick } from 'vuse-directive'
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Basic Usage
|
|
33
|
+
|
|
34
|
+
```vue
|
|
35
|
+
<!-- Default throttle interval: 300ms -->
|
|
36
|
+
<button v-throttle-click="handleClick">Submit</button>
|
|
37
|
+
|
|
38
|
+
<!-- Custom interval (in ms) -->
|
|
39
|
+
<button v-throttle-click:500="handleClick">Submit</button>
|
|
40
|
+
|
|
41
|
+
<!-- Interval 0: no throttle, modifiers still apply -->
|
|
42
|
+
<button v-throttle-click:0="handleClick">Submit</button>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Arg
|
|
46
|
+
|
|
47
|
+
| Arg | Description |
|
|
48
|
+
|------------|-----------------------------------------------------------------------------|
|
|
49
|
+
| _(omitted)_ | Default throttle interval: `300` ms |
|
|
50
|
+
| `0` | Disable throttle; callback fires on every click, modifiers still apply |
|
|
51
|
+
| _number_ | Custom throttle interval in milliseconds (e.g. `:500` → 500 ms) |
|
|
52
|
+
|
|
53
|
+
## Modifiers
|
|
54
|
+
|
|
55
|
+
### Throttle-specific
|
|
56
|
+
|
|
57
|
+
| Modifier | Description |
|
|
58
|
+
|------------|-------------------------------------------------------------------------------------------------|
|
|
59
|
+
| `.once` | Fire only once; all subsequent clicks are ignored |
|
|
60
|
+
| `.trailing`| If a click is blocked during cooldown, re-fire once after cooldown (or async call) ends |
|
|
61
|
+
| `.async` | Async mode: new clicks are blocked until the previous callback's Promise resolves/rejects |
|
|
62
|
+
|
|
63
|
+
### Event listener
|
|
64
|
+
|
|
65
|
+
| Modifier | Description |
|
|
66
|
+
|------------|---------------------------------------------------------------|
|
|
67
|
+
| `.right` | Listen to `contextmenu` (right-click) instead of `click` |
|
|
68
|
+
| `.capture` | Use capture phase for the event listener |
|
|
69
|
+
| `.passive` | Mark the listener as passive for better scroll performance |
|
|
70
|
+
|
|
71
|
+
### Vue `withModifiers` pass-through
|
|
72
|
+
|
|
73
|
+
| Modifier | Description |
|
|
74
|
+
|-----------|---------------------------------------------------------------------------|
|
|
75
|
+
| `.stop` | Call `event.stopPropagation()` |
|
|
76
|
+
| `.prevent`| Call `event.preventDefault()` |
|
|
77
|
+
| `.self` | Only trigger when `event.target` is the element itself |
|
|
78
|
+
| `.ctrl` | Only trigger when the Ctrl key is held |
|
|
79
|
+
| `.shift` | Only trigger when the Shift key is held |
|
|
80
|
+
| `.alt` | Only trigger when the Alt / Option key is held |
|
|
81
|
+
| `.meta` | Only trigger when the Meta / Command key is held |
|
|
82
|
+
| `.left` | Only trigger on left mouse button clicks |
|
|
83
|
+
| `.middle` | Only trigger on middle mouse button clicks |
|
|
84
|
+
| `.exact` | Only trigger when exactly the specified modifier keys are pressed |
|
|
85
|
+
|
|
86
|
+
## Callback Signature
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
type ClickHandler = (event: MouseEvent, ...args: unknown[]) => any
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The binding value receives a `MouseEvent` as the first argument.
|
|
93
|
+
|
|
94
|
+
In async mode, return a `Promise` to activate the async lock:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
async function handleAsync(e: MouseEvent) {
|
|
98
|
+
await fetch('/api/submit', { method: 'POST' })
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Behavior Details
|
|
103
|
+
|
|
104
|
+
### Default (trailing only)
|
|
105
|
+
|
|
106
|
+
On each click the handler fires immediately. Subsequent clicks within the cooldown window are dropped.
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
click ──── click ──── click ──── [cooldown ends]
|
|
110
|
+
↓ fire (drop) (drop)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### `.trailing`
|
|
114
|
+
|
|
115
|
+
If any click is blocked during the cooldown, the handler re-fires once when the cooldown ends, using the most recent blocked event.
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
click ──── click ──── click ──── [cooldown ends]
|
|
119
|
+
↓ fire (save) (save) ↓ fire (last saved)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### `.async`
|
|
123
|
+
|
|
124
|
+
New clicks are blocked while the previous call's Promise is pending. The cooldown timer does not apply in async mode; the lock releases only when the Promise settles.
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
click ──── click ──── [Promise resolves]
|
|
128
|
+
↓ fire (block) ↓ unlocked
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### `.async.trailing`
|
|
132
|
+
|
|
133
|
+
Same as `.async`, but if any click was blocked while the Promise was pending, the handler re-fires automatically after the Promise settles.
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
click ──── click ──── [Promise resolves]
|
|
137
|
+
↓ fire (save) ↓ fire (saved event)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### `.once`
|
|
141
|
+
|
|
142
|
+
The handler fires at most once for the lifetime of the element. All later clicks are silently ignored.
|
|
143
|
+
|
|
144
|
+
## Examples
|
|
145
|
+
|
|
146
|
+
```vue
|
|
147
|
+
<script setup lang="ts">
|
|
148
|
+
async function submitForm() {
|
|
149
|
+
await fetch('/api/submit', { method: 'POST' })
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function handleClick(e: MouseEvent) {
|
|
153
|
+
console.log('clicked', e)
|
|
154
|
+
}
|
|
155
|
+
</script>
|
|
156
|
+
|
|
157
|
+
<template>
|
|
158
|
+
<!-- Basic: 300ms throttle -->
|
|
159
|
+
<button v-throttle-click="handleClick">Click me</button>
|
|
160
|
+
|
|
161
|
+
<!-- Custom interval: 1000ms -->
|
|
162
|
+
<button v-throttle-click:1000="handleClick">1s throttle</button>
|
|
163
|
+
|
|
164
|
+
<!-- Async + trailing: blocks repeated clicks during async call,
|
|
165
|
+
fires the last blocked click after the call completes -->
|
|
166
|
+
<button v-throttle-click.async.trailing="submitForm">Submit</button>
|
|
167
|
+
|
|
168
|
+
<!-- Fire once only -->
|
|
169
|
+
<button v-throttle-click.once="handleClick">Click once</button>
|
|
170
|
+
|
|
171
|
+
<!-- Throttle right-click -->
|
|
172
|
+
<div v-throttle-click.right="openMenu">Right-click area</div>
|
|
173
|
+
|
|
174
|
+
<!-- Stop propagation -->
|
|
175
|
+
<button v-throttle-click.stop="handleClick">No bubble</button>
|
|
176
|
+
</template>
|
|
177
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vuse-directive",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Vue 3 directives collection",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/vue-directive.umd.cjs",
|
|
@@ -14,14 +14,16 @@
|
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
|
-
"dist"
|
|
17
|
+
"dist",
|
|
18
|
+
"docs"
|
|
18
19
|
],
|
|
19
20
|
"scripts": {
|
|
20
21
|
"test": "jest",
|
|
21
22
|
"build": "vite build",
|
|
22
23
|
"dev": "vite",
|
|
23
24
|
"format": "biome format --write .",
|
|
24
|
-
"
|
|
25
|
+
"check": "biome check ./src",
|
|
26
|
+
"prepare": "husky",
|
|
25
27
|
"prepublishOnly": "npm run build",
|
|
26
28
|
"release": "node scripts/publish.js"
|
|
27
29
|
},
|
|
@@ -29,10 +31,13 @@
|
|
|
29
31
|
"vue": "^3.0.0"
|
|
30
32
|
},
|
|
31
33
|
"devDependencies": {
|
|
32
|
-
"@biomejs/biome": "^2.
|
|
34
|
+
"@biomejs/biome": "^2.4.0",
|
|
33
35
|
"@types/jest": "^30.0.0",
|
|
34
36
|
"@types/node": "^25.3.0",
|
|
37
|
+
"@vitejs/plugin-vue": "^5.0.0",
|
|
35
38
|
"@vue/test-utils": "^2.4.6",
|
|
39
|
+
"element-plus": "^2.13.3",
|
|
40
|
+
"husky": "^9.0.0",
|
|
36
41
|
"jest": "^30.2.0",
|
|
37
42
|
"jest-environment-jsdom": "^30.2.0",
|
|
38
43
|
"ts-jest": "^29.4.6",
|
|
@@ -52,14 +57,6 @@
|
|
|
52
57
|
"sideEffects": false,
|
|
53
58
|
"author": "Stafan Hulk",
|
|
54
59
|
"license": "MIT",
|
|
55
|
-
"repository": {
|
|
56
|
-
"type": "git",
|
|
57
|
-
"url": "https://github.com/stafanhulk/vue-directive.git"
|
|
58
|
-
},
|
|
59
|
-
"homepage": "https://github.com/stafanhulk/vue-directive#readme",
|
|
60
|
-
"bugs": {
|
|
61
|
-
"url": "https://github.com/stafanhulk/vue-directive/issues"
|
|
62
|
-
},
|
|
63
60
|
"engines": {
|
|
64
61
|
"node": ">=18"
|
|
65
62
|
}
|