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(""+e+"
",1);case"col":return i(""+e+"
",2);case"tr":return i(""+e+"
",2);case"td":case"th":return i(""+e+"
",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(""+t+"
",1);case"col":return c(""+t+"
",2);case"tr":return c(""+t+"
",2);case"td":case"th":return c(""+t+"
",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