formagic 0.3.9 → 0.3.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (26) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/app/assets/javascripts/formagic.coffee +5 -2
  4. data/app/assets/javascripts/formagic/form.coffee +4 -1
  5. data/app/assets/javascripts/formagic/inputs/ace-css.coffee +25 -0
  6. data/app/assets/javascripts/formagic/inputs/ace-html.coffee +25 -0
  7. data/app/assets/javascripts/formagic/inputs/ace-js.coffee +25 -0
  8. data/app/assets/javascripts/formagic/inputs/ace-markdown.coffee +51 -0
  9. data/app/assets/javascripts/formagic/inputs/{markdown_toolbar.coffee → ace-markdown_toolbar.coffee} +27 -7
  10. data/app/assets/javascripts/formagic/inputs/{html.coffee → ace.coffee} +27 -29
  11. data/app/assets/javascripts/formagic/inputs/documents_reorder.coffee +3 -0
  12. data/app/assets/javascripts/formagic/inputs/list_reorder.coffee +2 -0
  13. data/app/assets/javascripts/formagic/inputs/url.coffee +1 -1
  14. data/app/assets/javascripts/vendor/ace.js +264 -123
  15. data/app/assets/javascripts/vendor/mode-css.js +1008 -0
  16. data/app/assets/javascripts/vendor/mode-html.js +488 -129
  17. data/app/assets/javascripts/vendor/mode-javascript.js +1154 -0
  18. data/app/assets/javascripts/vendor/mode-markdown.js +489 -129
  19. data/app/assets/javascripts/vendor/slip.js +792 -0
  20. data/app/assets/stylesheets/formagic.scss +9 -1
  21. data/app/assets/stylesheets/formagic/{nested-form.scss → documents.scss} +0 -0
  22. data/app/assets/stylesheets/formagic/markdown.scss +4 -1
  23. data/app/assets/stylesheets/formagic/switch.scss +1 -1
  24. data/lib/formagic/version.rb +1 -1
  25. metadata +12 -6
  26. data/app/assets/javascripts/formagic/inputs/markdown.coffee +0 -94
@@ -0,0 +1,792 @@
1
+ /*
2
+ Slip - swiping and reordering in lists of elements on touch screens, no fuss.
3
+
4
+ Fires these events on list elements:
5
+
6
+ • slip:swipe
7
+ When swipe has been done and user has lifted finger off the screen.
8
+ If you execute event.preventDefault() the element will be animated back to original position.
9
+ Otherwise it will be animated off the list and set to display:none.
10
+
11
+ • slip:beforeswipe
12
+ Fired before first swipe movement starts.
13
+ If you execute event.preventDefault() then element will not move at all.
14
+
15
+ • slip:reorder
16
+ Element has been dropped in new location. event.detail contains the location:
17
+ • insertBefore: DOM node before which element has been dropped (null is the end of the list). Use with node.insertBefore().
18
+ • spliceIndex: Index of element before which current element has been dropped, not counting the element iself.
19
+ For use with Array.splice() if the list is reflecting objects in some array.
20
+
21
+ • slip:beforereorder
22
+ When reordering movement starts.
23
+ Element being reordered gets class `slip-reordering`.
24
+ If you execute event.preventDefault() then element will not move at all.
25
+
26
+ • slip:beforewait
27
+ If you execute event.preventDefault() then reordering will begin immediately, blocking ability to scroll the page.
28
+
29
+ • slip:tap
30
+ When element was tapped without being swiped/reordered.
31
+
32
+ • slip:cancelswipe
33
+ Fired when the user stops dragging and the element returns to its original position.
34
+
35
+
36
+ Usage:
37
+
38
+ CSS:
39
+ You should set `user-select:none` (and WebKit prefixes, sigh) on list elements,
40
+ otherwise unstoppable and glitchy text selection in iOS will get in the way.
41
+
42
+ You should set `overflow-x: hidden` on the container or body to prevent horizontal scrollbar
43
+ appearing when elements are swiped off the list.
44
+
45
+
46
+ var list = document.querySelector('ul#slippylist');
47
+ new Slip(list);
48
+
49
+ list.addEventListener('slip:beforeswipe', function(e) {
50
+ if (shouldNotSwipe(e.target)) e.preventDefault();
51
+ });
52
+
53
+ list.addEventListener('slip:swipe', function(e) {
54
+ // e.target swiped
55
+ if (thatWasSwipeToRemove) {
56
+ e.target.parentNode.removeChild(e.target);
57
+ } else {
58
+ e.preventDefault(); // will animate back to original position
59
+ }
60
+ });
61
+
62
+ list.addEventListener('slip:beforereorder', function(e) {
63
+ if (shouldNotReorder(e.target)) e.preventDefault();
64
+ });
65
+
66
+ list.addEventListener('slip:reorder', function(e) {
67
+ // e.target reordered.
68
+ if (reorderedOK) {
69
+ e.target.parentNode.insertBefore(e.target, e.detail.insertBefore);
70
+ } else {
71
+ e.preventDefault();
72
+ }
73
+ });
74
+
75
+ Requires:
76
+ • Touch events
77
+ • CSS transforms
78
+ • Function.bind()
79
+
80
+ Caveats:
81
+ • Elements must not change size while reordering or swiping takes place (otherwise it will be visually out of sync)
82
+ */
83
+ /*! @license
84
+ Slip.js 1.2.0
85
+
86
+ © 2014 Kornel Lesiński <kornel@geekhood.net>. All rights reserved.
87
+
88
+ Redistribution and use in source and binary forms, with or without modification,
89
+ are permitted provided that the following conditions are met:
90
+
91
+ 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
92
+
93
+ 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and
94
+ the following disclaimer in the documentation and/or other materials provided with the distribution.
95
+
96
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
97
+ INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
98
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
99
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
100
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
101
+ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
102
+ USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
103
+ */
104
+
105
+ window['Slip'] = (function(){
106
+ 'use strict';
107
+
108
+ var damnYouChrome = /Chrome\/[34]/.test(navigator.userAgent); // For bugs that can't be programmatically detected :( Intended to catch all versions of Chrome 30-40
109
+ var needsBodyHandlerHack = damnYouChrome; // Otherwise I _sometimes_ don't get any touchstart events and only clicks instead.
110
+
111
+ /* When dragging elements down in Chrome (tested 34-37) dragged element may appear below stationary elements.
112
+ Looks like WebKit bug #61824, but iOS Safari doesn't have that problem. */
113
+ var compositorDoesNotOrderLayers = damnYouChrome;
114
+
115
+ // -webkit-mess
116
+ var testElement = document.createElement('div');
117
+
118
+ var transitionPrefix = "webkitTransition" in testElement.style ? "webkitTransition" : "transition";
119
+ var transformPrefix = "webkitTransform" in testElement.style ? "webkitTransform" : "transform";
120
+ var transformProperty = transformPrefix === "webkitTransform" ? "-webkit-transform" : "transform";
121
+ var userSelectPrefix = "webkitUserSelect" in testElement.style ? "webkitUserSelect" : "userSelect";
122
+
123
+ testElement.style[transformPrefix] = 'translateZ(0)';
124
+ var hwLayerMagic = testElement.style[transformPrefix] ? 'translateZ(0) ' : '';
125
+ var hwTopLayerMagic = testElement.style[transformPrefix] ? 'translateZ(1px) ' : '';
126
+ testElement = null;
127
+
128
+ var globalInstances = 0;
129
+ var attachedBodyHandlerHack = false;
130
+ var nullHandler = function(){};
131
+
132
+ function Slip(container, options) {
133
+ if ('string' === typeof container) container = document.querySelector(container);
134
+ if (!container || !container.addEventListener) throw new Error("Please specify DOM node to attach to");
135
+
136
+ if (!this || this === window) return new Slip(container, options);
137
+
138
+ this.options = options = options || {};
139
+ this.options.keepSwipingPercent = options.keepSwipingPercent || 0;
140
+ this.options.minimumSwipeVelocity = options.minimumSwipeVelocity || 1;
141
+ this.options.minimumSwipeTime = options.minimumSwipeTime || 110;
142
+
143
+ // Functions used for as event handlers need usable `this` and must not change to be removable
144
+ this.cancel = this.setState.bind(this, this.states.idle);
145
+ this.onTouchStart = this.onTouchStart.bind(this);
146
+ this.onTouchMove = this.onTouchMove.bind(this);
147
+ this.onTouchEnd = this.onTouchEnd.bind(this);
148
+ this.onMouseDown = this.onMouseDown.bind(this);
149
+ this.onMouseMove = this.onMouseMove.bind(this);
150
+ this.onMouseUp = this.onMouseUp.bind(this);
151
+ this.onMouseLeave = this.onMouseLeave.bind(this);
152
+ this.onSelection = this.onSelection.bind(this);
153
+
154
+ this.setState(this.states.idle);
155
+ this.attach(container);
156
+ }
157
+
158
+ function getTransform(node) {
159
+ var transform = node.style[transformPrefix];
160
+ if (transform) {
161
+ return {
162
+ value:transform,
163
+ original:transform,
164
+ };
165
+ }
166
+
167
+ if (window.getComputedStyle) {
168
+ var style = window.getComputedStyle(node).getPropertyValue(transformProperty);
169
+ if (style && style !== 'none') return {value:style, original:''};
170
+ }
171
+ return {value:'', original:''};
172
+ }
173
+
174
+ function findIndex(target, nodes) {
175
+ var originalIndex = 0;
176
+ var listCount = 0;
177
+
178
+ for (var i=0; i < nodes.length; i++) {
179
+ if (nodes[i].nodeType === 1) {
180
+ listCount++;
181
+ if (nodes[i] === target.node) {
182
+ originalIndex = listCount-1;
183
+ }
184
+ }
185
+ }
186
+
187
+ return originalIndex;
188
+ }
189
+
190
+ // All functions in states are going to be executed in context of Slip object
191
+ Slip.prototype = {
192
+
193
+ container: null,
194
+ options: {},
195
+ state: null,
196
+
197
+ target: null, // the tapped/swiped/reordered node with height and backed up styles
198
+
199
+ usingTouch: false, // there's no good way to detect touchscreen preference other than receiving a touch event (really, trust me).
200
+ mouseHandlersAttached: false,
201
+
202
+ startPosition: null, // x,y,time where first touch began
203
+ latestPosition: null, // x,y,time where the finger is currently
204
+ previousPosition: null, // x,y,time where the finger was ~100ms ago (for velocity calculation)
205
+
206
+ canPreventScrolling: false,
207
+
208
+ states: {
209
+ idle: function idleStateInit() {
210
+ this.target = null;
211
+ this.usingTouch = false;
212
+ this.removeMouseHandlers();
213
+
214
+ return {
215
+ allowTextSelection: true,
216
+ };
217
+ },
218
+
219
+ undecided: function undecidedStateInit() {
220
+ this.target.height = this.target.node.offsetHeight;
221
+ this.target.node.style[transitionPrefix] = '';
222
+
223
+ if (!this.dispatch(this.target.originalTarget, 'beforewait')) {
224
+ if (this.dispatch(this.target.originalTarget, 'beforereorder')) {
225
+ this.setState(this.states.reorder);
226
+ }
227
+ } else {
228
+ var holdTimer = setTimeout(function(){
229
+ var move = this.getAbsoluteMovement();
230
+ if (this.canPreventScrolling && move.x < 15 && move.y < 25) {
231
+ if (this.dispatch(this.target.originalTarget, 'beforereorder')) {
232
+ this.setState(this.states.reorder);
233
+ }
234
+ }
235
+ }.bind(this), 300);
236
+ }
237
+
238
+ return {
239
+ leaveState: function() {
240
+ clearTimeout(holdTimer);
241
+ },
242
+
243
+ onMove: function() {
244
+ var move = this.getAbsoluteMovement();
245
+
246
+ if (move.x > 20 && move.y < Math.max(100, this.target.height)) {
247
+ if (this.dispatch(this.target.originalTarget, 'beforeswipe', {directionX: move.directionX, directionY: move.directionY})) {
248
+ this.setState(this.states.swipe);
249
+ return false;
250
+ } else {
251
+ this.setState(this.states.idle);
252
+ }
253
+ }
254
+ if (move.y > 20) {
255
+ this.setState(this.states.idle);
256
+ }
257
+
258
+ // Chrome likes sideways scrolling :(
259
+ if (move.x > move.y*1.2) return false;
260
+ },
261
+
262
+ onLeave: function() {
263
+ this.setState(this.states.idle);
264
+ },
265
+
266
+ onEnd: function() {
267
+ var allowDefault = this.dispatch(this.target.originalTarget, 'tap');
268
+ this.setState(this.states.idle);
269
+ return allowDefault;
270
+ },
271
+ };
272
+ },
273
+
274
+ swipe: function swipeStateInit() {
275
+ var swipeSuccess = false;
276
+ var container = this.container;
277
+
278
+ var originalIndex = findIndex(this.target, this.container.childNodes);
279
+
280
+ container.className += ' slip-swiping-container';
281
+ function removeClass() {
282
+ container.className = container.className.replace(/(?:^| )slip-swiping-container/,'');
283
+ }
284
+
285
+ this.target.height = this.target.node.offsetHeight;
286
+
287
+ return {
288
+ leaveState: function() {
289
+ if (swipeSuccess) {
290
+ this.animateSwipe(function(target){
291
+ target.node.style[transformPrefix] = target.baseTransform.original;
292
+ target.node.style[transitionPrefix] = '';
293
+ if (this.dispatch(target.node, 'afterswipe')) {
294
+ removeClass();
295
+ return true;
296
+ } else {
297
+ this.animateToZero(undefined, target);
298
+ }
299
+ }.bind(this));
300
+ } else {
301
+ this.animateToZero(removeClass);
302
+ this.dispatch(this.target.node, 'cancelswipe');
303
+ }
304
+ },
305
+
306
+ onMove: function() {
307
+ var move = this.getTotalMovement();
308
+
309
+ if (Math.abs(move.y) < this.target.height+20) {
310
+ this.target.node.style[transformPrefix] = 'translate(' + move.x + 'px,0) ' + hwLayerMagic + this.target.baseTransform.value;
311
+ return false;
312
+ } else {
313
+ this.setState(this.states.idle);
314
+ }
315
+ },
316
+
317
+ onLeave: function() {
318
+ this.state.onEnd.call(this);
319
+ },
320
+
321
+ onEnd: function() {
322
+ var move = this.getAbsoluteMovement();
323
+ var velocity = move.x / move.time;
324
+
325
+ // How far out has the item been swiped?
326
+ var swipedPercent = Math.abs((this.startPosition.x - this.previousPosition.x) / this.container.clientWidth) * 100;
327
+
328
+ var swiped = (velocity > this.options.minimumSwipeVelocity && move.time > this.options.minimumSwipeTime) || (this.options.keepSwipingPercent && swipedPercent > this.options.keepSwipingPercent);
329
+
330
+ if (swiped) {
331
+ if (this.dispatch(this.target.node, 'swipe', {direction: move.directionX, originalIndex: originalIndex})) {
332
+ swipeSuccess = true; // can't animate here, leaveState overrides anim
333
+ }
334
+ }
335
+ this.setState(this.states.idle);
336
+ return !swiped;
337
+ },
338
+ };
339
+ },
340
+
341
+ reorder: function reorderStateInit() {
342
+ this.target.height = this.target.node.offsetHeight;
343
+
344
+ var nodes = this.container.childNodes;
345
+ var originalIndex = findIndex(this.target, nodes);
346
+ var mouseOutsideTimer;
347
+ var zero = this.target.node.offsetTop + this.target.height/2;
348
+ var otherNodes = [];
349
+ for(var i=0; i < nodes.length; i++) {
350
+ if (nodes[i].nodeType != 1 || nodes[i] === this.target.node) continue;
351
+ var t = nodes[i].offsetTop;
352
+ nodes[i].style[transitionPrefix] = transformProperty + ' 0.2s ease-in-out';
353
+ otherNodes.push({
354
+ node: nodes[i],
355
+ baseTransform: getTransform(nodes[i]),
356
+ pos: t + (t < zero ? nodes[i].offsetHeight : 0) - zero,
357
+ });
358
+ }
359
+
360
+ this.target.node.className += ' slip-reordering';
361
+ this.target.node.style.zIndex = '99999';
362
+ this.target.node.style[userSelectPrefix] = 'none';
363
+ if (compositorDoesNotOrderLayers) {
364
+ // Chrome's compositor doesn't sort 2D layers
365
+ this.container.style.webkitTransformStyle = 'preserve-3d';
366
+ }
367
+
368
+ function setPosition() {
369
+ /*jshint validthis:true */
370
+
371
+ if (mouseOutsideTimer) {
372
+ // don't care where the mouse is as long as it moves
373
+ clearTimeout(mouseOutsideTimer); mouseOutsideTimer = null;
374
+ }
375
+
376
+ var move = this.getTotalMovement();
377
+ this.target.node.style[transformPrefix] = 'translate(0,' + move.y + 'px) ' + hwTopLayerMagic + this.target.baseTransform.value;
378
+
379
+ var height = this.target.height;
380
+ otherNodes.forEach(function(o){
381
+ var off = 0;
382
+ if (o.pos < 0 && move.y < 0 && o.pos > move.y) {
383
+ off = height;
384
+ }
385
+ else if (o.pos > 0 && move.y > 0 && o.pos < move.y) {
386
+ off = -height;
387
+ }
388
+ // FIXME: should change accelerated/non-accelerated state lazily
389
+ o.node.style[transformPrefix] = off ? 'translate(0,'+off+'px) ' + hwLayerMagic + o.baseTransform.value : o.baseTransform.original;
390
+ });
391
+ return false;
392
+ }
393
+
394
+ setPosition.call(this);
395
+
396
+ return {
397
+ leaveState: function() {
398
+ if (mouseOutsideTimer) clearTimeout(mouseOutsideTimer);
399
+
400
+ if (compositorDoesNotOrderLayers) {
401
+ this.container.style.webkitTransformStyle = '';
402
+ }
403
+
404
+ this.target.node.className = this.target.node.className.replace(/(?:^| )slip-reordering/,'');
405
+ this.target.node.style[userSelectPrefix] = '';
406
+
407
+ this.animateToZero(function(target){
408
+ target.node.style.zIndex = '';
409
+ });
410
+ otherNodes.forEach(function(o){
411
+ o.node.style[transformPrefix] = o.baseTransform.original;
412
+ o.node.style[transitionPrefix] = ''; // FIXME: animate to new position
413
+ });
414
+ },
415
+
416
+ onMove: setPosition,
417
+
418
+ onLeave: function() {
419
+ // don't let element get stuck if mouse left the window
420
+ // but don't cancel immediately as it'd be annoying near window edges
421
+ if (mouseOutsideTimer) clearTimeout(mouseOutsideTimer);
422
+ mouseOutsideTimer = setTimeout(function(){
423
+ mouseOutsideTimer = null;
424
+ this.cancel();
425
+ }.bind(this), 700);
426
+ },
427
+
428
+ onEnd: function() {
429
+ var move = this.getTotalMovement();
430
+ if (move.y < 0) {
431
+ for(var i=0; i < otherNodes.length; i++) {
432
+ if (otherNodes[i].pos > move.y) {
433
+ this.dispatch(this.target.node, 'reorder', {spliceIndex:i, insertBefore:otherNodes[i].node, originalIndex: originalIndex});
434
+ break;
435
+ }
436
+ }
437
+ } else {
438
+ for(var i=otherNodes.length-1; i >= 0; i--) {
439
+ if (otherNodes[i].pos < move.y) {
440
+ this.dispatch(this.target.node, 'reorder', {spliceIndex:i+1, insertBefore:otherNodes[i+1] ? otherNodes[i+1].node : null, originalIndex: originalIndex});
441
+ break;
442
+ }
443
+ }
444
+ }
445
+ this.setState(this.states.idle);
446
+ return false;
447
+ },
448
+ };
449
+ },
450
+ },
451
+
452
+ attach: function(container) {
453
+ globalInstances++;
454
+ if (this.container) this.detach();
455
+
456
+ // In some cases taps on list elements send *only* click events and no touch events. Spotted only in Chrome 32+
457
+ // Having event listener on body seems to solve the issue (although AFAIK may disable smooth scrolling as a side-effect)
458
+ if (!attachedBodyHandlerHack && needsBodyHandlerHack) {
459
+ attachedBodyHandlerHack = true;
460
+ document.body.addEventListener('touchstart', nullHandler, false);
461
+ }
462
+
463
+ this.container = container;
464
+ this.otherNodes = [];
465
+
466
+ // selection on iOS interferes with reordering
467
+ document.addEventListener("selectionchange", this.onSelection, false);
468
+
469
+ // cancel is called e.g. when iOS detects multitasking gesture
470
+ this.container.addEventListener('touchcancel', this.cancel, false);
471
+ this.container.addEventListener('touchstart', this.onTouchStart, false);
472
+ this.container.addEventListener('touchmove', this.onTouchMove, false);
473
+ this.container.addEventListener('touchend', this.onTouchEnd, false);
474
+ this.container.addEventListener('mousedown', this.onMouseDown, false);
475
+ // mousemove and mouseup are attached dynamically
476
+ },
477
+
478
+ detach: function() {
479
+ this.cancel();
480
+
481
+ this.container.removeEventListener('mousedown', this.onMouseDown, false);
482
+ this.container.removeEventListener('touchend', this.onTouchEnd, false);
483
+ this.container.removeEventListener('touchmove', this.onTouchMove, false);
484
+ this.container.removeEventListener('touchstart', this.onTouchStart, false);
485
+ this.container.removeEventListener('touchcancel', this.cancel, false);
486
+
487
+ document.removeEventListener("selectionchange", this.onSelection, false);
488
+
489
+ globalInstances--;
490
+ if (!globalInstances && attachedBodyHandlerHack) {
491
+ attachedBodyHandlerHack = false;
492
+ document.body.removeEventListener('touchstart', nullHandler, false);
493
+ }
494
+ },
495
+
496
+ setState: function(newStateCtor){
497
+ if (this.state) {
498
+ if (this.state.ctor === newStateCtor) return;
499
+ if (this.state.leaveState) this.state.leaveState.call(this);
500
+ }
501
+
502
+ // Must be re-entrant in case ctor changes state
503
+ var prevState = this.state;
504
+ var nextState = newStateCtor.call(this);
505
+ if (this.state === prevState) {
506
+ nextState.ctor = newStateCtor;
507
+ this.state = nextState;
508
+ }
509
+ },
510
+
511
+ findTargetNode: function(targetNode) {
512
+ while(targetNode && targetNode.parentNode !== this.container) {
513
+ targetNode = targetNode.parentNode;
514
+ }
515
+ return targetNode;
516
+ },
517
+
518
+ onSelection: function(e) {
519
+ var isRelated = e.target === document || this.findTargetNode(e);
520
+ if (!isRelated) return;
521
+
522
+ if (e.cancelable || e.defaultPrevented) {
523
+ if (!this.state.allowTextSelection) {
524
+ e.preventDefault();
525
+ }
526
+ } else {
527
+ // iOS doesn't allow selection to be prevented
528
+ this.setState(this.states.idle);
529
+ }
530
+ },
531
+
532
+ addMouseHandlers: function() {
533
+ // unlike touch events, mousemove/up is not conveniently fired on the same element,
534
+ // but I don't need to listen to unrelated events all the time
535
+ if (!this.mouseHandlersAttached) {
536
+ this.mouseHandlersAttached = true;
537
+ document.documentElement.addEventListener('mouseleave', this.onMouseLeave, false);
538
+ window.addEventListener('mousemove', this.onMouseMove, true);
539
+ window.addEventListener('mouseup', this.onMouseUp, true);
540
+ window.addEventListener('blur', this.cancel, false);
541
+ }
542
+ },
543
+
544
+ removeMouseHandlers: function() {
545
+ if (this.mouseHandlersAttached) {
546
+ this.mouseHandlersAttached = false;
547
+ document.documentElement.removeEventListener('mouseleave', this.onMouseLeave, false);
548
+ window.removeEventListener('mousemove', this.onMouseMove, true);
549
+ window.removeEventListener('mouseup', this.onMouseUp, true);
550
+ window.removeEventListener('blur', this.cancel, false);
551
+ }
552
+ },
553
+
554
+ onMouseLeave: function(e) {
555
+ if (this.usingTouch) return;
556
+
557
+ if (e.target === document.documentElement || e.relatedTarget === document.documentElement) {
558
+ if (this.state.onLeave) {
559
+ this.state.onLeave.call(this);
560
+ }
561
+ }
562
+ },
563
+
564
+ onMouseDown: function(e) {
565
+ if (this.usingTouch || e.button != 0 || !this.setTarget(e)) return;
566
+
567
+ this.addMouseHandlers(); // mouseup, etc.
568
+
569
+ this.canPreventScrolling = true; // or rather it doesn't apply to mouse
570
+
571
+ this.startAtPosition({
572
+ x: e.clientX,
573
+ y: e.clientY,
574
+ time: e.timeStamp,
575
+ });
576
+ },
577
+
578
+ onTouchStart: function(e) {
579
+ this.usingTouch = true;
580
+ this.canPreventScrolling = true;
581
+
582
+ // This implementation cares only about single touch
583
+ if (e.touches.length > 1) {
584
+ this.setState(this.states.idle);
585
+ return;
586
+ }
587
+
588
+ if (!this.setTarget(e)) return;
589
+
590
+ this.startAtPosition({
591
+ x: e.touches[0].clientX,
592
+ y: e.touches[0].clientY - window.scrollY,
593
+ time: e.timeStamp,
594
+ });
595
+ },
596
+
597
+ setTarget: function(e) {
598
+ var targetNode = this.findTargetNode(e.target);
599
+ if (!targetNode) {
600
+ this.setState(this.states.idle);
601
+ return false;
602
+ }
603
+
604
+ //check for a scrollable parent
605
+ var scrollContainer = targetNode.parentNode;
606
+ while (scrollContainer){
607
+ if (scrollContainer.scrollHeight > scrollContainer.clientHeight && window.getComputedStyle(scrollContainer)['overflow-y'] != 'visible') break;
608
+ else scrollContainer = scrollContainer.parentNode;
609
+ }
610
+
611
+ this.target = {
612
+ originalTarget: e.target,
613
+ node: targetNode,
614
+ scrollContainer: scrollContainer,
615
+ baseTransform: getTransform(targetNode),
616
+ };
617
+ return true;
618
+ },
619
+
620
+ startAtPosition: function(pos) {
621
+ this.startPosition = this.previousPosition = this.latestPosition = pos;
622
+ this.setState(this.states.undecided);
623
+ },
624
+
625
+ updatePosition: function(e, pos) {
626
+ if(this.target == null)
627
+ return;
628
+ this.latestPosition = pos;
629
+
630
+ var triggerOffset = 40,
631
+ offset = 0;
632
+
633
+ var scrollable = this.target.scrollContainer || document.body,
634
+ containerRect = scrollable.getBoundingClientRect(),
635
+ targetRect = this.target.node.getBoundingClientRect(),
636
+ bottomOffset = Math.min(containerRect.bottom, window.innerHeight) - targetRect.bottom,
637
+ topOffset = targetRect.top - Math.max(containerRect.top, 0);
638
+
639
+ if (bottomOffset < triggerOffset){
640
+ offset = triggerOffset - bottomOffset;
641
+ }
642
+ else if (topOffset < triggerOffset){
643
+ offset = topOffset - triggerOffset;
644
+ }
645
+
646
+ var prevScrollTop = scrollable.scrollTop;
647
+ scrollable.scrollTop += offset;
648
+ if (prevScrollTop != scrollable.scrollTop) this.startPosition.y += prevScrollTop-scrollable.scrollTop;
649
+
650
+ if (this.state.onMove) {
651
+ if (this.state.onMove.call(this) === false) {
652
+ e.preventDefault();
653
+ }
654
+ }
655
+
656
+ // sample latestPosition 100ms for velocity
657
+ if (this.latestPosition.time - this.previousPosition.time > 100) {
658
+ this.previousPosition = this.latestPosition;
659
+ }
660
+ },
661
+
662
+ onMouseMove: function(e) {
663
+ this.updatePosition(e, {
664
+ x: e.clientX,
665
+ y: e.clientY,
666
+ time: e.timeStamp,
667
+ });
668
+ },
669
+
670
+ onTouchMove: function(e) {
671
+ this.updatePosition(e, {
672
+ x: e.touches[0].clientX,
673
+ y: e.touches[0].clientY - window.scrollY,
674
+ time: e.timeStamp,
675
+ });
676
+
677
+ // In Apple's touch model only the first move event after touchstart can prevent scrolling (and event.cancelable is broken)
678
+ this.canPreventScrolling = false;
679
+ },
680
+
681
+ onMouseUp: function(e) {
682
+ if (this.usingTouch || e.button !== 0) return;
683
+
684
+ if (this.state.onEnd && false === this.state.onEnd.call(this)) {
685
+ e.preventDefault();
686
+ }
687
+ },
688
+
689
+ onTouchEnd: function(e) {
690
+ if (e.touches.length > 1) {
691
+ this.cancel();
692
+ } else if (this.state.onEnd && false === this.state.onEnd.call(this)) {
693
+ e.preventDefault();
694
+ }
695
+ },
696
+
697
+ getTotalMovement: function() {
698
+ return {
699
+ x:this.latestPosition.x - this.startPosition.x,
700
+ y:this.latestPosition.y - this.startPosition.y,
701
+ };
702
+ },
703
+
704
+ getAbsoluteMovement: function() {
705
+ return {
706
+ x: Math.abs(this.latestPosition.x - this.startPosition.x),
707
+ y: Math.abs(this.latestPosition.y - this.startPosition.y),
708
+ time:this.latestPosition.time - this.startPosition.time,
709
+ directionX:this.latestPosition.x - this.startPosition.x < 0 ? 'left' : 'right',
710
+ directionY:this.latestPosition.y - this.startPosition.y < 0 ? 'up' : 'down',
711
+ };
712
+ },
713
+
714
+ dispatch: function(targetNode, eventName, detail) {
715
+ var event = document.createEvent('CustomEvent');
716
+ if (event && event.initCustomEvent) {
717
+ event.initCustomEvent('slip:' + eventName, true, true, detail);
718
+ } else {
719
+ event = document.createEvent('Event');
720
+ event.initEvent('slip:' + eventName, true, true);
721
+ event.detail = detail;
722
+ }
723
+ return targetNode.dispatchEvent(event);
724
+ },
725
+
726
+ getSiblings: function(target) {
727
+ var siblings = [];
728
+ var tmp = target.node.nextSibling;
729
+ while(tmp) {
730
+ if (tmp.nodeType == 1) siblings.push({
731
+ node: tmp,
732
+ baseTransform: getTransform(tmp),
733
+ });
734
+ tmp = tmp.nextSibling;
735
+ }
736
+ return siblings;
737
+ },
738
+
739
+ animateToZero: function(callback, target) {
740
+ // save, because this.target/container could change during animation
741
+ target = target || this.target;
742
+
743
+ target.node.style[transitionPrefix] = transformProperty + ' 0.1s ease-out';
744
+ target.node.style[transformPrefix] = 'translate(0,0) ' + hwLayerMagic + target.baseTransform.value;
745
+ setTimeout(function(){
746
+ target.node.style[transitionPrefix] = '';
747
+ target.node.style[transformPrefix] = target.baseTransform.original;
748
+ if (callback) callback.call(this, target);
749
+ }.bind(this), 101);
750
+ },
751
+
752
+ animateSwipe: function(callback) {
753
+ var target = this.target;
754
+ var siblings = this.getSiblings(target);
755
+ var emptySpaceTransform = 'translate(0,' + this.target.height + 'px) ' + hwLayerMagic + ' ';
756
+
757
+ // FIXME: animate with real velocity
758
+ target.node.style[transitionPrefix] = 'all 0.1s linear';
759
+ target.node.style[transformPrefix] = ' translate(' + (this.getTotalMovement().x > 0 ? '' : '-') + '100%,0) ' + hwLayerMagic + target.baseTransform.value;
760
+
761
+ setTimeout(function(){
762
+ if (callback.call(this, target)) {
763
+ siblings.forEach(function(o){
764
+ o.node.style[transitionPrefix] = '';
765
+ o.node.style[transformPrefix] = emptySpaceTransform + o.baseTransform.value;
766
+ });
767
+ setTimeout(function(){
768
+ siblings.forEach(function(o){
769
+ o.node.style[transitionPrefix] = transformProperty + ' 0.1s ease-in-out';
770
+ o.node.style[transformPrefix] = 'translate(0,0) ' + hwLayerMagic + o.baseTransform.value;
771
+ });
772
+ setTimeout(function(){
773
+ siblings.forEach(function(o){
774
+ o.node.style[transitionPrefix] = '';
775
+ o.node.style[transformPrefix] = o.baseTransform.original;
776
+ });
777
+ },101);
778
+ }, 1);
779
+ }
780
+ }.bind(this), 101);
781
+ },
782
+ };
783
+
784
+ // AMD
785
+ if ('function' === typeof define && define.amd) {
786
+ define(function(){
787
+ return Slip;
788
+ });
789
+ }
790
+ return Slip;
791
+ })();
792
+