/* * ajaxify.js * Ajaxify - The Ajax Plugin * https://4nf.org/ * * Copyright Arvind Gupta; MIT Licensed * * Version 8.3.1 */ /* 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 Ay; //to become the global handle for the main Ajaxify parent class - if used by you already, please rename and rebuild let iFn = function (a, b, c = false) { // if "DOMContentLoaded" - execute function, else - add event listener if ((this === document || this === window) && (a == "DOMContentLoaded")) { c = c ? Object.assign(c, { once: true }) : { once: true }; } return this && this.ael(a, b, c); }; EventTarget.prototype.ael = EventTarget.prototype.addEventListener; // store original method EventTarget.prototype.addEventListener = iFn; // start intercepting event listener addition function _won(a, b, c = false) { if(c === false) c = {once: true}; window.addEventListener(a, b, c) }; //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), qha=(e)=>qs("head").appendChild(e), prC=(e)=>e.parentNode.removeChild(e), dcE=(e)=>document.createElement(e), _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 => e.nodeValue == "ajy-body" || el.setAttribute(e.nodeName, e.nodeValue)); //low-level insertion }; // 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() Ay = this; //Options default values Ay.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 scrollDelay : false, // Minimal delay on all scroll effects in milliseconds, useful in case of e.g. smooth scroll bodyClasses : true, // Copy body attributes from target page, set to "false" to disable // 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 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 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 DCLDelay: 0 // "DOMContentLoaded delay" - 0 = no delay, false = no triggering of DOMContentLoaded at all, otherwise delay in milliseconds }; Ay.pass = 0; Ay.running = false; Ay.currentURL = ""; Ay.h = {}; Ay.parse = (s, pl) => (pl = dcE('div'), pl.insertAdjacentHTML('afterbegin', s), pl.firstElementChild); // HTML parser Ay.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(":"); }; // Trigger event Ay.triggerEvent = (v) => { if (Ay.s.DCLDelay !== false) { const evnt = new Event(v, { bubbles: true, cancelable: true }); const dw = window; //v === "DOMContentLoaded" ? document : window; Ay.s.DCLDelay === 0 ? dw.dispatchEvent(evnt) : setTimeout(() => { dw.dispatchEvent(evnt); }, Ay.s.DCLDelay); } }; Ay.trigger = (t, e) => { const ev = new Event("pronto." + t, { bubbles: true, cancelable: false }); ev.data = e ? e : Ay.Rq("e"); window.dispatchEvent(ev); }; 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){ Ay.s.verbosity && console && console.log(m); } // 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 //for html, head and body tag //Makes it possible to pre-fetch an entire page docType = /<\!DOCTYPE[^>]*>/i, tagso = /<(html|head)([\s\>])/gi, tagsod = /<(body)([\s\>])/gi, tagsc = /<\/(html|head|body)\>/gi, //Helper strings div12 = '<div class="ajy-$1"$2', divid12 = '<div id="ajy-$1"$2'; this.a = function (o, p, p2) { if (!o) return Ay.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 (!Ay.cache.g()) return; if (o === "body") return qs("#ajy-" + o, Ay.cache.g()); if (o === "script") return qa(o, Ay.cache.g()); return qs((o === "title") ? o : ".ajy-" + o, Ay.cache.g()); }; let _lSel = t => ( Ay.pass++, _lEls(t), qa("body > script").forEach(e => (e.classList.contains(inlineclass)) ? prC(e) : false), Ay.scripts.t(), Ay.scripts.s(), Ay.scripts.c() ), _lPage = (h, pre) => { if (h.iO("#")) h = h.split("#")[0]; if (Ay.Rq("is") || !Ay.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 => prC(e)); _copyAttributes(t, c, true); t.innerHTML = c.innerHTML; }, _lEls = t => Ay.cache.g() && !_isBody(t) && t.forEach(function(e) { _ld(e, qs("#" + e.getAttribute("id"), Ay.cache.g())); }), _isBody = t => t[0].tagName.toLowerCase() == "body" && (_ld(bdy, qs("#ajy-body", Ay.cache.g())), 1), _lAjax = (hin, pre) => { var ispost = Ay.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) ? Ay.Rq("d") : null, signal: ac.signal }).then(r => { if (!r.ok || !_isHtml(r)) { if (!pre) {location.href = hin;} 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 { Ay.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) => Ay.cache.s(Ay.parse(_parseHTML(h))) && (Ay.pages.p([href, Ay.cache.g()]), 1) && cb && cb(err), _isHtml = x => (ct = x.headers.get("content-type")) && (ct.iO("html") || ct.iO("form-")), _parseHTML = h => dcE("html").innerHTML = _replD(h).trim(), _replD = h => String(h).replace(docType, "").replace(tagso, div12).replace(tagsod, divid12).replace(tagsc, "</div>") }} // 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 fetch()) // 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 === Ay.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 === Ay.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=Ay.fn("s"); if (!xs.iO("0") && !p) Ay.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) Ay.s.memoryoff ? Ay.fn("a") : 1; //if fetch is processing prefetch request and new request is prefetch do nothing (see [options] comment below) //([semaphore options for requests] Ay.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(!Ay.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 fetch()) 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 (!Ay.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(Ay.s.forms, div), function(e) { //filter forms let c = e.getAttribute("action"); return(Ay.internal(c && c.length > 0 ? c : Ay.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 = Ay.currentURL; //not found -> select current URL Ay.Rq("v", q); //validate request if (g == "get") h = _b(h, p); //GET -> copy URL parameters else { Ay.Rq("is", true); //set is POST in request data Ay.Rq("d", p); //save data in request data } Ay.trigger("submit", h); //raise pronto.submit event Ay.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 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 gsl = 0, requestTimer = 0, pd = 150, ptim = 0; Ay.h.prefetchoff = new Hints(Ay.s.prefetchoff); this.a = function (sl, h) { if(!h) return; //ensure data if(h === "i") { //request to initialise bdy = document.body; if(!sl.length) sl = "body"; gsl = qa(sl); //copy selection to global selector Ay.frms = new Frms().a; //initialise forms sub-plugin if(Ay.s.idleTime) Ay.slides = new classSlides(Ay).a; //initialise optional slideshow sub-plugin Ay.scrolly = new Scrolly(); //initialise scroll effects sub-plugin (Ay.offsets = new Offsets()).f(); Ay.hApi = new HApi(); _init_p(); //initialise Pronto sub-plugin return sl; //return query selector for chaining } if(typeof(h) === "object") { //jump to internal page programmatically -> handler for forms sub-plugin Ay.Rq("h", h); _request(); return; } if(h.iO("/")) { //jump to internal page programmatically -> default handler Ay.Rq("h", h); _request(true); } }; let _init_p = () => { Ay.hApi.r(window.location.href); window.addEventListener("popstate", _onPop); if (Ay.s.prefetchoff !== true) { _on("mouseenter", Ay.s.selector, _preftime); // start prefetch timeout _on("mouseleave", Ay.s.selector, _prefstop); // stop prefetch timeout _on("touchstart", Ay.s.selector, _prefetch); } _on("click", Ay.s.selector, _click, bdy); Ay.frms("d", qa("body")); Ay.frms("a"); Ay.frms("d", gsl); if(Ay.s.idleTime) Ay.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(Ay.s.prefetchoff === true) return; if (!Ay.Rq("?", true)) return; var href = Ay.Rq("v", e, t); if (Ay.Rq("=", true) || !href || Ay.h.prefetchoff.find(href)) return; Ay.fn("+", href, () => false); }, _stopBubbling = e => ( e.preventDefault(), e.stopPropagation(), e.stopImmediatePropagation() ), _click = (t, e, notPush) => { if(!Ay.Rq("?")) return; var href = Ay.Rq("v", e, t); if(!href || _exoticKey(t)) return; if(href.substr(-1) ==="#") return true; if(_hashChange()) { Ay.hApi.r(href); return true; } Ay.scrolly.p(); _stopBubbling(e); if(Ay.Rq("=")) Ay.hApi.r(); if(Ay.s.refresh || !Ay.Rq("=")) _request(notPush); }, _request = notPush => { Ay.Rq("!"); if(notPush) Ay.Rq("p", false); Ay.trigger("request"); Ay.fn(Ay.Rq("h"), err => { if (err) { lg("Error in _request : " + err); Ay.trigger("error", err); } _render(); }); }, _render = () => { Ay.trigger("beforeload"); if(Ay.s.requestDelay) { if(requestTimer) clearTimeout(requestTimer); requestTimer = setTimeout(_doRender, Ay.s.requestDelay); } else _doRender(); }, _onPop = e => { var url = window.location.href; Ay.Rq("i"); Ay.Rq("h", url); Ay.Rq("p", false); Ay.scrolly.p(); if (!url || url === Ay.currentURL) return; Ay.trigger("request"); Ay.fn(url, _render); }, _doRender = () => { Ay.trigger("load"); if(Ay.s.bodyClasses) _copyAttributes(bdy, Ay.fn("body"), true); var href = Ay.Rq("h"), title; href = Ay.Rq("c", href); if(Ay.Rq("p")) Ay.hApi.p(href); else Ay.hApi.r(href); if(title = Ay.fn("title")) qs("title").innerHTML = title.innerHTML; Ay.Rq("C", Ay.fn("-", gsl)); Ay.frms("a"); Ay.scrolly.l(); _gaCaptureView(href); Ay.triggerEvent("DOMContentLoaded"); if(Ay.s.passCount) qs("#" + Ay.s.passCount).innerHTML = "Pass: " + Ay.pass; if(Ay.s.cb) Ay.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 = Ay.Rq("h"), e = Ay.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 = Ay.Rq("e"); return (e.hash && e.href.replace(e.hash, "") === window.location.href.replace(location.hash, "") || e.href === window.location.href + "#"); } }} Ay.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 Ay; } else return Ay.pronto(0, o); }; let run = () => { if(Ay.running) return; //avoid multiple run attempts Ay.running = true; Ay.s = Object.assign(Ay.s, options); (Ay.pages = new Pages()).f(); Ay.pronto = new Pronto().a; if (load()) { Ay.pronto(Ay.s.elements, "i"); if (Ay.s.deltas) Ay.scripts.o(); } }, load = () => { if (!(window.history && window.history.pushState && window.history.replaceState) || !Ay.s.pluginon) { lg("Gracefully exiting..."); return false; } lg("Ajaxify loaded..."); //verbosity option steers, whether this initialisation message is output Ay.scripts = new Scrpts(); Ay.h.inlinehints = new Hints(Ay.s.inlinehints); Ay.h.inlineskip = new Hints(Ay.s.inlineskip); Ay.cache = new Cache(); Ay.memory = new Memory(); Ay.h.memoryoff = new Hints(Ay.s.memoryoff); Ay.fn = Ay.getPage = new GetPage().a; Ay.detScripts = new DetScripts(); Ay.addAll = new AddAll(); Ay.h.alwayshints = new Hints(Ay.s.alwayshints); Ay.Rq = new RQ().a; return true; }; Ay.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 l(u){ let v = Ay.memory.l(u); return this.s(v === false ? v : Ay.pages.l(v)) } //lookup URL and load } // The stateful Memory class // Usage: Ay.memory.l(<URL>) - returns the same URL if not turned off internally class Memory { l(h){ if (!h || Ay.s.memoryoff === true) return false; if (Ay.s.memoryoff === false) return h; return Ay.h.memoryoff.find(h) ? false : h; } } // The stateful Pages class // this.d = Array of pages - [0] = URL // [1] = reference to whole page class Pages { f(){ this.d = [] } //flush l(u){ if (this.P(u)) return this.d[this.i][1] } //lookup URL and return page p(o){ if(this.P(o[0])) this.d[this.i]=o; else this.d.push(o) } //update or push page passed as an object P(u){ return (this.i = this.d.findIndex(e => e[0] == u)) + 1 } //lookup page index and store in "i" } // The DetScripts class - stands for "detach scripts" // Works on "s" <object> that is passed in and fills it class DetScripts { d(s) { if(!(this.h = Ay.pass ? Ay.fn("head") : qs("head"))) return true; //If pass is 0 -> fetch head from DOM, otherwise from target page this.lk = qa("link", this.h); // Fetch links from DOM s.j = Ay.pass ? Ay.fn("script") : qa("script"); //If pass is 0 -> fetch JSs from DOM, otherwise from target page s.c = this.x("stylesheet"); //Extract stylesheets s.y = qa("style", this.h); //Extract style tags s.can = this.x("canonical"); //Extract canonical tag } x(v){ return Array.prototype.filter.call(this.lk, e => e.getAttribute("rel").iO(v)) } //Extract link tags with given "rel" } // The Offsets class // this.d = Array of pages - [0] = URL // [1] = offset class Offsets { f(){ this.d = [] } //flush internal offsets array - must be performed by parent l(h){ if(h.iO("?")) h = h.split("?")[0]; //lookup page offset return this.O(h) ? this.d[this.i][1] : 0; //return if found otherwise 0 } p(h){ let us1 = h.iO("?") ? h.split("?")[0] : h, //initialise all helper variables in one go us = us1.iO("#") ? us1.split("#")[0] : us1, os = [us, (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop]; if(this.O(us)) this.d[this.i]=os; else this.d.push(os); // update if found, otherwise push } O(h){ return (this.i = this.d.findIndex(e => e[0] == h)) + 1 } //find URL in internal array - add 1 for convenience } // The Scrolly class // operates on Ay.currentURL class Scrolly { constructor() { if ('scrollRestoration' in history) history.scrollRestoration = 'manual' } p(){ Ay.s.scrolltop == "s" && Ay.offsets.p(Ay.currentURL) } l(){ let o = Ay.currentURL; if(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(); return this.s(box.top + window.pageYOffset - document.documentElement.clientTop); // ...animate to ID } if(Ay.s.scrolltop == "s") this.s(Ay.offsets.l(o)); //smart scroll -> lookup and restore offset else Ay.s.scrolltop && this.s(0); //scrolltop true -> scroll to top of page } s(o){ if(Ay.s.scrollDelay === false) window.scrollTo(0, o); //scroll to offset else setTimeout(() => window.scrollTo(0, o), Ay.s.scrollDelay); } } // The HAPi class // operates on Ay.currentURL - manages operations on the History API centrally(replaceState / pushState) class HApi { r(h){ let c = this.u(h); history.replaceState({ url: c }, "state-" + c, c); } //perform replaceState p(h){ let c = this.u(h); if (c !== window.location.href) history.pushState({ url: c }, "state-" + c, c); } //perform pushState u(h){ if(h) Ay.currentURL = h; return Ay.currentURL; } //update currentURL if given and return always } // The AddAll class // Works on a new selection of scripts to apply delta-loading to it class AddAll { constructor() { this.CSS = []; this.JS = []; } a(sl, pk) { //only public function if(!sl.length || Ay.s.deltas === "n") return; //ensure input and that delta-loading is enabled this.PK = pk; //Copy "primary key" into internal variable if(!Ay.s.deltas) return sl.forEach(e => this.iScript(e)); //process all scripts and return quickly //deltas presumed to be "true" -> proceed with normal delta-loading this.O = pk == "href" ? this.CSS : this.JS; //Load old stylesheets or JS sl.forEach(t => { //Iterate through selection let url = this.gA(t); //fetch URL if(!Ay.pass) return url && this.O.push(url); //Fill new array on initial load, nothing more if(t.getAttribute("data-class") == "always" || Ay.h.alwayshints.find(url)) { //Class always handling this.removeScript(); //remove from DOM this.iScript(t); //insert back single external script in the head return; } if(url) { //URL? if(!this.O.some(e => e == url)) { // Test, whether new this.O.push(url); //If yes: Push to old array this.iScript(t); } //Otherwise nothing to do return; } if(pk != "href" && !t.classList.contains("no-ajaxy")) Ay.scripts.i(t); //Inline JS script? -> inject into DOM }); } // Fetch full url of link or script tag gA(e){ return this.u = e[this.PK]; } iScript(S){ this.gA(S); if(this.PK == "href") return qha(Ay.parse('<link rel="stylesheet" href="*" />'.replace("*", this.u))); if(!this.u) return Ay.scripts.i(S); var sc = dcE("script"); sc.async = Ay.s.asyncdef; _copyAttributes(sc, S); qha(sc); } removeScript(){ qa((this.PK == "href" ? 'link[href*="!"]' : 'script[src*="!"]').replace("!", this.u)).forEach(e => prC(e)) } } // The stateful Scrpts plugin class Scrpts { constructor(){ this.S = {} } s(){ this.allstyle(this.S.y) } //all style tags o(){ this.d(); this.a() } //on initial load c(){ return Ay.s.canonical && this.S.can ? this.S.can.getAttribute("href") : false } //canonical href d(){ return Ay.detScripts.d(this.S) } //detach scripts - returning true indicates an error i(o){ this.one(o) } //inline script -> o is scripts object t(){ this.d() || this.a() } //former "true" / default method - detach and delta load a(){ this.delta(this.S) } //delta loading core allstyle(S){ !Ay.s.style || !S || ( qa("style", qs("head")).forEach(e => prC(e)), S.forEach(el => { let st = Ay.parse('<style>' + el.textContent + '</style>'); _copyAttributes(st, el); qha(st); //append to the head }) ) } one(S){ (!(this.x = S.textContent).iO("new Ajaxify(") && ((Ay.s.inline && !Ay.h.inlineskip.find(this.x)) || S.classList.contains("ajaxy") || Ay.h.inlinehints.find(this.x)) ) && this.add(S) } add(S){ if(!this.x || !this.x.length) return; if(Ay.s.inlineappend || (S.getAttribute("type") && !S.getAttribute("type").iO("text/javascript"))) try { return this.app(S); } catch (e) { } try { eval(this.x) } catch (e1) { lg("Error in inline script : " + this.x + "\nError code : " + e1); } } app(S){ let sc = dcE("script"); _copyAttributes(sc, S); sc.classList.add(inlineclass); try {sc.appendChild(document.createTextNode(this.x))} catch(e) {sc.text = this.x} return qs("body").appendChild(sc); } delta(S){ Ay.addAll.a(S.c, "href"), Ay.addAll.a(S.j, "src") } }