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