Tooltip
# 概述
Tooltip
是DivOverlay
的子类,用于显示一个可交互的提示框,同Popup
类似,也是一个覆盖图层。
# 源码分析
# 源码实现
Tooltip
的源码实现如下:
export var Tooltip = DivOverlay.extend({
options: {
pane: "tooltipPane", // 指定渲染的地图窗格
offset: [0, 0], // 像素偏移量
direction: "auto", // 显示位置 (auto|top|bottom|left|right|center)
permanent: false, // 是否永久显示(不依赖鼠标事件)
sticky: false, // 是否跟随鼠标移动(地图拖拽时)
opacity: 0.9, // 透明度
},
onAdd: function (map) {
DivOverlay.prototype.onAdd.call(this.map);
this.setOpacity(this.options.opacity);
map.fire("tooltipopen", { tooltip: false });
if(this._source){
this.addEventParent(this._source);
this._source.fire('tooltipopen',{tooltip:false})
}
},
onRemove:function(map){
DivOverlay.prototype.onRemove.call(this.map);
map.fire("tooltipclose", { tooltip: false });
if(this._source){
this.removeEventParent(this._source);
this._source.fire('tooltipclose',{tooltip:false},true)
}
},
getEvents:function(){
var events = DivOverlay.prototype.getEvents.call(this);
if (!this.options.permanent) {
events.preclick = this.close;
}
return events;
},
_initLayout:function(){
// 创建tooltip容器,添加类名leaflet-tooltip
var prefix = 'leaflet-tooltip',
className = prefix + ' ' + (this.options.className || '') + ' leaflet-zoom-' + (this._zoomAnimated ? 'animated' : 'hide');
this._contentNode = this._container = DomUtil.create('div', className); //
this._container.setAttribute('role', 'tooltip'); // ARIA 支持
this._container.setAttribute('id', 'leaflet-tooltip-' + Util.stamp(this));
},
_adjustPan:function(){},
_setPosition:function(pos){
var subX, subY,
map = this._map,
container = this._container,
centerPoint = map.latLngToContainerPoint(map.getCenter()),
tooltipPoint = map.layerPointToContainerPoint(pos),
direction = this.options.direction,
tooltipWidth = container.offsetWidth,
tooltipHeight = container.offsetHeight,
offset = toPoint(this.options.offset),
anchor = this._getAnchor();
// 根据direction计算subX和subY的值,即便宜基准点的像素偏移量
if (direction === 'top') {
subX = tooltipWidth / 2;
subY = tooltipHeight;
} else if (direction === 'bottom') {
subX = tooltipWidth / 2;
subY = 0;
} else if (direction === 'center') {
subX = tooltipWidth / 2;
subY = tooltipHeight / 2;
} else if (direction === 'right') {
subX = 0;
subY = tooltipHeight / 2;
} else if (direction === 'left') {
subX = tooltipWidth;
subY = tooltipHeight / 2;
} else if (tooltipPoint.x < centerPoint.x) {
direction = 'right';
subX = 0;
subY = tooltipHeight / 2;
} else {
direction = 'left';
subX = tooltipWidth + (offset.x + anchor.x) * 2;
subY = tooltipHeight / 2;
}
pos = pos.subtract(toPoint(subX, subY, true)).add(offset).add(anchor);
DomUtil.removeClass(container, 'leaflet-tooltip-right');
DomUtil.removeClass(container, 'leaflet-tooltip-left');
DomUtil.removeClass(container, 'leaflet-tooltip-top');
DomUtil.removeClass(container, 'leaflet-tooltip-bottom');
DomUtil.addClass(container, 'leaflet-tooltip-' + direction);
// 计算最终位置并设置tooltip容器的位置
DomUtil.setPosition(container, pos);
},
_updatePosition:function(pos){
var pos = this._map.latLngToLayerPoint(this._latlng);
this._setPosition(pos);
},
setOpacity:function(opacity){
this.options.opacity = opacity;
if (this._container) {
DomUtil.setOpacity(this._container, opacity);
}
},
_animateZoom:function(e){
var pos = this._map._latLngToNewLayerPoint(this._latlng, e.zoom, e.center);
this._setPosition(pos);
},
_getAnchor:function(){
return toPoint(this._source && this._source._getTooltipAnchor && !this.options.sticky ? this._source._getTooltipAnchor() : [0, 0]);
},
});
export var tooltip=function(options,source){
return new Tooltip(options,source);
}
Map.include({
openTooltip:function(tooltip,latlng,options){
this._initOverlay(Tooltip, tooltip, latlng, options)
.openOn(this);
return this;
},
closeTooltip:function(tooltip){
tooltip.close();
return this;
},
})
Layer.include({
bindTooltip:function(content,options){
if (this._tooltip && this.isTooltipOpen()) {
this.unbindTooltip();
}
this._tooltip = this._initOverlay(Tooltip, this._tooltip, content, options);
this._initTooltipInteractions();
if (this._tooltip.options.permanent && this._map && this._map.hasLayer(this)) {
this.openTooltip();
}
return this;
},
unbindTooltip:function(){
if (this._tooltip) {
this._initTooltipInteractions(true);
this.closeTooltip();
this._tooltip = null;
}
return this;
},
_initTooltipInteractions:function(remove){
if (!remove && this._tooltipHandlersAdded) { return; }
var onOff = remove ? 'off' : 'on',
events = {
remove: this.closeTooltip,
move: this._moveTooltip
};
if (!this._tooltip.options.permanent) {
events.mouseover = this._openTooltip;
events.mouseout = this.closeTooltip;
events.click = this._openTooltip;
if (this._map) {
this._addFocusListeners();
} else {
events.add = this._addFocusListeners;
}
} else {
events.add = this._openTooltip;
}
if (this._tooltip.options.sticky) {
events.mousemove = this._moveTooltip;
}
this[onOff](events);
this._tooltipHandlersAdded = !remove;
},
openTooltip:function(latlng){
if (this._tooltip) {
if (!(this instanceof FeatureGroup)) {
this._tooltip._source = this;
}
if (this._tooltip._prepareOpen(latlng)) {
this._tooltip.openOn(this._map);
if (this.getElement) {
this._setAriaDescribedByOnLayer(this);
} else if (this.eachLayer) {
this.eachLayer(this._setAriaDescribedByOnLayer, this);
}
}
}
return this;
},
closeTooltip:function(){
if (this._tooltip) {
return this._tooltip.close();
}
},
toggleTooltip:function(){
if (this._tooltip) {
this._tooltip.toggle(this);
}
return this;
},
isTooltipOpen:function(){
return this._tooltip.isOpen();
},
setTooltipContent:function(content){
if (this._tooltip) {
this._tooltip.setContent(content);
}
return this;
},
getTooltip:function(){
return this._tooltip;
},
_addFocusListeners:function(){
if (this.getElement) {
this._addFocusListenersOnLayer(this);
} else if (this.eachLayer) {
this.eachLayer(this._addFocusListenersOnLayer, this);
}
},
_addFocusListenersOnLayer:function(layer){
var el = typeof layer.getElement === 'function' && layer.getElement();
if (el) {
DomEvent.on(el, 'focus', function () {
this._tooltip._source = layer;
this.openTooltip();
}, this);
DomEvent.on(el, 'blur', this.closeTooltip, this);
}
},
_setAriaDescriedByOnLayer:function(layer){
var el = typeof layer.getElement === 'function' && layer.getElement();
if (el) {
el.setAttribute('aria-describedby', this._tooltip._container.id);
}
},
_openTooltip:function(e){
if (!this._tooltip || !this._map) {
return;
}
if (this._map.dragging && this._map.dragging.moving() && !this._openOnceFlag) {
this._openOnceFlag = true;
var that = this;
this._map.once('moveend', function () {
that._openOnceFlag = false;
that._openTooltip(e);
});
return;
}
this._tooltip._source = e.layer || e.target;
this.openTooltip(this._tooltip.options.sticky ? e.latlng : undefined);
},
_moveTooltip:function(e){
var latlng = e.latlng, containerPoint, layerPoint;
if (this._tooltip.options.sticky && e.originalEvent) {
// 计算鼠标位置对应的经纬度
containerPoint = this._map.mouseEventToContainerPoint(e.originalEvent);
layerPoint = this._map.containerPointToLayerPoint(containerPoint);
// 实时更新位置
latlng = this._map.layerPointToLatLng(layerPoint);
}
this._tooltip.setLatLng(latlng);
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
# 源码解析
# 关键设计机制
坐标转换链
1.地理坐标 →Layout Point
map.latLngToLayerPoint(latlng)
转换为相对于地图原点的像素坐标2.Layout Point→Container Point
map.layerPointToContainerPoint(layerPoint)
转换为相对于地图容器的像素坐标
# 与Popup
的对比
特性 | Popup | Tooltip |
---|---|---|
方向自适应 | 固定锚点(需图层定义) | 支持auto 模式 |
位置 | 固定位置 | 动态位置 |
交互 | 鼠标事件,默认鼠标悬停 | 鼠标事件,默认点击 |
生命周期 | 显示后自动关闭 | 显示后需要手动关闭 |
样式 | 固定样式 | 动态样式 |
性能 | 低 | 高 |
粘滞模式 | 不支持 | 支持(sticky:true ) |
DOM结构复杂度 | 包含关闭按钮、箭头等元素 | 单一节点 |
# 性能优化策略
1. 事件节流:地图拖拽时通过 _openOnceFlag
延迟打开,避免频繁计算。
2.CSS 变换:缩放时使用 leaflet-zoom-animated
类实现平滑动画。
3.最小化重绘:仅在方向或内容变化时更新 DOM 位置。
# 总结
Leaflet 的 Tooltip
模块通过以下设计实现高效提示:
1.轻量结构:单一 DOM 节点,复用 DivOverlay
基础能力。
2.智能定位:动态方向计算 + 粘滞模式满足复杂交互场景。
3.分层事件:根据 permanent
和 sticky
配置灵活绑定事件。
4.ARIA
支持:内置无障碍属性,符合现代 Web 标准。
此实现平衡了功能性与性能,成为地图要素信息提示的标准解决方案。
编辑 (opens new window)
上次更新: 2025/05/08, 01:30:03