@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.
@@ -22,10 +22,10 @@ interface RippleConfig {
22
22
  * 支持鼠标点击和触摸事件触发的波纹动画
23
23
  *
24
24
  * 样式管理策略:
25
- * - 只在容器 position 为 static 时设置 relative
26
- * - 只在容器 overflow 为 visible 时设置 hidden
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;
@@ -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
- * - 只在容器 position 为 static 时设置 relative
14
- * - 只在容器 overflow 为 visible 时设置 hidden
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
- if (count > 0) return;
119
- const computed = window.getComputedStyle(container);
120
- if (computed.position === "static") {
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
- if (computed.overflow === "visible") {
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 <= 1) {
137
- if (container.dataset[PositionKey] === "static") {
138
- container.style.position = "";
139
- delete container.dataset[PositionKey];
140
- }
141
- if (container.dataset[OverflowKey] === "visible") {
142
- container.style.overflow = "";
143
- delete container.dataset[OverflowKey];
144
- }
145
- delete container.dataset[CountKey];
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.setContainerStyle();
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
- this.container.appendChild(rippleEl);
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.amount--;
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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"}
@@ -32,3 +32,10 @@
32
32
  .u-ripple.is-removing {
33
33
  opacity: 0 !important;
34
34
  }
35
+ .u-ripple__layer {
36
+ position: absolute;
37
+ inset: 0;
38
+ overflow: hidden;
39
+ pointer-events: none;
40
+ border-radius: inherit;
41
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veltra/directives",
3
- "version": "1.13.3",
3
+ "version": "1.14.0",
4
4
  "homepage": "https://github.com/cabinet-fe/ultra-ui#readme",
5
5
  "bugs": {
6
6
  "url": "https://github.com/cabinet-fe/ultra-ui/issues"
@@ -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
- * - 只在容器 position 为 static 时设置 relative
37
- * - 只在容器 overflow 为 visible 时设置 hidden
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
- if (count > 0) return
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
- if (computed.overflow === 'visible') {
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
- if (count <= 1) {
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
- // 必须在创建ripple元素之前调用
207
- this.setContainerStyle()
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
- this.container.appendChild(rippleEl)
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.amount--
289
+ this.resetContainerStyle()
280
290
  }
281
291
 
282
292
  const transitionEndOrCancelHandler = (e: TransitionEvent) => {
@@ -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
  }