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