-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathDraggable.js
260 lines (226 loc) · 8.94 KB
/
Draggable.js
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
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* Owner: [email protected]
* @license MPL 2.0
* @copyright Famous Industries, Inc. 2014
*/
define(function(require, exports, module) {
var Transform = require('famous/core/Transform');
var Transitionable = require('famous/transitions/Transitionable');
var EventHandler = require('famous/core/EventHandler');
var Utilities = require('famous/math/Utilities');
var GenericSync = require('famous/inputs/GenericSync');
var MouseSync = require('famous/inputs/MouseSync');
var TouchSync = require('famous/inputs/TouchSync');
GenericSync.register({'mouse': MouseSync, 'touch': TouchSync});
/**
* Makes added render nodes responsive to drag beahvior.
* Emits events 'start', 'update', 'end'.
* @class Draggable
* @constructor
* @param {Object} [options] options configuration object.
* @param {Number} [options.snapX] grid width for snapping during drag
* @param {Number} [options.snapY] grid height for snapping during drag
* @param {Array.Number} [options.xRange] maxmimum [negative, positive] x displacement from start of drag
* @param {Array.Number} [options.yRange] maxmimum [negative, positive] y displacement from start of drag
* @param {Number} [options.scale] one pixel of input motion translates to this many pixels of output drag motion
* @param {Number} [options.projection] User should set to Draggable._direction.x or
* Draggable._direction.y to constrain to one axis.
*
*/
function Draggable(options) {
this.options = Object.create(Draggable.DEFAULT_OPTIONS);
if (options) this.setOptions(options);
this._positionState = new Transitionable([0,0]);
this._differential = [0,0];
this._active = true;
this.sync = new GenericSync(['mouse', 'touch'], {scale : this.options.scale});
this.eventOutput = new EventHandler();
EventHandler.setInputHandler(this, this.sync);
EventHandler.setOutputHandler(this, this.eventOutput);
_bindEvents.call(this);
}
//binary representation of directions for bitwise operations
var _direction = {
x : 0x01, //001
y : 0x02 //010
};
Draggable.DIRECTION_X = _direction.x;
Draggable.DIRECTION_Y = _direction.y;
var _clamp = Utilities.clamp;
Draggable.DEFAULT_OPTIONS = {
projection : _direction.x | _direction.y,
scale : 1,
xRange : null,
yRange : null,
snapX : 0,
snapY : 0,
transition : {duration : 0}
};
function _mapDifferential(differential) {
var opts = this.options;
var projection = opts.projection;
var snapX = opts.snapX;
var snapY = opts.snapY;
//axes
var tx = (projection & _direction.x) ? differential[0] : 0;
var ty = (projection & _direction.y) ? differential[1] : 0;
//snapping
if (snapX > 0) tx -= tx % snapX;
if (snapY > 0) ty -= ty % snapY;
return [tx, ty];
}
function _handleStart() {
if (!this._active) return;
if (this._positionState.isActive()) this._positionState.halt();
this.eventOutput.emit('start', {position : this.getPosition()});
}
function _handleMove(event) {
if (!this._active) return;
var options = this.options;
this._differential = event.position;
var newDifferential = _mapDifferential.call(this, this._differential);
//buffer the differential if snapping is set
this._differential[0] -= newDifferential[0];
this._differential[1] -= newDifferential[1];
var pos = this.getPosition();
//modify position, retain reference
pos[0] += newDifferential[0];
pos[1] += newDifferential[1];
//handle bounding box
if (options.xRange){
var xRange = [options.xRange[0] + 0.5 * options.snapX, options.xRange[1] - 0.5 * options.snapX];
pos[0] = _clamp(pos[0], xRange);
}
if (options.yRange){
var yRange = [options.yRange[0] + 0.5 * options.snapY, options.yRange[1] - 0.5 * options.snapY];
pos[1] = _clamp(pos[1], yRange);
}
this.eventOutput.emit('update', {position : pos});
}
function _handleEnd() {
if (!this._active) return;
this.eventOutput.emit('end', {position : this.getPosition()});
}
function _bindEvents() {
this.sync.on('start', _handleStart.bind(this));
this.sync.on('update', _handleMove.bind(this));
this.sync.on('end', _handleEnd.bind(this));
}
/**
* Set internal options, overriding any default options
*
* @method setOptions
*
* @param {Object} [options] overrides of default options. See constructor.
*/
Draggable.prototype.setOptions = function setOptions(options) {
var currentOptions = this.options;
if (options.projection !== undefined) {
var proj = options.projection;
this.options.projection = 0;
['x', 'y'].forEach(function(val) {
if (proj.indexOf(val) !== -1) currentOptions.projection |= _direction[val];
});
}
if (options.scale !== undefined) {
currentOptions.scale = options.scale;
this.sync.setOptions({
scale: options.scale
});
}
if (options.xRange !== undefined) currentOptions.xRange = options.xRange;
if (options.yRange !== undefined) currentOptions.yRange = options.yRange;
if (options.snapX !== undefined) currentOptions.snapX = options.snapX;
if (options.snapY !== undefined) currentOptions.snapY = options.snapY;
};
/**
* Get current delta in position from where this draggable started.
*
* @method getPosition
*
* @return {array<number>} [x, y] position delta from start.
*/
Draggable.prototype.getPosition = function getPosition() {
return this._positionState.get();
};
/**
* Transition the element to the desired relative position via provided transition.
* For example, calling this with [0,0] will not change the position.
* Callback will be executed on completion.
*
* @method setRelativePosition
*
* @param {array<number>} position end state to which we interpolate
* @param {transition} transition transition object specifying how object moves to new position
* @param {function} callback zero-argument function to call on observed completion
*/
Draggable.prototype.setRelativePosition = function setRelativePosition(position, transition, callback) {
var currPos = this.getPosition();
var relativePosition = [currPos[0] + position[0], currPos[1] + position[1]];
this.setPosition(relativePosition, transition, callback);
};
/**
* Transition the element to the desired absolute position via provided transition.
* Callback will be executed on completion.
*
* @method setPosition
*
* @param {array<number>} position end state to which we interpolate
* @param {transition} transition transition object specifying how object moves to new position
* @param {function} callback zero-argument function to call on observed completion
*/
Draggable.prototype.setPosition = function setPosition(position, transition, callback) {
if (this._positionState.isActive()) this._positionState.halt();
this._positionState.set(position, transition, callback);
};
/**
* Set this draggable to respond to user input.
*
* @method activate
*
*/
Draggable.prototype.activate = function activate() {
this._active = true;
};
/**
* Set this draggable to ignore user input.
*
* @method deactivate
*
*/
Draggable.prototype.deactivate = function deactivate() {
this._active = false;
};
/**
* Switch the input response stage between active and inactive.
*
* @method toggle
*
*/
Draggable.prototype.toggle = function toggle() {
this._active = !this._active;
};
/**
* Return render spec for this Modifier, applying to the provided
* target component. This is similar to render() for Surfaces.
*
* @private
* @method modify
*
* @param {Object} target (already rendered) render spec to
* which to apply the transform.
* @return {Object} render spec for this Modifier, including the
* provided target
*/
Draggable.prototype.modify = function modify(target) {
var pos = this.getPosition();
return {
transform: Transform.translate(pos[0], pos[1]),
target: target
};
};
module.exports = Draggable;
});