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