ajaxify

/* 
 * ajaxify.js 
 * Ajaxify - The Ajax Plugin
 * https://4nf.org/ 
 * 
 * Copyright Arvind Gupta; MIT Licensed 
 */ 
 
/* INTERFACE: See also https://4nf.org/interface/

Simplest plugin call:

let ajaxify = new Ajaxify({options});
Ajaxifies the whole site, dynamically replacing the elements specified in "elements" across pages

*/

let $;

//Module global helpers
let rootUrl = location.origin, inlineclass = "ajy-inline",
	bdy,
	qa=(s,o=document)=>o.querySelectorAll(s),
	qs=(s,o=document)=>o.querySelector(s);


// The main plugin - Ajaxify
// Is passed the global options 
// Checks for necessary pre-conditions - otherwise gracefully degrades
// Initialises sub-plugins
// Calls Pronto
class Ajaxify { constructor(options) {
String.prototype.iO = function(s) { return this.toString().indexOf(s) + 1; }; //Intuitively better understandable shorthand for String.indexOf() - String.iO()
$ = this;

//Options default values
$.s = {
//	basic config parameters
	elements: "body", //selector for element IDs that are going to be swapped (e.g. "#el1, #el2, #el3")
	selector : "a:not(.no-ajaxy)", //selector for links to trigger swapping - not elements to be swapped - i.e. a selection of links
	forms : "form:not(.no-ajaxy)", // selector for ajaxifying forms - set to "false" to disable
	canonical : false, // Fetch current URL from "canonical" link if given, updating the History API.  In case of a re-direct...
	refresh : false, // Refresh the page even if link clicked is current page
 
// visual effects settings
	requestDelay : 0, //in msec - Delay of Pronto request
	scrolltop : "s", // Smart scroll, true = always scroll to top of page, false = no scroll
	bodyClasses : false, // Copy body classes from target page, set to "true" to enable
 
// script and style handling settings, prefetch
	deltas : true, // true = deltas loaded, false = all scripts loaded
	asyncdef : true, // default async value for dynamically inserted external scripts, false = synchronous / true = asynchronous
	alwayshints : false, // strings, - separated by ", " - if matched in any external script URL - these are always loaded on every page load
	inline : true, // true = all inline scripts loaded, false = only specific inline scripts are loaded
	inlinesync : true, // synchronise inline scripts loading by adding a central tiny delay to all of them
	inlinehints : false, // strings - separated by ", " - if matched in any inline scripts - only these are executed - set "inline" to false beforehand
	inlineskip : "adsbygoogle", // strings - separated by ", " - if matched in any inline scripts - these are NOT are executed - set "inline" to true beforehand 
	inlineappend : true, // append scripts to the main content element, instead of "eval"-ing them
	intevents: true, // intercept events that are fired only on classic page load and simulate their trigger on ajax page load ("DOMContentLoaded")
	style : true, // true = all style tags in the head loaded, false = style tags on target page ignored
	prefetchoff : false, // Plugin pre-fetches pages on hoverIntent - true = set off completely // strings - separated by ", " - hints to select out
 
// debugging & advanced settings
	verbosity : 0, //Debugging level to console: default off.	Can be set to 10 and higher (in case of logging enabled)
	memoryoff : false, // strings - separated by ", " - if matched in any URLs - only these are NOT executed - set to "true" to disable memory completely
	cb : 0, // callback handler on completion of each Ajax request - default 0
	pluginon : true, // Plugin set "on" or "off" (==false) manually
	passCount: false // Show number of pass for debugging
};


$.pass = 0; $.currentURL = ""; $.h = {};
$.parse = (s, pl) => (pl = document.createElement('div'), pl.insertAdjacentHTML('afterbegin', s), pl.firstElementChild); // HTML parser
$.trigger = (t, e) => { let ev = document.createEvent('HTMLEvents'); ev.initEvent("pronto." + t, true, false); ev.data = e ? e : $.Rq("e"); window.dispatchEvent(ev); document.dispatchEvent(ev); };
$.internal = (url) => { if (!url) return false; if (typeof(url) === "object") url = url.href; if (url==="") return true; return url.substring(0,rootUrl.length) === rootUrl || !url.iO(":"); };
$.intevents = () => {
	let iFn = function (a, b, c = false) { if ((this === document || this === window) && a=="DOMContentLoaded") setTimeout(b); else this.ael(a,b,c);};  // if "DOMContentLoaded" - execute function, else - add event listener	
	EventTarget.prototype.ael = EventTarget.prototype.addEventListener; // store original method
	EventTarget.prototype.addEventListener = iFn; // start intercepting event listener addition
};

function _copyAttributes(el, $S, flush) { //copy all attributes of element generically
	if (flush) [...el.attributes].forEach(e => el.removeAttribute(e.name)); //delete all old attributes
	[...$S.attributes].forEach(e => el.setAttribute(e.nodeName, e.nodeValue)); //low-level insertion
}

function _on(eventName, elementSelector, handler, el = document) { //e.currentTarget is document when the handler is called
	el.addEventListener(eventName, function(e) {
		// loop parent nodes from the target to the delegation node
		for (var target = e.target; target && target != this; target = target.parentNode) {
			if (target.matches(elementSelector)) {
				handler(target, e);
				break;
			}
		}
	}, !!eventName.iO('mo'));
}

class Hints { constructor(h) { let _ = this;
	_.list = (typeof h === 'string' && h.length > 0) ? h.split(", ") : false; //hints are passed as a comma separated string 
	_.find = (t) => (!t || !_.list) ? false : _.list.some(h => t.iO(h)); //iterate through hints within passed text (t)
}}

function lg(m){ $.s.verbosity && console && console.log(m); }

// The stateful Pages class
// Usage - parameter "h" values:
// <URL> - returns page with specified URL from internal array
// <object> - saves the passed page in internal array
// false - returns false
class Pages { constructor() {
	let d = [], i = -1;
            
	this.a = function (h) {
		if (typeof h === "string") { 
			if(h === "f") d = []; 
			else if((i=_iPage(h)) !== -1) return d[i][1]; 
		}

		if (typeof h === "object") { 
			if((i=_iPage(h[0])) === -1) d.push(h); 
			else d[i] = h; 
		}

		if (typeof h === "boolean") return false; 
	};
		
	let _iPage = h => d.findIndex(e => e[0] == h)
}}

// The GetPage class
// First parameter (o) is a switch: 
// empty - returns cache
// <URL> - loads HTML via Ajax, second parameter "p" must be callback
// + - pre-fetches page, second parameter "p" must be URL, third parameter "p2" must be callback 
// - - loads page into DOM and handle scripts, second parameter "p" must hold selection to load
// x - returns response
// otherwise - returns selection of current page to client

class GetPage { constructor() {
	let rsp = 0, cb = 0, plus = 0, rt = "", ct = 0, rc = 0, ac = 0,

//Regexes for escaping fetched HTML of a whole page - best of Baluptons Ajaxify
//Makes it possible to pre-fetch an entire page
docType = /<\!DOCTYPE[^>]*>/i,
tagso = /<(html|head|link)([\s\>])/gi,
tagsod = /<(body)([\s\>])/gi,
tagsc = /<\/(html|head|body|link)\>/gi,

//Helper strings
div12 = '<div class="ajy-$1"$2',
divid12 = '<div id="ajy-$1"$2';

	this.a = function (o, p, p2) { 
		if (!o) return $.cache.g(); 

		if (o.iO("/")) { 
			cb = p; 
			if(plus == o) return; 
			return _lPage(o); 
		}

		if (o === "+")	{ 
			plus = p; 
			cb = p2; 
			return _lPage(p, true); 
		}

		if (o === "a") { if (rc > 0) {_cl(); ac.abort();} return; }
		if (o === "s") return ((rc) ? 1 : 0) + rt; 
		if (o === "-") return _lSel(p); 
		if (o === "x") return rsp; 

		if (!$.cache.g()) return;
		if (o === "body") return qs("#ajy-" + o, $.cache.g());
		if (o === "script") return qa(o, $.cache.g()); 

		return qs((o === "title") ?	o : ".ajy-" + o, $.cache.g()); 
};
let _lSel = $t => (
	$.pass++, 
	_lEls($t), 
	qa("body > script").forEach(e => (e.classList.contains(inlineclass)) ? e.parentNode.removeChild(e) : false), 
	$.scripts(true), 
	$.scripts("s"), 
	$.scripts("c") 
),
	_lPage = (h, pre) => { 
		if (h.iO("#")) h = h.split("#")[0]; 
		if ($.Rq("is") || !$.cache.l(h)) return _lAjax(h, pre); 

		plus = 0; 
		if (cb) return cb(); 
	},
	_ld = ($t, $h) => {
		if(!$h) return; //no input

		var $c = $h.cloneNode(true); // clone element node (true = deep clone)
		qa("script", $c).forEach(e => e.parentNode.removeChild(e));
		_copyAttributes($t, $c, true); 
		$t.innerHTML = $c.innerHTML;
	},
	_lEls = $t => 
		$.cache.g() && !_isBody($t) && $t.forEach(function($el) { 
			_ld($el, qs("#" + $el.getAttribute("id"), $.cache.g()));
		}),
	_isBody = $t => $t[0].tagName.toLowerCase() == "body" && (_ld(bdy, qs("#ajy-body", $.cache.g())), 1),
	_lAjax = (hin, pre) => { 
		var ispost = $.Rq("is"); 
		if (pre) rt="p"; else rt="c"; 

		ac = new AbortController(); // set abort controller
		rc++; // set active request counter
		fetch(hin, {
			method: ((ispost) ? "POST" : "GET"),
			cache: "default",
			mode: "same-origin",
			headers: {"X-Requested-With": "XMLHttpRequest"},
			body: (ispost) ? $.Rq("d") : null,
			signal: ac.signal
		}).then(r => {
			if (!r.ok || !_isHtml(r)) {
				if (!pre) {location.href = hin; _cl(); $.pronto(0, $.currentURL);}
				return;
			}
			rsp = r; // store response
			return r.text();
		}).then(r => {
			_cl(1); // clear only plus variable
			if (!r) return; // ensure data
			rsp.responseText = r; // store response text
			
			return _cache(hin, r);
		}).catch(err => {
			if(err.name === "AbortError") return;
			try {
				$.trigger("error", err); 
				lg("Response text : " + err.message); 
				return _cache(hin, err.message, err);
			} catch (e) {}
		}).finally(() => rc--); // reset active request counter
	},
	_cl = c => (plus = 0, (!c) ? cb = 0 : 0), // clear plus AND/OR callback
	_cache = (href, h, err) => $.cache.s($.parse(_parseHTML(h))) && ($.pages([href, $.cache.g()]), 1) && cb && cb(err),
	_isHtml = x => (ct = x.headers.get("content-type")) && (ct.iO("html") || ct.iO("form-")),
	_parseHTML = h => document.createElement("html").innerHTML = _replD(h).trim(),
	_replD = h => String(h).replace(docType, "").replace(tagso, div12).replace(tagsod, divid12).replace(tagsc, "</div>")
}}

// The stateful Scripts plugin
// First parameter "o" is switch:
// i - initailise options
// c - fetch canonical URL
// <object> - handle one inline script
// otherwise - delta loading
class Scripts { constructor() {
	let $s = false, txt = 0;
	$.h.inlinehints = new Hints($.s.inlinehints);
	$.h.inlineskip = new Hints($.s.inlineskip);
	
	this.a = function (o) {
		if (o === "i") { 
			if(!$s) $s = {}; 
			return true;
		}

		if (o === "s") return _allstyle($s.y); 

		if (o === "1") { 
			$.detScripts($s); 
			return _addScripts($s); 
		}

		if (o === "c") return $.s.canonical && $s.can ? $s.can.getAttribute("href") : false;
		if (o === "d") return $.detScripts($s);
		if (o && typeof o == "object") return _onetxt(o);

		if ($.scripts("d")) return;
		_addScripts($s);
};
let _allstyle = $s =>	 
	!$.s.style || !$s || (
	qa("style", qs("head")).forEach(e => e.parentNode.removeChild(e)),
	$s.forEach(el => _addstyle(el.textContent))
	),
	_onetxt = $s => 
		(!(txt = $s.textContent).iO(").ajaxify(") && (!txt.iO("new Ajaxify(")) && 
			(($.s.inline && !$.h.inlineskip.find(txt)) || $s.classList.contains("ajaxy") || 
			$.h.inlinehints.find(txt))
		) && _addtxt($s),
	_addtxt = $s => { 
		if(!txt || !txt.length) return; 
		if($.s.inlineappend || ($s.getAttribute("type") && !$s.getAttribute("type").iO("text/javascript"))) try { return _apptxt($s); } catch (e) { }

		try { eval(txt); } catch (e1) { 
			lg("Error in inline script : " + txt + "\nError code : " + e1);
		}
	},
	_apptxt = $s => { let sc = document.createElement("script"); _copyAttributes(sc, $s); sc.classList.add(inlineclass);
		try {sc.appendChild(document.createTextNode($s.textContent))} catch(e) {sc.text = $s.textContent};
		return qs("body").appendChild(sc);
	},
	_addstyle = t => qs("head").appendChild($.parse('<style>' + t + '</style>')),
	_addScripts = $s => ( $.addAll($s.c, "href"), $.s.inlinesync ? setTimeout(() => $.addAll($s.j, "src")) : $.addAll($s.j, "src"))
}}

// The DetScripts plugin - stands for "detach scripts"
// Works on "$s" <object> that is passed in and fills it
// Fetches all stylesheets in the head
// Fetches the canonical URL
// Fetches all external scripts on the page
// Fetches all inline scripts on the page
class DetScripts { constructor() {
	let head = 0, lk = 0, j = 0;
            
	this.a = function ($s) {
		head = $.pass ? $.fn("head") : qs("head"); //If "pass" is 0 -> fetch head from DOM, otherwise from target page
		if (!head) return true;
		lk = qa($.pass ? ".ajy-link" : "link", head); //If "pass" is 0 -> fetch links from DOM, otherwise from target page
		j = $.pass ? $.fn("script") : qa("script"); //If "pass" is 0 -> fetch JSs from DOM, otherwise from target page
		$s.c = _rel(lk, "stylesheet"); //Extract stylesheets
		$s.y = qa("style", head); //Extract style tags
		$s.can = _rel(lk, "canonical"); //Extract canonical tag
		$s.j = j; //Assign JSs to internal selection
	};
let _rel = (lk, v) => Array.prototype.filter.call(lk, e => e.getAttribute("rel").iO(v));
}}

// The AddAll plugin
// Works on a new selection of scripts to apply delta-loading to it 
// pk parameter:
// href - operate on stylesheets in the new selection
// src - operate on JS scripts
class AddAll { constructor() {
	let $scriptsO = [], $sCssO = [], $sO = [], PK = 0, url = 0, 
linki = '<link rel="stylesheet" href="*" />',
linkr = 'link[href*="!"]',
scrr = 'script[src*="!"]';

	$.h.alwayshints = new Hints($.s.alwayshints);

	this.a = function ($this, pk) {
		if(!$this.length) return; //ensure input
		if($.s.deltas === "n") return true; //Delta-loading completely disabled

		PK = pk; //Copy "primary key" into internal variable

		if(!$.s.deltas) return _allScripts($this); //process all scripts
		//deltas presumed to be "true" -> proceed with normal delta-loading

		$scriptsO = PK == "href" ? $sCssO : $sO; //Copy old.  Stylesheets or JS

		if(!$.pass) _newArray($this); //Fill new array on initial load, nothing more
		else $this.forEach(function(s) { //Iterate through selection
			var $t = s;
			url = $t.getAttribute(PK);
			if(_classAlways($t)) { //Class always handling
				_removeScript(); //remove from DOM
				_iScript($t); //insert back single external script in the head
				return;
			}
			if(url) { //URL?
				if(!$scriptsO.some(e => e == url)) { // Test, whether new
					$scriptsO.push(url); //If yes: Push to old array
					_iScript($t);
				}
				//Otherwise nothing to do
				return;
			}

			if(PK != "href" && !$t.classList.contains("no-ajaxy")) $.scripts($t); //Inline JS script? -> inject into DOM
		});
};
let _allScripts = $t => $t.forEach(e => _iScript(e)),
	_newArray = $t => $t.forEach(e => (url = e.getAttribute(PK)) ? $scriptsO.push(url) : 0),
	_classAlways = $t => $t.getAttribute("data-class") == "always" || $.h.alwayshints.find(url),
	_iScript = $S => { 
		url = $S.getAttribute(PK);

		if(PK == "href") return qs("head").appendChild($.parse(linki.replace("*", url))); 
		if(!url) return $.scripts($S); 
		
		var sc = document.createElement("script");
		sc.async = $.s.asyncdef; 
		_copyAttributes(sc, $S); 
		qs("head").appendChild(sc); 
	},
	_removeScript = () => qa((PK == "href" ? linkr : scrr).replace("!", url)).forEach(e => e.parentNode.removeChild(e))
}}

// The Rq plugin - stands for request
// Stores all kinds of and manages data concerning the pending request
// Simplifies the Pronto plugin by managing request data separately, instead of passing it around...
// Second parameter (p) : data
// First parameter (o) values:
// = - check whether internally stored "href" ("h") variable is the same as the global currentURL
// ! - update last request ("l") variable with passed href
// ? - Edin's intelligent plausibility check - can spawn an external fetch abort
// v - validate value passed in "p", which is expected to be a click event value - also performs "i" afterwards
// i - initialise request defaults and return "c" (currentTarget)
// h - access internal href hard
// e - set / get internal "e" (event)
// p - set / get internal "p" (push flag)
// is - set / get internal "ispost" (flag whether request is a POST)
// d - set / get internal "d" (data for central $.ajax())
// C - set / get internal "can" ("href" of canonical URL)
// c - check whether simple canonical URL is given and return, otherwise return value passed in "p"
class RQ { constructor() {
	let ispost = 0, data = 0, push = 0, can = 0, e = 0, c = 0, h = 0, l = false;
            
	this.a = function (o, p, t) {
		if(o === "=") { 
			if(p) return h === $.currentURL //check whether internally stored "href" ("h") variable is the same as the global currentURL
			|| h === l; //or href of last request ("l")
			return h === $.currentURL; //for click requests
		}

		if(o === "!") return l = h; //store href in "l" (last request)

		if(o === "?") { //Edin previously called this "isOK" - powerful intelligent plausibility check
			let xs=$.fn("s");
			if (!xs.iO("0") && !p) $.fn("a"); //if fetch is not idle and new request is standard one, do ac.abort() to set it free
			if (xs==="1c" && p) return false; //if fetch is processing standard request and new request is prefetch, cancel prefetch until fetch is finished
			if (xs==="1p" && p) $.s.memoryoff ? $.fn("a") : 1; //if fetch is processing prefetch request and new request is prefetch do nothing (see [options] comment below)
			//([semaphore options for requests] $.fn("a") -> abort previous, proceed with new | return false -> leave previous, stop new | return true -> proceed)
			return true;
		}

		if(o === "v") { //validate value passed in "p", which is expected to be a click event value - also performs "i" afterwards
			if(!p) return false; //ensure data
			_setE(p, t); //Set event and href in one go
			if(!$.internal(h)) return false; //if not internal -> report failure
			o = "i"; //continue with "i"
		}

		if(o === "i") { //initialise request defaults and return "c" (currentTarget)
			ispost = false; //GET assumed
			data = null; //reset data
			push = true; //assume we want to push URL to the History API
			can = false; //reset can (canonical URL)
			return h; //return "h" (href)
		}

		if(o === "h") { // Access href hard
			if(p) {
				if (typeof p === "string") e = 0; // Reset e -> default handler
				h = (p.href) ? p.href : p;	// Poke in href hard
			}

			return h; //href
		}

		if(o === "e") { //set / get internal "e" (event)
			if(p) _setE(p, t);	//Set event and href in one go
			return e ? e : h; // Return "e" or if not given "h"
		}

		if(o === "p") { //set / get internal "p" (push flag)
			if(p !== undefined) push = p;
			return push;
		}

		if(o === "is") { //set / get internal "ispost" (flag whether request is a POST)
			if(p !== undefined) ispost = p;
			return ispost;
		}

		if(o === "d") { //set / get internal "d" (data for central $.ajax())
			if(p) data = p;
			return data;
		}

		if(o === "C") { //set internal "can" ("href" of canonical URL)
			if(p !== undefined) can = p;
			return can;
		}

		if(o === "c") return can && can !== p && !p.iO("#") && !p.iO("?") ? can : p; //get internal "can" ("href" of canonical URL)
};
let _setE = (p, t) => h = typeof (e = p) !== "string" ? (e.currentTarget && e.currentTarget.href) || (t && t.href) || e.currentTarget.action || e.originalEvent.state.url : e
}}

// The Frms plugin - stands for forms
// Ajaxify all forms in the specified divs
// Switch (o) values:
// d - set divs variable
// a - Ajaxify all forms in divs
class Frms { constructor() {
	let fm = 0, divs = 0;

	this.a = function (o, p) {
		if (!$.s.forms || !o) return; //ensure data

		if(o === "d") divs = p; //set divs variable
		if(o === "a") divs.forEach(div => { //iterate through divs
		Array.prototype.filter.call(qa($.s.forms, div), function(e) { //filter forms
			let c = e.getAttribute("action");
			return($.internal(c && c.length > 0 ? c : $.currentURL)); //ensure "action"
		}).forEach(frm => { //iterate through forms
		frm.addEventListener("submit", q => { //create event listener
			fm = q.target; // fetch target

			p = _k(); //Serialise data
			var g = "get", //assume GET
			m = fm.getAttribute("method"); //fetch method attribute
			if (m.length > 0 && m.toLowerCase() == "post") g = "post"; //Override with "post"

			var h, a = fm.getAttribute("action"); //fetch action attribute
			if (a && a.length > 0) h = a; //found -> store
			else h = $.currentURL; //not found -> select current URL

			$.Rq("v", q); //validate request

			if (g == "get") h = _b(h, p); //GET -> copy URL parameters
			else {
				$.Rq("is", true); //set is POST in request data
				$.Rq("d", p); //save data in request data
			}

			$.trigger("submit", h); //raise pronto.submit event
			$.pronto(0, { href: h }); //programmatically change page

			q.preventDefault(); //prevent default form action
			return(false); //success -> disable default behaviour
		});
		});
	});
	};
let _k = () => {
		let o = new FormData(fm), n = qs("input[name][type=submit]", fm);

		if (n) o.append(n.getAttribute("name"), n.value);
		return o;
	},
	_b = (m, n) => {
		let s = "";
		if (m.iO("?")) m = m.substring(0, m.iO("?"));
		
		for (var [k, v] of n.entries()) s += `${k}=${encodeURIComponent(v)}&`;
		return `${m}?${s.slice(0,-1)}`;
	}
}}

// The stateful Offsets plugin
// Usage:
// 1) $.offsets(<URL>) - returns offset of specified URL from internal array
// 2) $.offsets() - saves the current URL + offset in internal array
class Offsets { constructor() {
	let d = [], i = -1;
            
	this.a = function (h) {
		if (typeof h === "string") { //Lookup page offset
			h = h.iO("?") ? h.split("?")[0] : h; //Handle root URL only from dynamic pages
			i = _iOffset(h); //Fetch offset
			if(i === -1) return 0; // scrollTop if not found
			return d[i][1]; //Return offset that was found
		}

		//Add page offset
		var u = $.currentURL, us1 = u.iO("?") ? u.split("?")[0] : u, us = us1.iO("#") ? us1.split("#")[0] : us1, os = [us, (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop];
		i = _iOffset(us); //get page index
		if(i === -1) d.push(os); //doesn't exist -> push to array
		else d[i] = os; //exists -> overwrite
	};
let _iOffset = h => d.findIndex(e => e[0] == h)
}}

// The Scrolly plugin - manages scroll effects centrally
// scrolltop values: "s" - "smart" (default), true - always scroll to top, false - no scroll
// Switch (o) values:
// + - add current page to offsets
// ! - scroll to current page offset
class Scrolly { constructor() {

	if ('scrollRestoration' in history) history.scrollRestoration = 'manual';

	this.a = function (o) {
		if(!o) return; //ensure operator

		var op = o; //cache operator

		if(o === "+" || o === "!") o = $.currentURL; //fetch currentURL for "+" and "-" operators

		if(op !== "+" && o.iO("#") && (o.iO("#") < o.length - 1)) { //if hash in URL and not standalone hash
			let $el = qs("#" + o.split("#")[1]); //fetch the element
			if (!$el) return; //nothing found -> return quickly
			let box = $el.getBoundingClientRect();
			_scrll(box.top + window.pageYOffset - document.documentElement.clientTop); // ...animate to ID
			return;
		}

		if($.s.scrolltop === "s") { //smart scroll enabled
			if(op === "+") $.offsets(); //add page offset
			if(op === "!") _scrll($.offsets(o)); //scroll to stored position of page

			return;
		}

		if(op !== "+" && $.s.scrolltop) _scrll(0); //otherwise scroll to top of page

		//default -> do nothing
	};
	let _scrll = o => setTimeout(() => window.scrollTo(0, o), 10) //delay of 10 milliseconds on all scroll effects
}}

// The hApi plugin - manages operations on the History API centrally
// Second parameter (p) - set global currentURL
// Switch (o) values:
// = - perform a replaceState, using currentURL
// otherwise - perform a pushState, using currentURL
class HApi { constructor() {

	this.a = function (o, p) {
		if(!o) return; //ensure operator
		if(p) $.currentURL = p; //if p given -> update current URL

		if(o === "=") history.replaceState({ url: $.currentURL }, "state-" + $.currentURL, $.currentURL); //perform replaceState
		else if ($.currentURL !== window.location.href) history.pushState({ url: $.currentURL }, "state-" + $.currentURL, $.currentURL); //perform pushState
	};
}}

// The Pronto plugin - Pronto variant of Ben Plum's Pronto plugin - low level event handling in general
// Works on a selection, passed to Pronto by the selection, which specifies, which elements to Ajaxify
// Switch (h) values:
// i - initialise Pronto
// <object> - fetch href part and continue with _request()
// <URL> - set "h" variable of Rq hard and continue with _request()
class Pronto { constructor() {
	let $gthis = 0, requestTimer = 0, pd = 150, ptim = 0;
	$.h.prefetchoff = new Hints($.s.prefetchoff);

	this.a = function ($this, h) {
		if(!h) return; //ensure data

		if(h === "i") { //request to initialise
			bdy = document.body;
			if(!$this.length) $this = "body";
			$gthis = qa($this); //copy selection to global selector
			$.frms = new Frms().a; //initialise forms sub-plugin
			if($.s.idleTime) $.slides = new classSlides($).a; //initialise optional slideshow sub-plugin
			$.scrolly = new Scrolly().a; //initialise scroll effects sub-plugin
			$.offsets = new Offsets().a;
			$.hApi = new HApi().a;
			_init_p(); //initialise Pronto sub-plugin
			return $this; //return query selector for chaining
		}

		if(typeof(h) === "object") { //jump to internal page programmatically -> handler for forms sub-plugin
			$.Rq("h", h);
			_request();
			return;
		}

		if(h.iO("/")) { //jump to internal page programmatically -> default handler
			$.Rq("h", h);
			_request(true);
		}
	};
let _init_p = () => {
	$.hApi("=", window.location.href);
	window.addEventListener("popstate", _onPop);
	if ($.s.prefetchoff !== true) {
		_on("mouseenter", $.s.selector, _preftime); // start prefetch timeout
		_on("mouseleave", $.s.selector, _prefstop); // stop prefetch timeout
		_on("touchstart", $.s.selector, _prefetch);
	}
	_on("click", $.s.selector, _click, bdy);
	$.frms("d", qa("body"));
	$.frms("a");
	$.frms("d", $gthis);
	if($.s.idleTime) $.slides("i");
},
	_preftime  = (t, e) => (_prefstop(), ptim = setTimeout(()=> _prefetch(t, e), pd)), // call prefetch if timeout expires without being cleared by _prefstop
	_prefstop = () => clearTimeout(ptim),
	_prefetch = (t, e) => {
		if($.s.prefetchoff === true) return;
		if (!$.Rq("?", true)) return;
		var href = $.Rq("v", e, t);
		if ($.Rq("=", true) || !href || $.h.prefetchoff.find(href)) return;
		$.fn("+", href, () => false);
	},
	_stopBubbling = e => (
		e.preventDefault(),
		e.stopPropagation(),
		e.stopImmediatePropagation()
	),
	_click = (t, e, notPush) => {
		if(!$.Rq("?")) return;
		var href = $.Rq("v", e, t);
		if(!href || _exoticKey(t)) return;
		if(href.substr(-1) ==="#") return true;
		if(_hashChange()) {
			$.hApi("=", href);
			return true;
		}

		$.scrolly("+");
		_stopBubbling(e);
		if($.Rq("=")) $.hApi("=");
		if($.s.refresh || !$.Rq("=")) _request(notPush);
	},
	_request = notPush => {
		$.Rq("!");
		if(notPush) $.Rq("p", false);
		$.trigger("request");
		$.fn($.Rq("h"), err => {
			if (err) {
				lg("Error in _request : " + err);
				$.trigger("error", err);
			}

			_render();
		});
	},
	_render = () => {
		$.trigger("beforeload");
		if($.s.requestDelay) {
			if(requestTimer) clearTimeout(requestTimer);
			requestTimer = setTimeout(_doRender, $.s.requestDelay);
		} else _doRender();
	},
	_onPop = e => {
		var url = window.location.href;

		$.Rq("i");
		$.Rq("h", url);
		$.Rq("p", false);
		$.scrolly("+");

		if (!url || url === $.currentURL) return;
		$.trigger("request");
		$.fn(url, _render);
	},
	_doRender = () => {
		$.trigger("load");
		if($.s.bodyClasses) { var classes = $.fn("body").getAttribute("class"); bdy.setAttribute("class", classes ? classes : ""); }

		var href = $.Rq("h"), title;
		href = $.Rq("c", href);

		$.hApi($.Rq("p") ? "+" : "=", href);
		if(title = $.fn("title")) qs("title").innerHTML = title.innerHTML;
		$.Rq("C", $.fn("-", $gthis));
		$.frms("a");

		$.scrolly("!");
		_gaCaptureView(href);
		$.trigger("render");
		if($.s.passCount) qs("#" + $.s.passCount).innerHTML = "Pass: " + $.pass;
		if($.s.cb) $.s.cb();
	},
	_gaCaptureView = href => {
		href = "/" + href.replace(rootUrl,"");
		if (typeof window.ga !== "undefined") window.ga("send", "pageview", href);
		else if (typeof window._gaq !== "undefined") window._gaq.push(["_trackPageview", href]);
	},
	_exoticKey = (t) => {
		var href = $.Rq("h"), e = $.Rq("e"), tgt = e.currentTarget.target || t.target;
		return (e.which > 1 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || tgt === "_blank"
			|| href.iO("wp-login") || href.iO("wp-admin"));
	},
	_hashChange = () => {
		var e = $.Rq("e");
		return (e.hash && e.href.replace(e.hash, "") === window.location.href.replace(location.hash, "") || e.href === window.location.href + "#");
	}
}}


$.init = () => {
	let o = options;
	if (!o || typeof(o) !== "string") {
		if (document.readyState === "complete" || 
			(document.readyState !== "loading" && !document.documentElement.doScroll)) run();
		else document.addEventListener('DOMContentLoaded', run);
		return $;
	}
	else return $.pronto(0, o);
};

let run = () => {
		$.s = Object.assign($.s, options);
		$.pages = new Pages().a;
		$.pronto = new Pronto().a;
		if (load()) { 
			$.pronto($.s.elements, "i"); 
			if ($.s.deltas) $.scripts("1"); 
		}
	},
	load = () => { 
		if (!(window.history && window.history.pushState && window.history.replaceState) || !$.s.pluginon) { 
			lg("Gracefully exiting...");
			return false;
		}
		
		lg("Ajaxify loaded..."); //verbosity option steers, whether this initialisation message is output
		
		if ($.s.intevents) $.intevents(); // intercept events
		$.scripts = new Scripts().a;
		$.scripts("i"); 
		$.cache = new Cache();
		$.memory = new Memory(); $.h.memoryoff = new Hints($.s.memoryoff);
		$.fn = $.getPage = new GetPage().a;
		$.detScripts = new DetScripts().a;
		$.addAll = new AddAll().a;
		$.Rq = new RQ().a;
		return true; 
	};
$.init(); // initialize Ajaxify on definition
}}

// The stateful Cache class
// this.d = entire current page (as an object)
class Cache {
	g(){ return this.d; } //getter
	s(v){ return this.d = v; } //setter
	f(){ $.pages("f"); } //delegate flush
	l(u){ return this.s($.pages($.memory.l(u))); } //lookup URL and load
}

// The stateful Memory class
// Usage: $.memory.l(<URL>) - returns the same URL if not turned off internally
class Memory {
	l(h) {
		if (!h || $.s.memoryoff === true) return false; 
		if ($.s.memoryoff === false) return h; 
		return $.h.memoryoff.find(h) ? false : h; 
	}
}