push.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483
  1. /* ========================================================================
  2. * Ratchet: push.js v2.0.2
  3. * http://goratchet.com/components#push
  4. * ========================================================================
  5. * inspired by @defunkt's jquery.pjax.js
  6. * Copyright 2014 Connor Sears
  7. * Licensed under MIT (https://github.com/twbs/ratchet/blob/master/LICENSE)
  8. * ======================================================================== */
  9. /* global _gaq: true */
  10. !(function () {
  11. 'use strict';
  12. var noop = function () {};
  13. // Pushstate caching
  14. // ==================
  15. var isScrolling;
  16. var maxCacheLength = 20;
  17. var cacheMapping = sessionStorage;
  18. var domCache = {};
  19. var transitionMap = {
  20. slideIn : 'slide-out',
  21. slideOut : 'slide-in',
  22. fade : 'fade'
  23. };
  24. var bars = {
  25. bartab : '.bar-tab',
  26. barnav : '.bar-nav',
  27. barfooter : '.bar-footer',
  28. barheadersecondary : '.bar-header-secondary'
  29. };
  30. var cacheReplace = function (data, updates) {
  31. PUSH.id = data.id;
  32. if (updates) {
  33. data = getCached(data.id);
  34. }
  35. cacheMapping[data.id] = JSON.stringify(data);
  36. window.history.replaceState(data.id, data.title, data.url);
  37. domCache[data.id] = document.body.cloneNode(true);
  38. };
  39. var cachePush = function () {
  40. var id = PUSH.id;
  41. var cacheForwardStack = JSON.parse(cacheMapping.cacheForwardStack || '[]');
  42. var cacheBackStack = JSON.parse(cacheMapping.cacheBackStack || '[]');
  43. cacheBackStack.push(id);
  44. while (cacheForwardStack.length) {
  45. delete cacheMapping[cacheForwardStack.shift()];
  46. }
  47. while (cacheBackStack.length > maxCacheLength) {
  48. delete cacheMapping[cacheBackStack.shift()];
  49. }
  50. window.history.pushState(null, '', cacheMapping[PUSH.id].url);
  51. cacheMapping.cacheForwardStack = JSON.stringify(cacheForwardStack);
  52. cacheMapping.cacheBackStack = JSON.stringify(cacheBackStack);
  53. };
  54. var cachePop = function (id, direction) {
  55. var forward = direction === 'forward';
  56. var cacheForwardStack = JSON.parse(cacheMapping.cacheForwardStack || '[]');
  57. var cacheBackStack = JSON.parse(cacheMapping.cacheBackStack || '[]');
  58. var pushStack = forward ? cacheBackStack : cacheForwardStack;
  59. var popStack = forward ? cacheForwardStack : cacheBackStack;
  60. if (PUSH.id) {
  61. pushStack.push(PUSH.id);
  62. }
  63. popStack.pop();
  64. cacheMapping.cacheForwardStack = JSON.stringify(cacheForwardStack);
  65. cacheMapping.cacheBackStack = JSON.stringify(cacheBackStack);
  66. };
  67. var getCached = function (id) {
  68. return JSON.parse(cacheMapping[id] || null) || {};
  69. };
  70. var getTarget = function (e) {
  71. var target = findTarget(e.target);
  72. if (!target ||
  73. e.which > 1 ||
  74. e.metaKey ||
  75. e.ctrlKey ||
  76. isScrolling ||
  77. location.protocol !== target.protocol ||
  78. location.host !== target.host ||
  79. !target.hash && /#/.test(target.href) ||
  80. target.hash && target.href.replace(target.hash, '') === location.href.replace(location.hash, '') ||
  81. target.getAttribute('data-ignore') === 'push') { return; }
  82. return target;
  83. };
  84. // Main event handlers (touchend, popstate)
  85. // ==========================================
  86. var touchend = function (e) {
  87. var target = getTarget(e);
  88. if (!target) {
  89. return;
  90. }
  91. e.preventDefault();
  92. PUSH({
  93. url : target.href,
  94. hash : target.hash,
  95. timeout : target.getAttribute('data-timeout'),
  96. transition : target.getAttribute('data-transition')
  97. });
  98. };
  99. var popstate = function (e) {
  100. var key;
  101. var barElement;
  102. var activeObj;
  103. var activeDom;
  104. var direction;
  105. var transition;
  106. var transitionFrom;
  107. var transitionFromObj;
  108. var id = e.state;
  109. if (!id || !cacheMapping[id]) {
  110. return;
  111. }
  112. direction = PUSH.id < id ? 'forward' : 'back';
  113. cachePop(id, direction);
  114. activeObj = getCached(id);
  115. activeDom = domCache[id];
  116. if (activeObj.title) {
  117. document.title = activeObj.title;
  118. }
  119. if (direction === 'back') {
  120. transitionFrom = JSON.parse(direction === 'back' ? cacheMapping.cacheForwardStack : cacheMapping.cacheBackStack);
  121. transitionFromObj = getCached(transitionFrom[transitionFrom.length - 1]);
  122. } else {
  123. transitionFromObj = activeObj;
  124. }
  125. if (direction === 'back' && !transitionFromObj.id) {
  126. return (PUSH.id = id);
  127. }
  128. transition = direction === 'back' ? transitionMap[transitionFromObj.transition] : transitionFromObj.transition;
  129. if (!activeDom) {
  130. return PUSH({
  131. id : activeObj.id,
  132. url : activeObj.url,
  133. title : activeObj.title,
  134. timeout : activeObj.timeout,
  135. transition : transition,
  136. ignorePush : true
  137. });
  138. }
  139. if (transitionFromObj.transition) {
  140. activeObj = extendWithDom(activeObj, '.content', activeDom.cloneNode(true));
  141. for (key in bars) {
  142. if (bars.hasOwnProperty(key)) {
  143. barElement = document.querySelector(bars[key]);
  144. if (activeObj[key]) {
  145. swapContent(activeObj[key], barElement);
  146. } else if (barElement) {
  147. barElement.parentNode.removeChild(barElement);
  148. }
  149. }
  150. }
  151. }
  152. swapContent(
  153. (activeObj.contents || activeDom).cloneNode(true),
  154. document.querySelector('.content'),
  155. transition
  156. );
  157. PUSH.id = id;
  158. document.body.offsetHeight; // force reflow to prevent scroll
  159. };
  160. // Core PUSH functionality
  161. // =======================
  162. var PUSH = function (options) {
  163. var key;
  164. var xhr = PUSH.xhr;
  165. options.container = options.container || options.transition ? document.querySelector('.content') : document.body;
  166. for (key in bars) {
  167. if (bars.hasOwnProperty(key)) {
  168. options[key] = options[key] || document.querySelector(bars[key]);
  169. }
  170. }
  171. if (xhr && xhr.readyState < 4) {
  172. xhr.onreadystatechange = noop;
  173. xhr.abort();
  174. }
  175. xhr = new XMLHttpRequest();
  176. xhr.open('GET', options.url, true);
  177. xhr.setRequestHeader('X-PUSH', 'true');
  178. xhr.onreadystatechange = function () {
  179. if (options._timeout) {
  180. clearTimeout(options._timeout);
  181. }
  182. if (xhr.readyState === 4) {
  183. xhr.status === 200 ? success(xhr, options) : failure(options.url);
  184. }
  185. };
  186. if (!PUSH.id) {
  187. cacheReplace({
  188. id : +new Date(),
  189. url : window.location.href,
  190. title : document.title,
  191. timeout : options.timeout,
  192. transition : null
  193. });
  194. }
  195. if (options.timeout) {
  196. options._timeout = setTimeout(function () { xhr.abort('timeout'); }, options.timeout);
  197. }
  198. xhr.send();
  199. if (xhr.readyState && !options.ignorePush) {
  200. cachePush();
  201. }
  202. };
  203. // Main XHR handlers
  204. // =================
  205. var success = function (xhr, options) {
  206. var key;
  207. var barElement;
  208. var data = parseXHR(xhr, options);
  209. if (!data.contents) {
  210. return locationReplace(options.url);
  211. }
  212. if (data.title) {
  213. document.title = data.title;
  214. }
  215. if (options.transition) {
  216. for (key in bars) {
  217. if (bars.hasOwnProperty(key)) {
  218. barElement = document.querySelector(bars[key]);
  219. if (data[key]) {
  220. swapContent(data[key], barElement);
  221. } else if (barElement) {
  222. barElement.parentNode.removeChild(barElement);
  223. }
  224. }
  225. }
  226. }
  227. swapContent(data.contents, options.container, options.transition, function () {
  228. cacheReplace({
  229. id : options.id || +new Date(),
  230. url : data.url,
  231. title : data.title,
  232. timeout : options.timeout,
  233. transition : options.transition
  234. }, options.id);
  235. triggerStateChange();
  236. });
  237. if (!options.ignorePush && window._gaq) {
  238. _gaq.push(['_trackPageview']); // google analytics
  239. }
  240. if (!options.hash) {
  241. return;
  242. }
  243. };
  244. var failure = function (url) {
  245. throw new Error('Could not get: ' + url);
  246. };
  247. // PUSH helpers
  248. // ============
  249. var swapContent = function (swap, container, transition, complete) {
  250. var enter;
  251. var containerDirection;
  252. var swapDirection;
  253. if (!transition) {
  254. if (container) {
  255. container.innerHTML = swap.innerHTML;
  256. } else if (swap.classList.contains('content')) {
  257. document.body.appendChild(swap);
  258. } else {
  259. document.body.insertBefore(swap, document.querySelector('.content'));
  260. }
  261. } else {
  262. enter = /in$/.test(transition);
  263. if (transition === 'fade') {
  264. container.classList.add('in');
  265. container.classList.add('fade');
  266. swap.classList.add('fade');
  267. }
  268. if (/slide/.test(transition)) {
  269. swap.classList.add('sliding-in', enter ? 'right' : 'left');
  270. swap.classList.add('sliding');
  271. container.classList.add('sliding');
  272. }
  273. container.parentNode.insertBefore(swap, container);
  274. }
  275. if (!transition) {
  276. complete && complete();
  277. }
  278. if (transition === 'fade') {
  279. container.offsetWidth; // force reflow
  280. container.classList.remove('in');
  281. var fadeContainerEnd = function () {
  282. container.removeEventListener('webkitTransitionEnd', fadeContainerEnd);
  283. swap.classList.add('in');
  284. swap.addEventListener('webkitTransitionEnd', fadeSwapEnd);
  285. };
  286. var fadeSwapEnd = function () {
  287. swap.removeEventListener('webkitTransitionEnd', fadeSwapEnd);
  288. container.parentNode.removeChild(container);
  289. swap.classList.remove('fade');
  290. swap.classList.remove('in');
  291. complete && complete();
  292. };
  293. container.addEventListener('webkitTransitionEnd', fadeContainerEnd);
  294. }
  295. if (/slide/.test(transition)) {
  296. var slideEnd = function () {
  297. swap.removeEventListener('webkitTransitionEnd', slideEnd);
  298. swap.classList.remove('sliding', 'sliding-in');
  299. swap.classList.remove(swapDirection);
  300. container.parentNode.removeChild(container);
  301. complete && complete();
  302. };
  303. container.offsetWidth; // force reflow
  304. swapDirection = enter ? 'right' : 'left';
  305. containerDirection = enter ? 'left' : 'right';
  306. container.classList.add(containerDirection);
  307. swap.classList.remove(swapDirection);
  308. swap.addEventListener('webkitTransitionEnd', slideEnd);
  309. }
  310. };
  311. var triggerStateChange = function () {
  312. var e = new CustomEvent('push', {
  313. detail: { state: getCached(PUSH.id) },
  314. bubbles: true,
  315. cancelable: true
  316. });
  317. window.dispatchEvent(e);
  318. };
  319. var findTarget = function (target) {
  320. var i;
  321. var toggles = document.querySelectorAll('a');
  322. for (; target && target !== document; target = target.parentNode) {
  323. for (i = toggles.length; i--;) {
  324. if (toggles[i] === target) {
  325. return target;
  326. }
  327. }
  328. }
  329. };
  330. var locationReplace = function (url) {
  331. window.history.replaceState(null, '', '#');
  332. window.location.replace(url);
  333. };
  334. var extendWithDom = function (obj, fragment, dom) {
  335. var i;
  336. var result = {};
  337. for (i in obj) {
  338. if (obj.hasOwnProperty(i)) {
  339. result[i] = obj[i];
  340. }
  341. }
  342. Object.keys(bars).forEach(function (key) {
  343. var el = dom.querySelector(bars[key]);
  344. if (el) {
  345. el.parentNode.removeChild(el);
  346. }
  347. result[key] = el;
  348. });
  349. result.contents = dom.querySelector(fragment);
  350. return result;
  351. };
  352. var parseXHR = function (xhr, options) {
  353. var head;
  354. var body;
  355. var data = {};
  356. var responseText = xhr.responseText;
  357. data.url = options.url;
  358. if (!responseText) {
  359. return data;
  360. }
  361. if (/<html/i.test(responseText)) {
  362. head = document.createElement('div');
  363. body = document.createElement('div');
  364. head.innerHTML = responseText.match(/<head[^>]*>([\s\S.]*)<\/head>/i)[0];
  365. body.innerHTML = responseText.match(/<body[^>]*>([\s\S.]*)<\/body>/i)[0];
  366. } else {
  367. head = body = document.createElement('div');
  368. head.innerHTML = responseText;
  369. }
  370. data.title = head.querySelector('title');
  371. var text = 'innerText' in data.title ? 'innerText' : 'textContent';
  372. data.title = data.title && data.title[text].trim();
  373. if (options.transition) {
  374. data = extendWithDom(data, '.content', body);
  375. } else {
  376. data.contents = body;
  377. }
  378. return data;
  379. };
  380. // Attach PUSH event handlers
  381. // ==========================
  382. window.addEventListener('touchstart', function () { isScrolling = false; });
  383. window.addEventListener('touchmove', function () { isScrolling = true; });
  384. window.addEventListener('touchend', touchend);
  385. window.addEventListener('click', function (e) { if (getTarget(e)) {e.preventDefault();} });
  386. window.addEventListener('popstate', popstate);
  387. window.PUSH = PUSH;
  388. }());