@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.
- package/dist/click-outside/index.d.ts +7 -0
- package/dist/click-outside/index.js +72 -0
- package/dist/click-outside/index.js.map +1 -0
- package/dist/focus/index.d.ts +7 -0
- package/dist/focus/index.js +13 -0
- package/dist/focus/index.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/ripple/index.d.ts +8 -0
- package/dist/ripple/index.js +66 -0
- package/dist/ripple/index.js.map +1 -0
- package/dist/ripple/ripple.d.ts +140 -0
- package/dist/ripple/ripple.js +237 -0
- package/dist/ripple/ripple.js.map +1 -0
- package/dist/ripple/style.d.ts +1 -0
- package/dist/ripple/style.js +1 -0
- package/dist/ripple/style2.css +34 -0
- package/package.json +41 -0
- package/src/click-outside/index.ts +81 -0
- package/src/env.d.ts +1 -0
- package/src/focus/index.ts +16 -0
- package/src/index.ts +5 -0
- package/src/ripple/index.ts +92 -0
- package/src/ripple/ripple.ts +348 -0
- package/src/ripple/style.scss +20 -0
- package/src/ripple/style.ts +1 -0
|
@@ -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,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"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -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,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'
|