Previously, Pjax would blur (remove focus) from the active element regardless of where it was on the page. This restricts that to happen only if the active element is contained by one of the elements represented by options.selectors, because only those are affected by Pjax. Fixes #4
346 lines
10 KiB
JavaScript
346 lines
10 KiB
JavaScript
var clone = require("./lib/clone.js")
|
||
var executeScripts = require("./lib/execute-scripts.js")
|
||
|
||
var forEachEls = require("./lib/foreach-els.js")
|
||
|
||
var newUid = require("./lib/uniqueid.js")
|
||
|
||
var noop = require("./lib/util/noop")
|
||
var contains = require("./lib/util/contains.js")
|
||
|
||
var on = require("./lib/events/on.js")
|
||
// var off = require("./lib/events/on.js")
|
||
var trigger = require("./lib/events/trigger.js")
|
||
|
||
var defaultSwitches = require("./lib/switches")
|
||
|
||
|
||
var Pjax = function(options) {
|
||
this.state = {
|
||
numPendingSwitches: 0,
|
||
href: null,
|
||
options: null
|
||
}
|
||
|
||
var parseOptions = require("./lib/proto/parse-options.js");
|
||
parseOptions.apply(this,[options])
|
||
this.log("Pjax options", this.options)
|
||
|
||
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
|
||
opt.history = false
|
||
opt.requestOptions = {};
|
||
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 = defaultSwitches
|
||
|
||
Pjax.prototype = {
|
||
log: require("./lib/proto/log.js"),
|
||
|
||
getElements: require("./lib/proto/get-elements.js"),
|
||
|
||
parseDOM: require("./lib/proto/parse-dom.js"),
|
||
|
||
refresh: require("./lib/proto/refresh.js"),
|
||
|
||
reload: require("./lib/reload.js"),
|
||
|
||
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)
|
||
},
|
||
|
||
// too much problem with the code below
|
||
// + it’s too dangerous
|
||
// switchFallback: function(fromEl, toEl) {
|
||
// this.switchSelectors(["head", "body"], fromEl, toEl)
|
||
// // execute script when DOM is like it should be
|
||
// Pjax.executeScripts(document.querySelector("head"))
|
||
// Pjax.executeScripts(document.querySelector("body"))
|
||
// }
|
||
|
||
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 <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.
|
||
// we clear focus on non form elements
|
||
if (document.activeElement && !document.activeElement.value && contains(this.options.selectors, document.activeElement)) {
|
||
try {
|
||
document.activeElement.blur()
|
||
} catch (e) { }
|
||
}
|
||
|
||
// try {
|
||
this.switchSelectors(this.options.selectors, tmpEl, document, options)
|
||
|
||
// }
|
||
// catch(e) {
|
||
// if (this.options.debug) {
|
||
// this.log("Pjax switch fail: ", e)
|
||
// }
|
||
// this.switchFallback(tmpEl, document)
|
||
// }
|
||
},
|
||
|
||
abortRequest: require("./lib/abort-request.js"),
|
||
|
||
doRequest: require("./lib/send-request.js"),
|
||
|
||
loadUrl: function(href, options) {
|
||
this.log("load href", href, options)
|
||
|
||
// Abort any previous request
|
||
this.abortRequest(this.request)
|
||
|
||
trigger(document, "pjax:send", options);
|
||
|
||
// Do the request
|
||
options.requestOptions.timeout = this.options.timeout
|
||
this.request = this.doRequest(href, options.requestOptions, function(html, request) {
|
||
// Fail if unable to load HTML via AJAX
|
||
if (html === false) {
|
||
trigger(document,"pjax:complete pjax:error", options)
|
||
|
||
return
|
||
}
|
||
|
||
// push scroll position to history
|
||
var currentState = window.history.state || {}
|
||
window.history.replaceState({
|
||
url: currentState.url || window.location.href,
|
||
title: currentState.title || document.title,
|
||
uid: currentState.uid || newUid(),
|
||
scrollPos: [document.documentElement.scrollLeft || document.body.scrollLeft,
|
||
document.documentElement.scrollTop || document.body.scrollTop]
|
||
},
|
||
document.title, window.location)
|
||
|
||
// Clear out any focused controls before inserting new page contents.
|
||
document.activeElement.blur()
|
||
|
||
var oldHref = href
|
||
if (request.responseURL) {
|
||
if (href !== request.responseURL) {
|
||
href = request.responseURL
|
||
}
|
||
}
|
||
else if (request.getResponseHeader("X-PJAX-URL")) {
|
||
href = request.getResponseHeader("X-PJAX-URL")
|
||
}
|
||
else if (request.getResponseHeader("X-XHR-Redirected-To")) {
|
||
href = request.getResponseHeader("X-XHR-Redirected-To")
|
||
}
|
||
|
||
// Add back the hash if it was removed
|
||
var a = document.createElement("a")
|
||
a.href = oldHref
|
||
var oldHash = a.hash
|
||
a.href = href
|
||
if (oldHash && !a.hash) {
|
||
a.hash = oldHash
|
||
href = a.href
|
||
}
|
||
|
||
this.state.href = href
|
||
this.state.options = clone(options)
|
||
|
||
try {
|
||
this.loadContent(html, options)
|
||
}
|
||
catch (e) {
|
||
if (!this.options.debug) {
|
||
if (console && console.error) {
|
||
console.error("Pjax switch fail: ", e)
|
||
}
|
||
this.latestChance(href)
|
||
return
|
||
}
|
||
else {
|
||
throw e
|
||
}
|
||
}
|
||
}.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)
|
||
|
||
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
|
||
}
|