/*-----------------------------------------------------------------------------\
| Requests a new page (or submits a <form>) via AJAX and updates the UI. |
| |
| This is the actual code used by this site, so it has lots of customization. |
+------------------------------------------------------------------------------+
| Usage: |
| <a class="ajax" href="/">AJAX Link</a> |
| <form class="ajax">Form Stuff></form> |
| |
| Result: |
| Clicking the link or submitting a form will update elements with an ID |
| of "header", "content", "aside", or "footer". |
+---------+-----------+----------+---------------------------------------------|
| @param | string | link | The URL of the requested page. |
| @param | string | scrollY | How far down the page to scroll. |
| @param | string | method | HTTP method to use. |
| @param | string | params | Parameters for a POST. |
| @param | object | params | Form element; use to auto-build POSTs. |
| @return | boolean | | |
\---------+-----------+----------+--------------------------------------------*/
function loadPage(link, scrollY, method, params){
/* Cancel any currently pending requests. */
if(typeof loadPage.xhr === "object" && loadPage.xhr.readyState !== loadPage.xhr.DONE){
if(loadPage.fileUpload && confirm("Cancel file upload?")) return;
loadPage.fileUpload = false;
loadPage.xhr.abort();
delete loadPage.xhr;
}
/* Default method. */
if(typeof method !== "string" || ["GET", "POST"].indexOf(method) < 0) method = "GET";
/* Construct parameters string from a POSTed <form>. */
if(method === "POST" && typeof params === "object"){
var form = new FormData();
loadPage.fileUpload = false;
for(var i = 0; i < params.length; i++){
/* Skip non-<input> elements. */
if(typeof params[i].matches !== "function" || !params[i].matches("input")) continue;
/* Input required validation. */
if(params[i].required && !params[i].value){
params[i].style.transition = "";
params[i].style.boxShadow = "var(--box-shadow-dark), 0 0 2px red inset";
params[i].style.color = "red";
setTimeout(function(){
try{focusInput(params[i])}catch(error){console.error(error)}
params[i].style.transition = "1s box-shadow, 1s color";
params[i].style.boxShadow = "";
params[i].style.color = "";
}, 10);
return false;
}
/* Input pattern validation. */
if(params[i].pattern && !RegExp(params[i].pattern).test(params[i].value)){
params[i].style.transition = "";
params[i].style.boxShadow = "var(--box-shadow-dark), 0 0 2px red inset";
params[i].style.color = "red";
setTimeout(function(){
try{focusInput(params[i])}catch(error){console.error(error)}
params[i].style.transition = "1s box-shadow, 1s color";
params[i].style.boxShadow = "";
params[i].style.color = "";
}, 10);
return false;
}
/* Append file upload. */
if(params[i].type === "file"){
if(params[i].multiple){
for(var j = 0; j < params[i].files.length; j++){
form.append(params[i].name + "_" + j, params[i].files[j]);
}
}else{
form.append(params[i].name, params[i].files[0]);
}
loadPage.fileUpload = true;
}
/* Append parameter. */
else{
if(params[i].name) form.append(params[i].name, params[i].value);
}
}
/* Overwrite params argument. */
params = form;
}
/* Prepare a new XMLHTTPRequest. */
loadPage.xhr = new XMLHttpRequest();
loadPage.xhr.open(method, link);
loadPage.xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
/* Feedback in <title>. */
try{document.title = "Loading... | " + document.title.split(" | ")[1];}catch(error){console.error(error)}
try{document.getElementById("loading-notify").innerHTML = " - Loading..."}catch(error){console.error(error)}
/* Callback on completion. */
loadPage.xhr.onreadystatechange = function(){
switch(this.readyState){
/*-----------------------------------------------------------------\
| Headers are received. |
\-----------------------------------------------------------------*/
case this.HEADERS_RECEIVED:
// Special processing for file downloads.
var contentDisposition = this.getResponseHeader("Content-Disposition");
if(typeof contentDisposition === "string" && contentDisposition.indexOf("attachment") !== -1 && this.responseType !== "blob"){
loadPage.xhr.responseType = "blob";
}
break;
/*-----------------------------------------------------------------\
| Finished loading. |
\-----------------------------------------------------------------*/
case this.DONE:
/* If redirected, update address bar. */
if(this.responseURL && this.responseURL.split("#")[0] !== window.location.href.split("#")[0]){
window.history.replaceState({pageYOffset: window.pageYOffset, pathname: link}, null, window.location.href);
window.history.pushState(null, null, this.responseURL);
}
/* Special processing for file downloads. */
var contentDisposition = this.getResponseHeader("Content-Disposition");
if(typeof contentDisposition === "string" && contentDisposition.indexOf("attachment") !== -1 && this.responseType === "blob"){
var a = document.createElement("a");
a.download = trim(trim(contentDisposition.split("filename=")[1], "\""), "'");
a.href = window.URL.createObjectURL(this.response);
a.dispatchEvent(new MouseEvent("click"));
loadPage(link, scrollY);
return;
}
/* Select response. */
var response = document.createElement("html");
response.innerHTML = this.responseText;
/* Response preprocessing; Can be used to preserve some elements. */
try{
var newClock = response.querySelector("#clock");
var oldClock = document.getElementById("clock");
newClock.parentNode.replaceChild(oldClock, newClock);
}catch(error){console.error(error)}
try{
var newChat = response.querySelector("#chat");
var oldChat = document.querySelector("#chat");
newChat.parentNode.replaceChild(oldChat, newChat);
}catch(error){console.error(error)}
try{
var logOpen = document.getElementById("log-toggle").checked;
}catch(error){console.error(error)}
/* Replace entire page. */
//document.documentElement.parentNode.replaceChild(response, document.documentElement);
/* Insert metadata. */
var meta = response.getElementsByTagName("meta");
var oldMeta = document.getElementsByTagName("meta");
for(var i = meta.length - 1; i >= 0; i--){
for(var j = oldMeta.length - 1; j >= 0; j--){
if(meta[i].name && meta[i].name === oldMeta[j].name
|| meta[i].httpEquiv && meta[i].httpEquiv === oldMeta[j].httpEquiv
|| meta[i].charset
){
if(meta[i].content !== oldMeta[j].content
|| meta[i].charset !== oldMeta[j].charset
){
try{oldMeta[j].parentNode.replaceChild(meta[i], oldMeta[j])}catch(error){console.error(error)}
}
break;
}
}
}
var base = response.getElementsByTagName("base");
var oldBase = document.getElementsByTagName("base");
try{oldBase[0].parentNode.replaceChild(base[0], oldBase[0])}catch(error){console.error(error)}
/* Insert response. */
var elements = ["style", "header", "content", "aside-left", "aside-left-2", "aside-right", "aside-right-2", "footer", "navbar"];
for(var i = elements.length - 1; i >= 0; i--){
var replacement = response.querySelector("#" + elements[i]);
if(replacement){
var incumbent = document.getElementById(elements[i]);
/* Ignore unchanged elements. */
if(incumbent.innerHTML === replacement.innerHTML) continue;
/* Replace element. */
replacement = replacement.cloneNode(true);
incumbent.parentNode.replaceChild(replacement, incumbent);
}
}
/* Best-attempt insert malformed response. */
if(!response.querySelector("#content")){
/* Highlight header link. */
try{headerLinkHighlighter()}catch(error){console.error(error)}
/* Insert content. */
var content = document.getElementById("content");
if(content) content.innerHTML = "<article><p>" + response.innerHTML + "</p></article>";
/* Zero-out aside-left. */
//var aside = document.getElementById("aside-left");
//if(aside) aside.innerHTML = "";
/* Resolve title. */
try{document.title = "Error | " + window.location.hostname.charAt(0).toUpperCase() + window.location.hostname.slice(1)}catch(error){console.error(error)}
try{document.getElementById("loading-notify").innerHTML = ""}catch(error){console.error(error)}
/* Add response code to footer. */
try{document.querySelector("#footer td:nth-child(3)").innerText = "Response: " + this.status}catch(error){console.error(error)}
/* Ask to reload the malformed page. */
//console.warn("Received malformed response.");
//if(confirm("Received malformed response. Would you like to reload the page?")){
// //window.location.reload();
// loadPage(window.location.pathname);
// break;
//}
}
/* Get CSP nonce if applicable. */
var nonce = "";
var scripts = document.querySelectorAll("#script script[nonce]");
for(var i = scripts.length - 1; i >= 0; i--){
nonce = scripts[i].getAttribute("nonce");
if(nonce) break;
}
/* Execute any embedded scripts. */
var content = document.getElementById("content");
for(var i = elements.length - 1; i >= 0; i--){
var section = document.getElementById(elements[i]);
if(section){
var script = section.getElementsByTagName("script");
for(var j = script.length - 1; j >= 0; j--){
var replacement = document.createElement("script");
replacement.appendChild(document.createTextNode(script[j].innerHTML));
if(script[j].getAttribute("id")) replacement.setAttribute("id", script[j].getAttribute("id"));
if(script[j].getAttribute("async")) replacement.setAttribute("async", script[j].getAttribute("async"));
if(script[j].getAttribute("src")) replacement.setAttribute("src", script[j].getAttribute("src"));
replacement.setAttribute("nonce", nonce);
content.appendChild(replacement);
}
}
}
/* Response post-processing; miscellaneous tasks. */
loadPage.fileUpload = false;
try{document.title = response.getElementsByTagName("title")[0].innerHTML}catch(error){document.title = window.location.host}
try{
var log = document.getElementById("log");
var newLog = response.querySelector("#log");
if(typeof log === "object" && typeof newLog === "object"){
log.innerHTML = newLog.innerHTML;
log.scrollTop = log.scrollHeight;
if(logOpen) document.getElementById("log-toggle").checked = true;
}
}catch(error){console.error(error)}
try{autoExpander()}catch(error){console.error(error)}
try{asideLinkHighlighter("aside-left")}catch(error){console.error(error)}
try{setTimeout(cssVars, 1000)}catch(error){console.error(error)}
try{setTimeout(userSelect, 2000)}catch(error){console.error(error)}
try{
var path = new URL(window.location.href);
ga("send", "pageview", path.pathname + path.search + path.hash);
}catch(error){console.error(error)}
try{adsbygoogle.push({})}catch(error){console.error(error)}
try{
var oldScript = document.querySelectorAll("#script script");
for(var i = oldScript.length - 1; i >= 0; i--) {
var script = response.querySelector("#" + oldScript[i].id);
if(!script) oldScript[i].parentNode.removeChild(oldScript[i]);
}
var script = response.querySelectorAll("#script script");
for(var i = script.length - 1; i >= 0; i--){
var replacement = document.createElement("script");
replacement.appendChild(document.createTextNode(script[i].innerHTML));
if(script[i].getAttribute("id")) replacement.setAttribute("id", script[i].getAttribute("id"));
if(script[i].getAttribute("async")) replacement.setAttribute("async", script[i].getAttribute("async"));
if(script[i].getAttribute("src")) replacement.setAttribute("src", script[i].getAttribute("src"));
replacement.setAttribute("nonce", nonce);
var oldScript = document.querySelector("#script #" + script[i].id);
if(!oldScript){
document.getElementById("script").appendChild(replacement);
}else if(oldScript.innerHTML !== script[i].innerHTML){
oldScript.parentNode.replaceChild(oldScript, replacement);
}
}
}catch(error){console.error(error)}
try{
glass(document.body, document.querySelector("#navbar"), "blur(2px)");
}catch(error){console.error(error)}
try{
autofocus = document.querySelectorAll("input[autofocus]");
focusInput(autofocus[autofocus.length - 1]);
}catch(error){console.error(error)}
//try{
// var id = document.getElementById(window.location.hash.replace(/^#+/, ""));
// if(id && !scrollY) scrollY = id.offsetTop - 58;
// window.scrollTo(0, scrollY)}catch(error){console.error(error)}
//try{if(typeof StyleFix === "object") StyleFix.process()}catch(error){console.error(error)}
break;
/*-----------------------------------------------------------------\
| Unknown state. |
\-----------------------------------------------------------------*/
default:
// Do nothing.
}
};
/* Callback during progress. */
loadPage.xhr.onprogress = function(event){
if(event.lengthComputable){
try{document.title = "Loading: " + (event.loaded / event.total).toFixed(2) + "% | " + document.title.split(" | ")[1]}catch(error){console.error(error)}
try{document.getElementById("loading-notify").innerHTML = " - Loading: " + (event.loaded / event.total).toFixed(2) + "%"}catch(error){console.error(error)}
}
};
/* Execute the request. */
loadPage.xhr.send(params);
}
/*-----------------------------------------------------------------------------\
| When links in the "ajax" class are clicked, load the page via AJAX. |
\-----------------------------------------------------------------------------*/
var ajaxEventMousedownLink = function(event){
if(event.button !== 0) return;
for(var target = event.target; target && target !== this; target = target.parentNode){
if(typeof target.matches === "function" && (target.matches("a.ajax") || target.matches("a[href^=\"#\"]"))){
event.preventDefault();
if(window.location.pathname !== target.pathname){
window.history.replaceState({pageYOffset: window.pageYOffset, pathname: target.pathname}, null, window.location.href);
window.history.pushState(null, null, target.href);
loadPage(target.href);
}else if(target.hash.length){
window.history.replaceState({pageYOffset: window.pageYOffset, pathname: target.pathname}, null, window.location.href);
window.history.pushState(null, null, target.href);
window.scrollTo(0, document.getElementById(target.hash.replace(/^#+/, "")).offsetTop);
}
return;
}
}
};
document.removeEventListener("mousedown", ajaxEventMousedownLink);
document.addEventListener("mousedown", ajaxEventMousedownLink);
/* Disable standard click; using mousedown instead. */
var ajaxEventClickLink = function(event){
if(event.button !== 0) return;
for(var target = event.target; target && target !== this; target = target.parentNode){
if(typeof target.matches === "function" && target.matches("a.ajax")){
event.preventDefault();
return;
}
}
};
document.removeEventListener("click", ajaxEventClickLink);
document.addEventListener("click", ajaxEventClickLink);
/*-----------------------------------------------------------------------------\
| When forms in the "ajax" class are submitted, send the form and load the |
| page via AJAX. |
\-----------------------------------------------------------------------------*/
var ajaxEventMousedownForm = function(event){
if(event.button !== 0) return;
for(var target = event.target; target && target !== this; target = target.parentNode){
if(typeof target.matches === "function" && target.form && target.form.matches("form.ajax") && target.matches("input[type=\"submit\"]")){
event.preventDefault();
if(target.form.action) href = target.form.action;
else href = window.location.href + window.location.hash;
loadPage(href, window.pageYOffset, "POST", target.form);
return;
}
}
};
document.removeEventListener("mousedown", ajaxEventMousedownForm);
document.addEventListener("mousedown", ajaxEventMousedownForm);
/* Disable standard click; using mousedown instead. */
var ajaxEventClickForm = function(event){
if(event.button !== 0) return;
for(var target = event.target; target && target !== this; target = target.parentNode){
if(typeof target.matches === "function" && target.form && target.form.matches("form.ajax") && target.matches("input[type=\"submit\"]")){
event.preventDefault();
return;
}
}
};
document.removeEventListener("click", ajaxEventClickForm);
document.addEventListener("click", ajaxEventClickForm);
/* Event handler for form submit on [Enter] keypress. */
var ajaxEventKeydownForm = function(event){
if(event.keyCode !== 13 || event.shiftKey || event.altKey || event.ctrlKey) return;
for(var target = event.target; target && target !== this; target = target.parentNode){
// Ignore keypress on non-submit buttons.
if(typeof target.matches === "function"
&& (target.matches("input[type=\"button\"]")
|| target.matches("input[type=\"checkbox\"]")
|| target.matches("input[type=\"color\"]")
|| target.matches("input[type=\"image\"]")
|| target.matches("input[type=\"radio\"]")
|| target.matches("input[type=\"reset\"]"))
){
target.click();
return;
// Resolve labels; ignore keypress on non-submit button labels.
}else if(target.matches("label")){
var htmlFor = document.getElementById(target.htmlFor);
if(typeof htmlFor.matches === "function"
&& (htmlFor.matches("input[type=\"button\"]")
|| htmlFor.matches("input[type=\"checkbox\"]")
|| htmlFor.matches("input[type=\"color\"]")
|| htmlFor.matches("input[type=\"image\"]")
|| htmlFor.matches("input[type=\"radio\"]")
|| htmlFor.matches("input[type=\"reset\"]"))
){
target.click();
return;
}
// Only process AJAX forms.
}else if(target.form
&& typeof target.form.matches === "function"
&& target.form.matches("form.ajax")
){
event.preventDefault();
if(target.form.action) href = target.form.action;
else href = window.location.href + window.location.hash;
loadPage(href, window.pageYOffset, "POST", target.form);
return;
}
}
};
document.removeEventListener("keydown", ajaxEventKeydownForm);
document.addEventListener("keydown", ajaxEventKeydownForm);
/*-----------------------------------------------------------------------------\
| When the back button is clicked, load the page via AJAX. |
\-----------------------------------------------------------------------------*/
window.onpopstate = function(event){
if(!event.state){
loadPage(window.location.pathname + window.location.hash, window.pageYOffset);
}
/* Location changed, load new page. */
else if(event.state.pathname !== window.location.pathname){
loadPage(window.location.pathname + window.location.hash, event.state.pageYOffset);
}
/* Only breadcrumb was changed, scroll to element. */
else if(window.location.hash.length){
try{window.scrollTo(0, document.getElementById(window.location.hash.replace(/^#+/, "")).offsetTop - 58)}catch(error){console.error(error)}
}
}