//=require element-closest/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 || {});