Remove capture of click handler
[ratchet] / dist / ratchet.js
1 /**
2  * ==================================
3  * Ratchet v1.0.0
4  * Licensed under The MIT License
5  * http://opensource.org/licenses/MIT
6  * ==================================
7  */
8
9 /* ----------------------------------
10  * MODAL v1.0.0
11  * Licensed under The MIT License
12  * http://opensource.org/licenses/MIT
13  * ---------------------------------- */
14
15 !function () {
16   var findModals = function (target) {
17     var i;
18     var modals = document.querySelectorAll('a');
19     for (; target && target !== document; target = target.parentNode) {
20       for (i = modals.length; i--;) { if (modals[i] === target) return target; }
21     }
22   };
23
24   var getModal = function (event) {
25     var modalToggle = findModals(event.target);
26     if (modalToggle && modalToggle.hash) return document.querySelector(modalToggle.hash);
27   };
28
29   window.addEventListener('touchend', function (event) {
30     var modal = getModal(event);
31     if (modal) modal.classList.toggle('active');
32   });
33 }();/* ----------------------------------
34  * POPOVER v1.0.0
35  * Licensed under The MIT License
36  * http://opensource.org/licenses/MIT
37  * ---------------------------------- */
38
39 !function () {
40
41   var popover;
42
43   var findPopovers = function (target) {
44     var i, popovers = document.querySelectorAll('a');
45     for (; target && target !== document; target = target.parentNode) {
46       for (i = popovers.length; i--;) { if (popovers[i] === target) return target; }
47     }
48   };
49
50   var onPopoverHidden = function () {
51     document.body.removeChild(backdrop);
52     popover.style.display = 'none';
53     popover.removeEventListener('webkitTransitionEnd', onPopoverHidden);
54   }
55
56   var backdrop = function () {
57     var element = document.createElement('div');
58
59     element.classList.add('backdrop');
60
61     element.addEventListener('touchend', function () {
62       popover.addEventListener('webkitTransitionEnd', onPopoverHidden);
63       popover.classList.remove('visible');
64     });
65
66     return element;
67   }();
68
69   var getPopover = function (e) {
70     var anchor = findPopovers(e.target);
71
72     if (!anchor || !anchor.hash) return;
73
74     popover = document.querySelector(anchor.hash);
75
76     if (!popover || !popover.classList.contains('popover')) return;
77
78     return popover;
79   }
80
81   window.addEventListener('touchend', function (e) {
82     var popover = getPopover(e);
83
84     if (!popover) return;
85
86     popover.style.display = 'block';
87     popover.offsetHeight;
88     popover.classList.add('visible');
89
90     popover.parentNode.appendChild(backdrop);
91   });
92
93   window.addEventListener('click', function (e) { if (getPopover(e)) e.preventDefault(); });
94
95 }();
96 /* ----------------------------------
97  * PUSH v1.0.0
98  * Licensed under The MIT License
99  * inspired by chris's jquery.pjax.js
100  * http://opensource.org/licenses/MIT
101  * ---------------------------------- */
102
103 !function () {
104
105   var noop = function () {};
106
107
108   // Pushstate cacheing
109   // ==================
110
111   var isScrolling;
112   var maxCacheLength = 20;
113   var cacheMapping   = sessionStorage;
114   var domCache       = {};
115   var transitionMap  = {
116     'slide-in'  : 'slide-out',
117     'slide-out' : 'slide-in',
118     'fade'      : 'fade'
119   };
120   var bars = {
121     bartab             : '.bar-tab',
122     bartitle           : '.bar-title',
123     barfooter          : '.bar-footer',
124     barheadersecondary : '.bar-header-secondary'
125   }
126
127   var cacheReplace = function (data, updates) {
128     PUSH.id = data.id;
129     if (updates) data = getCached(data.id);
130     cacheMapping[data.id] = JSON.stringify(data);
131     window.history.replaceState(data.id, data.title, data.url);
132     domCache[data.id] = document.body.cloneNode(true);
133   };
134
135   var cachePush = function () {
136     var id = PUSH.id;
137
138     var cacheForwardStack = JSON.parse(cacheMapping.cacheForwardStack || '[]');
139     var cacheBackStack    = JSON.parse(cacheMapping.cacheBackStack    || '[]');
140
141     cacheBackStack.push(id);
142
143     while (cacheForwardStack.length)               delete cacheMapping[cacheForwardStack.shift()];
144     while (cacheBackStack.length > maxCacheLength) delete cacheMapping[cacheBackStack.shift()];
145
146     window.history.pushState(null, '', cacheMapping[PUSH.id].url);
147
148     cacheMapping.cacheForwardStack = JSON.stringify(cacheForwardStack);
149     cacheMapping.cacheBackStack    = JSON.stringify(cacheBackStack);
150   };
151
152   var cachePop = function (id, direction) {
153     var forward           = direction == 'forward';
154     var cacheForwardStack = JSON.parse(cacheMapping.cacheForwardStack || '[]');
155     var cacheBackStack    = JSON.parse(cacheMapping.cacheBackStack    || '[]');
156     var pushStack         = forward ? cacheBackStack    : cacheForwardStack;
157     var popStack          = forward ? cacheForwardStack : cacheBackStack;
158
159     if (PUSH.id) pushStack.push(PUSH.id);
160     popStack.pop();
161
162     cacheMapping.cacheForwardStack = JSON.stringify(cacheForwardStack);
163     cacheMapping.cacheBackStack    = JSON.stringify(cacheBackStack);
164   };
165
166   var getCached = function (id) {
167     return JSON.parse(cacheMapping[id] || null) || {};
168   };
169
170   var getTarget = function (e) {
171     var target = findTarget(e.target);
172
173     if (
174       !  target
175       || e.which > 1
176       || e.metaKey
177       || e.ctrlKey
178       || isScrolling
179       || location.protocol !== target.protocol
180       || location.host     !== target.host
181       || !target.hash && /#/.test(target.href)
182       || target.hash && target.href.replace(target.hash, '') === location.href.replace(location.hash, '')
183       || target.getAttribute('data-ignore') == 'push'
184     ) return;
185
186     return target;
187   };
188
189
190   // Main event handlers (touchend, popstate)
191   // ==========================================
192
193   var touchend = function (e) {
194     var target = getTarget(e);
195
196     if (!target) return;
197
198     e.preventDefault();
199
200     PUSH({
201       url        : target.href,
202       hash       : target.hash,
203       timeout    : target.getAttribute('data-timeout'),
204       transition : target.getAttribute('data-transition')
205     });
206   };
207
208   var popstate = function (e) {
209     var key;
210     var barElement;
211     var activeObj;
212     var activeDom;
213     var direction;
214     var transition;
215     var transitionFrom;
216     var transitionFromObj;
217     var id = e.state;
218
219     if (!id || !cacheMapping[id]) return;
220
221     direction = PUSH.id < id ? 'forward' : 'back';
222
223     cachePop(id, direction);
224
225     activeObj = getCached(id);
226     activeDom = domCache[id];
227
228     if (activeObj.title) document.title = activeObj.title;
229
230     if (direction == 'back') {
231       transitionFrom    = JSON.parse(direction == 'back' ? cacheMapping.cacheForwardStack : cacheMapping.cacheBackStack);
232       transitionFromObj = getCached(transitionFrom[transitionFrom.length - 1]);
233     } else {
234       transitionFromObj = activeObj;
235     }
236
237     if (direction == 'back' && !transitionFromObj.id) return PUSH.id = id;
238
239     transition = direction == 'back' ? transitionMap[transitionFromObj.transition] : transitionFromObj.transition;
240
241     if (!activeDom) {
242       return PUSH({
243         id         : activeObj.id,
244         url        : activeObj.url,
245         title      : activeObj.title,
246         timeout    : activeObj.timeout,
247         transition : transition,
248         ignorePush : true
249       });
250     }
251
252     if (transitionFromObj.transition) {
253       activeObj = extendWithDom(activeObj, '.content', activeDom.cloneNode(true));
254       for (key in bars) {
255         barElement = document.querySelector(bars[key])
256         if (activeObj[key]) swapContent(activeObj[key], barElement);
257         else if (barElement) barElement.parentNode.removeChild(barElement);
258       }
259     }
260
261     swapContent(
262       (activeObj.contents || activeDom).cloneNode(true),
263       document.querySelector('.content'),
264       transition
265     );
266
267     PUSH.id = id;
268
269     document.body.offsetHeight; // force reflow to prevent scroll
270   };
271
272
273   // Core PUSH functionality
274   // =======================
275
276   var PUSH = function (options) {
277     var key;
278     var data = {};
279     var xhr  = PUSH.xhr;
280
281     options.container = options.container || options.transition ? document.querySelector('.content') : document.body;
282
283     for (key in bars) {
284       options[key] = options[key] || document.querySelector(bars[key]);
285     }
286
287     if (xhr && xhr.readyState < 4) {
288       xhr.onreadystatechange = noop;
289       xhr.abort()
290     }
291
292     xhr = new XMLHttpRequest();
293     xhr.open('GET', options.url, true);
294     xhr.setRequestHeader('X-PUSH', 'true');
295
296     xhr.onreadystatechange = function () {
297       if (options._timeout) clearTimeout(options._timeout);
298       if (xhr.readyState == 4) xhr.status == 200 ? success(xhr, options) : failure(options.url);
299     };
300
301     if (!PUSH.id) {
302       cacheReplace({
303         id         : +new Date,
304         url        : window.location.href,
305         title      : document.title,
306         timeout    : options.timeout,
307         transition : null
308       });
309     }
310
311     if (options.timeout) {
312       options._timeout = setTimeout(function () {  xhr.abort('timeout'); }, options.timeout);
313     }
314
315     xhr.send();
316
317     if (xhr.readyState && !options.ignorePush) cachePush();
318   };
319
320
321   // Main XHR handlers
322   // =================
323
324   var success = function (xhr, options) {
325     var key;
326     var barElement;
327     var data = parseXHR(xhr, options);
328
329     if (!data.contents) return locationReplace(options.url);
330
331     if (data.title) document.title = data.title;
332
333     if (options.transition) {
334       for (key in bars) {
335         barElement = document.querySelector(bars[key])
336         if (data[key]) swapContent(data[key], barElement);
337         else if (barElement) barElement.parentNode.removeChild(barElement);
338       }
339     }
340
341     swapContent(data.contents, options.container, options.transition, function () {
342       cacheReplace({
343         id         : options.id || +new Date,
344         url        : data.url,
345         title      : data.title,
346         timeout    : options.timeout,
347         transition : options.transition
348       }, options.id);
349       triggerStateChange();
350     });
351
352     if (!options.ignorePush && window._gaq) _gaq.push(['_trackPageview']) // google analytics
353     if (!options.hash) return;
354   };
355
356   var failure = function (url) {
357     throw new Error('Could not get: ' + url)
358   };
359
360
361   // PUSH helpers
362   // ============
363
364   var swapContent = function (swap, container, transition, complete) {
365     var enter;
366     var containerDirection;
367     var swapDirection;
368
369     if (!transition) {
370       if (container) container.innerHTML = swap.innerHTML;
371       else if (swap.classList.contains('content')) document.body.appendChild(swap);
372       else document.body.insertBefore(swap, document.querySelector('.content'));
373     } else {
374       enter  = /in$/.test(transition);
375
376       if (transition == 'fade') {
377         container.classList.add('in');
378         container.classList.add('fade');
379         swap.classList.add('fade');
380       }
381
382       if (/slide/.test(transition)) {
383         swap.classList.add(enter ? 'right' : 'left');
384         swap.classList.add('slide');
385         container.classList.add('slide');
386       }
387
388       container.parentNode.insertBefore(swap, container);
389     }
390
391     if (!transition) complete && complete();
392
393     if (transition == 'fade') {
394       container.offsetWidth; // force reflow
395       container.classList.remove('in');
396       container.addEventListener('webkitTransitionEnd', fadeContainerEnd);
397
398       function fadeContainerEnd() {
399         container.removeEventListener('webkitTransitionEnd', fadeContainerEnd);
400         swap.classList.add('in');
401         swap.addEventListener('webkitTransitionEnd', fadeSwapEnd);
402       }
403       function fadeSwapEnd () {
404         swap.removeEventListener('webkitTransitionEnd', fadeSwapEnd);
405         container.parentNode.removeChild(container);
406         swap.classList.remove('fade');
407         swap.classList.remove('in');
408         complete && complete();
409       }
410     }
411
412     if (/slide/.test(transition)) {
413       container.offsetWidth; // force reflow
414       swapDirection      = enter ? 'right' : 'left'
415       containerDirection = enter ? 'left' : 'right'
416       container.classList.add(containerDirection);
417       swap.classList.remove(swapDirection);
418       swap.addEventListener('webkitTransitionEnd', slideEnd);
419
420       function slideEnd() {
421         swap.removeEventListener('webkitTransitionEnd', slideEnd);
422         swap.classList.remove('slide');
423         swap.classList.remove(swapDirection);
424         container.parentNode.removeChild(container);
425         complete && complete();
426       }
427     }
428   };
429
430   var triggerStateChange = function () {
431     var e = new CustomEvent('push', {
432       detail: { state: getCached(PUSH.id) },
433       bubbles: true,
434       cancelable: true
435     });
436
437     window.dispatchEvent(e);
438   };
439
440   var findTarget = function (target) {
441     var i, toggles = document.querySelectorAll('a');
442     for (; target && target !== document; target = target.parentNode) {
443       for (i = toggles.length; i--;) { if (toggles[i] === target) return target; }
444     }
445   };
446
447   var locationReplace = function (url) {
448     window.history.replaceState(null, '', '#');
449     window.location.replace(url);
450   };
451
452   var parseURL = function (url) {
453     var a = document.createElement('a'); a.href = url; return a;
454   };
455
456   var extendWithDom = function (obj, fragment, dom) {
457     var i;
458     var result    = {};
459
460     for (i in obj) result[i] = obj[i];
461
462     Object.keys(bars).forEach(function (key) {
463       var el = dom.querySelector(bars[key]);
464       if (el) el.parentNode.removeChild(el);
465       result[key] = el;
466     });
467
468     result.contents = dom.querySelector(fragment);
469
470     return result;
471   };
472
473   var parseXHR = function (xhr, options) {
474     var head;
475     var body;
476     var data = {};
477     var responseText = xhr.responseText;
478
479     data.url = options.url;
480
481     if (!responseText) return data;
482
483     if (/<html/i.test(responseText)) {
484       head           = document.createElement('div');
485       body           = document.createElement('div');
486       head.innerHTML = responseText.match(/<head[^>]*>([\s\S.]*)<\/head>/i)[0]
487       body.innerHTML = responseText.match(/<body[^>]*>([\s\S.]*)<\/body>/i)[0]
488     } else {
489       head           = body = document.createElement('div');
490       head.innerHTML = responseText;
491     }
492
493     data.title = head.querySelector('title');
494     data.title = data.title && data.title.innerText.trim();
495
496     if (options.transition) data = extendWithDom(data, '.content', body);
497     else data.contents = body;
498
499     return data;
500   };
501
502
503   // Attach PUSH event handlers
504   // ==========================
505
506   window.addEventListener('touchstart', function () { isScrolling = false; });
507   window.addEventListener('touchmove', function () { isScrolling = true; })
508   window.addEventListener('touchend', touchend);
509   window.addEventListener('click', function (e) { if (getTarget(e)) e.preventDefault(); });
510   window.addEventListener('popstate', popstate);
511
512 }();/* ----------------------------------
513  * TABS v1.0.0
514  * Licensed under The MIT License
515  * http://opensource.org/licenses/MIT
516  * ---------------------------------- */
517
518 !function () {
519   var getTarget = function (target) {
520     var i, popovers = document.querySelectorAll('.segmented-controller li a');
521     for (; target && target !== document; target = target.parentNode) {
522       for (i = popovers.length; i--;) { if (popovers[i] === target) return target; }
523     }
524   };
525
526   window.addEventListener("touchend", function (e) {
527     var activeTab;
528     var activeBody;
529     var targetBody;
530     var targetTab;
531     var className     = 'active';
532     var classSelector = '.' + className;
533     var targetAnchor  = getTarget(e.target);
534
535     if (!targetAnchor) return;
536
537     targetTab = targetAnchor.parentNode;
538     activeTab = targetTab.parentNode.querySelector(classSelector);
539
540     if (activeTab) activeTab.classList.remove(className);
541
542     targetTab.classList.add(className);
543
544     if (!targetAnchor.hash) return;
545
546     targetBody = document.querySelector(targetAnchor.hash);
547
548     if (!targetBody) return;
549
550     activeBody = targetBody.parentNode.querySelector(classSelector);
551
552     if (activeBody) activeBody.classList.remove(className);
553
554     targetBody.classList.add(className)
555   });
556
557   window.addEventListener('click', function (e) { if (getTarget(e.target)) e.preventDefault(); });
558 }();/* ----------------------------------
559  * SLIDER v1.0.0
560  * Licensed under The MIT License
561  * Adapted from Brad Birdsall's swipe
562  * http://opensource.org/licenses/MIT
563  * ---------------------------------- */
564
565 !function () {
566
567   var pageX;
568   var pageY;
569   var slider;
570   var deltaX;
571   var deltaY;
572   var offsetX;
573   var lastSlide;
574   var startTime;
575   var resistance;
576   var sliderWidth;
577   var slideNumber;
578   var isScrolling;
579   var scrollableArea;
580
581   var getSlider = function (target) {
582     var i, sliders = document.querySelectorAll('.slider ul');
583     for (; target && target !== document; target = target.parentNode) {
584       for (i = sliders.length; i--;) { if (sliders[i] === target) return target; }
585     }
586   }
587
588   var getScroll = function () {
589     var translate3d = slider.style.webkitTransform.match(/translate3d\(([^,]*)/);
590     return parseInt(translate3d ? translate3d[1] : 0)
591   };
592
593   var setSlideNumber = function (offset) {
594     var round = offset ? (deltaX < 0 ? 'ceil' : 'floor') : 'round';
595     slideNumber = Math[round](getScroll() / ( scrollableArea / slider.children.length) );
596     slideNumber += offset;
597     slideNumber = Math.min(slideNumber, 0);
598     slideNumber = Math.max(-(slider.children.length - 1), slideNumber);
599   }
600
601   var onTouchStart = function (e) {
602     slider = getSlider(e.target);
603
604     if (!slider) return;
605
606     var firstItem  = slider.querySelector('li');
607
608     scrollableArea = firstItem.offsetWidth * slider.children.length;
609     isScrolling    = undefined;
610     sliderWidth    = slider.offsetWidth;
611     resistance     = 1;
612     lastSlide      = -(slider.children.length - 1);
613     startTime      = +new Date;
614     pageX          = e.touches[0].pageX;
615     pageY          = e.touches[0].pageY;
616
617     setSlideNumber(0);
618
619     slider.style['-webkit-transition-duration'] = 0;
620   };
621
622   var onTouchMove = function (e) {
623     if (e.touches.length > 1 || !slider) return; // Exit if a pinch || no slider
624
625     deltaX = e.touches[0].pageX - pageX;
626     deltaY = e.touches[0].pageY - pageY;
627     pageX  = e.touches[0].pageX;
628     pageY  = e.touches[0].pageY;
629
630     if (typeof isScrolling == 'undefined') {
631       isScrolling = Math.abs(deltaY) > Math.abs(deltaX);
632     }
633
634     if (isScrolling) return;
635
636     offsetX = (deltaX / resistance) + getScroll();
637
638     e.preventDefault();
639
640     resistance = slideNumber == 0         && deltaX > 0 ? (pageX / sliderWidth) + 1.25 :
641                  slideNumber == lastSlide && deltaX < 0 ? (Math.abs(pageX) / sliderWidth) + 1.25 : 1;
642
643     slider.style.webkitTransform = 'translate3d(' + offsetX + 'px,0,0)';
644   };
645
646   var onTouchEnd = function (e) {
647     if (!slider || isScrolling) return;
648
649     setSlideNumber(
650       (+new Date) - startTime < 1000 && Math.abs(deltaX) > 15 ? (deltaX < 0 ? -1 : 1) : 0
651     );
652
653     offsetX = slideNumber * sliderWidth;
654
655     slider.style['-webkit-transition-duration'] = '.2s';
656     slider.style.webkitTransform = 'translate3d(' + offsetX + 'px,0,0)';
657
658     e = new CustomEvent('slide', {
659       detail: { slideNumber: Math.abs(slideNumber) },
660       bubbles: true,
661       cancelable: true
662     });
663
664     slider.parentNode.dispatchEvent(e);
665   };
666
667   window.addEventListener('touchstart', onTouchStart);
668   window.addEventListener('touchmove', onTouchMove);
669   window.addEventListener('touchend', onTouchEnd);
670
671 }();
672 /* ----------------------------------
673  * TOGGLE v1.0.0
674  * Licensed under The MIT License
675  * http://opensource.org/licenses/MIT
676  * ---------------------------------- */
677
678 !function () {
679
680   var start     = {};
681   var touchMove = false;
682   var distanceX = false;
683   var toggle    = false;
684
685   var findToggle = function (target) {
686     var i, toggles = document.querySelectorAll('.toggle');
687     for (; target && target !== document; target = target.parentNode) {
688       for (i = toggles.length; i--;) { if (toggles[i] === target) return target; }
689     }
690   }
691
692   window.addEventListener('touchstart', function (e) {
693     e = e.originalEvent || e;
694
695     toggle = findToggle(e.target);
696
697     if (!toggle) return;
698
699     var handle      = toggle.querySelector('.toggle-handle');
700     var toggleWidth = toggle.offsetWidth;
701     var handleWidth = handle.offsetWidth;
702     var offset      = toggle.classList.contains('active') ? toggleWidth - handleWidth : 0;
703
704     start     = { pageX : e.touches[0].pageX - offset, pageY : e.touches[0].pageY };
705     touchMove = false;
706
707     // todo: probably should be moved to the css
708     toggle.style['-webkit-transition-duration'] = 0;
709   });
710
711   window.addEventListener('touchmove', function (e) {
712     e = e.originalEvent || e;
713
714     if (e.touches.length > 1) return; // Exit if a pinch
715
716     if (!toggle) return;
717
718     var handle      = toggle.querySelector('.toggle-handle');
719     var current     = e.touches[0];
720     var toggleWidth = toggle.offsetWidth;
721     var handleWidth = handle.offsetWidth;
722     var offset      = toggleWidth - handleWidth;
723
724     touchMove = true;
725     distanceX = current.pageX - start.pageX;
726
727     if (Math.abs(distanceX) < Math.abs(current.pageY - start.pageY)) return;
728
729     e.preventDefault();
730
731     if (distanceX < 0)      return handle.style.webkitTransform = 'translate3d(0,0,0)';
732     if (distanceX > offset) return handle.style.webkitTransform = 'translate3d(' + offset + 'px,0,0)';
733
734     handle.style.webkitTransform = 'translate3d(' + distanceX + 'px,0,0)';
735
736     toggle.classList[(distanceX > (toggleWidth/2 - handleWidth/2)) ? 'add' : 'remove']('active');
737   });
738
739   window.addEventListener('touchend', function (e) {
740     if (!toggle) return;
741
742     var handle      = toggle.querySelector('.toggle-handle');
743     var toggleWidth = toggle.offsetWidth;
744     var handleWidth = handle.offsetWidth;
745     var offset      = toggleWidth - handleWidth;
746     var slideOn     = (!touchMove && !toggle.classList.contains('active')) || (touchMove && (distanceX > (toggleWidth/2 - handleWidth/2)));
747
748     if (slideOn) handle.style.webkitTransform = 'translate3d(' + offset + 'px,0,0)';
749     else handle.style.webkitTransform = 'translate3d(0,0,0)';
750
751     toggle.classList[slideOn ? 'add' : 'remove']('active');
752
753     e = new CustomEvent('toggle', {
754       detail: { isActive: slideOn },
755       bubbles: true,
756       cancelable: true
757     });
758
759     toggle.dispatchEvent(e);
760
761     touchMove = false;
762     toggle    = false;
763   });
764
765 }();