Draggable拖拽实现
# 概述
Draggable
模块是Leaflet中用于实现元素拖拽功能的核心工具函数集合,支持鼠标和触摸操作,并处理了跨浏览器兼容性、事件过滤、状态管理等功能。
# 源码分析
# 源码实现
Draggable
源码实现如下:
var START = Browser.touch ? "touchstart mousedown" : "mousedown";
export var Draggable = Evented.extend({
options: {
clickTolerance: 3, //允许的点击容差(防止误拖拽)
},
initialize: function (element, dragStartTarget, preventOutline, options) {
Util.setOptions(this, options);
this._element = element; // 被拖拽的元素
this._dragStartTarget = dragStartTarget || element; //触发拖拽的目标元素
this._preventOutline = preventOutline; // 是否阻止轮廓线
},
enable: function () {
if (this._enabled) {
return;
}
DomEvent.on(this._dragStartTarget, START, this._onDown, this);
this._enabled = true;
},
disable: function () {
if (!this._enabled) {
return;
}
if (Draggable._dragging === this) {
this.finishDrag(true);
}
DomEvent.off(this._dragStartTarget, START, this._onDown, this);
this._enabled = false;
this._moved = false;
},
_onDown: function (e) {
//过滤无效状态
if (!this._enabled) {
return;
}
this._moved = false;
if (DomUtil.hasClass(this._element, "leaflet-zoom-anim")) {
return;
}
// 过滤多指触控
if (e.touches && e.touches.length !== 1) {
if (Draggable._dragging === this) {
this.finishDrag();
}
return;
}
// 过滤非左键或已有拖拽
if (
Draggable._dragging ||
e.shiftKey ||
(e.which !== 1 && e.button !== 1 && !e.touches)
) {
return;
}
Draggable._dragging = this; // 标记当前拖拽实例
if (this._preventOutline) { // 阻止轮廓线
DomUtil.preventOutline(this._element);
}
DomUtil.disableImageDrag(); // 禁用图片拖拽
DomUtil.disableTextSelection(); // 禁用文本选择
if (this._moving) {
return;
}
this.fire("down");
// 记录初始位置和父级缩放
var first = e.touches ? e.touches[0] : e,
sizedParent = DomUtil.getSizedParentNode(this._element);
this._startPoint = new Point(first.clientX, first.clientY); // 初始指针位置
this._startPos = DomUtil.getPosition(this._element); // 获取元素初始位置
this._parentScale = DomUtil.getScale(sizedParent); // 获取父级缩放比例
// 绑定移动和释放事件
var mouseevent = e.type === "mousedown";
DomEvent.on(
document,
mouseevent ? "mousemove" : "touchmove",
this._onMove,
this
);
DomEvent.on(
document,
mouseevent ? "mouseup" : "touchend touchcancel",
this._onUp,
this
);
},
_onMove: function (e) {
if (!this._enabled) {
return;
}
if (e.touches && e.touches.length > 1) {
this._moved = true;
return;
}
// 计算偏移量
var first = e.touches && e.touches.length === 1 ? e.touches[0] : e,
offset = new Point(first.clientX, first.clientY)._subtract(
this._startPoint
);
// 检查是否超过容差
if (!offset.x && !offset.y) {
return;
}
if (Math.abs(offset.x) + Math.abs(offset.y) < this.options.clickTolerance) {
return;
}
// 应用父级缩放修正
offset.x /= this._parentScale.x;
offset.y /= this._parentScale.y;
DomEvent.preventDefault(e); //阻止默认滚动行为
// 触发拖拽开始
if (!this._moved) {
this.fire("dragstart");
this._moved = true;
DomUtil.addClass(document.body, "leaflet-dragging"); // 全局样式标记
this._lastTarget = e.target || e.srcElement;
if (
window.SVGElementInstance &&
this._lastTarget instanceof window.SVGElementInstance
) {
this._lastTarget = this._lastTarget.correspondingUseElement; // SVG 兼容
}
DomUtil.addClass(this._lastTarget, "leaflet-drag-target"); //目标元素样式
}
this._newPos = this._startPos.add(offset); // 计算新位置
this._moving = true;
this._lastEvent = e;
this._updatePosition(); // 更新元素位置
},
_onUp: function () {
if (!this._enabled) {
return;
}
this.finishDrag();
},
_updatePosition: function (e) {
var e = { originalEvent: this._lastEvent };
this.fire("predrag", e); //预拖拽事件(可修改位置)
DomUtil.setPosition(this._element, this._newPos); // 实际更新元素位置
this.fire("drag", e); //拖拽中事件
},
finishDrag: function (noInertia) {
// 清理样式
DomUtil.removeClass(document.body, "leaflet-dragging");
if (this._lastTarget) {
DomUtil.removeClass(this._lastTarget, "leaflet-drag-target");
this._lastTarget = null;
}
// 解绑事件
DomEvent.off(document, "mousemove touchmove", this._onMove, this);
DomEvent.off(document, "mouseup touchend touchcancel", this._onUp, this);
DomUtil.enableImageDrag();
DomUtil.enableTextSelection();
// 触发拖拽结束事件
var fireDragend = this._moved && this._moving;
this._moving = false;
Draggable._dragging = false;
if (fireDragend) {
this.fire("dragend", {
noInertia: noInertia, //是否禁用惯性滑动
distance: this._newPos.distanceTo(this._startPos), // 拖拽总距离
});
}
},
});
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
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
# 源码详解
- 基础配置与初始化
- 关键点:
START
事件:适配跨端浏览器,根据浏览器是否支持触摸事件,选择事件类型touchstart mousedown
或mousedown
clickTolerance
:允许的点击容差(单位:px
),小于此值的移动视为点击而非拖拽- 初始化参数
dragStartTarget
:允许拖拽触发元素与目标元素分离(例如拖拽手柄)preventOutline
: 拖拽时阻止元素轮廓线(如聚焦时的边框)
- 启用/禁用拖拽
逻辑:
- enable():绑定拖拽开始事件(
START
)到目标元素。 - disable():解绑事件,强制结束拖拽,并重置状态。
- enable():绑定拖拽开始事件(
关键点:
- Draggable._dragging:静态变量,确保同一时间只有一个拖拽实例激活。
- 拖拽开始(
_onDown
)
- 核心逻辑:
- 过滤无效操作: 检查是否启用、是否在动画中、是否多指触控、是否非左键点击
- 初始化状态
_startPoint
:记录初始指针位置_startPos
:获取元素初始位置- 计算父级容器的缩放比例(
_parentScale
),用于后续坐标转换
- 绑定全局事件:在
document
上绑定mousemove/touchmove
和mouseup/touchend
- 拖拽移动(
_onMove
)
- 核心逻辑:
- 过滤无效操作:检查是否启用、是否多指触控、是否移动距离小于容差
- 计算偏移量:基于初始位置计算位移,并应用父级缩放修正
- 容差检查
- 样式标记
- 为
document.body
添加leaflet-dragging
类,可能在css 中定义拖拽时的全局样式 - 为目标元素添加
leaflet-drag-target
类,高亮拖拽目标
- 为
- 位置更新:计算新位置并调用
_updatePosition
- 位置更新(
_updatePosition
)
- 作用:通过
DomUtil.setPosition
更新元素位置,并触发事件predrag
:允许外部逻辑在更新位置前修改this._newPos
drag
:通知外部拖拽进行中
- 拖拽结束(
_onUp
和finishDrag
)
- 核心逻辑:
- 清理状态:移除样式标记,解绑事件,恢复浏览器默认行为。
- 触发事件:若实际发生了拖拽(
_moved
和_moving
为true
),触发dragend
事件。 - 参数传递:
noInertia
:可用于标记是否需要惯性滑动(代码中未实现,需外部处理)。distance
:拖拽总距离,供外部逻辑使用。
# 关键设计思想
跨平台兼容性:
- 统一处理
touch
和mouse
事件,适配移动端和桌面端。 - 处理 SVG 元素的兼容性(如
correspondingUseElement
)。
- 统一处理
性能优化:
- 仅在拖拽开始时绑定全局事件,减少不必要的监听。
- 使用
clickTolerance
避免误触拖拽。
事件驱动架构:
- 继承自
Evented
,通过fire
方法触发事件(dragstart
、drag
、dragend
)。 - 支持外部通过事件监听修改拖拽行为(如
predrag
)。
- 继承自
状态管理:
- 使用
_enabled
、_moved
、_moving
等状态变量精确控制拖拽生命周期。 - 静态变量
Draggable._dragging
确保单例拖拽。
- 使用
# 使用场景
- 地图拖拽:Leaflet 地图容器的拖拽平移。
- 标记拖拽:允许用户拖拽地图上的标记(Marker)调整位置。
- 自定义控件:实现可拖拽的工具栏或面板。
# 潜在问题与注意事项
惯性滑动:代码中未实现惯性滑动效果,需通过
dragend
事件自行处理。嵌套缩放容器:若父级容器有
CSS
缩放(transform: scale
),需通过_parentScale
修正坐标。浏览器兼容性:
- 依赖
DomUtil.getScale
计算父级缩放,需确保该方法正确实现。 - 部分旧浏览器可能不支持
touch
事件,需Polyfill
。
- 依赖
# 总结
Draggable
模块提供了一种简单且高效的方式来实现元素拖拽功能,支持鼠标和触摸操作,同时提供了事件驱动的架构,方便外部逻辑定制拖拽行为。
编辑 (opens new window)
上次更新: 2025/03/21, 06:53:11