vue-intercept-plugin 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 ADDED
@@ -0,0 +1,293 @@
1
+ # vue-intercept-plugin
2
+
3
+ [中文](README_ZH.md) | English
4
+
5
+ A lightweight Vue plugin providing the `v-intercept` custom directive for intercepting DOM events. Defaults to click, with support for any event type via directive arguments.
6
+
7
+ > The plugin does not handle permission checking — it does one thing: intercepts an event and passes it to your function.
8
+
9
+ ## How It Works
10
+
11
+ ### 🎯 Event Interception Mechanism
12
+
13
+ 1. **Directive Parsing**: Resolves the `v-intercept` value at runtime, supporting both function and array formats
14
+ 2. **Auto Binding**: Uses `addEventListener` to bind to the specified event type (default: click)
15
+ 3. **Parameter Forwarding**: When using array syntax, the event object is automatically appended as the last argument
16
+ 4. **Auto Cleanup**: Removes event listeners via `removeEventListener` on component unmount to prevent memory leaks
17
+
18
+ ### 📋 Directive Format Reference
19
+
20
+ | Syntax | Description |
21
+ |--------|-------------|
22
+ | `v-intercept="handleFn"` | Pass a function directly. Receives the event object automatically |
23
+ | `v-intercept="[handleFn, arg1, arg2]"` | Array syntax. First item is the function, rest are arguments. **Event object auto-appended to the end** |
24
+ | `v-intercept:change="handleFn"` | Specify event type via directive argument. Defaults to `click` |
25
+ | `v-intercept:change="[handleFn, arg1]"` | Custom event + array arguments, work together |
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ npm install vue-intercept-plugin
31
+ ```
32
+
33
+ ## Quick Start
34
+
35
+ ```typescript
36
+ import { createApp } from 'vue'
37
+ import VueInterceptPlugin from 'vue-intercept-plugin'
38
+
39
+ // Register plugin (no configuration needed)
40
+ const app = createApp(App)
41
+ app.use(VueInterceptPlugin)
42
+ ```
43
+
44
+ ```typescript
45
+ // Vue 2
46
+ import Vue from 'vue'
47
+ import VueInterceptPlugin from 'vue-intercept-plugin'
48
+
49
+ Vue.use(VueInterceptPlugin)
50
+ ```
51
+
52
+ ```vue
53
+ <template>
54
+ <!-- No arguments: pass function directly -->
55
+ <el-button v-intercept="handleDelete" type="danger">Delete</el-button>
56
+
57
+ <!-- Array syntax: [function, arg1, arg2, ...] -->
58
+ <el-button v-intercept="[handleDeleteById, 1001]" type="warning">
59
+ Delete Order #1001
60
+ </el-button>
61
+
62
+ <!-- Custom event type via directive argument -->
63
+ <select v-intercept:change="handleChange">
64
+ <option value="">Select...</option>
65
+ <option value="1">Option 1</option>
66
+ <option value="2">Option 2</option>
67
+ </select>
68
+ </template>
69
+
70
+ <script setup lang="ts">
71
+ const handleDelete = () => {
72
+ if (!hasPermission) {
73
+ ElMessage.warning('No permission')
74
+ return // Intercept
75
+ }
76
+ // Execute delete logic
77
+ }
78
+
79
+ const handleDeleteById = (id: number, event: MouseEvent) => {
80
+ if (!hasPermission) return
81
+ deleteApi(id)
82
+ }
83
+
84
+ const handleChange = (event: Event) => {
85
+ if (!canChange) return
86
+ const value = (event.target as HTMLSelectElement).value
87
+ // Handle selection logic
88
+ }
89
+ </script>
90
+ ```
91
+
92
+ ## API
93
+
94
+ ### `app.use(VueInterceptPlugin)`
95
+
96
+ Registers the plugin. No arguments required. Internally auto-detects the Vue version (Vue 3 uses `mounted`/`updated`/`unmounted` hooks; Vue 2 uses `bind`/`update`/`unbind` hooks).
97
+
98
+ ### v-intercept Directive
99
+
100
+ **Directive Argument (Event Type):**
101
+
102
+ | Syntax | Description |
103
+ |--------|-------------|
104
+ | `v-intercept="handler"` | Default: click |
105
+ | `v-intercept:click="handler"` | Explicit click |
106
+ | `v-intercept:change="handler"` | change event |
107
+ | `v-intercept:submit="handler"` | Form submit |
108
+ | `v-intercept:contextmenu="handler"` | Right-click menu |
109
+ | `v-intercept:dblclick="handler"` | Double-click |
110
+ | `v-intercept:dragstart="handler"` | Drag start |
111
+ | `v-intercept:copy="handler"` | Copy |
112
+ | `...` | Any native DOM event |
113
+
114
+ **Directive Value (Handler):**
115
+
116
+ | Format | Example | Description |
117
+ |--------|---------|-------------|
118
+ | `handler` | `v-intercept="handleDelete"` | Pass function directly. Receives event object |
119
+ | `[handler, ...args]` | `v-intercept="[handleDelete, 1001]"` | Array syntax. **Event object auto-appended to arguments** |
120
+ | `[handler]` | `v-intercept="[handleDelete]"` | Function only, no extra args. Equivalent to passing function directly |
121
+
122
+ **Function Signature Reference:**
123
+
124
+ | Template Syntax | Actual Arguments Received |
125
+ |----------------|-------------------------|
126
+ | `v-intercept="handle"` | `handle(event)` |
127
+ | `v-intercept="[handle, id]"` | `handle(id, event)` |
128
+ | `v-intercept="[handle, id, name]"` | `handle(id, name, event)` |
129
+ | `v-intercept:change="handle"` | `handle(event)` |
130
+ | `v-intercept:change="[handle, id]"` | `handle(id, event)` |
131
+
132
+ ## Advanced Usage
133
+
134
+ ### Permission-Guarded Buttons
135
+
136
+ ```vue
137
+ <el-button v-intercept="handleDelete" type="danger">Delete</el-button>
138
+ <el-button v-intercept="[handleApprove, 'order_001']" type="primary">Approve</el-button>
139
+ ```
140
+
141
+ ```typescript
142
+ const handleDelete = () => {
143
+ if (!userHasPermission('delete')) {
144
+ ElMessage.warning('No permission to delete')
145
+ return
146
+ }
147
+ deleteApi()
148
+ }
149
+ ```
150
+
151
+ ### Right-Click Menu Guard
152
+
153
+ ```vue
154
+ <div v-intercept:contextmenu="handleContextMenu" class="table-row">
155
+ Right-click this area
156
+ </div>
157
+ ```
158
+
159
+ ```typescript
160
+ const handleContextMenu = (event: MouseEvent) => {
161
+ event.preventDefault()
162
+ if (!hasRightClickPermission) return
163
+ showContextMenu(event.clientX, event.clientY)
164
+ }
165
+ ```
166
+
167
+ ### Select Change Guard
168
+
169
+ ```vue
170
+ <el-select v-intercept:change="[handleRoleChange, 'User Management']" placeholder="Select role">
171
+ <el-option label="Admin" value="admin" />
172
+ <el-option label="Editor" value="editor" />
173
+ <el-option label="Guest" value="guest" />
174
+ </el-select>
175
+ ```
176
+
177
+ ```typescript
178
+ const handleRoleChange = (section: string, event: Event) => {
179
+ if (!canChangeRole) {
180
+ ElMessage.warning(`Cannot change role for 「${section}」`)
181
+ return
182
+ }
183
+ updateRole()
184
+ }
185
+ ```
186
+
187
+ ### Toggle Switch Guard
188
+
189
+ ```vue
190
+ <el-switch v-intercept:change="[handleFeatureToggle, 'Export']" />
191
+ ```
192
+
193
+ ```typescript
194
+ const handleFeatureToggle = (feature: string, event: Event) => {
195
+ const checked = (event.target as HTMLInputElement).checked
196
+ if (checked && !canEnableFeature(feature)) {
197
+ ElMessage.warning(`Cannot enable 「${feature}」`)
198
+ return
199
+ }
200
+ toggleFeature(feature, checked)
201
+ }
202
+ ```
203
+
204
+ ### Double-Click Edit Guard
205
+
206
+ ```vue
207
+ <div v-intercept:dblclick="[handleDoubleClick, item.id]">
208
+ {{ item.name }}
209
+ </div>
210
+ ```
211
+
212
+ ```typescript
213
+ const handleDoubleClick = (id: number, event: MouseEvent) => {
214
+ if (!canEdit) return
215
+ enterEditMode(id)
216
+ }
217
+ ```
218
+
219
+ ### Drag Guard
220
+
221
+ ```vue
222
+ <div v-intercept:dragstart="[handleDragStart, file.name]" draggable="true">
223
+ {{ file.name }}
224
+ </div>
225
+ ```
226
+
227
+ ```typescript
228
+ const handleDragStart = (fileName: string, event: DragEvent) => {
229
+ if (!canDrag) {
230
+ event.preventDefault()
231
+ return
232
+ }
233
+ event.dataTransfer?.setData('text/plain', fileName)
234
+ }
235
+ ```
236
+
237
+ ## Build Artifacts
238
+
239
+ | File | Format | Description |
240
+ |------|--------|-------------|
241
+ | `dist/vue-intercept-plugin.cjs.js` | CJS | CommonJS for Node.js require |
242
+ | `dist/vue-intercept-plugin.esm.js` | ESM | ES Module, Tree-shakable |
243
+
244
+ ## Technical Details
245
+
246
+ ### Why Not Wrap Components?
247
+
248
+ When using third-party UI libraries like Element Plus, wrapping every permission-sensitive component (e.g., `PermissionButton`) leads to:
249
+ - High wrapping cost — requires handling extensive prop forwarding
250
+ - Poor coverage — hard to cover all UI components that need permission control (buttons, selects, switches, etc.)
251
+ - Duplicated effort — different UI libraries need separate wrappers
252
+
253
+ Vue custom directives operate directly on native DOM events — zero component wrapping, one line of code.
254
+
255
+ ### Vue 2 / Vue 3 Compatibility
256
+
257
+ The plugin auto-detects the Vue version via `app.version` and selects the appropriate directive hooks:
258
+
259
+ | Vue Version | Bind Hook | Update Hook | Unbind Hook |
260
+ |-------------|-----------|-------------|-------------|
261
+ | Vue 3 | `mounted` | `updated` | `unmounted` |
262
+ | Vue 2 | `bind` | `update` | `unbind` |
263
+
264
+ ### Memory Safety
265
+
266
+ - Old listeners are removed before re-binding to prevent duplicate handlers
267
+ - Listeners are automatically removed on component unmount, references are cleaned up
268
+ - Different event types use separate storage keys (`_intcpt_click` / `_intcpt_change`), keeping them isolated
269
+
270
+ ## Development
271
+
272
+ ```bash
273
+ # Install dependencies
274
+ npm install
275
+
276
+ # Build
277
+ npm run build
278
+
279
+ # Run tests
280
+ npm test
281
+ ```
282
+
283
+ ## Testing
284
+
285
+ Uses Vitest + jsdom with 28 test cases covering:
286
+
287
+ - `resolveHandler` — direct function passthrough, array argument forwarding with event auto-append, multiple arguments, invalid value warnings, null/undefined edge cases
288
+ - `bindHandler / unbindHandler` — click binding, change binding, auto-replacement on re-bind, unbind cleanup, event type isolation, contextmenu/dblclick special events
289
+ - Plugin install — Vue3/Vue2 directive registration, correct hook names, mounted event binding, custom event types, array arguments with event auto-append, unmounted cleanup, graceful handling of invalid values
290
+
291
+ ## License
292
+
293
+ MIT
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,27 @@
1
+ /**
2
+ * 解析指令值,生成实际的事件处理函数
3
+ * 支持两种格式:
4
+ * 1. 函数:v-intercept="handleDelete"
5
+ * 2. 数组:v-intercept="[handleDelete, 1001]"
6
+ */
7
+ declare function resolveHandler(value: unknown): EventListener | null;
8
+ /**
9
+ * 在元素上绑定事件
10
+ */
11
+ declare function bindHandler(el: HTMLElement, eventType: string, handler: EventListener): void;
12
+ /**
13
+ * 移除元素上的事件监听
14
+ */
15
+ declare function unbindHandler(el: HTMLElement, eventType: string): void;
16
+ /**
17
+ * Vue 事件拦截插件
18
+ *
19
+ * 提供 v-intercept 自定义指令,用于拦截事件。
20
+ * 默认拦截 click 事件,可通过指令参数指定其他事件类型。
21
+ * 开发者在使用时,在函数内部自行判断权限并决定是否放行。
22
+ */
23
+ declare const VueInterceptPlugin: {
24
+ install(app: any): void;
25
+ };
26
+ export default VueInterceptPlugin;
27
+ export { resolveHandler, bindHandler, unbindHandler };
@@ -0,0 +1,115 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ /**
6
+ * 用 WeakMap 存储元素 → 事件类型 → 监听函数的映射
7
+ * 避免在 DOM 元素上挂载自定义属性(el._intcpt_xxx)
8
+ */
9
+ const handlerMap = new WeakMap();
10
+ /**
11
+ * 获取元素上指定事件类型的监听函数
12
+ */
13
+ function getHandler(el, eventType) {
14
+ var _a;
15
+ return (_a = handlerMap.get(el)) === null || _a === void 0 ? void 0 : _a.get(eventType);
16
+ }
17
+ /**
18
+ * 设置元素上指定事件类型的监听函数
19
+ */
20
+ function setHandler(el, eventType, handler) {
21
+ let typeMap = handlerMap.get(el);
22
+ if (!typeMap) {
23
+ typeMap = new Map();
24
+ handlerMap.set(el, typeMap);
25
+ }
26
+ typeMap.set(eventType, handler);
27
+ }
28
+ /**
29
+ * 删除元素上指定事件类型的监听函数
30
+ */
31
+ function deleteHandler(el, eventType) {
32
+ var _a, _b;
33
+ (_a = handlerMap.get(el)) === null || _a === void 0 ? void 0 : _a.delete(eventType);
34
+ if (((_b = handlerMap.get(el)) === null || _b === void 0 ? void 0 : _b.size) === 0) {
35
+ handlerMap.delete(el);
36
+ }
37
+ }
38
+ /**
39
+ * 解析指令值,生成实际的事件处理函数
40
+ * 支持两种格式:
41
+ * 1. 函数:v-intercept="handleDelete"
42
+ * 2. 数组:v-intercept="[handleDelete, 1001]"
43
+ */
44
+ function resolveHandler(value) {
45
+ if (typeof value === 'function') {
46
+ return value;
47
+ }
48
+ if (Array.isArray(value)) {
49
+ const [fn, ...args] = value;
50
+ if (typeof fn !== 'function') {
51
+ console.warn('[vue-intercept-plugin] 数组第一项必须是一个函数');
52
+ return null;
53
+ }
54
+ return (event) => { fn(...args, event); };
55
+ }
56
+ console.warn('[vue-intercept-plugin] v-intercept 的值必须是函数或 [函数, ...参数] 数组');
57
+ return null;
58
+ }
59
+ /**
60
+ * 在元素上绑定事件
61
+ */
62
+ function bindHandler(el, eventType, handler) {
63
+ const oldHandler = getHandler(el, eventType);
64
+ if (oldHandler) {
65
+ el.removeEventListener(eventType, oldHandler);
66
+ }
67
+ el.addEventListener(eventType, handler);
68
+ setHandler(el, eventType, handler);
69
+ }
70
+ /**
71
+ * 移除元素上的事件监听
72
+ */
73
+ function unbindHandler(el, eventType) {
74
+ const handler = getHandler(el, eventType);
75
+ if (handler) {
76
+ el.removeEventListener(eventType, handler);
77
+ deleteHandler(el, eventType);
78
+ }
79
+ }
80
+ /**
81
+ * Vue 事件拦截插件
82
+ *
83
+ * 提供 v-intercept 自定义指令,用于拦截事件。
84
+ * 默认拦截 click 事件,可通过指令参数指定其他事件类型。
85
+ * 开发者在使用时,在函数内部自行判断权限并决定是否放行。
86
+ */
87
+ const VueInterceptPlugin = {
88
+ install(app) {
89
+ // SSR 安全:非浏览器环境直接跳过
90
+ if (typeof window === 'undefined') {
91
+ return;
92
+ }
93
+ const isVue3 = typeof app.version === 'string';
94
+ function bind(el, binding) {
95
+ const handler = resolveHandler(binding.value);
96
+ if (!handler)
97
+ return;
98
+ const eventType = binding.arg || 'click';
99
+ bindHandler(el, eventType, handler);
100
+ }
101
+ function unbind(el, binding) {
102
+ const eventType = binding.arg || 'click';
103
+ unbindHandler(el, eventType);
104
+ }
105
+ const directive = isVue3
106
+ ? { mounted: bind, updated: bind, unmounted: unbind }
107
+ : { bind, update: bind, unbind };
108
+ app.directive('intercept', directive);
109
+ },
110
+ };
111
+
112
+ exports.bindHandler = bindHandler;
113
+ exports.default = VueInterceptPlugin;
114
+ exports.resolveHandler = resolveHandler;
115
+ exports.unbindHandler = unbindHandler;
@@ -0,0 +1,108 @@
1
+ /**
2
+ * 用 WeakMap 存储元素 → 事件类型 → 监听函数的映射
3
+ * 避免在 DOM 元素上挂载自定义属性(el._intcpt_xxx)
4
+ */
5
+ const handlerMap = new WeakMap();
6
+ /**
7
+ * 获取元素上指定事件类型的监听函数
8
+ */
9
+ function getHandler(el, eventType) {
10
+ var _a;
11
+ return (_a = handlerMap.get(el)) === null || _a === void 0 ? void 0 : _a.get(eventType);
12
+ }
13
+ /**
14
+ * 设置元素上指定事件类型的监听函数
15
+ */
16
+ function setHandler(el, eventType, handler) {
17
+ let typeMap = handlerMap.get(el);
18
+ if (!typeMap) {
19
+ typeMap = new Map();
20
+ handlerMap.set(el, typeMap);
21
+ }
22
+ typeMap.set(eventType, handler);
23
+ }
24
+ /**
25
+ * 删除元素上指定事件类型的监听函数
26
+ */
27
+ function deleteHandler(el, eventType) {
28
+ var _a, _b;
29
+ (_a = handlerMap.get(el)) === null || _a === void 0 ? void 0 : _a.delete(eventType);
30
+ if (((_b = handlerMap.get(el)) === null || _b === void 0 ? void 0 : _b.size) === 0) {
31
+ handlerMap.delete(el);
32
+ }
33
+ }
34
+ /**
35
+ * 解析指令值,生成实际的事件处理函数
36
+ * 支持两种格式:
37
+ * 1. 函数:v-intercept="handleDelete"
38
+ * 2. 数组:v-intercept="[handleDelete, 1001]"
39
+ */
40
+ function resolveHandler(value) {
41
+ if (typeof value === 'function') {
42
+ return value;
43
+ }
44
+ if (Array.isArray(value)) {
45
+ const [fn, ...args] = value;
46
+ if (typeof fn !== 'function') {
47
+ console.warn('[vue-intercept-plugin] 数组第一项必须是一个函数');
48
+ return null;
49
+ }
50
+ return (event) => { fn(...args, event); };
51
+ }
52
+ console.warn('[vue-intercept-plugin] v-intercept 的值必须是函数或 [函数, ...参数] 数组');
53
+ return null;
54
+ }
55
+ /**
56
+ * 在元素上绑定事件
57
+ */
58
+ function bindHandler(el, eventType, handler) {
59
+ const oldHandler = getHandler(el, eventType);
60
+ if (oldHandler) {
61
+ el.removeEventListener(eventType, oldHandler);
62
+ }
63
+ el.addEventListener(eventType, handler);
64
+ setHandler(el, eventType, handler);
65
+ }
66
+ /**
67
+ * 移除元素上的事件监听
68
+ */
69
+ function unbindHandler(el, eventType) {
70
+ const handler = getHandler(el, eventType);
71
+ if (handler) {
72
+ el.removeEventListener(eventType, handler);
73
+ deleteHandler(el, eventType);
74
+ }
75
+ }
76
+ /**
77
+ * Vue 事件拦截插件
78
+ *
79
+ * 提供 v-intercept 自定义指令,用于拦截事件。
80
+ * 默认拦截 click 事件,可通过指令参数指定其他事件类型。
81
+ * 开发者在使用时,在函数内部自行判断权限并决定是否放行。
82
+ */
83
+ const VueInterceptPlugin = {
84
+ install(app) {
85
+ // SSR 安全:非浏览器环境直接跳过
86
+ if (typeof window === 'undefined') {
87
+ return;
88
+ }
89
+ const isVue3 = typeof app.version === 'string';
90
+ function bind(el, binding) {
91
+ const handler = resolveHandler(binding.value);
92
+ if (!handler)
93
+ return;
94
+ const eventType = binding.arg || 'click';
95
+ bindHandler(el, eventType, handler);
96
+ }
97
+ function unbind(el, binding) {
98
+ const eventType = binding.arg || 'click';
99
+ unbindHandler(el, eventType);
100
+ }
101
+ const directive = isVue3
102
+ ? { mounted: bind, updated: bind, unmounted: unbind }
103
+ : { bind, update: bind, unbind };
104
+ app.directive('intercept', directive);
105
+ },
106
+ };
107
+
108
+ export { bindHandler, VueInterceptPlugin as default, resolveHandler, unbindHandler };
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "vue-intercept-plugin",
3
+ "version": "1.0.0",
4
+ "description": "v-intercept 自定义指令,纯点击拦截,兼容 Vue 2 & Vue 3。",
5
+ "main": "dist/vue-intercept-plugin.cjs.js",
6
+ "module": "dist/vue-intercept-plugin.esm.js",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "scripts": {
11
+ "build": "rollup -c rollup.config.mjs",
12
+ "test": "vitest run",
13
+ "prepublishOnly": "npm run build"
14
+ },
15
+ "keywords": [
16
+ "vue",
17
+ "vue2",
18
+ "vue3",
19
+ "intercept",
20
+ "directive",
21
+ "plugin"
22
+ ],
23
+ "sideEffects": false,
24
+ "homepage": "https://github.com/wangkai000/vue-intercept-plugin",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/wangkai000/vue-intercept-plugin.git"
28
+ },
29
+ "author": "wangkai000",
30
+ "license": "MIT",
31
+ "devDependencies": {
32
+ "@rollup/plugin-commonjs": "^25.0.0",
33
+ "@rollup/plugin-node-resolve": "^15.0.0",
34
+ "@rollup/plugin-typescript": "^11.0.0",
35
+ "jsdom": "^29.1.1",
36
+ "rollup": "^4.0.0",
37
+ "tslib": "^2.6.0",
38
+ "typescript": "^5.3.0",
39
+ "vitest": "^4.1.6"
40
+ }
41
+ }