Last active
September 15, 2016 08:59
-
-
Save kristianmandrup/062c042d49daca355d1b to your computer and use it in GitHub Desktop.
Push.js used by Ratchet made more flexible in order to play well with React Virtual DOM strategy
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* ======================================================================== | |
* Ratchet: push.js v2.0.2 | |
* http://goratchet.com/components#push | |
* ======================================================================== | |
* inspired by @defunkt's jquery.pjax.js | |
* Copyright 2014 Connor Sears | |
* Licensed under MIT (https://github.com/twbs/ratchet/blob/master/LICENSE) | |
* ======================================================================== */ | |
/* | |
* To customize getContainer strategy | |
PUSH.getContainer = function() { | |
// your custom get DOM container element logic | |
} | |
See swapContent function | |
var swapContainer = PUSH.getContainer(); | |
... | |
*/ | |
/* global _gaq: true */ | |
!(function () { | |
'use strict'; | |
var noop = function () {}; | |
// Pushstate caching | |
// ================== | |
var isScrolling; | |
var maxCacheLength = 20; | |
var cacheMapping = sessionStorage; | |
var domCache = {}; | |
// Change these to unquoted camelcase in the next major version bump | |
var transitionMap = { | |
'slide-in' : 'slide-out', | |
'slide-out' : 'slide-in', | |
fade : 'fade' | |
}; | |
var bars = { | |
bartab : '.bar-tab', | |
barnav : '.bar-nav', | |
barfooter : '.bar-footer', | |
barheadersecondary : '.bar-header-secondary' | |
}; | |
var cacheReplace = function (data, updates) { | |
PUSH.id = data.id; | |
if (updates) { | |
data = getCached(data.id); | |
} | |
cacheMapping[data.id] = JSON.stringify(data); | |
window.history.replaceState(data.id, data.title, data.url); | |
}; | |
var cachePush = function () { | |
var id = PUSH.id; | |
var cacheForwardStack = JSON.parse(cacheMapping.cacheForwardStack || '[]'); | |
var cacheBackStack = JSON.parse(cacheMapping.cacheBackStack || '[]'); | |
cacheBackStack.push(id); | |
while (cacheForwardStack.length) { | |
delete cacheMapping[cacheForwardStack.shift()]; | |
} | |
while (cacheBackStack.length > maxCacheLength) { | |
delete cacheMapping[cacheBackStack.shift()]; | |
} | |
window.history.pushState(null, '', getCached(PUSH.id).url); | |
cacheMapping.cacheForwardStack = JSON.stringify(cacheForwardStack); | |
cacheMapping.cacheBackStack = JSON.stringify(cacheBackStack); | |
}; | |
var cachePop = function (id, direction) { | |
var forward = direction === 'forward'; | |
var cacheForwardStack = JSON.parse(cacheMapping.cacheForwardStack || '[]'); | |
var cacheBackStack = JSON.parse(cacheMapping.cacheBackStack || '[]'); | |
var pushStack = forward ? cacheBackStack : cacheForwardStack; | |
var popStack = forward ? cacheForwardStack : cacheBackStack; | |
if (PUSH.id) { | |
pushStack.push(PUSH.id); | |
} | |
popStack.pop(); | |
cacheMapping.cacheForwardStack = JSON.stringify(cacheForwardStack); | |
cacheMapping.cacheBackStack = JSON.stringify(cacheBackStack); | |
}; | |
var getCached = function (id) { | |
return JSON.parse(cacheMapping[id] || null) || {}; | |
}; | |
var getTarget = function (e) { | |
var target = findTarget(e.target); | |
if (!target || | |
e.which > 1 || | |
e.metaKey || | |
e.ctrlKey || | |
isScrolling || | |
location.protocol !== target.protocol || | |
location.host !== target.host || | |
!target.hash && /#/.test(target.href) || | |
target.hash && target.href.replace(target.hash, '') === location.href.replace(location.hash, '') || | |
target.getAttribute('data-ignore') === 'push') { return; } | |
return target; | |
}; | |
// Main event handlers (touchend, popstate) | |
// ========================================== | |
var touchend = function (e) { | |
var target = getTarget(e); | |
if (!target) { | |
return; | |
} | |
e.preventDefault(); | |
PUSH({ | |
url : target.href, | |
hash : target.hash, | |
timeout : target.getAttribute('data-timeout'), | |
transition : target.getAttribute('data-transition') | |
}); | |
}; | |
var popstate = function (e) { | |
var key; | |
var barElement; | |
var activeObj; | |
var activeDom; | |
var direction; | |
var transition; | |
var transitionFrom; | |
var transitionFromObj; | |
var id = e.state; | |
if (!id || !cacheMapping[id]) { | |
return; | |
} | |
direction = PUSH.id < id ? 'forward' : 'back'; | |
cachePop(id, direction); | |
activeObj = getCached(id); | |
activeDom = domCache[id]; | |
if (activeObj.title) { | |
document.title = activeObj.title; | |
} | |
if (direction === 'back') { | |
transitionFrom = JSON.parse(direction === 'back' ? cacheMapping.cacheForwardStack : cacheMapping.cacheBackStack); | |
transitionFromObj = getCached(transitionFrom[transitionFrom.length - 1]); | |
} else { | |
transitionFromObj = activeObj; | |
} | |
if (direction === 'back' && !transitionFromObj.id) { | |
return (PUSH.id = id); | |
} | |
transition = direction === 'back' ? transitionMap[transitionFromObj.transition] : transitionFromObj.transition; | |
if (!activeDom) { | |
return PUSH({ | |
id : activeObj.id, | |
url : activeObj.url, | |
title : activeObj.title, | |
timeout : activeObj.timeout, | |
transition : transition, | |
ignorePush : true | |
}); | |
} | |
if (transitionFromObj.transition) { | |
activeObj = extendWithDom(activeObj, '.content', activeDom.cloneNode(true)); | |
for (key in bars) { | |
if (bars.hasOwnProperty(key)) { | |
barElement = document.querySelector(bars[key]); | |
if (activeObj[key]) { | |
swapContent(activeObj[key], barElement); | |
} else if (barElement) { | |
barElement.parentNode.removeChild(barElement); | |
} | |
} | |
} | |
} | |
swapContent( | |
(activeObj.contents || activeDom).cloneNode(true), | |
document.querySelector('.content'), | |
transition, function() { | |
triggerStateChange(); | |
} | |
); | |
PUSH.id = id; | |
// Use getContainer here? | |
document.body.offsetHeight; // force reflow to prevent scroll | |
}; | |
// Core PUSH functionality | |
// ======================= | |
var PUSH = function (options) { | |
var key; | |
var xhr = PUSH.xhr; | |
// default way to get Push Container | |
this.getDefaultContainer = function() { | |
options.container || options.transition ? document.querySelector('.content') : document.body; | |
} | |
options.container = this.getDefaultContainer(); | |
this.getContainer = function() { | |
options.container(); | |
} | |
for (key in bars) { | |
if (bars.hasOwnProperty(key)) { | |
options[key] = options[key] || document.querySelector(bars[key]); | |
} | |
} | |
this.options = options; | |
if (xhr && xhr.readyState < 4) { | |
xhr.onreadystatechange = noop; | |
xhr.abort(); | |
} | |
xhr = new XMLHttpRequest(); | |
xhr.open('GET', options.url, true); | |
xhr.setRequestHeader('X-PUSH', 'true'); | |
xhr.onreadystatechange = function () { | |
if (options._timeout) { | |
clearTimeout(options._timeout); | |
} | |
if (xhr.readyState === 4) { | |
xhr.status === 200 ? success(xhr, options) : failure(options.url); | |
} | |
}; | |
if (!PUSH.id) { | |
cacheReplace({ | |
id : +new Date(), | |
url : window.location.href, | |
title : document.title, | |
timeout : options.timeout, | |
transition : options.transition | |
}); | |
} | |
cacheCurrentContent(); | |
if (options.timeout) { | |
options._timeout = setTimeout(function () { xhr.abort('timeout'); }, options.timeout); | |
} | |
xhr.send(); | |
if (xhr.readyState && !options.ignorePush) { | |
cachePush(); | |
} | |
}; | |
function cacheCurrentContent() { | |
domCache[PUSH.id] = document.body.cloneNode(true); | |
} | |
// Main XHR handlers | |
// ================= | |
var success = function (xhr, options) { | |
var key; | |
var barElement; | |
var data = parseXHR(xhr, options); | |
if (!data.contents) { | |
return locationReplace(options.url); | |
} | |
if (data.title) { | |
document.title = data.title; | |
} | |
if (options.transition) { | |
for (key in bars) { | |
if (bars.hasOwnProperty(key)) { | |
barElement = document.querySelector(bars[key]); | |
if (data[key]) { | |
swapContent(data[key], barElement); | |
} else if (barElement) { | |
barElement.parentNode.removeChild(barElement); | |
} | |
} | |
} | |
} | |
swapContent(data.contents, options.container, options.transition, function () { | |
cacheReplace({ | |
id : options.id || +new Date(), | |
url : data.url, | |
title : data.title, | |
timeout : options.timeout, | |
transition : options.transition | |
}, options.id); | |
triggerStateChange(); | |
}); | |
if (!options.ignorePush && window._gaq) { | |
_gaq.push(['_trackPageview']); // google analytics | |
} | |
if (!options.hash) { | |
return; | |
} | |
}; | |
var failure = function (url) { | |
throw new Error('Could not get: ' + url); | |
}; | |
// PUSH helpers | |
// ============ | |
var swapContent = function (swap, container, transition, complete) { | |
var enter; | |
var containerDirection; | |
var swapDirection; | |
if (!transition) { | |
if (container) { | |
container.innerHTML = swap.innerHTML; | |
} else { | |
var swapContainer = PUSH.getContainer(); | |
if (swap.classList.contains('content')) { | |
swapContainer.appendChild(swap); | |
} else { | |
swapContainer.insertBefore(swap, document.querySelector('.content')); | |
} | |
} | |
} else { | |
enter = /in$/.test(transition); | |
if (transition === 'fade') { | |
container.classList.add('in'); | |
container.classList.add('fade'); | |
swap.classList.add('fade'); | |
} | |
if (/slide/.test(transition)) { | |
swap.classList.add('sliding-in', enter ? 'right' : 'left'); | |
swap.classList.add('sliding'); | |
container.classList.add('sliding'); | |
} | |
// Change this? | |
container.parentNode.insertBefore(swap, container); | |
} | |
if (!transition) { | |
complete && complete(); | |
} | |
if (transition === 'fade') { | |
container.offsetWidth; // force reflow | |
container.classList.remove('in'); | |
var fadeContainerEnd = function () { | |
container.removeEventListener('webkitTransitionEnd', fadeContainerEnd); | |
swap.classList.add('in'); | |
swap.addEventListener('webkitTransitionEnd', fadeSwapEnd); | |
}; | |
var fadeSwapEnd = function () { | |
swap.removeEventListener('webkitTransitionEnd', fadeSwapEnd); | |
container.parentNode.removeChild(container); | |
swap.classList.remove('fade'); | |
swap.classList.remove('in'); | |
complete && complete(); | |
}; | |
container.addEventListener('webkitTransitionEnd', fadeContainerEnd); | |
} | |
if (/slide/.test(transition)) { | |
var slideEnd = function () { | |
swap.removeEventListener('webkitTransitionEnd', slideEnd); | |
swap.classList.remove('sliding', 'sliding-in'); | |
swap.classList.remove(swapDirection); | |
container.parentNode.removeChild(container); | |
complete && complete(); | |
}; | |
container.offsetWidth; // force reflow | |
swapDirection = enter ? 'right' : 'left'; | |
containerDirection = enter ? 'left' : 'right'; | |
container.classList.add(containerDirection); | |
swap.classList.remove(swapDirection); | |
swap.addEventListener('webkitTransitionEnd', slideEnd); | |
} | |
}; | |
var triggerStateChange = function () { | |
var e = new CustomEvent('push', { | |
detail: { state: getCached(PUSH.id) }, | |
bubbles: true, | |
cancelable: true | |
}); | |
window.dispatchEvent(e); | |
}; | |
var findTarget = function (target) { | |
var i; | |
var toggles = document.querySelectorAll('a'); | |
for (; target && target !== document; target = target.parentNode) { | |
for (i = toggles.length; i--;) { | |
if (toggles[i] === target) { | |
return target; | |
} | |
} | |
} | |
}; | |
var locationReplace = function (url) { | |
window.history.replaceState(null, '', '#'); | |
window.location.replace(url); | |
}; | |
var extendWithDom = function (obj, fragment, dom) { | |
var i; | |
var result = {}; | |
for (i in obj) { | |
if (obj.hasOwnProperty(i)) { | |
result[i] = obj[i]; | |
} | |
} | |
Object.keys(bars).forEach(function (key) { | |
var el = dom.querySelector(bars[key]); | |
if (el) { | |
el.parentNode.removeChild(el); | |
} | |
result[key] = el; | |
}); | |
result.contents = dom.querySelector(fragment); | |
return result; | |
}; | |
var parseXHR = function (xhr, options) { | |
var head; | |
var body; | |
var data = {}; | |
var responseText = xhr.responseText; | |
data.url = options.url; | |
if (!responseText) { | |
return data; | |
} | |
if (/<html/i.test(responseText)) { | |
head = document.createElement('div'); | |
body = document.createElement('div'); | |
head.innerHTML = responseText.match(/<head[^>]*>([\s\S.]*)<\/head>/i)[0]; | |
body.innerHTML = responseText.match(/<body[^>]*>([\s\S.]*)<\/body>/i)[0]; | |
} else { | |
head = body = document.createElement('div'); | |
head.innerHTML = responseText; | |
} | |
data.title = head.querySelector('title'); | |
var text = 'innerText' in data.title ? 'innerText' : 'textContent'; | |
data.title = data.title && data.title[text].trim(); | |
if (options.transition) { | |
data = extendWithDom(data, '.content', body); | |
} else { | |
data.contents = body; | |
} | |
return data; | |
}; | |
// Attach PUSH event handlers | |
// ========================== | |
window.addEventListener('touchstart', function () { isScrolling = false; }); | |
window.addEventListener('touchmove', function () { isScrolling = true; }); | |
window.addEventListener('touchend', touchend); | |
window.addEventListener('click', function (e) { if (getTarget(e)) {e.preventDefault();} }); | |
window.addEventListener('popstate', popstate); | |
window.PUSH = PUSH; | |
}()); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thanks! Works great.