PageSourceSearch

https://www.blackbirdt.com/plugin/slip.js

js blackbirdt.com collected 2026-10-02 01:24:12 UTC 42,392 bytes, 973 lines download raw bytes

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

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.