var executeScripts = require("./lib/execute-scripts.js") var forEachEls = require("./lib/foreach-els.js") var parseOptions = require("./lib/parse-options.js") var switches = require("./lib/switches") var newUid = require("./lib/uniqueid.js") var on = require("./lib/events/on.js") var trigger = require("./lib/events/trigger.js") var clone = require("./lib/util/clone.js") var contains = require("./lib/util/contains.js") var extend = require("./lib/util/extend.js") 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.js"), 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.js"), attachForm: require("./lib/proto/attach-form.js"), forEachSelectors: function(cb, context, DOMcontext) { return require("./lib/foreach-selectors.js").bind(this)(this.options.selectors, cb, context, DOMcontext) }, switchSelectors: function(selectors, fromEl, toEl, options) { return require("./lib/switches-selectors.js").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) { 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 ) var htmlRegex = /]+>/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) { } } this.switchSelectors(this.options.selectors, tmpEl, document, options) }, abortRequest: require("./lib/abort-request.js"), doRequest: require("./lib/send-request.js"), handleResponse: require("./lib/proto/handle-response.js"), 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.js") // arguably could do `if( require("./lib/is-supported.js")()) {` 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 }