@veltra/directives 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.
@@ -0,0 +1,7 @@
1
+ import { ObjectDirective } from "vue";
2
+
3
+ //#region src/click-outside/index.d.ts
4
+ declare const vClickOutside: ObjectDirective<HTMLElement>;
5
+ //#endregion
6
+ export { vClickOutside };
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,72 @@
1
+ import { createIncrease } from "@veltra/utils";
2
+ //#region src/click-outside/index.ts
3
+ const uid = createIncrease(1e3);
4
+ const targets = /* @__PURE__ */ new Map();
5
+ let eventAdded = false;
6
+ let mousedownEvent;
7
+ const documentClickHandler = (event) => {
8
+ if (!mousedownEvent) return;
9
+ if (mousedownEvent?.target !== event.target) {
10
+ mousedownEvent = void 0;
11
+ return;
12
+ }
13
+ targets.forEach(({ el, handler }) => {
14
+ if (el.contains(event.target)) return;
15
+ handler(event);
16
+ });
17
+ mousedownEvent = void 0;
18
+ };
19
+ function documentMousedownHandler(event) {
20
+ mousedownEvent = event;
21
+ }
22
+ function addEvent() {
23
+ if (eventAdded) return;
24
+ document.addEventListener("mousedown", documentMousedownHandler, true);
25
+ document.addEventListener("click", documentClickHandler, true);
26
+ eventAdded = true;
27
+ }
28
+ function removeEvent() {
29
+ if (!eventAdded) return;
30
+ document.removeEventListener("click", documentClickHandler, true);
31
+ document.removeEventListener("mousedown", documentMousedownHandler, true);
32
+ eventAdded = false;
33
+ }
34
+ function syncDocumentEvents() {
35
+ if (typeof document === "undefined") return;
36
+ if (targets.size > 0) addEvent();
37
+ else removeEvent();
38
+ }
39
+ const vClickOutside = {
40
+ mounted(el, binding) {
41
+ if (!binding.value) return;
42
+ const id = String(uid());
43
+ el.dataset.outsideId = id;
44
+ targets.set(id, {
45
+ handler: binding.value,
46
+ el
47
+ });
48
+ syncDocumentEvents();
49
+ },
50
+ updated(el, binding) {
51
+ if (!binding.value) {
52
+ if (!el.dataset.outsideId) return;
53
+ targets.delete(el.dataset.outsideId);
54
+ syncDocumentEvents();
55
+ } else {
56
+ if (!el.dataset.outsideId) el.dataset.outsideId = String(uid());
57
+ targets.set(el.dataset.outsideId, {
58
+ handler: binding.value,
59
+ el
60
+ });
61
+ syncDocumentEvents();
62
+ }
63
+ },
64
+ unmounted(el) {
65
+ targets.delete(el.dataset.outsideId);
66
+ syncDocumentEvents();
67
+ }
68
+ };
69
+ //#endregion
70
+ export { vClickOutside };
71
+
72
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/click-outside/index.ts"],"sourcesContent":["import { createIncrease } from '@veltra/utils'\nimport type { ObjectDirective } from 'vue'\n\nconst uid = createIncrease(1000)\nconst targets = new Map<string, { handler: (e: MouseEvent) => void; el: HTMLElement }>()\n\nlet eventAdded = false\nlet mousedownEvent: MouseEvent | undefined\n\nconst documentClickHandler = (event: MouseEvent) => {\n if (!mousedownEvent) return\n if (mousedownEvent?.target !== event.target) {\n return (mousedownEvent = undefined)\n }\n targets.forEach(({ el, handler }) => {\n if (el.contains(event.target as Node)) return\n handler(event)\n })\n mousedownEvent = undefined\n}\n\nfunction documentMousedownHandler(event: MouseEvent) {\n mousedownEvent = event\n}\n\nfunction addEvent() {\n if (eventAdded) return\n document.addEventListener('mousedown', documentMousedownHandler, true)\n document.addEventListener('click', documentClickHandler, true)\n eventAdded = true\n}\n\nfunction removeEvent() {\n if (!eventAdded) return\n document.removeEventListener('click', documentClickHandler, true)\n document.removeEventListener('mousedown', documentMousedownHandler, true)\n eventAdded = false\n}\n\nfunction syncDocumentEvents(): void {\n if (typeof document === 'undefined') return\n\n if (targets.size > 0) {\n addEvent()\n } else {\n removeEvent()\n }\n}\n\nexport const vClickOutside: ObjectDirective<HTMLElement> = {\n mounted(el, binding) {\n if (!binding.value) return\n\n // 为元素添加一个id, 并加入到字典中\n const id = String(uid())\n el.dataset.outsideId = id\n\n targets.set(id, { handler: binding.value, el })\n syncDocumentEvents()\n },\n\n updated(el, binding) {\n if (!binding.value) {\n if (!el.dataset.outsideId) return\n targets.delete(el.dataset.outsideId)\n syncDocumentEvents()\n } else {\n if (!el.dataset.outsideId) {\n el.dataset.outsideId = String(uid())\n }\n\n targets.set(el.dataset.outsideId!, { handler: binding.value, el })\n syncDocumentEvents()\n }\n },\n\n unmounted(el: HTMLElement) {\n targets.delete(el.dataset.outsideId!)\n syncDocumentEvents()\n }\n}\n"],"mappings":";;AAGA,MAAM,MAAM,eAAe,IAAK;AAChC,MAAM,0BAAU,IAAI,KAAoE;AAExF,IAAI,aAAa;AACjB,IAAI;AAEJ,MAAM,wBAAwB,UAAsB;AAClD,KAAI,CAAC,eAAgB;AACrB,KAAI,gBAAgB,WAAW,MAAM,QAAQ;AACnC,mBAAiB,KAAA;AAAzB;;AAEF,SAAQ,SAAS,EAAE,IAAI,cAAc;AACnC,MAAI,GAAG,SAAS,MAAM,OAAe,CAAE;AACvC,UAAQ,MAAM;GACd;AACF,kBAAiB,KAAA;;AAGnB,SAAS,yBAAyB,OAAmB;AACnD,kBAAiB;;AAGnB,SAAS,WAAW;AAClB,KAAI,WAAY;AAChB,UAAS,iBAAiB,aAAa,0BAA0B,KAAK;AACtE,UAAS,iBAAiB,SAAS,sBAAsB,KAAK;AAC9D,cAAa;;AAGf,SAAS,cAAc;AACrB,KAAI,CAAC,WAAY;AACjB,UAAS,oBAAoB,SAAS,sBAAsB,KAAK;AACjE,UAAS,oBAAoB,aAAa,0BAA0B,KAAK;AACzE,cAAa;;AAGf,SAAS,qBAA2B;AAClC,KAAI,OAAO,aAAa,YAAa;AAErC,KAAI,QAAQ,OAAO,EACjB,WAAU;KAEV,cAAa;;AAIjB,MAAa,gBAA8C;CACzD,QAAQ,IAAI,SAAS;AACnB,MAAI,CAAC,QAAQ,MAAO;EAGpB,MAAM,KAAK,OAAO,KAAK,CAAC;AACxB,KAAG,QAAQ,YAAY;AAEvB,UAAQ,IAAI,IAAI;GAAE,SAAS,QAAQ;GAAO;GAAI,CAAC;AAC/C,sBAAoB;;CAGtB,QAAQ,IAAI,SAAS;AACnB,MAAI,CAAC,QAAQ,OAAO;AAClB,OAAI,CAAC,GAAG,QAAQ,UAAW;AAC3B,WAAQ,OAAO,GAAG,QAAQ,UAAU;AACpC,uBAAoB;SACf;AACL,OAAI,CAAC,GAAG,QAAQ,UACd,IAAG,QAAQ,YAAY,OAAO,KAAK,CAAC;AAGtC,WAAQ,IAAI,GAAG,QAAQ,WAAY;IAAE,SAAS,QAAQ;IAAO;IAAI,CAAC;AAClE,uBAAoB;;;CAIxB,UAAU,IAAiB;AACzB,UAAQ,OAAO,GAAG,QAAQ,UAAW;AACrC,sBAAoB;;CAEvB"}
@@ -0,0 +1,7 @@
1
+ import { ObjectDirective } from "vue";
2
+
3
+ //#region src/focus/index.d.ts
4
+ declare const vFocus: ObjectDirective<HTMLElement>;
5
+ //#endregion
6
+ export { vFocus };
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,13 @@
1
+ //#region src/focus/index.ts
2
+ const vFocus = { mounted: (el) => {
3
+ if (el.tagName === "INPUT") el.focus();
4
+ else {
5
+ const input = el.querySelector("input");
6
+ if (input) input.focus();
7
+ else console.warn("v-focus 指令需要一个 input 元素");
8
+ }
9
+ } };
10
+ //#endregion
11
+ export { vFocus };
12
+
13
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/focus/index.ts"],"sourcesContent":["import type { ObjectDirective } from 'vue'\n\nexport const vFocus: ObjectDirective<HTMLElement> = {\n mounted: (el) => {\n if (el.tagName === 'INPUT') {\n el.focus()\n } else {\n const input = el.querySelector('input')\n if (input) {\n input.focus()\n } else {\n console.warn('v-focus 指令需要一个 input 元素')\n }\n }\n }\n}\n"],"mappings":";AAEA,MAAa,SAAuC,EAClD,UAAU,OAAO;AACf,KAAI,GAAG,YAAY,QACjB,IAAG,OAAO;MACL;EACL,MAAM,QAAQ,GAAG,cAAc,QAAQ;AACvC,MAAI,MACF,OAAM,OAAO;MAEb,SAAQ,KAAK,0BAA0B;;GAI9C"}
@@ -0,0 +1,5 @@
1
+ import { vFocus } from "./focus/index.js";
2
+ import { vClickOutside } from "./click-outside/index.js";
3
+ import { Ripple } from "./ripple/ripple.js";
4
+ import { vRipple } from "./ripple/index.js";
5
+ export { Ripple, vClickOutside, vFocus, vRipple };
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ import { vFocus } from "./focus/index.js";
2
+ import { vClickOutside } from "./click-outside/index.js";
3
+ import { Ripple } from "./ripple/ripple.js";
4
+ import { vRipple } from "./ripple/index.js";
5
+ export { Ripple, vClickOutside, vFocus, vRipple };
@@ -0,0 +1,8 @@
1
+ import { Ripple } from "./ripple.js";
2
+ import { ObjectDirective } from "vue";
3
+
4
+ //#region src/ripple/index.d.ts
5
+ declare const vRipple: ObjectDirective<HTMLElement>;
6
+ //#endregion
7
+ export { vRipple };
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,66 @@
1
+ import { Ripple } from "./ripple.js";
2
+ //#region src/ripple/index.ts
3
+ const rippleMap = /* @__PURE__ */ new WeakMap();
4
+ function handleMousedown(e) {
5
+ if (e.button !== 0) return;
6
+ const container = e.currentTarget;
7
+ const mouseupHandler = () => {
8
+ rippleMap.get(container)?.remove();
9
+ document.removeEventListener("mouseup", mouseupHandler);
10
+ };
11
+ document.addEventListener("mouseup", mouseupHandler);
12
+ let ripple = rippleMap.get(container);
13
+ if (!ripple) {
14
+ const { rippleClass, duration } = container.dataset;
15
+ ripple = new Ripple(container, {
16
+ rippleClass,
17
+ duration: duration ? Number(duration) : void 0
18
+ });
19
+ rippleMap.set(container, ripple);
20
+ }
21
+ ripple.showByEvent(e);
22
+ }
23
+ function handleMouseleave(e) {
24
+ rippleMap.get(e.currentTarget)?.remove();
25
+ }
26
+ /**
27
+ * 注册按下事件
28
+ * @param el
29
+ * @param binding
30
+ * @todo 如有必要将来可添加触摸事件
31
+ */
32
+ const registerEvents = (el, binding) => {
33
+ if (binding.value === false) return;
34
+ el.addEventListener("mousedown", handleMousedown);
35
+ el.addEventListener("mouseleave", handleMouseleave);
36
+ };
37
+ /**
38
+ * 注销按下事件
39
+ * @param el 元素
40
+ */
41
+ const unregisterEvents = (el) => {
42
+ el.removeEventListener("mousedown", handleMousedown);
43
+ el.removeEventListener("mouseleave", handleMouseleave);
44
+ };
45
+ const vRipple = {
46
+ mounted: (el, binding) => {
47
+ if (binding.value !== false) el.dataset.rippleClass = binding.value;
48
+ if (binding.arg) el.dataset.duration = binding.arg;
49
+ registerEvents(el, binding);
50
+ },
51
+ unmounted: (el) => {
52
+ unregisterEvents(el);
53
+ rippleMap.get(el)?.remove();
54
+ rippleMap.delete(el);
55
+ },
56
+ updated(el, binding) {
57
+ const registered = binding.oldValue !== false;
58
+ if (binding.value !== false) {
59
+ if (!registered) registerEvents(el, binding);
60
+ } else if (registered) unregisterEvents(el);
61
+ }
62
+ };
63
+ //#endregion
64
+ export { vRipple };
65
+
66
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/ripple/index.ts"],"sourcesContent":["import type { DirectiveBinding, ObjectDirective } from 'vue'\n\nimport { Ripple } from './ripple'\n\nconst rippleMap = new WeakMap<HTMLElement, Ripple>()\n\nfunction handleMousedown(e: MouseEvent) {\n if (e.button !== 0) return\n const container = e.currentTarget as HTMLElement\n\n const mouseupHandler = () => {\n rippleMap.get(container)?.remove()\n document.removeEventListener('mouseup', mouseupHandler)\n }\n\n document.addEventListener('mouseup', mouseupHandler)\n\n let ripple = rippleMap.get(container)\n if (!ripple) {\n const { rippleClass, duration } = container.dataset\n ripple = new Ripple(container, {\n rippleClass,\n duration: duration ? Number(duration) : undefined\n })\n rippleMap.set(container, ripple)\n }\n\n ripple.showByEvent(e)\n}\n\nfunction handleMouseleave(e: MouseEvent) {\n rippleMap.get(e.currentTarget as HTMLElement)?.remove()\n}\n\n/**\n * 注册按下事件\n * @param el\n * @param binding\n * @todo 如有必要将来可添加触摸事件\n */\nconst registerEvents = (el: HTMLElement, binding: DirectiveBinding<any>) => {\n // 如果指令绑定的值为false则不应用该事件.\n if (binding.value === false) return\n\n el.addEventListener('mousedown', handleMousedown)\n el.addEventListener('mouseleave', handleMouseleave)\n}\n\n/**\n * 注销按下事件\n * @param el 元素\n */\nconst unregisterEvents = (el: HTMLElement) => {\n el.removeEventListener('mousedown', handleMousedown)\n el.removeEventListener('mouseleave', handleMouseleave)\n}\n\nexport { Ripple }\n\nexport const vRipple: ObjectDirective<HTMLElement> = {\n mounted: (el, binding) => {\n if (binding.value !== false) {\n el.dataset.rippleClass = binding.value\n }\n\n if (binding.arg) {\n el.dataset.duration = binding.arg\n }\n\n registerEvents(el, binding)\n },\n\n unmounted: (el) => {\n unregisterEvents(el)\n rippleMap.get(el)?.remove()\n rippleMap.delete(el)\n },\n\n updated(el, binding) {\n const registered = binding.oldValue !== false\n\n if (binding.value !== false) {\n if (!registered) {\n registerEvents(el, binding)\n }\n } else {\n if (registered) {\n unregisterEvents(el)\n }\n }\n }\n}\n"],"mappings":";;AAIA,MAAM,4BAAY,IAAI,SAA8B;AAEpD,SAAS,gBAAgB,GAAe;AACtC,KAAI,EAAE,WAAW,EAAG;CACpB,MAAM,YAAY,EAAE;CAEpB,MAAM,uBAAuB;AAC3B,YAAU,IAAI,UAAU,EAAE,QAAQ;AAClC,WAAS,oBAAoB,WAAW,eAAe;;AAGzD,UAAS,iBAAiB,WAAW,eAAe;CAEpD,IAAI,SAAS,UAAU,IAAI,UAAU;AACrC,KAAI,CAAC,QAAQ;EACX,MAAM,EAAE,aAAa,aAAa,UAAU;AAC5C,WAAS,IAAI,OAAO,WAAW;GAC7B;GACA,UAAU,WAAW,OAAO,SAAS,GAAG,KAAA;GACzC,CAAC;AACF,YAAU,IAAI,WAAW,OAAO;;AAGlC,QAAO,YAAY,EAAE;;AAGvB,SAAS,iBAAiB,GAAe;AACvC,WAAU,IAAI,EAAE,cAA6B,EAAE,QAAQ;;;;;;;;AASzD,MAAM,kBAAkB,IAAiB,YAAmC;AAE1E,KAAI,QAAQ,UAAU,MAAO;AAE7B,IAAG,iBAAiB,aAAa,gBAAgB;AACjD,IAAG,iBAAiB,cAAc,iBAAiB;;;;;;AAOrD,MAAM,oBAAoB,OAAoB;AAC5C,IAAG,oBAAoB,aAAa,gBAAgB;AACpD,IAAG,oBAAoB,cAAc,iBAAiB;;AAKxD,MAAa,UAAwC;CACnD,UAAU,IAAI,YAAY;AACxB,MAAI,QAAQ,UAAU,MACpB,IAAG,QAAQ,cAAc,QAAQ;AAGnC,MAAI,QAAQ,IACV,IAAG,QAAQ,WAAW,QAAQ;AAGhC,iBAAe,IAAI,QAAQ;;CAG7B,YAAY,OAAO;AACjB,mBAAiB,GAAG;AACpB,YAAU,IAAI,GAAG,EAAE,QAAQ;AAC3B,YAAU,OAAO,GAAG;;CAGtB,QAAQ,IAAI,SAAS;EACnB,MAAM,aAAa,QAAQ,aAAa;AAExC,MAAI,QAAQ,UAAU;OAChB,CAAC,WACH,gBAAe,IAAI,QAAQ;aAGzB,WACF,kBAAiB,GAAG;;CAI3B"}
@@ -0,0 +1,140 @@
1
+ import { BEM } from "@veltra/utils";
2
+
3
+ //#region src/ripple/ripple.d.ts
4
+ /** 波纹位置接口 */
5
+ interface RipplePosition {
6
+ /** X 坐标 */
7
+ x: number;
8
+ /** Y 坐标 */
9
+ y: number;
10
+ }
11
+ /** 波纹配置接口 */
12
+ interface RippleConfig {
13
+ /** 波纹类 */
14
+ rippleClass?: string;
15
+ /** 波纹动画时长 */
16
+ duration?: number;
17
+ /** 是否自动移除 */
18
+ autoRemove?: boolean;
19
+ }
20
+ /**
21
+ * 波纹效果类
22
+ * 用于在指定容器中创建和管理波纹动画效果
23
+ * 支持鼠标点击和触摸事件触发的波纹动画
24
+ */
25
+ declare class Ripple {
26
+ /** 波纹样式类名生成器 */
27
+ static cls: BEM<'ripple'>;
28
+ /** 波纹容器元素 */
29
+ private container;
30
+ /** 容器矩形区域缓存 */
31
+ private containerRect?;
32
+ /** 容器计算样式缓存 */
33
+ private containerComputedStyle?;
34
+ /** 容器原始样式备份 */
35
+ private containerStyle?;
36
+ /** 当前活跃的波纹元素 */
37
+ private currentRippleEl?;
38
+ /** 波纹配置 */
39
+ private config?;
40
+ /** 波纹元素数量(私有) */
41
+ private _amount;
42
+ /**
43
+ * 设置波纹元素数量
44
+ * 当数量为 0 时自动重置容器样式
45
+ */
46
+ private set amount(value);
47
+ /** 获取当前波纹元素数量 */
48
+ private get amount();
49
+ /**
50
+ * 构造函数
51
+ * @param container 波纹容器元素
52
+ * @param config 波纹配置选项
53
+ */
54
+ constructor(container: HTMLElement, config?: RippleConfig);
55
+ /**
56
+ * 获取波纹容器元素
57
+ * @returns 容器元素
58
+ */
59
+ getContainer(): HTMLElement;
60
+ /**
61
+ * 获取容器矩形区域
62
+ * 使用缓存机制避免重复计算
63
+ * @returns 容器矩形区域
64
+ */
65
+ private getContainerRect;
66
+ /**
67
+ * 获取波纹圆心位置
68
+ * 根据鼠标或触摸事件计算相对于容器的坐标
69
+ * @param e 鼠标或触摸事件
70
+ * @returns 波纹圆心位置
71
+ */
72
+ private getRippleCenterPosition;
73
+ /**
74
+ * 标记元素过渡动画结束
75
+ * @param el 目标元素
76
+ */
77
+ private markTransitionEnd;
78
+ /**
79
+ * 标记元素为可移除状态
80
+ * @param el 目标元素
81
+ */
82
+ private markRemovable;
83
+ /**
84
+ * 计算波纹半径
85
+ * 根据点击位置计算能覆盖整个容器的最小半径
86
+ * @param centerPosition 波纹圆心位置
87
+ * @returns 波纹半径
88
+ */
89
+ private calcRippleRadius;
90
+ /**
91
+ * 设置容器样式
92
+ * 确保容器具有正确的定位和溢出隐藏属性以支持波纹效果
93
+ */
94
+ private setContainerStyle;
95
+ /**
96
+ * 重置容器样式
97
+ * 恢复容器的原始样式设置
98
+ */
99
+ private resetContainerStyle;
100
+ /**
101
+ * 创建波纹元素
102
+ * 生成波纹DOM元素并设置样式和动画
103
+ * @param centerPosition 波纹圆心位置
104
+ */
105
+ private createRipple;
106
+ /**
107
+ * 移除波纹元素
108
+ * 检查元素状态,符合条件时执行淡出动画并移除DOM
109
+ * @param rippleEl 要移除的波纹元素
110
+ */
111
+ private removeRippleEl;
112
+ /**
113
+ * 显示波纹
114
+ * 根据指定位置创建波纹效果
115
+ * @param centerPosition 波纹圆心位置
116
+ */
117
+ show(centerPosition: RipplePosition): void;
118
+ /**
119
+ * 根据事件对象显示波纹
120
+ * 从鼠标或触摸事件中提取位置信息并创建波纹
121
+ * @param e 鼠标或触摸事件对象
122
+ */
123
+ showByEvent(e: MouseEvent | TouchEvent): void;
124
+ /**
125
+ * 移除波纹
126
+ * 手动移除当前活跃的波纹元素
127
+ * 注意:如果已经在Ripple配置中增加了autoRemove属性,则不需要调用此方法
128
+ */
129
+ remove(): void;
130
+ /**
131
+ * 重置容器矩形
132
+ * 当容器大小发生变化时,需要重置容器矩形缓存
133
+ * 否则计算的波纹半径不准确
134
+ * 注意:大部分情况下不需要调用此方法
135
+ */
136
+ resetContainerRect(): void;
137
+ }
138
+ //#endregion
139
+ export { Ripple };
140
+ //# sourceMappingURL=ripple.d.ts.map
@@ -0,0 +1,237 @@
1
+ import { bem, nextFrame, removeStyles, setStyles } from "@veltra/utils";
2
+ //#region src/ripple/ripple.ts
3
+ function pick(obj, keys) {
4
+ const out = {};
5
+ for (const k of keys) if (k in obj) out[k] = obj[k];
6
+ return out;
7
+ }
8
+ /**
9
+ * 波纹效果类
10
+ * 用于在指定容器中创建和管理波纹动画效果
11
+ * 支持鼠标点击和触摸事件触发的波纹动画
12
+ */
13
+ var Ripple = class Ripple {
14
+ /** 波纹样式类名生成器 */
15
+ static cls = bem("ripple");
16
+ /** 波纹容器元素 */
17
+ container;
18
+ /** 容器矩形区域缓存 */
19
+ containerRect;
20
+ /** 容器计算样式缓存 */
21
+ containerComputedStyle;
22
+ /** 容器原始样式备份 */
23
+ containerStyle;
24
+ /** 当前活跃的波纹元素 */
25
+ currentRippleEl;
26
+ /** 波纹配置 */
27
+ config;
28
+ /** 波纹元素数量(私有) */
29
+ _amount = 0;
30
+ /**
31
+ * 设置波纹元素数量
32
+ * 当数量为 0 时自动重置容器样式
33
+ */
34
+ set amount(amount) {
35
+ this._amount = amount;
36
+ amount === 0 && this.resetContainerStyle();
37
+ }
38
+ /** 获取当前波纹元素数量 */
39
+ get amount() {
40
+ return this._amount;
41
+ }
42
+ /**
43
+ * 构造函数
44
+ * @param container 波纹容器元素
45
+ * @param config 波纹配置选项
46
+ */
47
+ constructor(container, config) {
48
+ this.container = container;
49
+ if (config) this.config = config;
50
+ }
51
+ /**
52
+ * 获取波纹容器元素
53
+ * @returns 容器元素
54
+ */
55
+ getContainer() {
56
+ return this.container;
57
+ }
58
+ /**
59
+ * 获取容器矩形区域
60
+ * 使用缓存机制避免重复计算
61
+ * @returns 容器矩形区域
62
+ */
63
+ getContainerRect() {
64
+ this.containerRect = this.container.getBoundingClientRect();
65
+ return this.containerRect;
66
+ }
67
+ /**
68
+ * 获取波纹圆心位置
69
+ * 根据鼠标或触摸事件计算相对于容器的坐标
70
+ * @param e 鼠标或触摸事件
71
+ * @returns 波纹圆心位置
72
+ */
73
+ getRippleCenterPosition(e) {
74
+ const { left, top } = this.getContainerRect();
75
+ const { clientX, clientY } = e;
76
+ return {
77
+ x: clientX - left,
78
+ y: clientY - top
79
+ };
80
+ }
81
+ /**
82
+ * 标记元素过渡动画结束
83
+ * @param el 目标元素
84
+ */
85
+ markTransitionEnd(el) {
86
+ el.dataset.transitionend = "true";
87
+ }
88
+ /**
89
+ * 标记元素为可移除状态
90
+ * @param el 目标元素
91
+ */
92
+ markRemovable(el) {
93
+ el.dataset.removable = "true";
94
+ }
95
+ /**
96
+ * 计算波纹半径
97
+ * 根据点击位置计算能覆盖整个容器的最小半径
98
+ * @param centerPosition 波纹圆心位置
99
+ * @returns 波纹半径
100
+ */
101
+ calcRippleRadius(centerPosition) {
102
+ const { x, y } = centerPosition;
103
+ const { width, height } = this.getContainerRect();
104
+ const edgeA = Math.max(x, width - x);
105
+ const edgeB = Math.max(y, height - y);
106
+ return Math.ceil(Math.sqrt(edgeA ** 2 + edgeB ** 2));
107
+ }
108
+ /**
109
+ * 设置容器样式
110
+ * 确保容器具有正确的定位和溢出隐藏属性以支持波纹效果
111
+ */
112
+ setContainerStyle() {
113
+ const { container } = this;
114
+ if (this.amount === 0 && !this.containerStyle) this.containerStyle = {
115
+ position: container.style.position,
116
+ overflow: container.style.overflow
117
+ };
118
+ if (!this.containerComputedStyle) this.containerComputedStyle = pick(window.getComputedStyle(container), ["overflow", "position"]);
119
+ const { position, overflow } = this.containerComputedStyle;
120
+ const style = {};
121
+ if (position === "static") style.position = "relative";
122
+ if (overflow !== "hidden") style.overflow = "hidden";
123
+ setStyles(container, style);
124
+ }
125
+ /**
126
+ * 重置容器样式
127
+ * 恢复容器的原始样式设置
128
+ */
129
+ resetContainerStyle() {
130
+ const { container, containerStyle } = this;
131
+ if (!containerStyle) return removeStyles(container, ["overflow", "position"]);
132
+ const attrsToRemoved = [];
133
+ if (!containerStyle.overflow) attrsToRemoved.push("overflow");
134
+ if (!containerStyle.position) attrsToRemoved.push("position");
135
+ removeStyles(container, attrsToRemoved);
136
+ if (!attrsToRemoved.length) setStyles(container, containerStyle);
137
+ }
138
+ /**
139
+ * 创建波纹元素
140
+ * 生成波纹DOM元素并设置样式和动画
141
+ * @param centerPosition 波纹圆心位置
142
+ */
143
+ createRipple(centerPosition) {
144
+ const { config } = this;
145
+ this.setContainerStyle();
146
+ const rippleEl = document.createElement("span");
147
+ this.currentRippleEl = rippleEl;
148
+ rippleEl.classList.add(Ripple.cls.b);
149
+ config?.rippleClass && rippleEl.classList.add(config.rippleClass);
150
+ const radius = this.calcRippleRadius(centerPosition);
151
+ const diameter = radius * 2;
152
+ const rippleStyle = {
153
+ width: `${diameter}px`,
154
+ height: `${diameter}px`,
155
+ left: `${centerPosition.x - radius}px`,
156
+ top: `${centerPosition.y - radius}px`
157
+ };
158
+ if (config?.duration) rippleStyle.transitionDuration = `${config.duration}ms`;
159
+ setStyles(rippleEl, rippleStyle);
160
+ const transitionEndHandler = (e) => {
161
+ if (e.propertyName !== "transform") return;
162
+ rippleEl.removeEventListener("transitionend", transitionEndHandler);
163
+ this.markTransitionEnd(rippleEl);
164
+ this.removeRippleEl(rippleEl);
165
+ };
166
+ rippleEl.addEventListener("transitionend", transitionEndHandler);
167
+ this.container.appendChild(rippleEl);
168
+ this.amount++;
169
+ if (config?.autoRemove) this.markRemovable(rippleEl);
170
+ nextFrame(() => {
171
+ setStyles(rippleEl, { transform: "scale3d(1, 1, 1)" });
172
+ });
173
+ }
174
+ /**
175
+ * 移除波纹元素
176
+ * 检查元素状态,符合条件时执行淡出动画并移除DOM
177
+ * @param rippleEl 要移除的波纹元素
178
+ */
179
+ removeRippleEl(rippleEl) {
180
+ const { transitionend, removable } = rippleEl.dataset;
181
+ if (transitionend !== "true" || removable !== "true") return;
182
+ const transitionEndOrCancelHandler = (e) => {
183
+ if (e.propertyName !== "opacity") return;
184
+ rippleEl.removeEventListener("transitionend", transitionEndOrCancelHandler);
185
+ rippleEl.removeEventListener("transitioncancel", transitionEndOrCancelHandler);
186
+ rippleEl.remove();
187
+ this.amount--;
188
+ };
189
+ rippleEl.addEventListener("transitionend", transitionEndOrCancelHandler);
190
+ rippleEl.addEventListener("transitioncancel", transitionEndOrCancelHandler);
191
+ rippleEl.classList.add(bem.is("removing"));
192
+ }
193
+ /**
194
+ * 显示波纹
195
+ * 根据指定位置创建波纹效果
196
+ * @param centerPosition 波纹圆心位置
197
+ */
198
+ show(centerPosition) {
199
+ this.createRipple(centerPosition);
200
+ }
201
+ /**
202
+ * 根据事件对象显示波纹
203
+ * 从鼠标或触摸事件中提取位置信息并创建波纹
204
+ * @param e 鼠标或触摸事件对象
205
+ */
206
+ showByEvent(e) {
207
+ const centerPosition = this.getRippleCenterPosition(e instanceof MouseEvent ? e : e.touches[0]);
208
+ this.createRipple(centerPosition);
209
+ }
210
+ /**
211
+ * 移除波纹
212
+ * 手动移除当前活跃的波纹元素
213
+ * 注意:如果已经在Ripple配置中增加了autoRemove属性,则不需要调用此方法
214
+ */
215
+ remove() {
216
+ let el = this.currentRippleEl;
217
+ if (el) {
218
+ this.markRemovable(el);
219
+ this.removeRippleEl(el);
220
+ el = void 0;
221
+ this.currentRippleEl = void 0;
222
+ }
223
+ }
224
+ /**
225
+ * 重置容器矩形
226
+ * 当容器大小发生变化时,需要重置容器矩形缓存
227
+ * 否则计算的波纹半径不准确
228
+ * 注意:大部分情况下不需要调用此方法
229
+ */
230
+ resetContainerRect() {
231
+ this.containerRect = void 0;
232
+ }
233
+ };
234
+ //#endregion
235
+ export { Ripple };
236
+
237
+ //# sourceMappingURL=ripple.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ripple.js","names":[],"sources":["../../src/ripple/ripple.ts"],"sourcesContent":["import { bem, nextFrame, removeStyles, setStyles, type BEM } from '@veltra/utils'\nimport type { CSSProperties } from 'vue'\n\nfunction pick<T extends object, K extends keyof T>(obj: T, keys: readonly K[]): Pick<T, K> {\n const out: Partial<Pick<T, K>> = {}\n for (const k of keys) {\n if (k in obj) out[k] = obj[k]\n }\n return out as Pick<T, K>\n}\n\n/** 鼠标或触摸事件类型 */\ntype MouseOrTouchEvent = MouseEvent | Touch\n\n/** 波纹位置接口 */\ninterface RipplePosition {\n /** X 坐标 */\n x: number\n /** Y 坐标 */\n y: number\n}\n\n/** 波纹配置接口 */\ninterface RippleConfig {\n /** 波纹类 */\n rippleClass?: string\n /** 波纹动画时长 */\n duration?: number\n /** 是否自动移除 */\n autoRemove?: boolean\n}\n\n/** 容器样式接口 */\ninterface ContainerStyle {\n /** 定位方式 */\n position: string\n /** 溢出处理 */\n overflow: string\n}\n\n/**\n * 波纹效果类\n * 用于在指定容器中创建和管理波纹动画效果\n * 支持鼠标点击和触摸事件触发的波纹动画\n */\nexport class Ripple {\n /** 波纹样式类名生成器 */\n static cls: BEM<'ripple'> = bem('ripple')\n\n /** 波纹容器元素 */\n private container: HTMLElement\n /** 容器矩形区域缓存 */\n private containerRect?: DOMRect\n /** 容器计算样式缓存 */\n private containerComputedStyle?: ContainerStyle\n /** 容器原始样式备份 */\n private containerStyle?: ContainerStyle\n\n /** 当前活跃的波纹元素 */\n private currentRippleEl?: HTMLElement\n\n /** 波纹配置 */\n private config?: RippleConfig\n\n /** 波纹元素数量(私有) */\n private _amount = 0\n\n /**\n * 设置波纹元素数量\n * 当数量为 0 时自动重置容器样式\n */\n private set amount(amount: number) {\n this._amount = amount\n amount === 0 && this.resetContainerStyle()\n }\n\n /** 获取当前波纹元素数量 */\n private get amount(): number {\n return this._amount\n }\n\n /**\n * 构造函数\n * @param container 波纹容器元素\n * @param config 波纹配置选项\n */\n constructor(container: HTMLElement, config?: RippleConfig) {\n this.container = container\n if (config) {\n this.config = config\n }\n }\n\n /**\n * 获取波纹容器元素\n * @returns 容器元素\n */\n getContainer(): HTMLElement {\n return this.container\n }\n\n /**\n * 获取容器矩形区域\n * 使用缓存机制避免重复计算\n * @returns 容器矩形区域\n */\n private getContainerRect(): DOMRect {\n // if (this.containerRect) return this.containerRect\n const rect = this.container.getBoundingClientRect()\n this.containerRect = rect\n return this.containerRect\n }\n\n /**\n * 获取波纹圆心位置\n * 根据鼠标或触摸事件计算相对于容器的坐标\n * @param e 鼠标或触摸事件\n * @returns 波纹圆心位置\n */\n private getRippleCenterPosition(e: MouseOrTouchEvent): { x: number; y: number } {\n const { left, top } = this.getContainerRect()\n const { clientX, clientY } = e\n return { x: clientX - left, y: clientY - top }\n }\n\n /**\n * 标记元素过渡动画结束\n * @param el 目标元素\n */\n private markTransitionEnd(el: HTMLElement) {\n el.dataset.transitionend = 'true'\n }\n\n /**\n * 标记元素为可移除状态\n * @param el 目标元素\n */\n private markRemovable(el: HTMLElement) {\n el.dataset.removable = 'true'\n }\n\n /**\n * 计算波纹半径\n * 根据点击位置计算能覆盖整个容器的最小半径\n * @param centerPosition 波纹圆心位置\n * @returns 波纹半径\n */\n private calcRippleRadius(centerPosition: RipplePosition) {\n const { x, y } = centerPosition\n const { width, height } = this.getContainerRect()\n const edgeA = Math.max(x, width - x)\n const edgeB = Math.max(y, height - y)\n return Math.ceil(Math.sqrt(edgeA ** 2 + edgeB ** 2))\n }\n\n /**\n * 设置容器样式\n * 确保容器具有正确的定位和溢出隐藏属性以支持波纹效果\n */\n private setContainerStyle() {\n const { container } = this\n\n // 获取元素原本的内联样式\n if (this.amount === 0 && !this.containerStyle) {\n this.containerStyle = {\n position: container.style.position,\n overflow: container.style.overflow\n }\n }\n\n if (!this.containerComputedStyle) {\n this.containerComputedStyle = pick(window.getComputedStyle(container), [\n 'overflow',\n 'position'\n ])\n }\n const { position, overflow } = this.containerComputedStyle\n const style: CSSProperties = {}\n\n // 如果是静态定位,改为相对定位以支持绝对定位的波纹元素\n if (position === 'static') {\n style.position = 'relative'\n }\n\n // 如果溢出不是隐藏,设置为隐藏以裁剪波纹边界\n if (overflow !== 'hidden') {\n style.overflow = 'hidden'\n }\n\n setStyles(container, style)\n }\n\n /**\n * 重置容器样式\n * 恢复容器的原始样式设置\n */\n private resetContainerStyle() {\n const { container, containerStyle } = this\n if (!containerStyle) {\n return removeStyles(container, ['overflow', 'position'])\n }\n\n const attrsToRemoved: string[] = []\n\n if (!containerStyle.overflow) {\n attrsToRemoved.push('overflow')\n }\n\n if (!containerStyle.position) {\n attrsToRemoved.push('position')\n }\n\n removeStyles(container, attrsToRemoved)\n\n if (!attrsToRemoved.length) {\n setStyles(container, containerStyle as CSSProperties)\n }\n }\n\n /**\n * 创建波纹元素\n * 生成波纹DOM元素并设置样式和动画\n * @param centerPosition 波纹圆心位置\n */\n private createRipple(centerPosition: RipplePosition) {\n const { config } = this\n\n // 必须在创建ripple元素之前调用\n this.setContainerStyle()\n\n const rippleEl = document.createElement('span')\n this.currentRippleEl = rippleEl\n rippleEl.classList.add(Ripple.cls.b)\n config?.rippleClass && rippleEl.classList.add(config.rippleClass)\n\n const radius = this.calcRippleRadius(centerPosition)\n const diameter = radius * 2\n\n // 设置波纹元素的基础样式\n const rippleStyle: CSSProperties = {\n width: `${diameter}px`,\n height: `${diameter}px`,\n left: `${centerPosition.x - radius}px`,\n top: `${centerPosition.y - radius}px`\n }\n\n // 设置自定义动画时长\n if (config?.duration) {\n rippleStyle.transitionDuration = `${config.duration}ms`\n }\n\n setStyles(rippleEl, rippleStyle)\n\n // 监听过渡动画结束事件\n const transitionEndHandler = (e: TransitionEvent) => {\n if (e.propertyName !== 'transform') return\n\n rippleEl.removeEventListener('transitionend', transitionEndHandler)\n this.markTransitionEnd(rippleEl)\n // 尝试移除\n this.removeRippleEl(rippleEl)\n }\n\n rippleEl.addEventListener('transitionend', transitionEndHandler)\n\n this.container.appendChild(rippleEl)\n this.amount++\n\n // 如果配置了自动移除,标记为可移除\n if (config?.autoRemove) {\n this.markRemovable(rippleEl)\n }\n\n // 触发缩放动画\n nextFrame(() => {\n setStyles(rippleEl, { transform: 'scale3d(1, 1, 1)' })\n })\n }\n\n /**\n * 移除波纹元素\n * 检查元素状态,符合条件时执行淡出动画并移除DOM\n * @param rippleEl 要移除的波纹元素\n */\n private removeRippleEl(rippleEl: HTMLElement): void {\n const { transitionend, removable } = rippleEl.dataset\n\n // 只有在过渡结束且标记为可移除时才执行移除\n if (transitionend !== 'true' || removable !== 'true') return\n\n const transitionEndOrCancelHandler = (e: TransitionEvent) => {\n if (e.propertyName !== 'opacity') return\n\n rippleEl.removeEventListener('transitionend', transitionEndOrCancelHandler)\n rippleEl.removeEventListener('transitioncancel', transitionEndOrCancelHandler)\n rippleEl.remove()\n this.amount--\n }\n\n rippleEl.addEventListener('transitionend', transitionEndOrCancelHandler)\n rippleEl.addEventListener('transitioncancel', transitionEndOrCancelHandler)\n rippleEl.classList.add(bem.is('removing'))\n }\n\n /**\n * 显示波纹\n * 根据指定位置创建波纹效果\n * @param centerPosition 波纹圆心位置\n */\n show(centerPosition: RipplePosition): void {\n this.createRipple(centerPosition)\n }\n\n /**\n * 根据事件对象显示波纹\n * 从鼠标或触摸事件中提取位置信息并创建波纹\n * @param e 鼠标或触摸事件对象\n */\n showByEvent(e: MouseEvent | TouchEvent): void {\n const centerPosition = this.getRippleCenterPosition(e instanceof MouseEvent ? e : e.touches[0]!)\n this.createRipple(centerPosition)\n }\n\n /**\n * 移除波纹\n * 手动移除当前活跃的波纹元素\n * 注意:如果已经在Ripple配置中增加了autoRemove属性,则不需要调用此方法\n */\n remove(): void {\n let el = this.currentRippleEl\n if (el) {\n this.markRemovable(el)\n this.removeRippleEl(el)\n el = undefined\n this.currentRippleEl = undefined\n }\n }\n\n /**\n * 重置容器矩形\n * 当容器大小发生变化时,需要重置容器矩形缓存\n * 否则计算的波纹半径不准确\n * 注意:大部分情况下不需要调用此方法\n */\n resetContainerRect(): void {\n this.containerRect = undefined\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1 @@
1
+ import "./style2.css";
@@ -0,0 +1,34 @@
1
+ @charset "UTF-8";
2
+ /** flex布局 */
3
+ /**
4
+ * 设置css变量
5
+ * 参数1:变量前缀
6
+ * 参数2: 一个由值组成的列表或者由key, value组成的map
7
+ * css-var(height, (
8
+ * large: 40px
9
+ * default: 32px
10
+ * small: 24px
11
+ * ))
12
+ * 生成的css变量:
13
+ * --u-height-large: 40px;
14
+ * --u-height-default: 32px;
15
+ * --u-height-small: 24px;
16
+ */
17
+ /** 暗色:data-theme 与系统偏好,与 UITheme.injectBuiltInThemes 选择器策略一致 */
18
+ .u-ripple {
19
+ position: absolute;
20
+ background-color: rgba(0, 0, 0, 0.2);
21
+ pointer-events: none;
22
+ border-radius: 50%;
23
+ will-change: transform;
24
+ z-index: 0;
25
+ opacity: 0.6;
26
+ transform: scale3d(0.2, 0.2, 1);
27
+ transform-origin: center;
28
+ transition-property: transform, opacity !important;
29
+ transition-timing-function: cubic-bezier(0.82, 0.84, 0.28, 0.92) !important;
30
+ transition-duration: 300ms;
31
+ }
32
+ .u-ripple.is-removing {
33
+ opacity: 0 !important;
34
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@veltra/directives",
3
+ "version": "1.0.0",
4
+ "files": [
5
+ "dist",
6
+ "src"
7
+ ],
8
+ "type": "module",
9
+ "sideEffects": [
10
+ "dist/**/style.js",
11
+ "dist/**/*.css"
12
+ ],
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "development": "./src/index.ts",
17
+ "import": "./dist/index.js"
18
+ },
19
+ "./*": {
20
+ "types": "./dist/*",
21
+ "development": "./src/*",
22
+ "import": "./dist/*/*"
23
+ }
24
+ },
25
+ "scripts": {
26
+ "build": "tsdown",
27
+ "check-types": "tsc -p tsconfig.json --noEmit"
28
+ },
29
+ "dependencies": {
30
+ "@veltra/styles": "workspace:*",
31
+ "@veltra/utils": "workspace:*"
32
+ },
33
+ "devDependencies": {
34
+ "@tsdown/css": "^0.21.7",
35
+ "sass-embedded": "^1.99.0",
36
+ "tsdown": "^0.21.7"
37
+ },
38
+ "peerDependencies": {
39
+ "vue": "^3.5.0"
40
+ }
41
+ }
@@ -0,0 +1,81 @@
1
+ import { createIncrease } from '@veltra/utils'
2
+ import type { ObjectDirective } from 'vue'
3
+
4
+ const uid = createIncrease(1000)
5
+ const targets = new Map<string, { handler: (e: MouseEvent) => void; el: HTMLElement }>()
6
+
7
+ let eventAdded = false
8
+ let mousedownEvent: MouseEvent | undefined
9
+
10
+ const documentClickHandler = (event: MouseEvent) => {
11
+ if (!mousedownEvent) return
12
+ if (mousedownEvent?.target !== event.target) {
13
+ return (mousedownEvent = undefined)
14
+ }
15
+ targets.forEach(({ el, handler }) => {
16
+ if (el.contains(event.target as Node)) return
17
+ handler(event)
18
+ })
19
+ mousedownEvent = undefined
20
+ }
21
+
22
+ function documentMousedownHandler(event: MouseEvent) {
23
+ mousedownEvent = event
24
+ }
25
+
26
+ function addEvent() {
27
+ if (eventAdded) return
28
+ document.addEventListener('mousedown', documentMousedownHandler, true)
29
+ document.addEventListener('click', documentClickHandler, true)
30
+ eventAdded = true
31
+ }
32
+
33
+ function removeEvent() {
34
+ if (!eventAdded) return
35
+ document.removeEventListener('click', documentClickHandler, true)
36
+ document.removeEventListener('mousedown', documentMousedownHandler, true)
37
+ eventAdded = false
38
+ }
39
+
40
+ function syncDocumentEvents(): void {
41
+ if (typeof document === 'undefined') return
42
+
43
+ if (targets.size > 0) {
44
+ addEvent()
45
+ } else {
46
+ removeEvent()
47
+ }
48
+ }
49
+
50
+ export const vClickOutside: ObjectDirective<HTMLElement> = {
51
+ mounted(el, binding) {
52
+ if (!binding.value) return
53
+
54
+ // 为元素添加一个id, 并加入到字典中
55
+ const id = String(uid())
56
+ el.dataset.outsideId = id
57
+
58
+ targets.set(id, { handler: binding.value, el })
59
+ syncDocumentEvents()
60
+ },
61
+
62
+ updated(el, binding) {
63
+ if (!binding.value) {
64
+ if (!el.dataset.outsideId) return
65
+ targets.delete(el.dataset.outsideId)
66
+ syncDocumentEvents()
67
+ } else {
68
+ if (!el.dataset.outsideId) {
69
+ el.dataset.outsideId = String(uid())
70
+ }
71
+
72
+ targets.set(el.dataset.outsideId!, { handler: binding.value, el })
73
+ syncDocumentEvents()
74
+ }
75
+ },
76
+
77
+ unmounted(el: HTMLElement) {
78
+ targets.delete(el.dataset.outsideId!)
79
+ syncDocumentEvents()
80
+ }
81
+ }
package/src/env.d.ts ADDED
@@ -0,0 +1 @@
1
+ declare module '*.scss' {}
@@ -0,0 +1,16 @@
1
+ import type { ObjectDirective } from 'vue'
2
+
3
+ export const vFocus: ObjectDirective<HTMLElement> = {
4
+ mounted: (el) => {
5
+ if (el.tagName === 'INPUT') {
6
+ el.focus()
7
+ } else {
8
+ const input = el.querySelector('input')
9
+ if (input) {
10
+ input.focus()
11
+ } else {
12
+ console.warn('v-focus 指令需要一个 input 元素')
13
+ }
14
+ }
15
+ }
16
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * from './focus'
2
+
3
+ export * from './click-outside'
4
+
5
+ export * from './ripple'
@@ -0,0 +1,92 @@
1
+ import type { DirectiveBinding, ObjectDirective } from 'vue'
2
+
3
+ import { Ripple } from './ripple'
4
+
5
+ const rippleMap = new WeakMap<HTMLElement, Ripple>()
6
+
7
+ function handleMousedown(e: MouseEvent) {
8
+ if (e.button !== 0) return
9
+ const container = e.currentTarget as HTMLElement
10
+
11
+ const mouseupHandler = () => {
12
+ rippleMap.get(container)?.remove()
13
+ document.removeEventListener('mouseup', mouseupHandler)
14
+ }
15
+
16
+ document.addEventListener('mouseup', mouseupHandler)
17
+
18
+ let ripple = rippleMap.get(container)
19
+ if (!ripple) {
20
+ const { rippleClass, duration } = container.dataset
21
+ ripple = new Ripple(container, {
22
+ rippleClass,
23
+ duration: duration ? Number(duration) : undefined
24
+ })
25
+ rippleMap.set(container, ripple)
26
+ }
27
+
28
+ ripple.showByEvent(e)
29
+ }
30
+
31
+ function handleMouseleave(e: MouseEvent) {
32
+ rippleMap.get(e.currentTarget as HTMLElement)?.remove()
33
+ }
34
+
35
+ /**
36
+ * 注册按下事件
37
+ * @param el
38
+ * @param binding
39
+ * @todo 如有必要将来可添加触摸事件
40
+ */
41
+ const registerEvents = (el: HTMLElement, binding: DirectiveBinding<any>) => {
42
+ // 如果指令绑定的值为false则不应用该事件.
43
+ if (binding.value === false) return
44
+
45
+ el.addEventListener('mousedown', handleMousedown)
46
+ el.addEventListener('mouseleave', handleMouseleave)
47
+ }
48
+
49
+ /**
50
+ * 注销按下事件
51
+ * @param el 元素
52
+ */
53
+ const unregisterEvents = (el: HTMLElement) => {
54
+ el.removeEventListener('mousedown', handleMousedown)
55
+ el.removeEventListener('mouseleave', handleMouseleave)
56
+ }
57
+
58
+ export { Ripple }
59
+
60
+ export const vRipple: ObjectDirective<HTMLElement> = {
61
+ mounted: (el, binding) => {
62
+ if (binding.value !== false) {
63
+ el.dataset.rippleClass = binding.value
64
+ }
65
+
66
+ if (binding.arg) {
67
+ el.dataset.duration = binding.arg
68
+ }
69
+
70
+ registerEvents(el, binding)
71
+ },
72
+
73
+ unmounted: (el) => {
74
+ unregisterEvents(el)
75
+ rippleMap.get(el)?.remove()
76
+ rippleMap.delete(el)
77
+ },
78
+
79
+ updated(el, binding) {
80
+ const registered = binding.oldValue !== false
81
+
82
+ if (binding.value !== false) {
83
+ if (!registered) {
84
+ registerEvents(el, binding)
85
+ }
86
+ } else {
87
+ if (registered) {
88
+ unregisterEvents(el)
89
+ }
90
+ }
91
+ }
92
+ }
@@ -0,0 +1,348 @@
1
+ import { bem, nextFrame, removeStyles, setStyles, type BEM } from '@veltra/utils'
2
+ import type { CSSProperties } from 'vue'
3
+
4
+ function pick<T extends object, K extends keyof T>(obj: T, keys: readonly K[]): Pick<T, K> {
5
+ const out: Partial<Pick<T, K>> = {}
6
+ for (const k of keys) {
7
+ if (k in obj) out[k] = obj[k]
8
+ }
9
+ return out as Pick<T, K>
10
+ }
11
+
12
+ /** 鼠标或触摸事件类型 */
13
+ type MouseOrTouchEvent = MouseEvent | Touch
14
+
15
+ /** 波纹位置接口 */
16
+ interface RipplePosition {
17
+ /** X 坐标 */
18
+ x: number
19
+ /** Y 坐标 */
20
+ y: number
21
+ }
22
+
23
+ /** 波纹配置接口 */
24
+ interface RippleConfig {
25
+ /** 波纹类 */
26
+ rippleClass?: string
27
+ /** 波纹动画时长 */
28
+ duration?: number
29
+ /** 是否自动移除 */
30
+ autoRemove?: boolean
31
+ }
32
+
33
+ /** 容器样式接口 */
34
+ interface ContainerStyle {
35
+ /** 定位方式 */
36
+ position: string
37
+ /** 溢出处理 */
38
+ overflow: string
39
+ }
40
+
41
+ /**
42
+ * 波纹效果类
43
+ * 用于在指定容器中创建和管理波纹动画效果
44
+ * 支持鼠标点击和触摸事件触发的波纹动画
45
+ */
46
+ export class Ripple {
47
+ /** 波纹样式类名生成器 */
48
+ static cls: BEM<'ripple'> = bem('ripple')
49
+
50
+ /** 波纹容器元素 */
51
+ private container: HTMLElement
52
+ /** 容器矩形区域缓存 */
53
+ private containerRect?: DOMRect
54
+ /** 容器计算样式缓存 */
55
+ private containerComputedStyle?: ContainerStyle
56
+ /** 容器原始样式备份 */
57
+ private containerStyle?: ContainerStyle
58
+
59
+ /** 当前活跃的波纹元素 */
60
+ private currentRippleEl?: HTMLElement
61
+
62
+ /** 波纹配置 */
63
+ private config?: RippleConfig
64
+
65
+ /** 波纹元素数量(私有) */
66
+ private _amount = 0
67
+
68
+ /**
69
+ * 设置波纹元素数量
70
+ * 当数量为 0 时自动重置容器样式
71
+ */
72
+ private set amount(amount: number) {
73
+ this._amount = amount
74
+ amount === 0 && this.resetContainerStyle()
75
+ }
76
+
77
+ /** 获取当前波纹元素数量 */
78
+ private get amount(): number {
79
+ return this._amount
80
+ }
81
+
82
+ /**
83
+ * 构造函数
84
+ * @param container 波纹容器元素
85
+ * @param config 波纹配置选项
86
+ */
87
+ constructor(container: HTMLElement, config?: RippleConfig) {
88
+ this.container = container
89
+ if (config) {
90
+ this.config = config
91
+ }
92
+ }
93
+
94
+ /**
95
+ * 获取波纹容器元素
96
+ * @returns 容器元素
97
+ */
98
+ getContainer(): HTMLElement {
99
+ return this.container
100
+ }
101
+
102
+ /**
103
+ * 获取容器矩形区域
104
+ * 使用缓存机制避免重复计算
105
+ * @returns 容器矩形区域
106
+ */
107
+ private getContainerRect(): DOMRect {
108
+ // if (this.containerRect) return this.containerRect
109
+ const rect = this.container.getBoundingClientRect()
110
+ this.containerRect = rect
111
+ return this.containerRect
112
+ }
113
+
114
+ /**
115
+ * 获取波纹圆心位置
116
+ * 根据鼠标或触摸事件计算相对于容器的坐标
117
+ * @param e 鼠标或触摸事件
118
+ * @returns 波纹圆心位置
119
+ */
120
+ private getRippleCenterPosition(e: MouseOrTouchEvent): { x: number; y: number } {
121
+ const { left, top } = this.getContainerRect()
122
+ const { clientX, clientY } = e
123
+ return { x: clientX - left, y: clientY - top }
124
+ }
125
+
126
+ /**
127
+ * 标记元素过渡动画结束
128
+ * @param el 目标元素
129
+ */
130
+ private markTransitionEnd(el: HTMLElement) {
131
+ el.dataset.transitionend = 'true'
132
+ }
133
+
134
+ /**
135
+ * 标记元素为可移除状态
136
+ * @param el 目标元素
137
+ */
138
+ private markRemovable(el: HTMLElement) {
139
+ el.dataset.removable = 'true'
140
+ }
141
+
142
+ /**
143
+ * 计算波纹半径
144
+ * 根据点击位置计算能覆盖整个容器的最小半径
145
+ * @param centerPosition 波纹圆心位置
146
+ * @returns 波纹半径
147
+ */
148
+ private calcRippleRadius(centerPosition: RipplePosition) {
149
+ const { x, y } = centerPosition
150
+ const { width, height } = this.getContainerRect()
151
+ const edgeA = Math.max(x, width - x)
152
+ const edgeB = Math.max(y, height - y)
153
+ return Math.ceil(Math.sqrt(edgeA ** 2 + edgeB ** 2))
154
+ }
155
+
156
+ /**
157
+ * 设置容器样式
158
+ * 确保容器具有正确的定位和溢出隐藏属性以支持波纹效果
159
+ */
160
+ private setContainerStyle() {
161
+ const { container } = this
162
+
163
+ // 获取元素原本的内联样式
164
+ if (this.amount === 0 && !this.containerStyle) {
165
+ this.containerStyle = {
166
+ position: container.style.position,
167
+ overflow: container.style.overflow
168
+ }
169
+ }
170
+
171
+ if (!this.containerComputedStyle) {
172
+ this.containerComputedStyle = pick(window.getComputedStyle(container), [
173
+ 'overflow',
174
+ 'position'
175
+ ])
176
+ }
177
+ const { position, overflow } = this.containerComputedStyle
178
+ const style: CSSProperties = {}
179
+
180
+ // 如果是静态定位,改为相对定位以支持绝对定位的波纹元素
181
+ if (position === 'static') {
182
+ style.position = 'relative'
183
+ }
184
+
185
+ // 如果溢出不是隐藏,设置为隐藏以裁剪波纹边界
186
+ if (overflow !== 'hidden') {
187
+ style.overflow = 'hidden'
188
+ }
189
+
190
+ setStyles(container, style)
191
+ }
192
+
193
+ /**
194
+ * 重置容器样式
195
+ * 恢复容器的原始样式设置
196
+ */
197
+ private resetContainerStyle() {
198
+ const { container, containerStyle } = this
199
+ if (!containerStyle) {
200
+ return removeStyles(container, ['overflow', 'position'])
201
+ }
202
+
203
+ const attrsToRemoved: string[] = []
204
+
205
+ if (!containerStyle.overflow) {
206
+ attrsToRemoved.push('overflow')
207
+ }
208
+
209
+ if (!containerStyle.position) {
210
+ attrsToRemoved.push('position')
211
+ }
212
+
213
+ removeStyles(container, attrsToRemoved)
214
+
215
+ if (!attrsToRemoved.length) {
216
+ setStyles(container, containerStyle as CSSProperties)
217
+ }
218
+ }
219
+
220
+ /**
221
+ * 创建波纹元素
222
+ * 生成波纹DOM元素并设置样式和动画
223
+ * @param centerPosition 波纹圆心位置
224
+ */
225
+ private createRipple(centerPosition: RipplePosition) {
226
+ const { config } = this
227
+
228
+ // 必须在创建ripple元素之前调用
229
+ this.setContainerStyle()
230
+
231
+ const rippleEl = document.createElement('span')
232
+ this.currentRippleEl = rippleEl
233
+ rippleEl.classList.add(Ripple.cls.b)
234
+ config?.rippleClass && rippleEl.classList.add(config.rippleClass)
235
+
236
+ const radius = this.calcRippleRadius(centerPosition)
237
+ const diameter = radius * 2
238
+
239
+ // 设置波纹元素的基础样式
240
+ const rippleStyle: CSSProperties = {
241
+ width: `${diameter}px`,
242
+ height: `${diameter}px`,
243
+ left: `${centerPosition.x - radius}px`,
244
+ top: `${centerPosition.y - radius}px`
245
+ }
246
+
247
+ // 设置自定义动画时长
248
+ if (config?.duration) {
249
+ rippleStyle.transitionDuration = `${config.duration}ms`
250
+ }
251
+
252
+ setStyles(rippleEl, rippleStyle)
253
+
254
+ // 监听过渡动画结束事件
255
+ const transitionEndHandler = (e: TransitionEvent) => {
256
+ if (e.propertyName !== 'transform') return
257
+
258
+ rippleEl.removeEventListener('transitionend', transitionEndHandler)
259
+ this.markTransitionEnd(rippleEl)
260
+ // 尝试移除
261
+ this.removeRippleEl(rippleEl)
262
+ }
263
+
264
+ rippleEl.addEventListener('transitionend', transitionEndHandler)
265
+
266
+ this.container.appendChild(rippleEl)
267
+ this.amount++
268
+
269
+ // 如果配置了自动移除,标记为可移除
270
+ if (config?.autoRemove) {
271
+ this.markRemovable(rippleEl)
272
+ }
273
+
274
+ // 触发缩放动画
275
+ nextFrame(() => {
276
+ setStyles(rippleEl, { transform: 'scale3d(1, 1, 1)' })
277
+ })
278
+ }
279
+
280
+ /**
281
+ * 移除波纹元素
282
+ * 检查元素状态,符合条件时执行淡出动画并移除DOM
283
+ * @param rippleEl 要移除的波纹元素
284
+ */
285
+ private removeRippleEl(rippleEl: HTMLElement): void {
286
+ const { transitionend, removable } = rippleEl.dataset
287
+
288
+ // 只有在过渡结束且标记为可移除时才执行移除
289
+ if (transitionend !== 'true' || removable !== 'true') return
290
+
291
+ const transitionEndOrCancelHandler = (e: TransitionEvent) => {
292
+ if (e.propertyName !== 'opacity') return
293
+
294
+ rippleEl.removeEventListener('transitionend', transitionEndOrCancelHandler)
295
+ rippleEl.removeEventListener('transitioncancel', transitionEndOrCancelHandler)
296
+ rippleEl.remove()
297
+ this.amount--
298
+ }
299
+
300
+ rippleEl.addEventListener('transitionend', transitionEndOrCancelHandler)
301
+ rippleEl.addEventListener('transitioncancel', transitionEndOrCancelHandler)
302
+ rippleEl.classList.add(bem.is('removing'))
303
+ }
304
+
305
+ /**
306
+ * 显示波纹
307
+ * 根据指定位置创建波纹效果
308
+ * @param centerPosition 波纹圆心位置
309
+ */
310
+ show(centerPosition: RipplePosition): void {
311
+ this.createRipple(centerPosition)
312
+ }
313
+
314
+ /**
315
+ * 根据事件对象显示波纹
316
+ * 从鼠标或触摸事件中提取位置信息并创建波纹
317
+ * @param e 鼠标或触摸事件对象
318
+ */
319
+ showByEvent(e: MouseEvent | TouchEvent): void {
320
+ const centerPosition = this.getRippleCenterPosition(e instanceof MouseEvent ? e : e.touches[0]!)
321
+ this.createRipple(centerPosition)
322
+ }
323
+
324
+ /**
325
+ * 移除波纹
326
+ * 手动移除当前活跃的波纹元素
327
+ * 注意:如果已经在Ripple配置中增加了autoRemove属性,则不需要调用此方法
328
+ */
329
+ remove(): void {
330
+ let el = this.currentRippleEl
331
+ if (el) {
332
+ this.markRemovable(el)
333
+ this.removeRippleEl(el)
334
+ el = undefined
335
+ this.currentRippleEl = undefined
336
+ }
337
+ }
338
+
339
+ /**
340
+ * 重置容器矩形
341
+ * 当容器大小发生变化时,需要重置容器矩形缓存
342
+ * 否则计算的波纹半径不准确
343
+ * 注意:大部分情况下不需要调用此方法
344
+ */
345
+ resetContainerRect(): void {
346
+ this.containerRect = undefined
347
+ }
348
+ }
@@ -0,0 +1,20 @@
1
+ @use 'pkg:@veltra/styles/mixins' as m;
2
+
3
+ @include m.b(ripple) {
4
+ position: absolute;
5
+ background-color: rgba($color: #000000, $alpha: 0.2);
6
+ pointer-events: none;
7
+ border-radius: 50%;
8
+ will-change: transform;
9
+ z-index: 0;
10
+ opacity: 0.6;
11
+ transform: scale3d(0.2, 0.2, 1);
12
+ transform-origin: center;
13
+ transition-property: transform, opacity !important;
14
+ transition-timing-function: cubic-bezier(0.82, 0.84, 0.28, 0.92) !important;
15
+ transition-duration: 300ms;
16
+
17
+ @include m.is(removing) {
18
+ opacity: 0 !important;
19
+ }
20
+ }
@@ -0,0 +1 @@
1
+ import './style.scss'