@veltra/directives 1.13.3 → 1.14.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/ripple/ripple.d.ts +18 -14
- package/dist/ripple/ripple.js +47 -42
- package/dist/ripple/ripple.js.map +1 -1
- package/dist/ripple/style2.css +7 -0
- package/package.json +1 -1
- package/src/ripple/ripple.ts +60 -50
- package/src/ripple/style.scss +9 -0
package/dist/ripple/ripple.d.ts
CHANGED
|
@@ -22,10 +22,10 @@ interface RippleConfig {
|
|
|
22
22
|
* 支持鼠标点击和触摸事件触发的波纹动画
|
|
23
23
|
*
|
|
24
24
|
* 样式管理策略:
|
|
25
|
-
* -
|
|
26
|
-
* - 只在容器
|
|
25
|
+
* - 波纹统一渲染在内部波纹层 `.u-ripple__layer` 中,由层负责裁剪与定位,宿主不再需要 overflow: hidden
|
|
26
|
+
* - 只在容器 position 为 static 时设置 relative,作为波纹层的包含块,不改变布局尺寸
|
|
27
27
|
* - 使用 dataset 做引用计数,多实例可安全协作
|
|
28
|
-
* -
|
|
28
|
+
* - 最后一个波纹移除时,移除波纹层并精确恢复容器样式
|
|
29
29
|
*/
|
|
30
30
|
export declare class Ripple {
|
|
31
31
|
/** 波纹样式类名生成器 */
|
|
@@ -38,15 +38,6 @@ export declare class Ripple {
|
|
|
38
38
|
private currentRippleEl?;
|
|
39
39
|
/** 波纹配置 */
|
|
40
40
|
private config?;
|
|
41
|
-
/** 波纹元素数量(私有) */
|
|
42
|
-
private _amount;
|
|
43
|
-
/**
|
|
44
|
-
* 设置波纹元素数量
|
|
45
|
-
* 当数量为 0 时自动重置容器样式
|
|
46
|
-
*/
|
|
47
|
-
private set amount(value);
|
|
48
|
-
/** 获取当前波纹元素数量 */
|
|
49
|
-
private get amount();
|
|
50
41
|
/**
|
|
51
42
|
* 构造函数
|
|
52
43
|
* @param container 波纹容器元素
|
|
@@ -64,6 +55,18 @@ export declare class Ripple {
|
|
|
64
55
|
* @returns 容器矩形区域
|
|
65
56
|
*/
|
|
66
57
|
private getContainerRect;
|
|
58
|
+
/**
|
|
59
|
+
* 获取已有波纹层
|
|
60
|
+
* 只查找直接子级,避免嵌套的波纹容器层被误取
|
|
61
|
+
* @returns 波纹层元素或 null
|
|
62
|
+
*/
|
|
63
|
+
private getLayer;
|
|
64
|
+
/**
|
|
65
|
+
* 获取或创建波纹层
|
|
66
|
+
* 波纹元素统一挂载到该层,由层裁剪,宿主无需再设置 overflow: hidden
|
|
67
|
+
* @returns 波纹层元素
|
|
68
|
+
*/
|
|
69
|
+
private getOrCreateLayer;
|
|
67
70
|
/**
|
|
68
71
|
* 获取波纹圆心位置
|
|
69
72
|
* 根据鼠标或触摸事件计算相对于容器的坐标
|
|
@@ -89,12 +92,13 @@ export declare class Ripple {
|
|
|
89
92
|
*/
|
|
90
93
|
private calcRippleRadius;
|
|
91
94
|
/**
|
|
92
|
-
*
|
|
95
|
+
* 设置容器样式并准备好波纹层
|
|
93
96
|
* 使用引用计数 + dataset 标记,确保多实例协作安全
|
|
97
|
+
* @returns 波纹元素应挂载到的波纹层
|
|
94
98
|
*/
|
|
95
99
|
private setContainerStyle;
|
|
96
100
|
/**
|
|
97
|
-
*
|
|
101
|
+
* 重置容器样式并移除波纹层
|
|
98
102
|
* 根据 dataset 标记精确恢复,避免误删其他内联样式
|
|
99
103
|
*/
|
|
100
104
|
private resetContainerStyle;
|
package/dist/ripple/ripple.js
CHANGED
|
@@ -2,18 +2,19 @@ import { bem, nextFrame, setStyles } from "@veltra/utils";
|
|
|
2
2
|
//#region src/ripple/ripple.ts
|
|
3
3
|
/** 数据集 key,用于标记 Ripple 修改过的样式 */
|
|
4
4
|
const PositionKey = "uRipplePosition";
|
|
5
|
-
const OverflowKey = "uRippleOverflow";
|
|
6
5
|
const CountKey = "uRippleActiveCount";
|
|
6
|
+
/** 波纹层类名,波纹元素统一渲染在该层内 */
|
|
7
|
+
const LayerClass = bem("ripple").e("layer");
|
|
7
8
|
/**
|
|
8
9
|
* 波纹效果类
|
|
9
10
|
* 用于在指定容器中创建和管理波纹动画效果
|
|
10
11
|
* 支持鼠标点击和触摸事件触发的波纹动画
|
|
11
12
|
*
|
|
12
13
|
* 样式管理策略:
|
|
13
|
-
* -
|
|
14
|
-
* - 只在容器
|
|
14
|
+
* - 波纹统一渲染在内部波纹层 `.u-ripple__layer` 中,由层负责裁剪与定位,宿主不再需要 overflow: hidden
|
|
15
|
+
* - 只在容器 position 为 static 时设置 relative,作为波纹层的包含块,不改变布局尺寸
|
|
15
16
|
* - 使用 dataset 做引用计数,多实例可安全协作
|
|
16
|
-
* -
|
|
17
|
+
* - 最后一个波纹移除时,移除波纹层并精确恢复容器样式
|
|
17
18
|
*/
|
|
18
19
|
var Ripple = class Ripple {
|
|
19
20
|
/** 波纹样式类名生成器 */
|
|
@@ -26,20 +27,6 @@ var Ripple = class Ripple {
|
|
|
26
27
|
currentRippleEl;
|
|
27
28
|
/** 波纹配置 */
|
|
28
29
|
config;
|
|
29
|
-
/** 波纹元素数量(私有) */
|
|
30
|
-
_amount = 0;
|
|
31
|
-
/**
|
|
32
|
-
* 设置波纹元素数量
|
|
33
|
-
* 当数量为 0 时自动重置容器样式
|
|
34
|
-
*/
|
|
35
|
-
set amount(amount) {
|
|
36
|
-
this._amount = amount;
|
|
37
|
-
amount === 0 && this.resetContainerStyle();
|
|
38
|
-
}
|
|
39
|
-
/** 获取当前波纹元素数量 */
|
|
40
|
-
get amount() {
|
|
41
|
-
return this._amount;
|
|
42
|
-
}
|
|
43
30
|
/**
|
|
44
31
|
* 构造函数
|
|
45
32
|
* @param container 波纹容器元素
|
|
@@ -67,6 +54,27 @@ var Ripple = class Ripple {
|
|
|
67
54
|
return this.containerRect;
|
|
68
55
|
}
|
|
69
56
|
/**
|
|
57
|
+
* 获取已有波纹层
|
|
58
|
+
* 只查找直接子级,避免嵌套的波纹容器层被误取
|
|
59
|
+
* @returns 波纹层元素或 null
|
|
60
|
+
*/
|
|
61
|
+
getLayer() {
|
|
62
|
+
return this.container.querySelector(`:scope > .${LayerClass}`);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 获取或创建波纹层
|
|
66
|
+
* 波纹元素统一挂载到该层,由层裁剪,宿主无需再设置 overflow: hidden
|
|
67
|
+
* @returns 波纹层元素
|
|
68
|
+
*/
|
|
69
|
+
getOrCreateLayer() {
|
|
70
|
+
const layer = this.getLayer();
|
|
71
|
+
if (layer) return layer;
|
|
72
|
+
const el = document.createElement("span");
|
|
73
|
+
el.classList.add(LayerClass);
|
|
74
|
+
this.container.appendChild(el);
|
|
75
|
+
return el;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
70
78
|
* 获取波纹圆心位置
|
|
71
79
|
* 根据鼠标或触摸事件计算相对于容器的坐标
|
|
72
80
|
* @param e 鼠标或触摸事件
|
|
@@ -108,42 +116,39 @@ var Ripple = class Ripple {
|
|
|
108
116
|
return Math.ceil(Math.sqrt(edgeA ** 2 + edgeB ** 2));
|
|
109
117
|
}
|
|
110
118
|
/**
|
|
111
|
-
*
|
|
119
|
+
* 设置容器样式并准备好波纹层
|
|
112
120
|
* 使用引用计数 + dataset 标记,确保多实例协作安全
|
|
121
|
+
* @returns 波纹元素应挂载到的波纹层
|
|
113
122
|
*/
|
|
114
123
|
setContainerStyle() {
|
|
115
124
|
const { container } = this;
|
|
116
125
|
const count = Number(container.dataset[CountKey] || 0);
|
|
117
126
|
container.dataset[CountKey] = String(count + 1);
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (
|
|
127
|
+
const layer = this.getOrCreateLayer();
|
|
128
|
+
if (count > 0) return layer;
|
|
129
|
+
if (window.getComputedStyle(container).position === "static") {
|
|
121
130
|
container.dataset[PositionKey] = "static";
|
|
122
131
|
container.style.position = "relative";
|
|
123
132
|
}
|
|
124
|
-
|
|
125
|
-
container.dataset[OverflowKey] = "visible";
|
|
126
|
-
container.style.overflow = "hidden";
|
|
127
|
-
}
|
|
133
|
+
return layer;
|
|
128
134
|
}
|
|
129
135
|
/**
|
|
130
|
-
*
|
|
136
|
+
* 重置容器样式并移除波纹层
|
|
131
137
|
* 根据 dataset 标记精确恢复,避免误删其他内联样式
|
|
132
138
|
*/
|
|
133
139
|
resetContainerStyle() {
|
|
134
140
|
const { container } = this;
|
|
135
141
|
const count = Number(container.dataset[CountKey] || 0);
|
|
136
|
-
if (count
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
} else container.dataset[CountKey] = String(count - 1);
|
|
142
|
+
if (count > 1) {
|
|
143
|
+
container.dataset[CountKey] = String(count - 1);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (container.dataset[PositionKey] === "static") {
|
|
147
|
+
container.style.position = "";
|
|
148
|
+
delete container.dataset[PositionKey];
|
|
149
|
+
}
|
|
150
|
+
this.getLayer()?.remove();
|
|
151
|
+
delete container.dataset[CountKey];
|
|
147
152
|
}
|
|
148
153
|
/**
|
|
149
154
|
* 创建波纹元素
|
|
@@ -152,7 +157,8 @@ var Ripple = class Ripple {
|
|
|
152
157
|
*/
|
|
153
158
|
createRipple(centerPosition) {
|
|
154
159
|
const { config } = this;
|
|
155
|
-
this.
|
|
160
|
+
this.remove();
|
|
161
|
+
const layer = this.setContainerStyle();
|
|
156
162
|
const rippleEl = document.createElement("span");
|
|
157
163
|
this.currentRippleEl = rippleEl;
|
|
158
164
|
rippleEl.classList.add(Ripple.cls.b);
|
|
@@ -174,8 +180,7 @@ var Ripple = class Ripple {
|
|
|
174
180
|
this.removeRippleEl(rippleEl);
|
|
175
181
|
};
|
|
176
182
|
rippleEl.addEventListener("transitionend", transitionEndHandler);
|
|
177
|
-
|
|
178
|
-
this.amount++;
|
|
183
|
+
layer.appendChild(rippleEl);
|
|
179
184
|
if (config?.autoRemove) this.markRemovable(rippleEl);
|
|
180
185
|
nextFrame(() => {
|
|
181
186
|
setStyles(rippleEl, { transform: "scale3d(1, 1, 1)" });
|
|
@@ -198,7 +203,7 @@ var Ripple = class Ripple {
|
|
|
198
203
|
rippleEl.removeEventListener("transitionend", transitionEndOrCancelHandler);
|
|
199
204
|
rippleEl.removeEventListener("transitioncancel", transitionEndOrCancelHandler);
|
|
200
205
|
rippleEl.remove();
|
|
201
|
-
this.
|
|
206
|
+
this.resetContainerStyle();
|
|
202
207
|
};
|
|
203
208
|
const transitionEndOrCancelHandler = (e) => {
|
|
204
209
|
if (e.propertyName !== "opacity") return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ripple.js","names":[],"sources":["../../src/ripple/ripple.ts"],"sourcesContent":["import { bem, nextFrame, setStyles, type BEM } from '@veltra/utils'\nimport type { CSSProperties } from 'vue'\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/** 数据集 key,用于标记 Ripple 修改过的样式 */\nconst PositionKey = 'uRipplePosition'\nconst OverflowKey = 'uRippleOverflow'\nconst CountKey = 'uRippleActiveCount'\n\n/**\n * 波纹效果类\n * 用于在指定容器中创建和管理波纹动画效果\n * 支持鼠标点击和触摸事件触发的波纹动画\n *\n * 样式管理策略:\n * - 只在容器 position 为 static 时设置 relative\n * - 只在容器 overflow 为 visible 时设置 hidden\n * - 使用 dataset 做引用计数,多实例可安全协作\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 /** 当前活跃的波纹元素 */\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 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 * 使用引用计数 + dataset 标记,确保多实例协作安全\n */\n private setContainerStyle() {\n const { container } = this\n const count = Number(container.dataset[CountKey] || 0)\n container.dataset[CountKey] = String(count + 1)\n\n // 已经有其他波纹在运行,样式已设置,直接返回\n if (count > 0) return\n\n const computed = window.getComputedStyle(container)\n\n if (computed.position === 'static') {\n container.dataset[PositionKey] = 'static'\n container.style.position = 'relative'\n }\n\n if (computed.overflow === 'visible') {\n container.dataset[OverflowKey] = 'visible'\n container.style.overflow = 'hidden'\n }\n }\n\n /**\n * 重置容器样式\n * 根据 dataset 标记精确恢复,避免误删其他内联样式\n */\n private resetContainerStyle() {\n const { container } = this\n const count = Number(container.dataset[CountKey] || 0)\n\n if (count <= 1) {\n // 最后一个波纹,执行恢复\n if (container.dataset[PositionKey] === 'static') {\n container.style.position = ''\n delete container.dataset[PositionKey]\n }\n\n if (container.dataset[OverflowKey] === 'visible') {\n container.style.overflow = ''\n delete container.dataset[OverflowKey]\n }\n\n delete container.dataset[CountKey]\n } else {\n // 还有其他波纹在运行,只减计数\n container.dataset[CountKey] = String(count - 1)\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 let removed = false\n let fallbackTimer: ReturnType<typeof setTimeout> | undefined\n\n const cleanup = () => {\n if (removed) return\n removed = true\n clearTimeout(fallbackTimer)\n rippleEl.removeEventListener('transitionend', transitionEndOrCancelHandler)\n rippleEl.removeEventListener('transitioncancel', transitionEndOrCancelHandler)\n rippleEl.remove()\n this.amount--\n }\n\n const transitionEndOrCancelHandler = (e: TransitionEvent) => {\n if (e.propertyName !== 'opacity') return\n cleanup()\n }\n\n rippleEl.addEventListener('transitionend', transitionEndOrCancelHandler)\n rippleEl.addEventListener('transitioncancel', transitionEndOrCancelHandler)\n rippleEl.classList.add(bem.is('removing'))\n\n // 兜底:元素被裁剪/隐藏时 transitionend 可能不触发,超时强制回收,避免内联样式长期残留\n fallbackTimer = setTimeout(cleanup, (this.config?.duration ?? 300) + 100)\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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"}
|
|
1
|
+
{"version":3,"file":"ripple.js","names":[],"sources":["../../src/ripple/ripple.ts"],"sourcesContent":["import { bem, nextFrame, setStyles, type BEM } from '@veltra/utils'\nimport type { CSSProperties } from 'vue'\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/** 数据集 key,用于标记 Ripple 修改过的样式 */\nconst PositionKey = 'uRipplePosition'\nconst CountKey = 'uRippleActiveCount'\n\n/** 波纹层类名,波纹元素统一渲染在该层内 */\nconst LayerClass = bem('ripple').e('layer')\n\n/**\n * 波纹效果类\n * 用于在指定容器中创建和管理波纹动画效果\n * 支持鼠标点击和触摸事件触发的波纹动画\n *\n * 样式管理策略:\n * - 波纹统一渲染在内部波纹层 `.u-ripple__layer` 中,由层负责裁剪与定位,宿主不再需要 overflow: hidden\n * - 只在容器 position 为 static 时设置 relative,作为波纹层的包含块,不改变布局尺寸\n * - 使用 dataset 做引用计数,多实例可安全协作\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 /** 当前活跃的波纹元素 */\n private currentRippleEl?: HTMLElement\n\n /** 波纹配置 */\n private config?: RippleConfig\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 const rect = this.container.getBoundingClientRect()\n this.containerRect = rect\n return this.containerRect\n }\n\n /**\n * 获取已有波纹层\n * 只查找直接子级,避免嵌套的波纹容器层被误取\n * @returns 波纹层元素或 null\n */\n private getLayer(): HTMLElement | null {\n return this.container.querySelector<HTMLElement>(`:scope > .${LayerClass}`)\n }\n\n /**\n * 获取或创建波纹层\n * 波纹元素统一挂载到该层,由层裁剪,宿主无需再设置 overflow: hidden\n * @returns 波纹层元素\n */\n private getOrCreateLayer(): HTMLElement {\n const layer = this.getLayer()\n if (layer) return layer\n\n const el = document.createElement('span')\n el.classList.add(LayerClass)\n this.container.appendChild(el)\n return el\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 * 使用引用计数 + dataset 标记,确保多实例协作安全\n * @returns 波纹元素应挂载到的波纹层\n */\n private setContainerStyle(): HTMLElement {\n const { container } = this\n const count = Number(container.dataset[CountKey] || 0)\n container.dataset[CountKey] = String(count + 1)\n\n const layer = this.getOrCreateLayer()\n\n // 已经有其他波纹在运行,宿主样式已设置,直接复用波纹层\n if (count > 0) return layer\n\n // 波纹层是绝对定位,宿主需作为其包含块;static 提升为 relative 不影响布局尺寸\n const computed = window.getComputedStyle(container)\n\n if (computed.position === 'static') {\n container.dataset[PositionKey] = 'static'\n container.style.position = 'relative'\n }\n\n return layer\n }\n\n /**\n * 重置容器样式并移除波纹层\n * 根据 dataset 标记精确恢复,避免误删其他内联样式\n */\n private resetContainerStyle() {\n const { container } = this\n const count = Number(container.dataset[CountKey] || 0)\n\n // 还有其他波纹在运行,只减计数\n if (count > 1) {\n container.dataset[CountKey] = String(count - 1)\n return\n }\n\n // 最后一个波纹:恢复容器定位,并移除空波纹层\n if (container.dataset[PositionKey] === 'static') {\n container.style.position = ''\n delete container.dataset[PositionKey]\n }\n\n this.getLayer()?.remove()\n delete container.dataset[CountKey]\n }\n\n /**\n * 创建波纹元素\n * 生成波纹DOM元素并设置样式和动画\n * @param centerPosition 波纹圆心位置\n */\n private createRipple(centerPosition: RipplePosition) {\n const { config } = this\n\n // 上一个波纹可能仍在动画中(同一容器快速连按):先标记移除,\n // 否则它会成为没有回收者的孤儿波纹,计数无法归零、波纹层永远删不掉\n this.remove()\n\n // 必须在创建 ripple 元素之前调用,确保波纹层与宿主定位就绪\n const layer = 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 layer.appendChild(rippleEl)\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 let removed = false\n let fallbackTimer: ReturnType<typeof setTimeout> | undefined\n\n const cleanup = () => {\n if (removed) return\n removed = true\n clearTimeout(fallbackTimer)\n rippleEl.removeEventListener('transitionend', transitionEndOrCancelHandler)\n rippleEl.removeEventListener('transitioncancel', transitionEndOrCancelHandler)\n rippleEl.remove()\n this.resetContainerStyle()\n }\n\n const transitionEndOrCancelHandler = (e: TransitionEvent) => {\n if (e.propertyName !== 'opacity') return\n cleanup()\n }\n\n rippleEl.addEventListener('transitionend', transitionEndOrCancelHandler)\n rippleEl.addEventListener('transitioncancel', transitionEndOrCancelHandler)\n rippleEl.classList.add(bem.is('removing'))\n\n // 兜底:元素被裁剪/隐藏时 transitionend 可能不触发,超时强制回收,避免内联样式长期残留\n fallbackTimer = setTimeout(cleanup, (this.config?.duration ?? 300) + 100)\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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"}
|
package/dist/ripple/style2.css
CHANGED
package/package.json
CHANGED
package/src/ripple/ripple.ts
CHANGED
|
@@ -24,19 +24,21 @@ interface RippleConfig {
|
|
|
24
24
|
|
|
25
25
|
/** 数据集 key,用于标记 Ripple 修改过的样式 */
|
|
26
26
|
const PositionKey = 'uRipplePosition'
|
|
27
|
-
const OverflowKey = 'uRippleOverflow'
|
|
28
27
|
const CountKey = 'uRippleActiveCount'
|
|
29
28
|
|
|
29
|
+
/** 波纹层类名,波纹元素统一渲染在该层内 */
|
|
30
|
+
const LayerClass = bem('ripple').e('layer')
|
|
31
|
+
|
|
30
32
|
/**
|
|
31
33
|
* 波纹效果类
|
|
32
34
|
* 用于在指定容器中创建和管理波纹动画效果
|
|
33
35
|
* 支持鼠标点击和触摸事件触发的波纹动画
|
|
34
36
|
*
|
|
35
37
|
* 样式管理策略:
|
|
36
|
-
* -
|
|
37
|
-
* - 只在容器
|
|
38
|
+
* - 波纹统一渲染在内部波纹层 `.u-ripple__layer` 中,由层负责裁剪与定位,宿主不再需要 overflow: hidden
|
|
39
|
+
* - 只在容器 position 为 static 时设置 relative,作为波纹层的包含块,不改变布局尺寸
|
|
38
40
|
* - 使用 dataset 做引用计数,多实例可安全协作
|
|
39
|
-
* -
|
|
41
|
+
* - 最后一个波纹移除时,移除波纹层并精确恢复容器样式
|
|
40
42
|
*/
|
|
41
43
|
export class Ripple {
|
|
42
44
|
/** 波纹样式类名生成器 */
|
|
@@ -53,23 +55,6 @@ export class Ripple {
|
|
|
53
55
|
/** 波纹配置 */
|
|
54
56
|
private config?: RippleConfig
|
|
55
57
|
|
|
56
|
-
/** 波纹元素数量(私有) */
|
|
57
|
-
private _amount = 0
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* 设置波纹元素数量
|
|
61
|
-
* 当数量为 0 时自动重置容器样式
|
|
62
|
-
*/
|
|
63
|
-
private set amount(amount: number) {
|
|
64
|
-
this._amount = amount
|
|
65
|
-
amount === 0 && this.resetContainerStyle()
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** 获取当前波纹元素数量 */
|
|
69
|
-
private get amount(): number {
|
|
70
|
-
return this._amount
|
|
71
|
-
}
|
|
72
|
-
|
|
73
58
|
/**
|
|
74
59
|
* 构造函数
|
|
75
60
|
* @param container 波纹容器元素
|
|
@@ -101,6 +86,30 @@ export class Ripple {
|
|
|
101
86
|
return this.containerRect
|
|
102
87
|
}
|
|
103
88
|
|
|
89
|
+
/**
|
|
90
|
+
* 获取已有波纹层
|
|
91
|
+
* 只查找直接子级,避免嵌套的波纹容器层被误取
|
|
92
|
+
* @returns 波纹层元素或 null
|
|
93
|
+
*/
|
|
94
|
+
private getLayer(): HTMLElement | null {
|
|
95
|
+
return this.container.querySelector<HTMLElement>(`:scope > .${LayerClass}`)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 获取或创建波纹层
|
|
100
|
+
* 波纹元素统一挂载到该层,由层裁剪,宿主无需再设置 overflow: hidden
|
|
101
|
+
* @returns 波纹层元素
|
|
102
|
+
*/
|
|
103
|
+
private getOrCreateLayer(): HTMLElement {
|
|
104
|
+
const layer = this.getLayer()
|
|
105
|
+
if (layer) return layer
|
|
106
|
+
|
|
107
|
+
const el = document.createElement('span')
|
|
108
|
+
el.classList.add(LayerClass)
|
|
109
|
+
this.container.appendChild(el)
|
|
110
|
+
return el
|
|
111
|
+
}
|
|
112
|
+
|
|
104
113
|
/**
|
|
105
114
|
* 获取波纹圆心位置
|
|
106
115
|
* 根据鼠标或触摸事件计算相对于容器的坐标
|
|
@@ -144,17 +153,21 @@ export class Ripple {
|
|
|
144
153
|
}
|
|
145
154
|
|
|
146
155
|
/**
|
|
147
|
-
*
|
|
156
|
+
* 设置容器样式并准备好波纹层
|
|
148
157
|
* 使用引用计数 + dataset 标记,确保多实例协作安全
|
|
158
|
+
* @returns 波纹元素应挂载到的波纹层
|
|
149
159
|
*/
|
|
150
|
-
private setContainerStyle() {
|
|
160
|
+
private setContainerStyle(): HTMLElement {
|
|
151
161
|
const { container } = this
|
|
152
162
|
const count = Number(container.dataset[CountKey] || 0)
|
|
153
163
|
container.dataset[CountKey] = String(count + 1)
|
|
154
164
|
|
|
155
|
-
|
|
156
|
-
|
|
165
|
+
const layer = this.getOrCreateLayer()
|
|
166
|
+
|
|
167
|
+
// 已经有其他波纹在运行,宿主样式已设置,直接复用波纹层
|
|
168
|
+
if (count > 0) return layer
|
|
157
169
|
|
|
170
|
+
// 波纹层是绝对定位,宿主需作为其包含块;static 提升为 relative 不影响布局尺寸
|
|
158
171
|
const computed = window.getComputedStyle(container)
|
|
159
172
|
|
|
160
173
|
if (computed.position === 'static') {
|
|
@@ -162,37 +175,31 @@ export class Ripple {
|
|
|
162
175
|
container.style.position = 'relative'
|
|
163
176
|
}
|
|
164
177
|
|
|
165
|
-
|
|
166
|
-
container.dataset[OverflowKey] = 'visible'
|
|
167
|
-
container.style.overflow = 'hidden'
|
|
168
|
-
}
|
|
178
|
+
return layer
|
|
169
179
|
}
|
|
170
180
|
|
|
171
181
|
/**
|
|
172
|
-
*
|
|
182
|
+
* 重置容器样式并移除波纹层
|
|
173
183
|
* 根据 dataset 标记精确恢复,避免误删其他内联样式
|
|
174
184
|
*/
|
|
175
185
|
private resetContainerStyle() {
|
|
176
186
|
const { container } = this
|
|
177
187
|
const count = Number(container.dataset[CountKey] || 0)
|
|
178
188
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (container.dataset[PositionKey] === 'static') {
|
|
182
|
-
container.style.position = ''
|
|
183
|
-
delete container.dataset[PositionKey]
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (container.dataset[OverflowKey] === 'visible') {
|
|
187
|
-
container.style.overflow = ''
|
|
188
|
-
delete container.dataset[OverflowKey]
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
delete container.dataset[CountKey]
|
|
192
|
-
} else {
|
|
193
|
-
// 还有其他波纹在运行,只减计数
|
|
189
|
+
// 还有其他波纹在运行,只减计数
|
|
190
|
+
if (count > 1) {
|
|
194
191
|
container.dataset[CountKey] = String(count - 1)
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 最后一个波纹:恢复容器定位,并移除空波纹层
|
|
196
|
+
if (container.dataset[PositionKey] === 'static') {
|
|
197
|
+
container.style.position = ''
|
|
198
|
+
delete container.dataset[PositionKey]
|
|
195
199
|
}
|
|
200
|
+
|
|
201
|
+
this.getLayer()?.remove()
|
|
202
|
+
delete container.dataset[CountKey]
|
|
196
203
|
}
|
|
197
204
|
|
|
198
205
|
/**
|
|
@@ -203,8 +210,12 @@ export class Ripple {
|
|
|
203
210
|
private createRipple(centerPosition: RipplePosition) {
|
|
204
211
|
const { config } = this
|
|
205
212
|
|
|
206
|
-
//
|
|
207
|
-
|
|
213
|
+
// 上一个波纹可能仍在动画中(同一容器快速连按):先标记移除,
|
|
214
|
+
// 否则它会成为没有回收者的孤儿波纹,计数无法归零、波纹层永远删不掉
|
|
215
|
+
this.remove()
|
|
216
|
+
|
|
217
|
+
// 必须在创建 ripple 元素之前调用,确保波纹层与宿主定位就绪
|
|
218
|
+
const layer = this.setContainerStyle()
|
|
208
219
|
|
|
209
220
|
const rippleEl = document.createElement('span')
|
|
210
221
|
this.currentRippleEl = rippleEl
|
|
@@ -241,8 +252,7 @@ export class Ripple {
|
|
|
241
252
|
|
|
242
253
|
rippleEl.addEventListener('transitionend', transitionEndHandler)
|
|
243
254
|
|
|
244
|
-
|
|
245
|
-
this.amount++
|
|
255
|
+
layer.appendChild(rippleEl)
|
|
246
256
|
|
|
247
257
|
// 如果配置了自动移除,标记为可移除
|
|
248
258
|
if (config?.autoRemove) {
|
|
@@ -276,7 +286,7 @@ export class Ripple {
|
|
|
276
286
|
rippleEl.removeEventListener('transitionend', transitionEndOrCancelHandler)
|
|
277
287
|
rippleEl.removeEventListener('transitioncancel', transitionEndOrCancelHandler)
|
|
278
288
|
rippleEl.remove()
|
|
279
|
-
this.
|
|
289
|
+
this.resetContainerStyle()
|
|
280
290
|
}
|
|
281
291
|
|
|
282
292
|
const transitionEndOrCancelHandler = (e: TransitionEvent) => {
|
package/src/ripple/style.scss
CHANGED
|
@@ -17,4 +17,13 @@
|
|
|
17
17
|
@include m.is(removing) {
|
|
18
18
|
opacity: 0 !important;
|
|
19
19
|
}
|
|
20
|
+
|
|
21
|
+
// 波纹层:定位与裁剪波纹的独立容器,宿主不再需要 overflow: hidden;继承宿主圆角以裁切波纹
|
|
22
|
+
@include m.e(layer) {
|
|
23
|
+
position: absolute;
|
|
24
|
+
inset: 0;
|
|
25
|
+
overflow: hidden;
|
|
26
|
+
pointer-events: none;
|
|
27
|
+
border-radius: inherit;
|
|
28
|
+
}
|
|
20
29
|
}
|