You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
173 lines
5.5 KiB
JavaScript
173 lines
5.5 KiB
JavaScript
//=require element-closest/element-closest.js
|
|
//=require whatwg-fetch/fetch.js
|
|
//=require flexismooth/flexismooth.js
|
|
|
|
(function(app) {
|
|
'use strict';
|
|
|
|
// Cache for successful requests
|
|
var cache = {};
|
|
var hasOwnProperty = cache.hasOwnProperty;
|
|
|
|
/**
|
|
* Navigates to another page using AJAX
|
|
*
|
|
* @param {Element/string} target Anchor element or URL string to navigate to
|
|
* @param {string} mode Either 'normal' or 'popstate', defaults to 'normal'
|
|
* @return {boolean} Whether the event will be handled by this function
|
|
* May be used to prevent default browser behavior
|
|
*/
|
|
app.go = function(target, mode) {
|
|
if(mode === undefined) mode = 'normal';
|
|
|
|
// Refuse to do anything if the History API is not supported
|
|
if(!history.pushState) return false;
|
|
|
|
// Validate params
|
|
if(typeof target !== 'string' && target.nodeType !== 1) {
|
|
throw new TypeError('Invalid parameter target.');
|
|
}
|
|
if(mode !== 'normal' && mode !== 'popstate') {
|
|
throw new TypeError('Invalid parameter mode.');
|
|
}
|
|
|
|
// Convert to anchor element if param is a string
|
|
if(typeof target === 'string') {
|
|
var link = document.createElement('a');
|
|
link.href = target;
|
|
target = link;
|
|
}
|
|
|
|
// Only do something if:
|
|
// - The user clicked on a link
|
|
// - The link is internal
|
|
// - The link doesn't contain a file extension
|
|
var targetOrigin, windowOrigin;
|
|
if(target.origin) {
|
|
targetOrigin = target.origin;
|
|
windowOrigin = window.location.origin;
|
|
} else {
|
|
targetOrigin = target.protocol + '//' + target.hostname;
|
|
windowOrigin = window.location.protocol + '//' + window.location.hostname;
|
|
}
|
|
if(target.nodeName !== 'A' ||
|
|
targetOrigin !== windowOrigin ||
|
|
target.pathname.indexOf('.') !== -1) return false;
|
|
|
|
// Just scroll to the top/element if the link points to the current page
|
|
// In this case, we also don't want to reload, so we return true
|
|
if(mode === 'normal' && target.pathname === window.location.pathname) {
|
|
var element;
|
|
if(target.hash) {
|
|
if(target.hash !== '#top') {
|
|
history.pushState({}, document.title, target.href);
|
|
}
|
|
element = document.getElementById(target.hash.substr(1));
|
|
}
|
|
|
|
Flexismooth(element || 0, 500);
|
|
return true;
|
|
}
|
|
|
|
// Success, we will navigate using AJAX
|
|
// Hide the content using CSS and trigger event
|
|
document.body.classList.add('ajax-transition');
|
|
app.emit('ajax.before', target);
|
|
|
|
// Scroll to the top of the page and ignore interruption
|
|
var scrollPromise = Flexismooth(0, 500).catch(function() {});
|
|
|
|
// Check if the resource is in cache
|
|
var dataPromise;
|
|
if(hasOwnProperty.call(cache, target.href)) {
|
|
console.info('Loading new page ' + target.href + ' (cache)');
|
|
|
|
dataPromise = Promise.resolve(cache[target.href]);
|
|
} else {
|
|
console.info('Loading new page ' + target.href + ' (ajax)');
|
|
|
|
var options = {
|
|
credentials: 'same-origin',
|
|
headers: {'Accept': 'application/json'}
|
|
};
|
|
dataPromise = fetch(target.href, options)
|
|
.then(app.validateHttpStatus)
|
|
.then(app.parseJsonResponse)
|
|
.then(function(data) {
|
|
// Cache data if the page explicitly agrees
|
|
if(data.cachable === true) {
|
|
cache[data.url] = data;
|
|
}
|
|
|
|
return data;
|
|
});
|
|
}
|
|
|
|
// Prefetch external resources
|
|
dataPromise = dataPromise.then(function(data) {
|
|
var container = document.createElement('div');
|
|
container.innerHTML = data.main;
|
|
|
|
return data;
|
|
});
|
|
|
|
// Wait until both data fetching and scrolling are done
|
|
// Wait at least 600 ms for the page transition fading even if scrolling is interrupted
|
|
Promise.all([dataPromise, scrollPromise, app.timerPromise(600)])
|
|
.then(function(values) {
|
|
// Get the result of the data promise
|
|
var data = values[0];
|
|
|
|
// Set browser metadata
|
|
if(mode === 'normal') history.pushState({}, data.title, data.url + target.hash);
|
|
document.title = data.title;
|
|
|
|
// Update content blocks
|
|
document.querySelector('.main').innerHTML = data.main;
|
|
|
|
// Trigger event
|
|
app.emit('ajax.after', target, data);
|
|
|
|
// Display the content again
|
|
document.body.classList.remove('ajax-transition');
|
|
|
|
// If a hash was given, scroll to that element
|
|
if(target.hash) {
|
|
var element = document.getElementById(target.hash.substr(1));
|
|
if(element) Flexismooth(element, 500);
|
|
}
|
|
})
|
|
.catch(function(err) {
|
|
console.error(err);
|
|
|
|
// Fall back to normal browser behavior
|
|
window.location.href = target.href;
|
|
});
|
|
|
|
return true;
|
|
};
|
|
|
|
// Listen on click events on body (events from links will bubble up)
|
|
document.body.addEventListener('click', function(e) {
|
|
// Only continue if no modifier was used (open in new tab etc.)
|
|
if(e.ctrlKey || e.shiftKey || e.metaKey || (e.button && e.button === 1)) return;
|
|
|
|
// Find closest link parent
|
|
var link = e.target.closest('a');
|
|
|
|
// Only continue if the link was found
|
|
if(!link) return;
|
|
|
|
// Prevent the browser from navigating if app.go() wants to be responsible for the event
|
|
if(app.go(link)) e.preventDefault();
|
|
});
|
|
|
|
// Replace the state for the normal page load
|
|
history.replaceState({}, document.title, window.location.href);
|
|
|
|
// Listen on the popstate event on window
|
|
window.addEventListener('popstate', function(e) {
|
|
if(e.state !== null) app.go(window.location.href, 'popstate');
|
|
});
|
|
})(window.app = window.app || {});
|