leafer-x-tooltip-canvas 1.0.1 → 1.0.2

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "leafer-x-tooltip-canvas",
3
3
  "description": "A tooltip plugin for Leafer-ui.",
4
- "version": "1.0.1",
4
+ "version": "1.0.2",
5
5
  "author": "214L",
6
6
  "license": "MIT",
7
7
  "type": "module",
@@ -29,35 +29,35 @@
29
29
  "test": "vitest"
30
30
  },
31
31
  "devDependencies": {
32
- "@rollup/plugin-commonjs": "^25.0.4",
33
- "@rollup/plugin-html": "^1.0.2",
32
+ "@rollup/plugin-commonjs": "^28.0.9",
33
+ "@rollup/plugin-html": "^2.0.0",
34
34
  "@rollup/plugin-multi-entry": "^6.0.0",
35
- "@rollup/plugin-node-resolve": "^15.2.1",
36
- "@rollup/plugin-terser": "^0.4.3",
37
- "@rollup/plugin-typescript": "^11.1.3",
35
+ "@rollup/plugin-node-resolve": "^16.0.3",
36
+ "@rollup/plugin-terser": "^0.4.4",
37
+ "@rollup/plugin-typescript": "^12.3.0",
38
38
  "@typescript-eslint/eslint-plugin": "^5.54.1",
39
39
  "@typescript-eslint/parser": "^5.54.1",
40
- "rollup": "^3.29.2",
41
- "rollup-plugin-dts": "^6.0.2",
40
+ "cross-env": "^7.0.3",
41
+ "eslint": "^8.49.0",
42
+ "jsdom": "^22.0.0",
43
+ "leafer-ui": "^1.12.2",
44
+ "rollup": "^4.54.0",
45
+ "rollup-plugin-copy": "^3.5.0",
46
+ "rollup-plugin-dts": "^6.3.0",
42
47
  "rollup-plugin-livereload": "^2.0.5",
43
48
  "rollup-plugin-serve": "^2.0.2",
44
49
  "rollup-plugin-string": "^3.0.0",
45
- "rollup-plugin-copy": "^3.5.0",
46
50
  "tslib": "^2.6.2",
47
51
  "typescript": "^5.2.2",
48
52
  "vite": "^4.4.9",
49
- "vitest": "^0.34.4",
50
- "jsdom": "^22.0.0",
51
- "cross-env": "^7.0.3",
52
- "eslint": "^8.49.0",
53
- "leafer-ui": "^1.0.4"
53
+ "vitest": "^0.34.4"
54
54
  },
55
55
  "dependencies": {
56
- "@leafer-ui/core": "^1.0.4",
57
- "leafer-ui": "^1.0.4",
58
- "@leafer-in/flow": "^1.0.4"
56
+ "@leafer-in/flow": "^1.12.2",
57
+ "@leafer-ui/core": "^1.12.2",
58
+ "leafer-ui": "^1.12.2"
59
59
  },
60
60
  "publishConfig": {
61
61
  "registry": "https://registry.npmjs.org"
62
62
  }
63
- }
63
+ }
package/src/Tooltip.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  } from 'leafer-ui'
10
10
  import { IPos, IUserConfig } from './interface'
11
11
  import { handleTextStyle } from './utils'
12
+ import { TOOLTIP_TAG, TOOLTIP_CLASS_NAME } from './constants'
12
13
 
13
14
  interface ITooltip extends IPen {
14
15
  target?: ILeaf
@@ -40,9 +41,9 @@ export class TooltipData extends PenData implements ITooltipData {
40
41
  @registerUI()
41
42
  export class Tooltip extends Pen implements ITooltip {
42
43
  public get __tag() {
43
- return 'Tooltip'
44
+ return TOOLTIP_TAG
44
45
  }
45
- public className: 'leafer-x-tooltip'
46
+ public className: typeof TOOLTIP_CLASS_NAME
46
47
  @dataProcessor(TooltipData)
47
48
  public declare __: ITooltipData
48
49
 
@@ -93,7 +94,7 @@ export class Tooltip extends Pen implements ITooltip {
93
94
  })
94
95
  this.add(
95
96
  new Text({
96
- className: 'leafer-x-tooltip',
97
+ className: TOOLTIP_CLASS_NAME,
97
98
  fill: color,
98
99
  fontSize,
99
100
  fontWeight,
@@ -130,8 +131,7 @@ export class Tooltip extends Pen implements ITooltip {
130
131
  this.clearShowHideTimers()
131
132
  this.showTimerId = setTimeout(() => {
132
133
  this.createShapes(pos)
133
- clearTimeout(this.showTimerId)
134
- this.showTimerId = null
134
+ this.showTimerId = null // 定时器执行完毕,清空引用
135
135
  }, this.config.showDelay)
136
136
  }
137
137
 
@@ -140,19 +140,21 @@ export class Tooltip extends Pen implements ITooltip {
140
140
  if (immediate) {
141
141
  this.destroy()
142
142
  } else {
143
- if (!this.hideTimerId) {
144
- this.hideTimerId = setTimeout(() => {
145
- this.destroy()
146
- }, this.config.hideDelay)
147
- }
143
+ // 移除 if 判断,确保每次 hide() 都能正确设置定时器
144
+ this.hideTimerId = setTimeout(() => {
145
+ this.destroy()
146
+ this.hideTimerId = null
147
+ }, this.config.hideDelay)
148
148
  }
149
149
  }
150
150
 
151
151
  public update(pos: IPos) {
152
152
  this.clearShowHideTimers()
153
153
  if (this.isShow) {
154
+ // 已显示,立即更新位置
154
155
  this.createShapes(pos)
155
156
  } else {
157
+ // 未显示,启动 show 定时器
156
158
  this.show(pos)
157
159
  }
158
160
  }
@@ -4,6 +4,7 @@ import { IUserConfig } from './interface'
4
4
  import { Tooltip } from './Tooltip'
5
5
  import { getTooltipId } from './utils'
6
6
  import { defaultConfig } from './defaultConfig'
7
+ import { TOOLTIP_TAG, TOOLTIP_CLASS_NAME } from './constants'
7
8
 
8
9
  export class TooltipPlugin {
9
10
  /**
@@ -24,29 +25,85 @@ export class TooltipPlugin {
24
25
  */
25
26
  private readonly pointEventId: IEventListenerId
26
27
 
28
+ /**
29
+ * @param tooltipCache - Tooltip 实例缓存,避免重复 DOM 查询
30
+ * @private
31
+ */
32
+ private tooltipCache: Map<string, Tooltip> = new Map()
33
+
34
+ /**
35
+ * @param 类型过滤 Set 缓存,将数组转为 Set 提升查询性能 O(n) -> O(1)
36
+ * @private
37
+ */
38
+ private includesTypeSet: Set<string> = new Set()
39
+ private excludesTypeSet: Set<string> = new Set()
40
+ private ignoreTypeSet: Set<string> = new Set()
41
+
27
42
  constructor(instance: ILeafer | App, config?: IUserConfig) {
28
43
  this.instance = instance
29
- this.config = Object.assign({}, defaultConfig, config)
44
+ this.config = this.mergeConfig(defaultConfig, config)
30
45
  this.handleConfig()
31
46
  this.initState()
32
47
  this.pointEventId = this.initEvent()
33
48
  }
34
49
 
50
+ /**
51
+ * @description 深度合并配置,避免实例间相互影响
52
+ * @private
53
+ */
54
+ private mergeConfig(base: IUserConfig, override?: IUserConfig): IUserConfig {
55
+ if (!override) {
56
+ // 深拷贝默认配置
57
+ return {
58
+ ...base,
59
+ style: { ...base.style },
60
+ includesType: [...(base.includesType || [])],
61
+ excludesType: [...(base.excludesType || [])],
62
+ ignoreType: [...(base.ignoreType || [])],
63
+ info: [...(base.info || [])],
64
+ }
65
+ }
66
+
67
+ // 深度合并用户配置
68
+ return {
69
+ ...base,
70
+ ...override,
71
+ style: {
72
+ ...base.style,
73
+ ...override.style,
74
+ },
75
+ includesType: override.includesType || base.includesType,
76
+ excludesType: override.excludesType || base.excludesType,
77
+ ignoreType: override.ignoreType || base.ignoreType,
78
+ info: override.info || base.info,
79
+ }
80
+ }
81
+
82
+ /**
83
+ * @description 类型守卫: 判断实例是否为 App
84
+ * @private
85
+ */
86
+ private isAppInstance(instance: ILeafer | App): instance is App {
87
+ return instance.isApp === true
88
+ }
89
+
35
90
  /**
36
91
  * @description 初始化状态
37
92
  */
38
93
  private initState() {
39
- if (this.instance.isApp) {
40
- const app = this.instance as App
41
- if (app.sky === undefined) {
42
- app.sky = app.addLeafer({
94
+ if (this.isAppInstance(this.instance)) {
95
+ // TypeScript 自动推断 this.instance App 类型
96
+ if (this.instance.sky === undefined) {
97
+ this.instance.sky = this.instance.addLeafer({
43
98
  type: 'draw',
44
99
  usePartRender: false,
45
100
  })
46
101
  }
47
- this.aimLeafer = app.sky
102
+ this.aimLeafer = this.instance.sky
48
103
  } else if (this.instance.isLeafer) {
49
104
  this.aimLeafer = this.instance
105
+ } else {
106
+ throw new Error('TooltipPlugin: Instance must be either App or Leafer')
50
107
  }
51
108
  }
52
109
 
@@ -55,6 +112,11 @@ export class TooltipPlugin {
55
112
  this.config.style.backgroundColor = 'black'
56
113
  this.config.style.color = 'white'
57
114
  }
115
+
116
+ // 初始化类型过滤 Set,提升查询性能
117
+ this.includesTypeSet = new Set(this.config.includesType || [])
118
+ this.excludesTypeSet = new Set(this.config.excludesType || [])
119
+ this.ignoreTypeSet = new Set(this.config.ignoreType || [])
58
120
  }
59
121
 
60
122
  /**
@@ -92,14 +154,18 @@ export class TooltipPlugin {
92
154
  this.handleTooltip(event, target)
93
155
  }
94
156
  private filterTarget(list: ILeaf[]): ILeaf | null {
95
- const { ignoreType, excludesType, throughExcludes } = this.config
96
- const arr = throughExcludes ? ignoreType.concat(excludesType) : ignoreType
97
-
157
+ const { throughExcludes } = this.config
158
+
98
159
  const pureResult = list.filter((item) => {
160
+ // 使用 Set.has() 代替 Array.includes(),性能提升
161
+ const shouldIgnore = this.ignoreTypeSet.has(item?.tag)
162
+ const shouldExclude = throughExcludes && this.excludesTypeSet.has(item?.tag)
163
+
99
164
  if (
100
- arr.includes(item?.tag) ||
101
- item?.parent?.tag === 'Tooltip' ||
102
- item?.className === 'leafer-x-tooltip'
165
+ shouldIgnore ||
166
+ shouldExclude ||
167
+ item?.parent?.tag === TOOLTIP_TAG ||
168
+ item?.className === TOOLTIP_CLASS_NAME
103
169
  ) {
104
170
  return false
105
171
  }
@@ -114,28 +180,48 @@ export class TooltipPlugin {
114
180
  * @param target 目标节点
115
181
  * @returns
116
182
  */
117
- private handleAllowed(target: ILeaf) {
118
- const infoArr = ['#' + target.id, '.' + target.className, target.tag]
119
- const { includesType, excludesType } = this.config
183
+ private handleAllowed(target: ILeaf): boolean {
184
+ const targetIdentifiers = ['#' + target.id, '.' + target.className, target.tag]
185
+
186
+ const hasIncludesRule = this.includesTypeSet.size > 0
187
+ const hasExcludesRule = this.excludesTypeSet.size > 0
188
+
189
+ // 如果没有配置任何过滤规则,默认允许
190
+ if (!hasIncludesRule && !hasExcludesRule) {
191
+ return true
192
+ }
120
193
 
121
- if (includesType.length === 0 && excludesType.length === 0) return true
194
+ const matchesInclude = targetIdentifiers.some((id) => this.includesTypeSet.has(id))
195
+ const matchesExclude = targetIdentifiers.some((id) => this.excludesTypeSet.has(id))
122
196
 
123
- const isInclude = infoArr.some((string) => includesType.includes(string))
124
- const isExclude = infoArr.some((string) => excludesType.includes(string))
197
+ // includes 优先级高于 excludes
198
+ if (matchesInclude) return true // 匹配白名单,直接允许(即使也在黑名单中)
199
+ if (matchesExclude) return false // 不在白名单但在黑名单,拒绝
125
200
 
126
- if (!isExclude && includesType.length === 0) return true
127
- if (!isInclude && excludesType.length === 0) return false
128
- return isInclude || !isExclude
201
+ // 两者都不匹配的情况
202
+ if (hasIncludesRule) return false // 有白名单规则但不匹配 拒绝
203
+ return true // 只有黑名单规则且不匹配 → 允许
129
204
  }
130
205
 
131
206
  /**
132
207
  * @description 隐藏 tooltip
133
208
  */
134
209
  private hideTooltip() {
135
- const tooltipList = this.aimLeafer.find('Tooltip') as Tooltip[]
136
- tooltipList.forEach((tooltip: Tooltip) => {
137
- tooltip.hide()
210
+ // 使用缓存而不是 DOM 查询,提升性能
211
+ // 同时清理已销毁的实例
212
+ const invalidIds: string[] = []
213
+
214
+ this.tooltipCache.forEach((tooltip, id) => {
215
+ if (tooltip.parent) {
216
+ tooltip.hide()
217
+ } else {
218
+ // 标记无效的缓存条目
219
+ invalidIds.push(id)
220
+ }
138
221
  })
222
+
223
+ // 清理无效缓存
224
+ invalidIds.forEach(id => this.tooltipCache.delete(id))
139
225
  }
140
226
 
141
227
  /**
@@ -143,26 +229,33 @@ export class TooltipPlugin {
143
229
  */
144
230
  private handleTooltip(event: PointerEvent, target: ILeaf) {
145
231
  const id = getTooltipId(target)
146
- const tooltipList = this.aimLeafer.find('Tooltip') as Tooltip[]
147
- let processed = false
148
- for (const tooltip of tooltipList) {
149
- if (tooltip.id === id) {
150
- tooltip.update({ x: event.x, y: event.y })
151
- processed = true
152
- } else {
232
+
233
+ // 隐藏其他 tooltip
234
+ this.tooltipCache.forEach((tooltip, cacheId) => {
235
+ if (cacheId !== id) {
153
236
  tooltip.hide()
154
237
  }
155
- }
238
+ })
156
239
 
157
- if (!processed) {
158
- this.aimLeafer.add(
159
- new Tooltip({
160
- id,
161
- pointerPos: { x: event.x, y: event.y },
162
- target,
163
- config: this.config,
164
- })
165
- )
240
+ // 检查缓存中的实例是否仍然有效(未被销毁)
241
+ const cachedTooltip = this.tooltipCache.get(id)
242
+ if (cachedTooltip && cachedTooltip.parent) {
243
+ // 实例有效,直接更新
244
+ cachedTooltip.update({ x: event.x, y: event.y })
245
+ } else {
246
+ // 实例已被销毁或不存在,移除无效缓存并创建新实例
247
+ if (cachedTooltip) {
248
+ this.tooltipCache.delete(id)
249
+ }
250
+
251
+ const tooltip = new Tooltip({
252
+ id,
253
+ pointerPos: { x: event.x, y: event.y },
254
+ target,
255
+ config: this.config,
256
+ })
257
+ this.aimLeafer.add(tooltip)
258
+ this.tooltipCache.set(id, tooltip)
166
259
  }
167
260
  }
168
261
 
@@ -170,14 +263,24 @@ export class TooltipPlugin {
170
263
  * @description 销毁
171
264
  */
172
265
  public destroy() {
173
- const tooltipList = this.aimLeafer.find('Tooltip') as Tooltip[]
174
- if (tooltipList) {
175
- tooltipList.forEach((tooltip) => {
266
+ // 防止重复销毁
267
+ if (!this.instance) return
268
+
269
+ // 清理缓存中的所有 tooltip
270
+ if (this.tooltipCache) {
271
+ this.tooltipCache.forEach((tooltip) => {
176
272
  tooltip.destroyTooltip()
177
- tooltip.parent.remove(tooltip)
273
+ // 使用可选链,防止 parent 为 null/undefined
274
+ tooltip.parent?.remove(tooltip)
178
275
  })
276
+ this.tooltipCache.clear()
179
277
  }
180
- this.instance.off_(this.pointEventId)
278
+
279
+ // 确保事件被正确清理
280
+ if (this.pointEventId) {
281
+ this.instance.off_(this.pointEventId)
282
+ }
283
+
181
284
  this.instance = null
182
285
  this.aimLeafer = null
183
286
  }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @description 插件常量定义
3
+ */
4
+
5
+ /**
6
+ * Tooltip 标签名
7
+ */
8
+ export const TOOLTIP_TAG = 'Tooltip'
9
+
10
+ /**
11
+ * Tooltip class 名称
12
+ */
13
+ export const TOOLTIP_CLASS_NAME = 'leafer-x-tooltip'
14
+
15
+ /**
16
+ * 显示类型枚举
17
+ */
18
+ export enum ShowType {
19
+ VALUE = 'value',
20
+ KEY_VALUE = 'key-value',
21
+ }
package/src/utils.ts CHANGED
@@ -1,6 +1,13 @@
1
1
  import { Box } from 'leafer-ui'
2
2
  import { ILeaf } from '@leafer-ui/interface'
3
3
  import { IUserConfig } from './interface'
4
+ import { ShowType } from './constants'
5
+
6
+ /**
7
+ * @description 文本尺寸缓存,避免重复计算
8
+ */
9
+ const textSizeCache = new Map<string, { width: number; height: number }>()
10
+
4
11
  /**
5
12
  * @description 获取uuid 考虑兼容性问题采用此方法
6
13
  * @param length id长度
@@ -9,44 +16,98 @@ import { IUserConfig } from './interface'
9
16
  export const getTooltipId = function (target: ILeaf) {
10
17
  return target.tag + target.innerId
11
18
  }
19
+
12
20
  export const handleTextStyle = function (target: ILeaf, config: IUserConfig) {
21
+ // 参数校验
22
+ if (!target || !config) {
23
+ console.error('handleTextStyle: Invalid parameters')
24
+ return { width: 100, height: 30, text: '' }
25
+ }
26
+
13
27
  const str = handleContent(target, config)
14
28
  const { fontSize, fontFamily, fontWeight, padding } = config.style
15
- const box = new Box({
16
- children: [
17
- {
18
- tag: 'Text',
19
- text: str,
20
- fontSize,
21
- fontFamily,
22
- fontWeight,
23
- padding,
24
- },
25
- ],
26
- })
27
-
28
- const { width, height } = box.getBounds()
29
- return { width, height, text: str }
29
+
30
+ // 生成缓存 key,包含所有影响尺寸的因素
31
+ const cacheKey = `${str}:${fontSize}:${fontFamily}:${fontWeight}:${padding}`
32
+
33
+ // 检查缓存
34
+ if (textSizeCache.has(cacheKey)) {
35
+ const cached = textSizeCache.get(cacheKey)!
36
+ return { ...cached, text: str }
37
+ }
38
+
39
+ try {
40
+ const box = new Box({
41
+ children: [
42
+ {
43
+ tag: 'Text',
44
+ text: str,
45
+ fontSize,
46
+ fontFamily,
47
+ fontWeight,
48
+ padding,
49
+ },
50
+ ],
51
+ })
52
+
53
+ const bounds = box.getBounds()
54
+ // 检查 bounds 是否有效
55
+ if (!bounds || bounds.width === undefined || bounds.height === undefined) {
56
+ console.warn('handleTextStyle: Invalid bounds, using default size')
57
+ return { width: 100, height: 30, text: str }
58
+ }
59
+
60
+ const { width, height } = bounds
61
+
62
+ // 存入缓存
63
+ textSizeCache.set(cacheKey, { width, height })
64
+
65
+ return { width, height, text: str }
66
+ } catch (error) {
67
+ console.error('handleTextStyle: Failed to calculate text size', error)
68
+ return { width: 100, height: 30, text: str }
69
+ }
30
70
  }
31
71
 
32
72
  function handleContent(target: ILeaf, config: IUserConfig) {
33
73
  let str = ''
34
- const data = target as { [key: string]: any }
35
-
74
+ // 保持 ILeaf 类型,使用 Record 进行更安全的索引访问
75
+ const data = target as ILeaf & Record<string, unknown>
76
+
36
77
  // 如果formatter函数存在,则使用formatter函数进行格式化
37
- if (config.formatter(data) !== undefined) {
38
- str = config.formatter(data)
39
- } else {
40
- // 如果formatter函数不存在,则根据showType进行默认格式化
41
- if (config.showType == 'value') {
78
+ if (config.formatter && typeof config.formatter === 'function') {
79
+ try {
80
+ const formatted = config.formatter(data)
81
+ if (formatted !== undefined) {
82
+ str = formatted
83
+ }
84
+ } catch (error) {
85
+ console.error('handleContent: Formatter function error', error)
86
+ // formatter 失败时降级到默认格式化
87
+ }
88
+ }
89
+
90
+ // 如果formatter函数不存在或执行失败,则根据showType进行默认格式化
91
+ if (!str) {
92
+ if (config.showType === ShowType.VALUE) {
42
93
  str += config.info
43
- .map((dataName: string) => `${data[dataName]}`)
94
+ .map((dataName: string) => {
95
+ const value = data[dataName]
96
+ // 检查值是否存在且可转换为字符串
97
+ return value !== null && value !== undefined ? String(value) : ''
98
+ })
44
99
  .join('\n')
45
- } else if (config.showType == 'key-value') {
100
+ } else if (config.showType === ShowType.KEY_VALUE) {
46
101
  str += config.info
47
- .map((dataName: string) => `${dataName} : ${data[dataName]}`)
102
+ .map((dataName: string) => {
103
+ const value = data[dataName]
104
+ // 检查值是否存在且可转换为字符串
105
+ const displayValue = value !== null && value !== undefined ? String(value) : ''
106
+ return `${dataName} : ${displayValue}`
107
+ })
48
108
  .join('\n')
49
109
  }
50
110
  }
111
+
51
112
  return str
52
113
  }
package/types/index.d.ts CHANGED
@@ -35,7 +35,13 @@ declare class TooltipPlugin {
35
35
  private aimLeafer;
36
36
  private readonly config;
37
37
  private readonly pointEventId;
38
+ private tooltipCache;
39
+ private includesTypeSet;
40
+ private excludesTypeSet;
41
+ private ignoreTypeSet;
38
42
  constructor(instance: ILeafer | App, config?: IUserConfig);
43
+ private mergeConfig;
44
+ private isAppInstance;
39
45
  private initState;
40
46
  private handleConfig;
41
47
  private initEvent;