MouseFollower.js
4.73 KB
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
/* Creates a clone of an element and lets it track the mouse as it moves
----------------------------------------------------------------------------------------------------------------------*/
var MouseFollower = Class.extend({
options: null,
sourceEl: null, // the element that will be cloned and made to look like it is dragging
el: null, // the clone of `sourceEl` that will track the mouse
parentEl: null, // the element that `el` (the clone) will be attached to
// the initial position of el, relative to the offset parent. made to match the initial offset of sourceEl
top0: null,
left0: null,
// the initial position of the mouse
mouseY0: null,
mouseX0: null,
// the number of pixels the mouse has moved from its initial position
topDelta: null,
leftDelta: null,
mousemoveProxy: null, // document mousemove handler, bound to the MouseFollower's `this`
isFollowing: false,
isHidden: false,
isAnimating: false, // doing the revert animation?
constructor: function(sourceEl, options) {
this.options = options = options || {};
this.sourceEl = sourceEl;
this.parentEl = options.parentEl ? $(options.parentEl) : sourceEl.parent(); // default to sourceEl's parent
},
// Causes the element to start following the mouse
start: function(ev) {
if (!this.isFollowing) {
this.isFollowing = true;
this.mouseY0 = ev.pageY;
this.mouseX0 = ev.pageX;
this.topDelta = 0;
this.leftDelta = 0;
if (!this.isHidden) {
this.updatePosition();
}
$(document).on('mousemove', this.mousemoveProxy = proxy(this, 'mousemove'));
}
},
// Causes the element to stop following the mouse. If shouldRevert is true, will animate back to original position.
// `callback` gets invoked when the animation is complete. If no animation, it is invoked immediately.
stop: function(shouldRevert, callback) {
var _this = this;
var revertDuration = this.options.revertDuration;
function complete() {
this.isAnimating = false;
_this.destroyEl();
this.top0 = this.left0 = null; // reset state for future updatePosition calls
if (callback) {
callback();
}
}
if (this.isFollowing && !this.isAnimating) { // disallow more than one stop animation at a time
this.isFollowing = false;
$(document).off('mousemove', this.mousemoveProxy);
if (shouldRevert && revertDuration && !this.isHidden) { // do a revert animation?
this.isAnimating = true;
this.el.animate({
top: this.top0,
left: this.left0
}, {
duration: revertDuration,
complete: complete
});
}
else {
complete();
}
}
},
// Gets the tracking element. Create it if necessary
getEl: function() {
var el = this.el;
if (!el) {
this.sourceEl.width(); // hack to force IE8 to compute correct bounding box
el = this.el = this.sourceEl.clone()
.css({
position: 'absolute',
visibility: '', // in case original element was hidden (commonly through hideEvents())
display: this.isHidden ? 'none' : '', // for when initially hidden
margin: 0,
right: 'auto', // erase and set width instead
bottom: 'auto', // erase and set height instead
width: this.sourceEl.width(), // explicit height in case there was a 'right' value
height: this.sourceEl.height(), // explicit width in case there was a 'bottom' value
opacity: this.options.opacity || '',
zIndex: this.options.zIndex
})
.appendTo(this.parentEl);
}
return el;
},
// Removes the tracking element if it has already been created
destroyEl: function() {
if (this.el) {
this.el.remove();
this.el = null;
}
},
// Update the CSS position of the tracking element
updatePosition: function() {
var sourceOffset;
var origin;
this.getEl(); // ensure this.el
// make sure origin info was computed
if (this.top0 === null) {
this.sourceEl.width(); // hack to force IE8 to compute correct bounding box
sourceOffset = this.sourceEl.offset();
origin = this.el.offsetParent().offset();
this.top0 = sourceOffset.top - origin.top;
this.left0 = sourceOffset.left - origin.left;
}
this.el.css({
top: this.top0 + this.topDelta,
left: this.left0 + this.leftDelta
});
},
// Gets called when the user moves the mouse
mousemove: function(ev) {
this.topDelta = ev.pageY - this.mouseY0;
this.leftDelta = ev.pageX - this.mouseX0;
if (!this.isHidden) {
this.updatePosition();
}
},
// Temporarily makes the tracking element invisible. Can be called before following starts
hide: function() {
if (!this.isHidden) {
this.isHidden = true;
if (this.el) {
this.el.hide();
}
}
},
// Show the tracking element after it has been temporarily hidden
show: function() {
if (this.isHidden) {
this.isHidden = false;
this.updatePosition();
this.getEl().show();
}
}
});