Files
pjax/index.js
2018-03-08 23:17:34 -05:00

337 lines
9.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
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 <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) { }
}
this.switchSelectors(this.options.selectors, tmpEl, document, options)
},
abortRequest: require("./lib/abort-request.js"),
doRequest: require("./lib/send-request.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, 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)
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)
}
return this.latestChance(href)
}
else {
throw e
}
}
}.bind(this))
},
afterAllSwitches: function() {
// FF bug: Wont 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 isnt 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
}