UNPKG

pjax

Version:

Easily enable fast AJAX navigation on any website (using pushState + XHR)

319 lines (264 loc) 8.59 kB
var executeScripts = require("./lib/execute-scripts"); var forEachEls = require("./lib/foreach-els"); var parseOptions = require("./lib/parse-options"); var switches = require("./lib/switches"); var newUid = require("./lib/uniqueid"); var on = require("./lib/events/on"); var trigger = require("./lib/events/trigger"); var clone = require("./lib/util/clone"); var contains = require("./lib/util/contains"); var extend = require("./lib/util/extend"); var noop = require("./lib/util/noop"); var Pjax = function(options) { this.state = { numPendingSwitches: 0, href: null, options: null }; this.options = parseOptions(options); this.log("Pjax options", this.options); if (this.options.scrollRestoration && "scrollRestoration" in history) { history.scrollRestoration = "manual"; } this.maxUid = this.lastUid = newUid(); this.parseDOM(document); on( window, "popstate", function(st) { if (st.state) { var opt = clone(this.options); opt.url = st.state.url; opt.title = st.state.title; // Since state already exists, prevent it from being pushed again opt.history = false; opt.scrollPos = st.state.scrollPos; if (st.state.uid < this.lastUid) { opt.backward = true; } else { opt.forward = true; } this.lastUid = st.state.uid; // @todo implement history cache here, based on uid this.loadUrl(st.state.url, opt); } }.bind(this) ); }; Pjax.switches = switches; Pjax.prototype = { log: require("./lib/proto/log"), getElements: function(el) { return el.querySelectorAll(this.options.elements); }, parseDOM: function(el) { var parseElement = require("./lib/proto/parse-element"); forEachEls(this.getElements(el), parseElement, this); }, refresh: function(el) { this.parseDOM(el || document); }, reload: function() { window.location.reload(); }, attachLink: require("./lib/proto/attach-link"), attachForm: require("./lib/proto/attach-form"), forEachSelectors: function(cb, context, DOMcontext) { return require("./lib/foreach-selectors").bind(this)( this.options.selectors, cb, context, DOMcontext ); }, switchSelectors: function(selectors, fromEl, toEl, options) { return require("./lib/switches-selectors").bind(this)( this.options.switches, this.options.switchesOptions, selectors, fromEl, toEl, options ); }, latestChance: function(href) { window.location = href; }, onSwitch: function() { trigger(window, "resize scroll"); this.state.numPendingSwitches--; // debounce calls, so we only run this once after all switches are finished. if (this.state.numPendingSwitches === 0) { this.afterAllSwitches(); } }, loadContent: function(html, options) { if (typeof html !== "string") { trigger(document, "pjax:complete pjax:error", options); return; } var tmpEl = document.implementation.createHTMLDocument("pjax"); // parse HTML attributes to copy them // since we are forced to use documentElement.innerHTML (outerHTML can't be used for <html>) var htmlRegex = /<html[^>]+>/gi; var htmlAttribsRegex = /\s?[a-z:]+(?:=['"][^'">]+['"])*/gi; var matches = html.match(htmlRegex); if (matches && matches.length) { matches = matches[0].match(htmlAttribsRegex); if (matches.length) { matches.shift(); matches.forEach(function(htmlAttrib) { var attr = htmlAttrib.trim().split("="); if (attr.length === 1) { tmpEl.documentElement.setAttribute(attr[0], true); } else { tmpEl.documentElement.setAttribute(attr[0], attr[1].slice(1, -1)); } }); } } tmpEl.documentElement.innerHTML = html; this.log( "load content", tmpEl.documentElement.attributes, tmpEl.documentElement.innerHTML.length ); // Clear out any focused controls before inserting new page contents. if ( document.activeElement && contains(document, this.options.selectors, document.activeElement) ) { try { document.activeElement.blur(); } catch (e) {} // eslint-disable-line no-empty } this.switchSelectors(this.options.selectors, tmpEl, document, options); }, abortRequest: require("./lib/abort-request"), doRequest: require("./lib/send-request"), handleResponse: require("./lib/proto/handle-response"), loadUrl: function(href, options) { options = typeof options === "object" ? extend({}, this.options, options) : clone(this.options); this.log("load href", href, options); // Abort any previous request this.abortRequest(this.request); trigger(document, "pjax:send", options); // Do the request this.request = this.doRequest( href, options, this.handleResponse.bind(this) ); }, afterAllSwitches: function() { // FF bug: Won’t autofocus fields that are inserted via JS. // This behavior is incorrect. So if theres no current focus, autofocus // the last field. // // http://www.w3.org/html/wg/drafts/html/master/forms.html var autofocusEl = Array.prototype.slice .call(document.querySelectorAll("[autofocus]")) .pop(); if (autofocusEl && document.activeElement !== autofocusEl) { autofocusEl.focus(); } // execute scripts when DOM have been completely updated this.options.selectors.forEach(function(selector) { forEachEls(document.querySelectorAll(selector), function(el) { executeScripts(el); }); }); var state = this.state; if (state.options.history) { if (!window.history.state) { this.lastUid = this.maxUid = newUid(); window.history.replaceState( { url: window.location.href, title: document.title, uid: this.maxUid, scrollPos: [0, 0] }, document.title ); } // Update browser history this.lastUid = this.maxUid = newUid(); window.history.pushState( { url: state.href, title: state.options.title, uid: this.maxUid, scrollPos: [0, 0] }, state.options.title, state.href ); } this.forEachSelectors(function(el) { this.parseDOM(el); }, this); // Fire Events trigger(document, "pjax:complete pjax:success", state.options); if (typeof state.options.analytics === "function") { state.options.analytics(); } if (state.options.history) { // First parse url and check for hash to override scroll var a = document.createElement("a"); a.href = this.state.href; if (a.hash) { var name = a.hash.slice(1); name = decodeURIComponent(name); var curtop = 0; var target = document.getElementById(name) || document.getElementsByName(name)[0]; if (target) { // http://stackoverflow.com/questions/8111094/cross-browser-javascript-function-to-find-actual-position-of-an-element-in-page if (target.offsetParent) { do { curtop += target.offsetTop; target = target.offsetParent; } while (target); } } window.scrollTo(0, curtop); } else if (state.options.scrollTo !== false) { // Scroll page to top on new page load if (state.options.scrollTo.length > 1) { window.scrollTo(state.options.scrollTo[0], state.options.scrollTo[1]); } else { window.scrollTo(0, state.options.scrollTo); } } } else if (state.options.scrollRestoration && state.options.scrollPos) { window.scrollTo(state.options.scrollPos[0], state.options.scrollPos[1]); } this.state = { numPendingSwitches: 0, href: null, options: null }; } }; Pjax.isSupported = require("./lib/is-supported"); // arguably could do `if( require("./lib/is-supported")()) {` but that might be a little to simple if (Pjax.isSupported()) { module.exports = Pjax; } // if there isn’t required browser functions, returning stupid api else { var stupidPjax = noop; for (var key in Pjax.prototype) { if ( Pjax.prototype.hasOwnProperty(key) && typeof Pjax.prototype[key] === "function" ) { stupidPjax[key] = noop; } } module.exports = stupidPjax; }