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