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