diff --git a/TODO.md b/TODO.md
index 6fb8fbad..23fa16f5 100644
--- a/TODO.md
+++ b/TODO.md
@@ -7,19 +7,19 @@
## TODOS
* Testing
+ * history
+ * polling
+ * kt-boost
* interval parsing
* table elements in responses
* scrolling/'revealed' event
* checkbox inputs
- * history
- * kt-boost
* kt-swap-oob (verify, chrome coverage tool bad?)
* X-KT-Trigger response header
* SSE stuff
* kt-trigger delay
* class operation parsing
* class toggling
- * polling
* transition model for content swaps
diff --git a/dist/kutty.js b/dist/kutty.js
index 1db52df1..0cfa821c 100644
--- a/dist/kutty.js
+++ b/dist/kutty.js
@@ -8,6 +8,21 @@ var kutty = kutty || (function () {
// Utilities
//====================================================================
+ function makeBrowserHistorySupport() {
+ return {
+ getFullPath: function () {
+ return location.pathname + location.search;
+ },
+ getLocalStorage: function () {
+ return localStorage;
+ },
+ getHistory: function () {
+ return history;
+ }
+ };
+ }
+ var browserHistorySupport = makeBrowserHistorySupport();
+
function parseInterval(str) {
if (str === "null" || str === "false" || str === "") {
return null;
@@ -36,6 +51,10 @@ var kutty = kutty || (function () {
function getDocument() {
return document;
}
+
+ function getBody() {
+ return getDocument().body;
+ }
function getClosestMatch(elt, condition) {
if (condition(elt)) {
@@ -147,7 +166,7 @@ var kutty = kutty || (function () {
}
function bodyContains(elt) {
- return getDocument().body.contains(elt);
+ return getBody().contains(elt);
}
function concat(arr1, arr2) {
@@ -179,7 +198,7 @@ var kutty = kutty || (function () {
} else {
var data = getInternalData(elt);
if (data.boosted) {
- return getDocument().body;
+ return getBody();
} else {
return elt;
}
@@ -208,7 +227,7 @@ var kutty = kutty || (function () {
settleTasks = settleTasks.concat(swapOuterHTML(target, fragment));
} else {
child.parentNode.removeChild(child);
- triggerEvent(getDocument().body, "oobErrorNoTarget.kutty", {content: child})
+ triggerEvent(getBody(), "oobErrorNoTarget.kutty", {content: child})
}
}
});
@@ -665,14 +684,14 @@ var kutty = kutty || (function () {
var target, event, listener;
if (isFunction(arg1)) {
ready(function(){
- target = getDocument().body;
+ target = getBody();
event = "all.kutty";
listener = arg1;
target.addEventListener(event, listener);
})
} else if (isFunction(arg2)) {
ready(function () {
- target = getDocument().body;
+ target = getBody();
event = arg1;
listener = arg2;
target.addEventListener(event, listener);
@@ -690,7 +709,7 @@ var kutty = kutty || (function () {
//====================================================================
function getHistoryElement() {
var historyElt = getDocument().querySelector('[kt-history-elt]');
- return historyElt || getDocument().body;
+ return historyElt || getBody();
}
function purgeOldestPaths(paths, historyTimestamps) {
@@ -702,32 +721,32 @@ var kutty = kutty || (function () {
slot++;
if (slot > 20) {
delete historyTimestamps[path];
- localStorage.removeItem(path);
+ browserHistorySupport.getLocalStorage().removeItem(path);
}
});
}
function bumpHistoryAccessDate(pathAndSearch) {
- var historyTimestamps = JSON.parse(localStorage.getItem("kt-history-timestamps")) || {};
+ var historyTimestamps = JSON.parse(browserHistorySupport.getLocalStorage().getItem("kt-history-timestamps")) || {};
historyTimestamps[pathAndSearch] = Date.now();
var paths = Object.keys(historyTimestamps);
if (paths.length > 20) {
purgeOldestPaths(paths, historyTimestamps);
}
- localStorage.setItem("kt-history-timestamps", JSON.stringify(historyTimestamps));
+ browserHistorySupport.getLocalStorage().setItem("kt-history-timestamps", JSON.stringify(historyTimestamps));
}
function saveHistory() {
var elt = getHistoryElement();
- var pathAndSearch = location.pathname+location.search;
- triggerEvent(getDocument().body, "historyUpdate.kutty", {path:pathAndSearch, historyElt:elt});
- history.replaceState({}, getDocument().title, window.location.href);
- localStorage.setItem('kt-history:' + pathAndSearch, elt.innerHTML);
+ var pathAndSearch = browserHistorySupport.getFullPath();
+ triggerEvent(getBody(), "historyUpdate.kutty", {path:pathAndSearch, historyElt:elt});
+ browserHistorySupport.getHistory().replaceState({}, getDocument().title);
+ browserHistorySupport.getLocalStorage().setItem('kt-history:' + pathAndSearch, elt.innerHTML);
bumpHistoryAccessDate(pathAndSearch);
}
function pushUrlIntoHistory(url) {
- history.pushState({}, "", url );
+ browserHistorySupport.getHistory().pushState({}, "", url );
}
function settleImmediately(settleTasks) {
@@ -739,25 +758,25 @@ var kutty = kutty || (function () {
function loadHistoryFromServer(pathAndSearch) {
var request = new XMLHttpRequest();
var details = {path: pathAndSearch, xhr:request};
- triggerEvent(getDocument().body, "historyCacheMiss.kutty", details);
+ triggerEvent(getBody(), "historyCacheMiss.kutty", details);
request.open('GET', pathAndSearch, true);
request.onload = function () {
if (this.status >= 200 && this.status < 400) {
- triggerEvent(getDocument().body, "historyCacheMissLoad.kutty", details);
+ triggerEvent(getBody(), "historyCacheMissLoad.kutty", details);
var fragment = makeFragment(this.response);
fragment = fragment.querySelector('[kt-history-elt]') || fragment;
settleImmediately(swapInnerHTML(getHistoryElement(), fragment));
} else {
- triggerEvent(getDocument().body, "historyCacheMissLoadError.kutty", details);
+ triggerEvent(getBody(), "historyCacheMissLoadError.kutty", details);
}
};
request.send();
}
function restoreHistory() {
- var pathAndSearch = location.pathname+location.search;
- triggerEvent(getDocument().body, "historyRestore.kutty", {path:pathAndSearch});
- var content = localStorage.getItem('kt-history:' + pathAndSearch);
+ var pathAndSearch = browserHistorySupport.getFullPath();
+ triggerEvent(getBody(), "historyRestore.kutty", {path:pathAndSearch});
+ var content = browserHistorySupport.getLocalStorage().getItem('kt-history:' + pathAndSearch);
if (content) {
bumpHistoryAccessDate(pathAndSearch);
settleImmediately(swapInnerHTML(getHistoryElement(), makeFragment(content)));
@@ -1138,7 +1157,7 @@ var kutty = kutty || (function () {
// initialize the document
ready(function () {
- var body = getDocument().body;
+ var body = getBody();
processNode(body);
triggerEvent(body, 'load.kutty', {});
window.onpopstate = function () {
@@ -1164,6 +1183,14 @@ var kutty = kutty || (function () {
}
}
+ function setBrowserHistorySupport(mock) {
+ browserHistorySupport = mock;
+ }
+
+ function restoreBrowserHistorySupport() {
+ browserHistorySupport = browserHistorySupport();
+ }
+
// Public API
return {
processElement: processNode,
diff --git a/dist/kutty.min.js b/dist/kutty.min.js
index 93a5a15a..2ac4fa22 100644
--- a/dist/kutty.min.js
+++ b/dist/kutty.min.js
@@ -1 +1 @@
-var kutty=kutty||function(){"use strict";var e=["get","post","put","delete","patch"];function g(e){if(e==="null"||e==="false"||e===""){return null}else if(e.lastIndexOf("ms")===e.length-2){return parseFloat(e.substr(0,e.length-2))}else if(e.lastIndexOf("s")===e.length-1){return parseFloat(e.substr(0,e.length-1))*1e3}else{return parseFloat(e)}}function h(e,t){return e.getAttribute&&e.getAttribute(t)}function u(e,t){return h(e,t)||h(e,"data-"+t)}function n(e){return e.parentElement}function o(){return document}function s(e,t){if(t(e)){return e}else if(n(e)){return s(n(e),t)}else{return null}}function p(e,t){var r=null;s(e,function(e){return r=h(e,t)});return r}function l(e,t){var r=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector;return e!=null&&r!=null&&r.call(e,t)}function a(e,t){do{if(e==null||l(e,t))return e}while(e=e&&n(e))}function r(e){var t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;var r=t.exec(e);if(r){return r[1].toLowerCase()}else{return""}}function i(e,t){var r=new DOMParser;var n=r.parseFromString(e,"text/html");var i=n.body;while(t>0){t--;i=i.firstChild}return i}function f(e){var t=r(e);switch(t){case"thead":case"tbody":case"tfoot":case"colgroup":case"caption":return i("
",1);case"col":return i("",2);case"tr":return i("",2);case"td":case"th":return i("",3);default:return i(e,0)}}function t(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function c(e){return t(e,"Function")}function v(e){return t(e,"Object")}function m(e){var t="kutty-internal-data";var r=e[t];if(!r){r=e[t]={}}return r}function k(e,t){if(e){for(var r=0;r=0}function y(e){return o().body.contains(e)}function b(e,t){return e.concat(t)}function w(e){return e.split(/\s+/)}function S(e){var t=o().styleSheets[0];t.insertRule(e,t.cssRules.length)}function E(e){var t=s(e,function(e){return h(e,"kt-target")!==null});if(t){var r=h(t,"kt-target");if(r==="this"){return t}else{return o().querySelector(r)}}else{var n=m(e);if(n.boosted){return o().body}else{return e}}}function L(t,r){k(t.attributes,function(e){if(!r.hasAttribute(e.name)){t.removeAttribute(e.name)}});k(r.attributes,function(e){t.setAttribute(e.name,e.value)})}function O(e){var n=[];k(e.children,function(e){if(u(e,"kt-swap-oob")==="true"){var t=o().getElementById(e.id);if(t){var r=o().createDocumentFragment();r.appendChild(e);n=n.concat(x(t,r))}else{e.parentNode.removeChild(e);te(o().body,"oobErrorNoTarget.kutty",{content:e})}}});return n}function T(n,e){var i=[];k(e.querySelectorAll("[id]"),function(e){var t=n.querySelector(e.tagName+"[id="+e.id+"]");if(t){var r=e.cloneNode();L(e,t);i.push(function(){L(e,r)})}});return i}function C(e,t,r){var n=T(e,r);while(r.childNodes.length>0){var i=r.firstChild;e.insertBefore(i,t);if(i.nodeType!==Node.TEXT_NODE){te(i,"load.kutty",{});Z(i)}}return n}function x(e,t){if(e.tagName==="BODY"){return R(e,t)}else{var r=C(n(e),e,t);n(e).removeChild(e);return r}}function N(e,t){return C(e,e.firstChild,t)}function q(e,t){return C(n(e),e,t)}function A(e,t){return C(e,null,t)}function M(e,t){return C(n(e),e.nextSibling,t)}function R(e,t){var r=e.firstChild;var n=C(e,r,t);if(r){while(r.nextSibling){e.removeChild(r.nextSibling)}e.removeChild(r)}return n}function D(e,t){var r=p(e,"kt-select");if(r){var n=o().createDocumentFragment();k(t.querySelectorAll(r),function(e){n.appendChild(e)});t=n}return t}function I(e,t,r,n){var i=f(n);if(i){var a=O(i);i=D(r,i);switch(e){case"outerHTML":return b(a,x(t,i));case"afterbegin":return b(a,N(t,i));case"beforebegin":return b(a,q(t,i));case"beforeend":return b(a,A(t,i));case"afterend":return b(a,M(t,i));default:return b(a,R(t,i))}}}function F(e,t){if(t){if(t.indexOf("{")===0){var r=JSON.parse(t);for(var n in r){if(r.hasOwnProperty(n)){var i=r[n];if(!v(i)){i={value:i}}te(e,n,i)}}}else{te(e,t,[])}}}function H(e){var t={trigger:"click"};var r=u(e,"kt-trigger");if(r){var n=w(r);if(n.length>0){var i=n[0];if(i==="every"){t.pollInterval=g(n[1])}else if(i.indexOf("sse:")===0){t.sseEvent=i.substr(4)}else{t["trigger"]=i;for(var a=1;a1){var r=t[0];var n=t[1].trim();var i;var a;if(n.indexOf(":")>0){var o=n.split(":");i=o[0];a=g(o[1])}else{i=n;a=100}return{operation:r,cssClass:i,delay:a}}else{return null}}function X(i,e,t){k(e.split("&"),function(e){var n=0;k(e.split(","),function(e){var t=e.trim();var r=P(t);if(r){if(r.operation==="toggle"){setTimeout(function(){setInterval(function(){i.classList[r.operation].call(i.classList,r.cssClass)},r.delay)},n);n=n+r.delay}else{n=n+r.delay;setTimeout(function(){i.classList[r.operation].call(i.classList,r.cssClass)},n)}}})})}function U(e,t,r,n){var i=m(e);i.timeout=setTimeout(function(){if(y(e)){Oe(e,t,r);U(e,t,u(e,"kt-"+t),n)}},n)}function j(e){return location.hostname===e.hostname&&h(e,"href")&&!h(e,"href").startsWith("#")}function B(e,t,r){if(e.tagName==="A"&&j(e)||e.tagName==="FORM"){t.boosted=true;var n,i;if(e.tagName==="A"){n="get";i=h(e,"href")}else{var a=h(e,"method");n=a?a.toLowerCase():"get";i=h(e,"action")}K(e,n,i,t,r,true)}}function J(e){return e.tagName==="FORM"||l(e,'input[type="submit"], button')&&a(e,"form")!==null||e.tagName==="A"&&e.href&&e.href.indexOf("#")!==0}function K(i,a,o,e,u,s){var t=function(e){if(s||J(i))e.preventDefault();var t=m(e);var r=m(i);if(!t.handled){t.handled=true;if(u.once){if(r.triggeredOnce){return}else{r.triggeredOnce=true}}if(u.changed){if(r.lastValue===i.value){return}else{r.lastValue=i.value}}if(r.delayed){clearTimeout(r.delayed)}var n=function(){Oe(i,a,o,e.target)};if(u.delay){r.delayed=setTimeout(n,u.delay)}else{n()}}};e.trigger=u.trigger;e.eventListener=t;i.addEventListener(u.trigger,t)}function V(){if(!window["kuttyScrollHandler"]){var e=function(){k(o().querySelectorAll("[kt-trigger='revealed']"),function(e){z(e)})};window["kuttyScrollHandler"]=e;window.addEventListener("scroll",e)}}function z(e){var t=m(e);if(!t.revealed&&d(e)){t.revealed=true;Oe(e,t.verb,t.path)}}function G(e){if(!y(e)){e.sseSource.close();return true}}function _(t,e){var r={config:{withCredentials:true}};te(t,"initSSE.kutty",r);var n=new EventSource(e,r.config);n.onerror=function(e){te(t,"sseError.kutty",{error:e,source:n});G(t)};m(t).sseSource=n}function W(e,t,r,n){var i=s(e,function(e){return e.sseSource});if(i){var a=function(){if(!G(i)){if(y(e)){Oe(e,t,r)}else{i.sseSource.removeEventListener(n,a)}}};i.sseSource.addEventListener(n,a)}else{te(e,"noSSESourceError.kutty")}}function Y(e,t,r,n,i){var a=function(){if(!n.loaded){n.loaded=true;Oe(e,t,r)}};if(i){setTimeout(a,i)}else{a()}}function Q(r,n,i){var a=false;k(e,function(e){var t=u(r,"kt-"+e);if(t){a=true;n.path=t;n.verb=e;if(i.sseEvent){W(r,e,t,i.sseEvent)}else if(i.trigger==="revealed"){V();z(r)}else if(i.trigger==="load"){Y(r,e,t,n,i.delay)}else if(i.pollInterval){n.polling=true;U(r,e,t,i.pollInterval)}else{K(r,e,t,n,i)}}});return a}function Z(e){var t=m(e);if(!t.processed){t.processed=true;var r=H(e);var n=Q(e,t,r);if(!n&&p(e,"kt-boost")==="true"){B(e,t,r)}var i=u(e,"kt-sse-source");if(i){_(e,i)}var a=u(e,"kt-classes");if(a){X(e,a)}}if(e.children){k(e.children,function(e){Z(e)})}}function $(e,t,r){var n=p(e,"kt-error-url");if(n){var i=new XMLHttpRequest;i.open("POST",n);i.setRequestHeader("Content-Type","application/json;charset=UTF-8");i.send(JSON.stringify({elt:e.id,event:t,detail:r}))}}function ee(e,t){var r;if(window.CustomEvent&&typeof window.CustomEvent==="function"){r=new CustomEvent(e,{bubbles:true,cancelable:true,detail:t})}else{r=o().createEvent("CustomEvent");r.initCustomEvent(e,true,true,t)}return r}function te(e,t,r){r["elt"]=e;var n=ee(t,r);if(kutty.logger){kutty.logger(e,t,r);if(t.indexOf("Error")>0){$(e,t,r)}}var i=e.dispatchEvent(n);var a=e.dispatchEvent(ee("all.kutty",{originalDetail:r,originalEvent:n}));return i&&a}function re(e,t,r){var n,i,a;if(c(e)){Te(function(){n=o().body;i="all.kutty";a=e;n.addEventListener(i,a)})}else if(c(t)){Te(function(){n=o().body;i=e;a=t;n.addEventListener(i,a)})}else{n=e;i=t;a=r;n.addEventListener(i,a)}}function ne(){var e=o().querySelector("[kt-history-elt]");return e||o().body}function ie(e,r){e=e.sort(function(e,t){return r[t]-r[e]});var t=0;k(e,function(e){t++;if(t>20){delete r[e];localStorage.removeItem(e)}})}function ae(e){var t=JSON.parse(localStorage.getItem("kt-history-timestamps"))||{};t[e]=Date.now();var r=Object.keys(t);if(r.length>20){ie(r,t)}localStorage.setItem("kt-history-timestamps",JSON.stringify(t))}function oe(){var e=ne();var t=location.pathname+location.search;te(o().body,"historyUpdate.kutty",{path:t,historyElt:e});history.replaceState({},o().title,window.location.href);localStorage.setItem("kt-history:"+t,e.innerHTML);ae(t)}function ue(e){history.pushState({},"",e)}function se(e){k(e,function(e){e.call()})}function le(e){var t=new XMLHttpRequest;var r={path:e,xhr:t};te(o().body,"historyCacheMiss.kutty",r);t.open("GET",e,true);t.onload=function(){if(this.status>=200&&this.status<400){te(o().body,"historyCacheMissLoad.kutty",r);var e=f(this.response);e=e.querySelector("[kt-history-elt]")||e;se(R(ne(),e))}else{te(o().body,"historyCacheMissLoadError.kutty",r)}};t.send()}function fe(){var e=location.pathname+location.search;te(o().body,"historyRestore.kutty",{path:e});var t=localStorage.getItem("kt-history:"+e);if(t){ae(e);se(R(ne(),f(t)))}else{le(e)}}function ce(e){return p(e,"kt-push-url")==="true"||e.tagName==="A"&&m(e).boosted}function ve(e){ye(e,"add")}function de(e){ye(e,"remove")}function ye(e,t){var r=p(e,"kt-indicator");if(r){var n=o().querySelectorAll(r)}else{n=[e]}k(n,function(e){e.classList[t].call(e.classList,"kutty-request")})}function ge(e,t){for(var r=0;r0){r["swapStyle"]=n[0];for(var i=1;i=200&&this.status<400){if(this.status!==204){if(!te(o,"beforeSwap.kutty",y))return;var i=this.response;if(n){oe()}var a=Le(o);u.classList.add("kutty-swapping");var e=function(){try{var e=I(a.swapStyle,u,o,i);u.classList.remove("kutty-swapping");u.classList.add("kutty-settling");te(o,"afterSwap.kutty",y);var t=function(){k(e,function(e){e.call()});u.classList.remove("kutty-settling");if(n){ue(r||v);oe()}te(o,"afterSettle.kutty",y)};if(a.settleDelay>0){setTimeout(t,a.settleDelay)}else{t()}}catch(e){te(o,"swapError.kutty",y);throw e}};if(a.swapDelay>0){setTimeout(e,g(a.swapDelay))}else{e()}}}else{te(o,"responseError.kutty",y)}}catch(e){y["exception"]=e;te(o,"onLoadError.kutty",y);throw e}finally{de(o);s();te(o,"afterOnLoad.kutty",y)}};f.onerror=function(){de(o);te(o,"sendError.kutty",y);s()};if(!te(o,"beforeRequest.kutty",y))return s();ve(o);f.send(e==="get"?null:be(c))}function Te(e){if(o().readyState!=="loading"){e()}else{o().addEventListener("DOMContentLoaded",e)}}S(".kutty-indicator{opacity:0;transition: opacity 200ms ease-in;}");S(".kutty-request .kutty-indicator{opacity:1}");S(".kutty-request.kutty-indicator{opacity:1}");Te(function(){var e=o().body;Z(e);te(e,"load.kutty",{});window.onpopstate=function(){fe()}});function Ce(e){return eval(e)}function xe(t){kutty.on("load.kutty",function(e){t(e.detail.elt)})}function Ne(){kutty.logger=function(e,t,r){if(console){console.log(t,e,r)}}}return{processElement:Z,on:re,onLoad:xe,logAll:Ne,version:"0.0.1",_:Ce}}();
\ No newline at end of file
+var kutty=kutty||function(){"use strict";var t=["get","post","put","delete","patch"];function e(){return{getFullPath:function(){return location.pathname+location.search},getLocalStorage:function(){return localStorage},getHistory:function(){return history}}}var n=e();function y(t){if(t==="null"||t==="false"||t===""){return null}else if(t.lastIndexOf("ms")===t.length-2){return parseFloat(t.substr(0,t.length-2))}else if(t.lastIndexOf("s")===t.length-1){return parseFloat(t.substr(0,t.length-1))*1e3}else{return parseFloat(t)}}function h(t,e){return t.getAttribute&&t.getAttribute(e)}function u(t,e){return h(t,e)||h(t,"data-"+e)}function i(t){return t.parentElement}function a(){return document}function o(){return a().body}function s(t,e){if(e(t)){return t}else if(i(t)){return s(i(t),e)}else{return null}}function p(t,e){var r=null;s(t,function(t){return r=h(t,e)});return r}function l(t,e){var r=t.matches||t.matchesSelector||t.msMatchesSelector||t.mozMatchesSelector||t.webkitMatchesSelector||t.oMatchesSelector;return t!=null&&r!=null&&r.call(t,e)}function f(t,e){do{if(t==null||l(t,e))return t}while(t=t&&i(t))}function r(t){var e=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;var r=e.exec(t);if(r){return r[1].toLowerCase()}else{return""}}function c(t,e){var r=new DOMParser;var n=r.parseFromString(t,"text/html");var i=n.body;while(e>0){e--;i=i.firstChild}return i}function v(t){var e=r(t);switch(e){case"thead":case"tbody":case"tfoot":case"colgroup":case"caption":return c("",1);case"col":return c("",2);case"tr":return c("",2);case"td":case"th":return c("",3);default:return c(t,0)}}function d(t,e){return Object.prototype.toString.call(t)==="[object "+e+"]"}function g(t){return d(t,"Function")}function m(t){return d(t,"Object")}function k(t){var e="kutty-internal-data";var r=t[e];if(!r){r=t[e]={}}return r}function b(t,e){if(t){for(var r=0;r=0}function w(t){return o().contains(t)}function E(t,e){return t.concat(e)}function L(t){return t.split(/\s+/)}function O(t){var e=a().styleSheets[0];e.insertRule(t,e.cssRules.length)}function T(t){var e=s(t,function(t){return h(t,"kt-target")!==null});if(e){var r=h(e,"kt-target");if(r==="this"){return e}else{return a().querySelector(r)}}else{var n=k(t);if(n.boosted){return o()}else{return t}}}function C(e,r){b(e.attributes,function(t){if(!r.hasAttribute(t.name)){e.removeAttribute(t.name)}});b(r.attributes,function(t){e.setAttribute(t.name,t.value)})}function x(t){var n=[];b(t.children,function(t){if(u(t,"kt-swap-oob")==="true"){var e=a().getElementById(t.id);if(e){var r=a().createDocumentFragment();r.appendChild(t);n=n.concat(A(e,r))}else{t.parentNode.removeChild(t);it(o(),"oobErrorNoTarget.kutty",{content:t})}}});return n}function N(n,t){var i=[];b(t.querySelectorAll("[id]"),function(t){var e=n.querySelector(t.tagName+"[id="+t.id+"]");if(e){var r=t.cloneNode();C(t,e);i.push(function(){C(t,r)})}});return i}function q(t,e,r){var n=N(t,r);while(r.childNodes.length>0){var i=r.firstChild;t.insertBefore(i,e);if(i.nodeType!==Node.TEXT_NODE){it(i,"load.kutty",{});et(i)}}return n}function A(t,e){if(t.tagName==="BODY"){return H(t,e)}else{var r=q(i(t),t,e);i(t).removeChild(t);return r}}function M(t,e){return q(t,t.firstChild,e)}function R(t,e){return q(i(t),t,e)}function D(t,e){return q(t,null,e)}function F(t,e){return q(i(t),t.nextSibling,e)}function H(t,e){var r=t.firstChild;var n=q(t,r,e);if(r){while(r.nextSibling){t.removeChild(r.nextSibling)}t.removeChild(r)}return n}function I(t,e){var r=p(t,"kt-select");if(r){var n=a().createDocumentFragment();b(e.querySelectorAll(r),function(t){n.appendChild(t)});e=n}return e}function P(t,e,r,n){var i=v(n);if(i){var a=x(i);i=I(r,i);switch(t){case"outerHTML":return E(a,A(e,i));case"afterbegin":return E(a,M(e,i));case"beforebegin":return E(a,R(e,i));case"beforeend":return E(a,D(e,i));case"afterend":return E(a,F(e,i));default:return E(a,H(e,i))}}}function X(t,e){if(e){if(e.indexOf("{")===0){var r=JSON.parse(e);for(var n in r){if(r.hasOwnProperty(n)){var i=r[n];if(!m(i)){i={value:i}}it(t,n,i)}}}else{it(t,e,[])}}}function U(t){var e={trigger:"click"};var r=u(t,"kt-trigger");if(r){var n=L(r);if(n.length>0){var i=n[0];if(i==="every"){e.pollInterval=y(n[1])}else if(i.indexOf("sse:")===0){e.sseEvent=i.substr(4)}else{e["trigger"]=i;for(var a=1;a1){var r=e[0];var n=e[1].trim();var i;var a;if(n.indexOf(":")>0){var o=n.split(":");i=o[0];a=y(o[1])}else{i=n;a=100}return{operation:r,cssClass:i,delay:a}}else{return null}}function B(i,t,e){b(t.split("&"),function(t){var n=0;b(t.split(","),function(t){var e=t.trim();var r=j(e);if(r){if(r.operation==="toggle"){setTimeout(function(){setInterval(function(){i.classList[r.operation].call(i.classList,r.cssClass)},r.delay)},n);n=n+r.delay}else{n=n+r.delay;setTimeout(function(){i.classList[r.operation].call(i.classList,r.cssClass)},n)}}})})}function J(t,e,r,n){var i=k(t);i.timeout=setTimeout(function(){if(w(t)){xt(t,e,r);J(t,e,u(t,"kt-"+e),n)}},n)}function K(t){return location.hostname===t.hostname&&h(t,"href")&&!h(t,"href").startsWith("#")}function V(t,e,r){if(t.tagName==="A"&&K(t)||t.tagName==="FORM"){e.boosted=true;var n,i;if(t.tagName==="A"){n="get";i=h(t,"href")}else{var a=h(t,"method");n=a?a.toLowerCase():"get";i=h(t,"action")}G(t,n,i,e,r,true)}}function z(t){return t.tagName==="FORM"||l(t,'input[type="submit"], button')&&f(t,"form")!==null||t.tagName==="A"&&t.href&&t.href.indexOf("#")!==0}function G(i,a,o,t,u,s){var e=function(t){if(s||z(i))t.preventDefault();var e=k(t);var r=k(i);if(!e.handled){e.handled=true;if(u.once){if(r.triggeredOnce){return}else{r.triggeredOnce=true}}if(u.changed){if(r.lastValue===i.value){return}else{r.lastValue=i.value}}if(r.delayed){clearTimeout(r.delayed)}var n=function(){xt(i,a,o,t.target)};if(u.delay){r.delayed=setTimeout(n,u.delay)}else{n()}}};t.trigger=u.trigger;t.eventListener=e;i.addEventListener(u.trigger,e)}function _(){if(!window["kuttyScrollHandler"]){var t=function(){b(a().querySelectorAll("[kt-trigger='revealed']"),function(t){W(t)})};window["kuttyScrollHandler"]=t;window.addEventListener("scroll",t)}}function W(t){var e=k(t);if(!e.revealed&&S(t)){e.revealed=true;xt(t,e.verb,e.path)}}function Y(t){if(!w(t)){t.sseSource.close();return true}}function Q(e,t){var r={config:{withCredentials:true}};it(e,"initSSE.kutty",r);var n=new EventSource(t,r.config);n.onerror=function(t){it(e,"sseError.kutty",{error:t,source:n});Y(e)};k(e).sseSource=n}function Z(t,e,r,n){var i=s(t,function(t){return t.sseSource});if(i){var a=function(){if(!Y(i)){if(w(t)){xt(t,e,r)}else{i.sseSource.removeEventListener(n,a)}}};i.sseSource.addEventListener(n,a)}else{it(t,"noSSESourceError.kutty")}}function $(t,e,r,n,i){var a=function(){if(!n.loaded){n.loaded=true;xt(t,e,r)}};if(i){setTimeout(a,i)}else{a()}}function tt(r,n,i){var a=false;b(t,function(t){var e=u(r,"kt-"+t);if(e){a=true;n.path=e;n.verb=t;if(i.sseEvent){Z(r,t,e,i.sseEvent)}else if(i.trigger==="revealed"){_();W(r)}else if(i.trigger==="load"){$(r,t,e,n,i.delay)}else if(i.pollInterval){n.polling=true;J(r,t,e,i.pollInterval)}else{G(r,t,e,n,i)}}});return a}function et(t){var e=k(t);if(!e.processed){e.processed=true;var r=U(t);var n=tt(t,e,r);if(!n&&p(t,"kt-boost")==="true"){V(t,e,r)}var i=u(t,"kt-sse-source");if(i){Q(t,i)}var a=u(t,"kt-classes");if(a){B(t,a)}}if(t.children){b(t.children,function(t){et(t)})}}function rt(t,e,r){var n=p(t,"kt-error-url");if(n){var i=new XMLHttpRequest;i.open("POST",n);i.setRequestHeader("Content-Type","application/json;charset=UTF-8");i.send(JSON.stringify({elt:t.id,event:e,detail:r}))}}function nt(t,e){var r;if(window.CustomEvent&&typeof window.CustomEvent==="function"){r=new CustomEvent(t,{bubbles:true,cancelable:true,detail:e})}else{r=a().createEvent("CustomEvent");r.initCustomEvent(t,true,true,e)}return r}function it(t,e,r){r["elt"]=t;var n=nt(e,r);if(kutty.logger){kutty.logger(t,e,r);if(e.indexOf("Error")>0){rt(t,e,r)}}var i=t.dispatchEvent(n);var a=t.dispatchEvent(nt("all.kutty",{originalDetail:r,originalEvent:n}));return i&&a}function at(t,e,r){var n,i,a;if(g(t)){Nt(function(){n=o();i="all.kutty";a=t;n.addEventListener(i,a)})}else if(g(e)){Nt(function(){n=o();i=t;a=e;n.addEventListener(i,a)})}else{n=t;i=e;a=r;n.addEventListener(i,a)}}function ot(){var t=a().querySelector("[kt-history-elt]");return t||o()}function ut(t,r){t=t.sort(function(t,e){return r[e]-r[t]});var e=0;b(t,function(t){e++;if(e>20){delete r[t];n.getLocalStorage().removeItem(t)}})}function st(t){var e=JSON.parse(n.getLocalStorage().getItem("kt-history-timestamps"))||{};e[t]=Date.now();var r=Object.keys(e);if(r.length>20){ut(r,e)}n.getLocalStorage().setItem("kt-history-timestamps",JSON.stringify(e))}function lt(){var t=ot();var e=n.getFullPath();it(o(),"historyUpdate.kutty",{path:e,historyElt:t});n.getHistory().replaceState({},a().title);n.getLocalStorage().setItem("kt-history:"+e,t.innerHTML);st(e)}function ft(t){n.getHistory().pushState({},"",t)}function ct(t){b(t,function(t){t.call()})}function vt(t){var e=new XMLHttpRequest;var r={path:t,xhr:e};it(o(),"historyCacheMiss.kutty",r);e.open("GET",t,true);e.onload=function(){if(this.status>=200&&this.status<400){it(o(),"historyCacheMissLoad.kutty",r);var t=v(this.response);t=t.querySelector("[kt-history-elt]")||t;ct(H(ot(),t))}else{it(o(),"historyCacheMissLoadError.kutty",r)}};e.send()}function dt(){var t=n.getFullPath();it(o(),"historyRestore.kutty",{path:t});var e=n.getLocalStorage().getItem("kt-history:"+t);if(e){st(t);ct(H(ot(),v(e)))}else{vt(t)}}function gt(t){return p(t,"kt-push-url")==="true"||t.tagName==="A"&&k(t).boosted}function yt(t){pt(t,"add")}function ht(t){pt(t,"remove")}function pt(t,e){var r=p(t,"kt-indicator");if(r){var n=a().querySelectorAll(r)}else{n=[t]}b(n,function(t){t.classList[e].call(t.classList,"kutty-request")})}function mt(t,e){for(var r=0;r0){r["swapStyle"]=n[0];for(var i=1;i=200&&this.status<400){if(this.status!==204){if(!it(o,"beforeSwap.kutty",g))return;var i=this.response;if(n){lt()}var a=Ct(o);u.classList.add("kutty-swapping");var t=function(){try{var t=P(a.swapStyle,u,o,i);u.classList.remove("kutty-swapping");u.classList.add("kutty-settling");it(o,"afterSwap.kutty",g);var e=function(){b(t,function(t){t.call()});u.classList.remove("kutty-settling");if(n){ft(r||v);lt()}it(o,"afterSettle.kutty",g)};if(a.settleDelay>0){setTimeout(e,a.settleDelay)}else{e()}}catch(t){it(o,"swapError.kutty",g);throw t}};if(a.swapDelay>0){setTimeout(t,y(a.swapDelay))}else{t()}}}else{it(o,"responseError.kutty",g)}}catch(t){g["exception"]=t;it(o,"onLoadError.kutty",g);throw t}finally{ht(o);s();it(o,"afterOnLoad.kutty",g)}};f.onerror=function(){ht(o);it(o,"sendError.kutty",g);s()};if(!it(o,"beforeRequest.kutty",g))return s();yt(o);f.send(t==="get"?null:Et(c))}function Nt(t){if(a().readyState!=="loading"){t()}else{a().addEventListener("DOMContentLoaded",t)}}O(".kutty-indicator{opacity:0;transition: opacity 200ms ease-in;}");O(".kutty-request .kutty-indicator{opacity:1}");O(".kutty-request.kutty-indicator{opacity:1}");Nt(function(){var t=o();et(t);it(t,"load.kutty",{});window.onpopstate=function(){dt()}});function qt(t){return eval(t)}function At(e){kutty.on("load.kutty",function(t){e(t.detail.elt)})}function Mt(){kutty.logger=function(t,e,r){if(console){console.log(e,t,r)}}}function Rt(t){n=t}function Dt(){n=n()}return{processElement:et,on:at,onLoad:At,logAll:Mt,version:"0.0.1",_:qt}}();
\ No newline at end of file
diff --git a/dist/kutty.min.js.gz b/dist/kutty.min.js.gz
index 275dda95..47c55f0c 100644
Binary files a/dist/kutty.min.js.gz and b/dist/kutty.min.js.gz differ
diff --git a/src/kutty.js b/src/kutty.js
index 1db52df1..2464259d 100644
--- a/src/kutty.js
+++ b/src/kutty.js
@@ -1,5 +1,5 @@
// noinspection JSUnusedAssignment
-var kutty = kutty || (function () {
+var kutty = (function () {
'use strict';
var VERBS = ['get', 'post', 'put', 'delete', 'patch']
@@ -60,7 +60,7 @@ var kutty = kutty || (function () {
var matchesFunction = elt.matches ||
elt.matchesSelector || elt.msMatchesSelector || elt.mozMatchesSelector
|| elt.webkitMatchesSelector || elt.oMatchesSelector;
- return (elt != null) && matchesFunction != null && matchesFunction.call(elt, selector);
+ return matchesFunction && matchesFunction.call(elt, selector);
}
function closest(elt, selector) {
@@ -395,7 +395,7 @@ var kutty = kutty || (function () {
}
}
- function processClassList(elt, classList, operation) {
+ function processClassList(elt, classList) {
forEach(classList.split("&"), function (run) {
var currentRunTime = 0;
forEach(run.split(","), function(value){
@@ -688,46 +688,49 @@ var kutty = kutty || (function () {
//====================================================================
// History Support
//====================================================================
+ var currentPathForHistory = null;
+
function getHistoryElement() {
var historyElt = getDocument().querySelector('[kt-history-elt]');
return historyElt || getDocument().body;
}
- function purgeOldestPaths(paths, historyTimestamps) {
- paths = paths.sort(function (path1, path2) {
- return historyTimestamps[path2] - historyTimestamps[path1]
- });
- var slot = 0;
- forEach(paths, function (path) {
- slot++;
- if (slot > 20) {
- delete historyTimestamps[path];
- localStorage.removeItem(path);
+ function saveToHistoryCache(url, content, title, scroll) {
+ var historyCache = JSON.parse(localStorage.getItem("kutty-history-cache")) || [];
+ for (var i = 0; i < historyCache.length; i++) {
+ if (historyCache[i].url === url) {
+ historyCache = historyCache.slice(i, 1);
+ break;
}
- });
+ }
+ historyCache.push({url:url, content: content, title:title, scroll:scroll})
+ while (historyCache.length > kutty.config.historyCacheSize) {
+ historyCache.shift();
+ }
+ localStorage.setItem("kutty-history-cache", JSON.stringify(historyCache));
}
- function bumpHistoryAccessDate(pathAndSearch) {
- var historyTimestamps = JSON.parse(localStorage.getItem("kt-history-timestamps")) || {};
- historyTimestamps[pathAndSearch] = Date.now();
- var paths = Object.keys(historyTimestamps);
- if (paths.length > 20) {
- purgeOldestPaths(paths, historyTimestamps);
+ function getCachedHistory(url) {
+ var historyCache = JSON.parse(localStorage.getItem("kutty-history-cache")) || [];
+ for (var i = 0; i < historyCache.length; i++) {
+ if (historyCache[i].url === url) {
+ return historyCache[i];
+ }
}
- localStorage.setItem("kt-history-timestamps", JSON.stringify(historyTimestamps));
+ return null;
}
function saveHistory() {
var elt = getHistoryElement();
- var pathAndSearch = location.pathname+location.search;
- triggerEvent(getDocument().body, "historyUpdate.kutty", {path:pathAndSearch, historyElt:elt});
- history.replaceState({}, getDocument().title, window.location.href);
- localStorage.setItem('kt-history:' + pathAndSearch, elt.innerHTML);
- bumpHistoryAccessDate(pathAndSearch);
+ var path = currentPathForHistory || location.pathname+location.search;
+ triggerEvent(getDocument().body, "beforeHistorySave.kutty", {path:path, historyElt:elt});
+ if(kutty.config.historyEnabled) history.replaceState({}, getDocument().title, window.location.href);
+ saveToHistoryCache(path, elt.innerHTML, getDocument().title, window.scrollY);
}
- function pushUrlIntoHistory(url) {
- history.pushState({}, "", url );
+ function pushUrlIntoHistory(path) {
+ if(kutty.config.historyEnabled) history.pushState({}, "", path);
+ currentPathForHistory = path;
}
function settleImmediately(settleTasks) {
@@ -736,17 +739,18 @@ var kutty = kutty || (function () {
});
}
- function loadHistoryFromServer(pathAndSearch) {
+ function loadHistoryFromServer(path) {
var request = new XMLHttpRequest();
- var details = {path: pathAndSearch, xhr:request};
+ var details = {path: path, xhr:request};
triggerEvent(getDocument().body, "historyCacheMiss.kutty", details);
- request.open('GET', pathAndSearch, true);
+ request.open('GET', path, true);
request.onload = function () {
if (this.status >= 200 && this.status < 400) {
triggerEvent(getDocument().body, "historyCacheMissLoad.kutty", details);
var fragment = makeFragment(this.response);
fragment = fragment.querySelector('[kt-history-elt]') || fragment;
settleImmediately(swapInnerHTML(getHistoryElement(), fragment));
+ currentPathForHistory = path;
} else {
triggerEvent(getDocument().body, "historyCacheMissLoadError.kutty", details);
}
@@ -754,15 +758,18 @@ var kutty = kutty || (function () {
request.send();
}
- function restoreHistory() {
- var pathAndSearch = location.pathname+location.search;
- triggerEvent(getDocument().body, "historyRestore.kutty", {path:pathAndSearch});
- var content = localStorage.getItem('kt-history:' + pathAndSearch);
- if (content) {
- bumpHistoryAccessDate(pathAndSearch);
- settleImmediately(swapInnerHTML(getHistoryElement(), makeFragment(content)));
+ function restoreHistory(path) {
+ saveHistory(currentPathForHistory);
+ path = path || location.pathname+location.search;
+ triggerEvent(getDocument().body, "historyRestore.kutty", {path:path});
+ var cached = getCachedHistory(path);
+ if (cached) {
+ settleImmediately(swapInnerHTML(getHistoryElement(), makeFragment(cached.content)));
+ document.title = cached.title;
+ window.scrollTo(0, cached.scroll);
+ currentPathForHistory = path;
} else {
- loadHistoryFromServer(pathAndSearch);
+ loadHistoryFromServer(path);
}
}
@@ -958,9 +965,9 @@ var kutty = kutty || (function () {
function getSwapSpecification(elt) {
var swapInfo = getClosestAttributeValue(elt, "kt-swap");
var swapSpec = {
- "swapStyle" : "innerHTML",
- "swapDelay" : 0,
- "settleDelay" : 100
+ "swapStyle" : kutty.config.defaultSwapStyle,
+ "swapDelay" : kutty.config.defaultSwapDelay,
+ "settleDelay" : kutty.config.defaultSettleDelay
}
if (swapInfo) {
var split = splitOnWhitespace(swapInfo);
@@ -1075,7 +1082,6 @@ var kutty = kutty || (function () {
// push URL and save new page
if (shouldSaveHistory) {
pushUrlIntoHistory(pushedUrl || requestURL );
- saveHistory();
}
triggerEvent(elt, 'afterSettle.kutty', eventDetail);
}
@@ -1136,8 +1142,19 @@ var kutty = kutty || (function () {
addRule(".kutty-request .kutty-indicator{opacity:1}");
addRule(".kutty-request.kutty-indicator{opacity:1}");
+ function getConfig() {
+ return Object.assign({
+ historyEnabled:true,
+ historyCacheSize:10,
+ defaultSwapStyle:'innerHTML',
+ defaultSwapDelay:0,
+ defaultSettleDelay:100
+ }, JSON.parse(getDocument().querySelector('meta[name="kutty-config"]').content))
+ }
+
// initialize the document
ready(function () {
+ kutty.config = getConfig();
var body = getDocument().body;
processNode(body);
triggerEvent(body, 'load.kutty', {});
@@ -1170,8 +1187,10 @@ var kutty = kutty || (function () {
on: addKuttyEventListener,
onLoad: onLoadHelper,
logAll : logAll,
+ logger : null,
+ config : null,
version: "0.0.1",
_:internalEval
}
}
-)();
\ No newline at end of file
+)() || kutty;
\ No newline at end of file
diff --git a/test/browser-only.html b/test/browser-only.html
index adf19f95..71525c1e 100644
--- a/test/browser-only.html
+++ b/test/browser-only.html
@@ -18,8 +18,85 @@
-
-
+
+
diff --git a/test/kt-push-url.js b/test/kt-push-url.js
index 3806acfe..e60ff618 100644
--- a/test/kt-push-url.js
+++ b/test/kt-push-url.js
@@ -1,77 +1,77 @@
describe("kt-push-url attribute", function() {
+ var KUTTY_HISTORY_CACHE = "kutty-history-cache";
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
+ localStorage.removeItem(KUTTY_HISTORY_CACHE);
});
afterEach(function () {
this.server.restore();
clearWorkArea();
+ localStorage.removeItem(KUTTY_HISTORY_CACHE);
});
- it("should handle a basic back button click", function (done) {
+ it("navigation should push an element into the cache ", function () {
this.server.respondWith("GET", "/test", "second");
-
getWorkArea().innerHTML.should.be.equal("");
var div = make('first
');
div.click();
this.server.respond();
getWorkArea().textContent.should.equal("second")
- history.back();
- setTimeout(function(){
- getWorkArea().textContent.should.equal("first");
- done();
- }, 20);
+ var cache = JSON.parse(localStorage.getItem(KUTTY_HISTORY_CACHE));
+ cache.length.should.equal(1);
});
- it("should handle two forward clicks then back twice", function (done) {
- var i = 0;
- this.server.respondWith("GET", "/test", function(xhr){
- i++;
- xhr.respond(200, {}, "" + i);
- });
-
- getWorkArea().innerHTML.should.equal("");
- var div = make('0
');
- div.click();
- this.server.respond();
- getWorkArea().textContent.should.equal("1")
-
- div.click();
- this.server.respond();
- getWorkArea().textContent.should.equal("2")
-
- history.back();
- setTimeout(function(){
- getWorkArea().textContent.should.equal("1");
- history.back();
- setTimeout(function(){
- getWorkArea().textContent.should.equal("0");
- done();
- }, 20);
- }, 20);
- })
-
- it("should handle a back, forward, back button click", function (done) {
+ it("restore should return old value", function () {
this.server.respondWith("GET", "/test", "second");
-
- getWorkArea().innerHTML.should.equal("");
- var div = make('first
');
+ getWorkArea().innerHTML.should.be.equal("");
+ var div = make('first
');
div.click();
this.server.respond();
getWorkArea().textContent.should.equal("second")
- history.back();
- setTimeout(function(){
- getWorkArea().textContent.should.equal("first");
- history.forward();
- setTimeout(function() {
- getWorkArea().textContent.should.equal("second");
- history.back();
- setTimeout(function() {
- getWorkArea().textContent.should.equal("first");
- done();
- }, 20);
- }, 20);
- }, 20);
- })
-});
+ kutty._('restoreHistory')(location.pathname+location.search)
+ getWorkArea().textContent.should.equal("first")
+ });
+
+ it("cache should only store 10 entries", function () {
+ var x = 0;
+ this.server.respondWith("GET", /test.*/, function(xhr){
+ x++;
+ xhr.respond(200, {}, '')
+ });
+ getWorkArea().innerHTML.should.be.equal("");
+ make('');
+ for (var i = 0; i < 20; i++) { // issue 20 requests
+ byId("d1").click();
+ this.server.respond();
+ }
+ var cache = JSON.parse(localStorage.getItem(KUTTY_HISTORY_CACHE));
+ cache.length.should.equal(10); // should only be 10 elements
+ });
+
+ it("cache miss should issue another GET", function () {
+ this.server.respondWith("GET", "/test1", 'test1
');
+ this.server.respondWith("GET", "/test2", 'test2
');
+
+ make('init
');
+
+ byId("d1").click();
+ this.server.respond();
+ var workArea = getWorkArea();
+ workArea.textContent.should.equal("test1")
+
+ byId("d2").click();
+ this.server.respond();
+ workArea.textContent.should.equal("test2")
+
+ var cache = JSON.parse(localStorage.getItem(KUTTY_HISTORY_CACHE));
+
+ cache.length.should.equal(2);
+ localStorage.removeItem(KUTTY_HISTORY_CACHE); // clear cache
+ kutty._('restoreHistory')("/test1")
+ this.server.respond();
+ getWorkArea().textContent.should.equal("test1")
+ });
+
+});
\ No newline at end of file
diff --git a/www/events.md b/www/events.md
index 9e3c964f..24b038ff 100644
--- a/www/events.md
+++ b/www/events.md
@@ -97,7 +97,7 @@ This event is triggered when kutty handles a history restoration action
* `detail.path` - the path and query of the page being restored
-### Event - [`historyUpdate.kutty`](#historyUpdate.kutty)
+### Event - [`beforeHistorySave.kutty`](#beforeHistorySave.kutty)
This event is triggered when kutty handles a history restoration action
diff --git a/www/reference.md b/www/reference.md
index c65fbc20..07ee601e 100644
--- a/www/reference.md
+++ b/www/reference.md
@@ -76,7 +76,7 @@ title: > kutty - Attributes
| [`historyCacheMissError.kutty`](/events#historyCacheMissError.kutty) | triggered on a unsuccessful remote retrieval
| [`historyCacheMissLoad.kutty`](/events#historyCacheMissLoad.kutty) | triggered on a succesful remote retrieval
| [`historyRestore.kutty`](/events#historyRestore.kutty) | triggered when kutty handles a history restoration action
-| [`historyUpdate.kutty`](/events#historyUpdate.kutty) | triggered when a new history element is added to the local cache
+| [`beforeHistorySave.kutty`](/events#beforeHistorySave.kutty) | triggered before content is saved to the history cache
| [`initSSE.kutty`](/events#initSSE.kutty) | triggered when a new Server Sent Event source is created
| [`load.kutty`](/events#load.kutty) | triggered when new content is added to the DOM
| [`noSSESourceError.kutty`](/events#noSSESourceError.kutty) | triggered when an element refers to a SSE event in its trigger, but no parent SSE source has been defined