diff --git a/TODO.md b/TODO.md index 38aa6d37..d8c35b0e 100644 --- a/TODO.md +++ b/TODO.md @@ -17,6 +17,7 @@ * polling cancellation API 205 code * meta config tag * simple logging API +* hx-toggle-class * Testing * polling diff --git a/dist/htmx.js b/dist/htmx.js index 0de78c93..41b3559d 100644 --- a/dist/htmx.js +++ b/dist/htmx.js @@ -117,6 +117,10 @@ var HTMx = HTMx || (function () { return getDocument().body.contains(elt); } + function concat(arr1, arr2) { + return arr1.concat(arr2); + } + //==================================================================== // Node processing //==================================================================== @@ -152,19 +156,21 @@ var HTMx = HTMx || (function () { } function handleOutOfBandSwaps(fragment) { + var settleTasks = []; forEach(fragment.children, function(child){ if (getAttributeValue(child, "hx-swap-oob") === "true") { var target = getDocument().getElementById(child.id); if (target) { var fragment = new DocumentFragment() fragment.append(child); - swapOuterHTML(target, fragment); + settleTasks = settleTasks.concat(swapOuterHTML(target, fragment)); } else { child.parentNode.removeChild(child); triggerEvent(getDocument().body, "oobErrorNoTarget.hx", {id:child.id, content:child}) } } }) + return settleTasks; } function handleAttributes(parentNode, fragment) { @@ -179,11 +185,7 @@ var HTMx = HTMx || (function () { }); } }); - setTimeout(function () { - forEach(attributeSwaps, function (swap) { - swap.call(); - }); - }, 100); + return attributeSwaps; } function insertNodesBefore(parentNode, insertBefore, fragment) { @@ -200,32 +202,37 @@ var HTMx = HTMx || (function () { function swapOuterHTML(target, fragment) { if (target.tagName === "BODY") { - swapInnerHTML(target, fragment); + return swapInnerHTML(target, fragment); } else { - insertNodesBefore(parentElt(target), target, fragment); + var settleTasks = insertNodesBefore(parentElt(target), target, fragment); parentElt(target).removeChild(target); + return settleTasks; } } function swapPrepend(target, fragment) { - insertNodesBefore(target, target.firstChild, fragment); + return insertNodesBefore(target, target.firstChild, fragment); } function swapPrependBefore(target, fragment) { - insertNodesBefore(parentElt(target), target, fragment); + return insertNodesBefore(parentElt(target), target, fragment); } function swapAppend(target, fragment) { - insertNodesBefore(target, null, fragment); + return insertNodesBefore(target, null, fragment); } function swapAppendAfter(target, fragment) { - insertNodesBefore(parentElt(target), target.nextSibling, fragment); + return insertNodesBefore(parentElt(target), target.nextSibling, fragment); } function swapInnerHTML(target, fragment) { - target.innerHTML = ""; - insertNodesBefore(target, null, fragment); + var firstChild = target.firstChild; + return insertNodesBefore(target, firstChild, fragment); + while (firstChild.nextSibling) { + target.removeChild(firstChild.nextSibling); + } + target.removeChild(firstChild); } function maybeSelectFromResponse(elt, fragment) { @@ -240,30 +247,20 @@ var HTMx = HTMx || (function () { return fragment; } - function swapResponse(target, elt, responseText, callBack) { - + function swapResponse(target, elt, responseText) { var fragment = makeFragment(responseText); - handleOutOfBandSwaps(fragment); + var settleTasks = handleOutOfBandSwaps(fragment); fragment = maybeSelectFromResponse(elt, fragment); var swapStyle = getClosestAttributeValue(elt, "hx-swap"); - if (swapStyle === "outerHTML") { - swapOuterHTML(target, fragment); - } else if (swapStyle === "prepend") { - swapPrepend(target, fragment); - } else if (swapStyle === "prependBefore") { - swapPrependBefore(target, fragment); - } else if (swapStyle === "append") { - swapAppend(target, fragment); - } else if (swapStyle === "appendAfter") { - swapAppendAfter(target, fragment); - } else { - swapInnerHTML(target, fragment); - } - - if(callBack) { - callBack.call(); + switch(swapStyle) { + case "outerHTML": return concat(settleTasks, swapOuterHTML(target, fragment)); + case "prepend": return concat(settleTasks, swapPrepend(target, fragment)); + case "prependBefore": return concat(settleTasks, swapPrependBefore(target, fragment)); + case "append": return concat(settleTasks, swapAppend(target, fragment)); + case "appendAfter": return concat(settleTasks, swapAppendAfter(target, fragment)); + default: return concat(settleTasks, swapInnerHTML(target, fragment)); } } @@ -583,62 +580,78 @@ var HTMx = HTMx || (function () { //==================================================================== // History Support //==================================================================== - - function makeHistoryId() { - return Math.random().toString(36).substr(3, 9); - } - function getHistoryElement() { - var historyElt = getDocument().getElementsByClassName('hx-history-element'); - if (historyElt.length > 0) { - return historyElt[0]; - } else { - return getDocument().body; + var historyElt = getDocument().querySelector('.hx-history-element'); + return historyElt || getDocument().body; + } + + function purgeOldestPaths(paths, historyTimestamps) { + var 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 bumpHistoryAccessDate(pathAndSearch) { + var historyTimestamps = JSON.parse(localStorage.getItem("hx-history-timestamps")) || {}; + historyTimestamps[pathAndSearch] = Date.now; + var paths = Object.keys(historyTimestamps); + if (paths.length > 20) { + purgeOldestPaths(paths, historyTimestamps); } + localStorage.setItem("hx-history-timestamps", JSON.stringify(historyTimestamps)); } - function saveLocalHistoryData(historyData) { - localStorage.setItem('hx-history', JSON.stringify(historyData)); - } - - function getLocalHistoryData() { - var historyEntry = localStorage.getItem('hx-history'); - var historyData; - if (historyEntry) { - historyData = JSON.parse(historyEntry); - } else { - var initialId = makeHistoryId(); - historyData = {"current": initialId, "slots": [initialId]}; - saveLocalHistoryData(historyData); - } - return historyData; - } - - function newHistoryData() { - var historyData = getLocalHistoryData(); - var newId = makeHistoryId(); - var slots = historyData.slots; - if (slots.length > 20) { - var toEvict = slots.shift(); - localStorage.removeItem('hx-history-' + toEvict); - } - slots.push(newId); - historyData.current = newId; - saveLocalHistoryData(historyData); - } - - function updateCurrentHistoryContent() { + function saveHistory() { var elt = getHistoryElement(); - var historyData = getLocalHistoryData(); - history.replaceState({"hx-history-key": historyData.current}, getDocument().title, window.location.href); - localStorage.setItem('hx-history-' + historyData.current, elt.innerHTML); + var pathAndSearch = location.pathname+location.search; + triggerEvent(getDocument().body, "historyUpdate.hx", {path:pathAndSearch, historyElement:elt}); + history.replaceState({}, getDocument().title, window.location.href); + localStorage.setItem('hx-history-content-' + pathAndSearch, elt.innerHTML); + bumpHistoryAccessDate(pathAndSearch); } - function restoreHistory(data) { - var historyKey = data['hx-history-key']; - var content = localStorage.getItem('hx-history-' + historyKey); - var elt = getHistoryElement(); - swapInnerHTML(elt, makeFragment(content)); + function pushUrlIntoHistory(url) { + history.pushState({}, "", url ); + } + + function settleImmediately(settleTasks) { + forEach(settleTasks, function (task) { + task.call(); + }); + } + + function loadHistoryFromServer(pathAndSearch) { + triggerEvent(getDocument().body, "historyCacheMiss.hx", {path: pathAndSearch}); + var request = new XMLHttpRequest(); + request.open('GET', pathAndSearch, true); + request.onload = function () { + triggerEvent(getDocument().body, "historyCacheMissLoad.hx", {path: pathAndSearch}); + if (this.status >= 200 && this.status < 400) { + var fragment = makeFragment(this.response); + fragment = fragment.querySelector('.hx-history-element') || fragment; + settleImmediately(swapInnerHTML(getHistoryElement(), fragment)); + } + }; + } + + function restoreHistory() { + var pathAndSearch = location.pathname+location.search; + triggerEvent(getDocument().body, "historyUpdate.hx", {path:pathAndSearch}); + var content = localStorage.getItem('hx-history-content-' + pathAndSearch); + if (content) { + bumpHistoryAccessDate(pathAndSearch); + settleImmediately(swapInnerHTML(getHistoryElement(), makeFragment(content))); + } else { + loadHistoryFromServer(pathAndSearch); + } } function shouldPush(elt) { @@ -646,21 +659,6 @@ var HTMx = HTMx || (function () { (elt.tagName === "A" && getInternalData(elt).boosted); } - function snapshotForCurrentHistoryEntry(elt) { - if (shouldPush(elt)) { - // TODO event to allow de-initialization of HTML elements in target - updateCurrentHistoryContent(); - } - } - - function initNewHistoryEntry(elt, url) { - if (shouldPush(elt)) { - newHistoryData(); - history.pushState({}, "", url); - updateCurrentHistoryContent(); - } - } - function addRequestIndicatorClasses(elt) { mutateRequestIndicatorClasses(elt, "add"); } @@ -804,11 +802,14 @@ var HTMx = HTMx || (function () { if(!triggerEvent(elt, 'values.hx', {values: inputValues, target:target})) return endRequestLock(); // request type + var requestURL; if (verb === 'get') { var noValues = Object.keys(inputValues).length === 0; - xhr.open('GET', path + (noValues ? "" : "?" + urlEncode(inputValues)), true); + requestURL = path + (noValues ? "" : "?" + urlEncode(inputValues)); + xhr.open('GET', requestURL, true); } else { - xhr.open('POST', path, true); + requestURL = path; + xhr.open('POST', requestURL, true); setHeader(xhr,'Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8', true); if (verb !== 'post') { setHeader(xhr, 'X-HTTP-Method-Override', verb.toUpperCase(), true); @@ -841,30 +842,56 @@ var HTMx = HTMx || (function () { xhr.onload = function () { try { if (!triggerEvent(elt, 'beforeOnLoad.hx', {xhr: xhr, target: target})) return; - snapshotForCurrentHistoryEntry(elt, path); - var trigger = this.getResponseHeader("X-HX-Trigger"); - handleTrigger(elt, trigger); - initNewHistoryEntry(elt, path); + + handleTrigger(elt, this.getResponseHeader("X-HX-Trigger")); + var pushedUrl = this.getResponseHeader("X-HX-Push") + + var shouldSaveHistory = shouldPush(elt) || pushedUrl; + if (this.status >= 200 && this.status < 400) { // don't process 'No Content' response if (this.status !== 204) { // Success! var resp = this.response; if (!triggerEvent(elt, 'beforeSwap.hx', {xhr: xhr, target: target})) return; + + // Save current page + if (shouldSaveHistory) { + saveHistory(); + } + target.classList.add("hx-swapping"); var doSwap = function () { try { - swapResponse(target, elt, resp, function () { - target.classList.remove("hx-swapping"); - updateCurrentHistoryContent(); - triggerEvent(elt, 'afterSwap.hx', {xhr: xhr, target: target}); - }); + var settleTasks = swapResponse(target, elt, resp); + target.classList.remove("hx-swapping"); + target.classList.add("hx-settling"); + triggerEvent(elt, 'afterSwap.hx', {xhr: xhr, target: target}); + + var doSettle = function(){ + forEach(settleTasks, function (task) { + task.call(); + }); + target.classList.remove("hx-settling"); + // push URL and save new page + pushUrlIntoHistory(pushedUrl || requestURL ); + saveHistory(); + triggerEvent(elt, 'afterSettle.hx', {xhr: xhr, target: target}); + } + + var settleDelayStr = getAttributeValue(elt, "hx-settle-delay") || "100ms"; + if (settleDelayStr) { + setTimeout(doSettle, parseInterval(settleDelayStr)) + } else { + doSettle(); + } } catch (e) { triggerEvent(elt, 'swapError.hx', {xhr: xhr, response: xhr.response, status: xhr.status, target: target}); throw e; } }; - var swapDelayStr = getAttributeValue(elt, "hx-swap-delay"); + + var swapDelayStr = getAttributeValue(elt, "hx-swap-delay") || "100ms"; if (swapDelayStr) { setTimeout(doSwap, parseInterval(swapDelayStr)) } else { @@ -908,7 +935,7 @@ var HTMx = HTMx || (function () { ready(function () { processNode(getDocument().body); window.onpopstate = function (event) { - restoreHistory(event.state); + restoreHistory(); }; }) diff --git a/dist/htmx.min.js b/dist/htmx.min.js index 1f67df9d..d3dfefca 100644 --- a/dist/htmx.min.js +++ b/dist/htmx.min.js @@ -1 +1 @@ -var HTMx=HTMx||function(){"use strict";var e=["get","post","put","delete","patch"];function h(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 d(e,t){return e.getAttribute&&e.getAttribute(t)}function g(e,t){return d(e,t)||d(e,"data-"+t)}function i(e){return e.parentElement}function p(){return document}function a(e,t){if(t(e)){return e}else if(i(e)){return a(i(e),t)}else{return null}}function m(e,t){var r=null;a(e,function(e){return r=d(e,t)});return r}function u(e,t){return e!=null&&(e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector).call(e,t)}function o(e,t){do{if(e==null||u(e,t))return e}while(e=e&&i(e))}function s(e){var t=p().createRange();return t.createContextualFragment(e)}function t(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function l(e){return t(e,"Function")}function f(e){return t(e,"Object")}function x(e){var t="hx-data-internal";var r=e[t];if(!r){r=e[t]={}}return r}function r(e){var t=[];c(e,function(e){t.push(e)});return t}function c(e,t){for(var r=0;r=0}function v(e){return p().body.contains(e)}function y(e){var t=a(e,function(e){return d(e,"hx-target")!==null});if(t){var r=d(t,"hx-target");if(r==="this"){return t}else{return p().querySelector(r)}}else{var n=x(e);if(n.boosted){return p().body}else{return e}}}function S(t,r){c(t.attributes,function(e){if(!r.hasAttribute(e.name)){t.removeAttribute(e.name)}});c(r.attributes,function(e){t.setAttribute(e.name,e.value)})}function w(e){c(e.children,function(e){if(g(e,"hx-swap-oob")==="true"){var t=p().getElementById(e.id);if(t){var r=new DocumentFragment;r.append(e);T(t,r)}else{e.parentNode.removeChild(e);Y(p().body,"oobErrorNoTarget.hx",{id:e.id,content:e})}}})}function E(n,e){var i=[];c(e.querySelectorAll("[id]"),function(e){var t=n.querySelector(e.tagName+"[id="+e.id+"]");if(t){var r=e.cloneNode();S(e,t);i.push(function(){S(e,r)})}});setTimeout(function(){c(i,function(e){e.call()})},100)}function b(e,t,r){E(e,r);while(r.childNodes.length>0){var n=r.firstChild;e.insertBefore(n,t);if(n.nodeType!==Node.TEXT_NODE){Y(n,"load.hx",{elt:n,parent:i(n)});_(n)}}}function T(e,t){if(e.tagName==="BODY"){N(e,t)}else{b(i(e),e,t);i(e).removeChild(e)}}function O(e,t){b(e,e.firstChild,t)}function L(e,t){b(i(e),e,t)}function C(e,t){b(e,null,t)}function A(e,t){b(i(e),e.nextSibling,t)}function N(e,t){e.innerHTML="";b(e,null,t)}function H(e,t){var r=m(e,"hx-select");if(r){var n=new DocumentFragment;c(t.querySelectorAll(r),function(e){n.append(e)});t=n}return t}function M(e,t,r,n){var i=s(r);w(i);i=H(t,i);var o=m(t,"hx-swap");if(o==="outerHTML"){T(e,i)}else if(o==="prepend"){O(e,i)}else if(o==="prependBefore"){L(e,i)}else if(o==="append"){C(e,i)}else if(o==="appendAfter"){A(e,i)}else{N(e,i)}if(n){n.call()}}function q(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(!f(i)){i={value:i}}Y(e,n,i)}}}else{Y(e,t,[])}}}function I(e){var t=m(e,"hx-trigger");if(t){return t}else{if(u(e,"button")){return"click"}else if(u(e,"form")){return"submit"}else if(u(e,"input, textarea, select")){return"change"}else{return"click"}}}function F(o,e,a){var t=e.split(",");c(t,function(e){var t="";var r=50;var n=e.trim();if(n.indexOf(":")>0){var i=n.split(":");t=i[0];r=h(i[1])}else{t=n}setTimeout(function(){o.classList[a].call(o.classList,t)},r)})}function R(e,t,r){var n=I(e);var i=x(e);if(n.trim().indexOf("every ")===0){var o=n.split(/\s+/);var a=o[1];if(a){var u=h(a);i.timeout=setTimeout(function(){if(v(e)){pe(e,t,r);R(e,t,g(e,"hx-"+t))}},u)}}}function X(e){return location.hostname===e.hostname&&d(e,"href")&&!d(e,"href").startsWith("#")}function D(e,t,r){if(e.tagName==="A"&&X(e)||e.tagName==="FORM"){t.boosted=true;var n,i;if(e.tagName==="A"){n="get";i=d(e,"href")}else{var o=d(e,"method");n=o?o.toLowerCase():"get";i=d(e,"action")}k(e,n,i,t,r,true)}}function k(o,a,u,e,t,s){var r=function(e){if(s)e.preventDefault();var t=x(e);var r=x(o);if(!t.handled){t.handled=true;if(g(o,"hx-trigger-once")==="true"){if(r.triggeredOnce){return}else{r.triggeredOnce=true}}if(g(o,"hx-trigger-changed-only")==="true"){if(r.lastValue===o.value){return}else{r.lastValue=o.value}}if(r.delayed){clearTimeout(r.delayed)}var n=g(o,"hx-trigger-delay");var i=function(){pe(o,a,u,e.target)};if(n){r.delayed=setTimeout(i,h(n))}else{i()}}};e.trigger=t;e.eventListener=r;o.addEventListener(t,r)}function B(){if(!window["hxScrollHandler"]){var e=function(){c(p().querySelectorAll("[hx-trigger='reveal']"),function(e){P(e)})};window["hxScrollHandler"]=e;window.addEventListener("scroll",e)}}function P(e){var t=x(e);if(!t.revealed&&n(e)){t.revealed=true;pe(e,t.verb,t.path)}}function U(e){if(!v(e)){e.sseSource.close();return true}}function j(t,e){var r={initializer:function(){new EventSource(e,r.config)},config:{withCredentials:true}};Y(t,"initSSE.mx",{config:r});var n=r.initializer();n.onerror=function(e){Y(t,"sseError.mx",{error:e,source:n});U(t)};x(t).sseSource=n}function J(e,t,r,n){var i=a(t,function(e){return e.sseSource});if(i){var o=function(){if(!U(i)){if(v(t)){pe(t,r,n)}else{i.sseSource.removeEventListener(e,o)}}};i.sseSource.addEventListener(e,o)}else{Y(t,"noSSESourceError.mx")}}function z(e,t,r,n){if(!e.loaded){e.loaded=true;pe(t,r,n)}}function V(r,n,i){var o=false;c(e,function(e){var t=g(r,"hx-"+e);if(t){o=true;n.path=t;n.verb=e;if(i.indexOf("sse:")===0){J(i.substr(4),r,e,t)}else if(i==="revealed"){B();P(r)}else if(i==="load"){z(n,r,e,t)}else if(i.trim().indexOf("every ")===0){n.polling=true;R(r,e,t)}else{k(r,e,t,n,i)}}});return o}function _(e){var t=x(e);if(!t.processed){t.processed=true;var r=I(e);var n=V(e,t,r);if(!n&&m(e,"hx-boost")==="true"){D(e,t,r)}var i=g(e,"hx-sse-source");if(i){j(e,i)}var o=g(e,"hx-add-class");if(o){F(e,o,"add")}var a=g(e,"hx-remove-class");if(a){F(e,a,"remove")}}c(e.children,function(e){_(e)})}function G(e,t,r){var n=m(e,"hx-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,details:r}))}}function W(e,t){var r;if(window.CustomEvent&&typeof window.CustomEvent==="function"){r=new CustomEvent(e,{detail:t})}else{r=p().createEvent("CustomEvent");r.initCustomEvent(e,true,true,t)}return r}function Y(e,t,r){r["elt"]=e;var n=W(t,r);if(HTMx.logger){HTMx.logger(e,t,r);if(t.indexOf("Error")>0){G(e,t,r)}}var i=e.dispatchEvent(n);var o=e.dispatchEvent(W("all.hx",{elt:e,originalDetails:r,originalEvent:n}));return i&&o}function K(e,t,r){var n,i,o;if(l(e)){n=p().body;i="all.hx";o=e}else if(l(t)){n=p().body;i=e;o=t}else{n=e;i=t;o=r}return n.addEventListener(i,o)}function Q(){return Math.random().toString(36).substr(3,9)}function Z(){var e=p().getElementsByClassName("hx-history-element");if(e.length>0){return e[0]}else{return p().body}}function $(e){localStorage.setItem("hx-history",JSON.stringify(e))}function ee(){var e=localStorage.getItem("hx-history");var t;if(e){t=JSON.parse(e)}else{var r=Q();t={current:r,slots:[r]};$(t)}return t}function te(){var e=ee();var t=Q();var r=e.slots;if(r.length>20){var n=r.shift();localStorage.removeItem("hx-history-"+n)}r.push(t);e.current=t;$(e)}function re(){var e=Z();var t=ee();history.replaceState({"hx-history-key":t.current},p().title,window.location.href);localStorage.setItem("hx-history-"+t.current,e.innerHTML)}function ne(e){var t=e["hx-history-key"];var r=localStorage.getItem("hx-history-"+t);var n=Z();N(n,s(r))}function ie(e){return m(e,"hx-push-url")==="true"||e.tagName==="A"&&x(e).boosted}function oe(e){if(ie(e)){re()}}function ae(e,t){if(ie(e)){te();history.pushState({},"",t);re()}}function ue(e){le(e,"add")}function se(e){le(e,"remove")}function le(e,t){var r=m(e,"hx-indicator");if(r){var n=p().querySelectorAll(r)}else{n=[e]}c(n,function(e){e.classList[t].call(e.classList,"hx-show-indicator")})}function fe(e,t){for(var r=0;r=200&&this.status<400){if(this.status!==204){var t=this.response;if(!Y(i,"beforeSwap.hx",{xhr:f,target:u}))return;u.classList.add("hx-swapping");var r=function(){try{M(u,i,t,function(){u.classList.remove("hx-swapping");re();Y(i,"afterSwap.hx",{xhr:f,target:u})})}catch(e){Y(i,"swapError.hx",{xhr:f,response:f.response,status:f.status,target:u});throw e}};var n=g(i,"hx-swap-delay");if(n){setTimeout(r,h(n))}else{r()}}}else{Y(i,"responseError.hx",{xhr:f,response:f.response,status:f.status,target:u})}}catch(e){Y(i,"onLoadError.hx",{xhr:f,response:f.response,status:f.status,target:u});throw e}finally{se(i);a();Y(i,"afterOnLoad.hx",{xhr:f,response:f.response,status:f.status,target:u})}};f.onerror=function(){se(i);Y(i,"loadError.hx",{xhr:f});a()};if(!Y(i,"beforeRequest.hx",{xhr:f,values:c,target:u}))return a();ue(i);f.send(e==="get"?null:de(c))}function me(e){if(p().readyState!=="loading"){e()}else{p().addEventListener("DOMContentLoaded",e)}}me(function(){_(p().body);window.onpopstate=function(e){ne(e.state)}});function xe(e){return eval(e)}return{processElement:_,on:K,version:"0.0.2",_:xe}}(); \ No newline at end of file +var HTMx=HTMx||function(){"use strict";var e=["get","post","put","delete","patch"];function d(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 p(e,t){return e.getAttribute&&e.getAttribute(t)}function g(e,t){return p(e,t)||p(e,"data-"+t)}function i(e){return e.parentElement}function m(){return document}function o(e,t){if(t(e)){return e}else if(i(e)){return o(i(e),t)}else{return null}}function x(e,t){var r=null;o(e,function(e){return r=p(e,t)});return r}function u(e,t){return e!=null&&(e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector).call(e,t)}function a(e,t){do{if(e==null||u(e,t))return e}while(e=e&&i(e))}function s(e){var t=m().createRange();return t.createContextualFragment(e)}function t(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function l(e){return t(e,"Function")}function f(e){return t(e,"Object")}function y(e){var t="hx-data-internal";var r=e[t];if(!r){r=e[t]={}}return r}function r(e){var t=[];S(e,function(e){t.push(e)});return t}function S(e,t){for(var r=0;r=0}function c(e){return m().body.contains(e)}function v(e,t){return e.concat(t)}function w(e){var t=o(e,function(e){return p(e,"hx-target")!==null});if(t){var r=p(t,"hx-target");if(r==="this"){return t}else{return m().querySelector(r)}}else{var n=y(e);if(n.boosted){return m().body}else{return e}}}function h(t,r){S(t.attributes,function(e){if(!r.hasAttribute(e.name)){t.removeAttribute(e.name)}});S(r.attributes,function(e){t.setAttribute(e.name,e.value)})}function b(e){var n=[];S(e.children,function(e){if(g(e,"hx-swap-oob")==="true"){var t=m().getElementById(e.id);if(t){var r=new DocumentFragment;r.append(e);n=n.concat(O(t,r))}else{e.parentNode.removeChild(e);K(m().body,"oobErrorNoTarget.hx",{id:e.id,content:e})}}});return n}function E(n,e){var i=[];S(e.querySelectorAll("[id]"),function(e){var t=n.querySelector(e.tagName+"[id="+e.id+"]");if(t){var r=e.cloneNode();h(e,t);i.push(function(){h(e,r)})}});return i}function T(e,t,r){E(e,r);while(r.childNodes.length>0){var n=r.firstChild;e.insertBefore(n,t);if(n.nodeType!==Node.TEXT_NODE){K(n,"load.hx",{elt:n,parent:i(n)});_(n)}}}function O(e,t){if(e.tagName==="BODY"){return q(e,t)}else{var r=T(i(e),e,t);i(e).removeChild(e);return r}}function L(e,t){return T(e,e.firstChild,t)}function C(e,t){return T(i(e),e,t)}function A(e,t){return T(e,null,t)}function H(e,t){return T(i(e),e.nextSibling,t)}function q(e,t){var r=e.firstChild;return T(e,r,t);while(r.nextSibling){e.removeChild(r.nextSibling)}e.removeChild(r)}function M(e,t){var r=x(e,"hx-select");if(r){var n=new DocumentFragment;S(t.querySelectorAll(r),function(e){n.append(e)});t=n}return t}function N(e,t,r){var n=s(r);var i=b(n);n=M(t,n);var a=x(t,"hx-swap");switch(a){case"outerHTML":return v(i,O(e,n));case"prepend":return v(i,L(e,n));case"prependBefore":return v(i,C(e,n));case"append":return v(i,A(e,n));case"appendAfter":return v(i,H(e,n));default:return v(i,q(e,n))}}function I(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(!f(i)){i={value:i}}K(e,n,i)}}}else{K(e,t,[])}}}function R(e){var t=x(e,"hx-trigger");if(t){return t}else{if(u(e,"button")){return"click"}else if(u(e,"form")){return"submit"}else if(u(e,"input, textarea, select")){return"change"}else{return"click"}}}function F(a,e,o){var t=e.split(",");S(t,function(e){var t="";var r=50;var n=e.trim();if(n.indexOf(":")>0){var i=n.split(":");t=i[0];r=d(i[1])}else{t=n}setTimeout(function(){a.classList[o].call(a.classList,t)},r)})}function X(e,t,r){var n=R(e);var i=y(e);if(n.trim().indexOf("every ")===0){var a=n.split(/\s+/);var o=a[1];if(o){var u=d(o);i.timeout=setTimeout(function(){if(c(e)){ge(e,t,r);X(e,t,g(e,"hx-"+t))}},u)}}}function D(e){return location.hostname===e.hostname&&p(e,"href")&&!p(e,"href").startsWith("#")}function U(e,t,r){if(e.tagName==="A"&&D(e)||e.tagName==="FORM"){t.boosted=true;var n,i;if(e.tagName==="A"){n="get";i=p(e,"href")}else{var a=p(e,"method");n=a?a.toLowerCase():"get";i=p(e,"action")}P(e,n,i,t,r,true)}}function P(a,o,u,e,t,s){var r=function(e){if(s)e.preventDefault();var t=y(e);var r=y(a);if(!t.handled){t.handled=true;if(g(a,"hx-trigger-once")==="true"){if(r.triggeredOnce){return}else{r.triggeredOnce=true}}if(g(a,"hx-trigger-changed-only")==="true"){if(r.lastValue===a.value){return}else{r.lastValue=a.value}}if(r.delayed){clearTimeout(r.delayed)}var n=g(a,"hx-trigger-delay");var i=function(){ge(a,o,u,e.target)};if(n){r.delayed=setTimeout(i,d(n))}else{i()}}};e.trigger=t;e.eventListener=r;a.addEventListener(t,r)}function j(){if(!window["hxScrollHandler"]){var e=function(){S(m().querySelectorAll("[hx-trigger='reveal']"),function(e){k(e)})};window["hxScrollHandler"]=e;window.addEventListener("scroll",e)}}function k(e){var t=y(e);if(!t.revealed&&n(e)){t.revealed=true;ge(e,t.verb,t.path)}}function B(e){if(!c(e)){e.sseSource.close();return true}}function J(t,e){var r={initializer:function(){new EventSource(e,r.config)},config:{withCredentials:true}};K(t,"initSSE.mx",{config:r});var n=r.initializer();n.onerror=function(e){K(t,"sseError.mx",{error:e,source:n});B(t)};y(t).sseSource=n}function z(e,t,r,n){var i=o(t,function(e){return e.sseSource});if(i){var a=function(){if(!B(i)){if(c(t)){ge(t,r,n)}else{i.sseSource.removeEventListener(e,a)}}};i.sseSource.addEventListener(e,a)}else{K(t,"noSSESourceError.mx")}}function V(e,t,r,n){if(!e.loaded){e.loaded=true;ge(t,r,n)}}function G(r,n,i){var a=false;S(e,function(e){var t=g(r,"hx-"+e);if(t){a=true;n.path=t;n.verb=e;if(i.indexOf("sse:")===0){z(i.substr(4),r,e,t)}else if(i==="revealed"){j();k(r)}else if(i==="load"){V(n,r,e,t)}else if(i.trim().indexOf("every ")===0){n.polling=true;X(r,e,t)}else{P(r,e,t,n,i)}}});return a}function _(e){var t=y(e);if(!t.processed){t.processed=true;var r=R(e);var n=G(e,t,r);if(!n&&x(e,"hx-boost")==="true"){U(e,t,r)}var i=g(e,"hx-sse-source");if(i){J(e,i)}var a=g(e,"hx-add-class");if(a){F(e,a,"add")}var o=g(e,"hx-remove-class");if(o){F(e,o,"remove")}}S(e.children,function(e){_(e)})}function W(e,t,r){var n=x(e,"hx-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,details:r}))}}function Y(e,t){var r;if(window.CustomEvent&&typeof window.CustomEvent==="function"){r=new CustomEvent(e,{detail:t})}else{r=m().createEvent("CustomEvent");r.initCustomEvent(e,true,true,t)}return r}function K(e,t,r){r["elt"]=e;var n=Y(t,r);if(HTMx.logger){HTMx.logger(e,t,r);if(t.indexOf("Error")>0){W(e,t,r)}}var i=e.dispatchEvent(n);var a=e.dispatchEvent(Y("all.hx",{elt:e,originalDetails:r,originalEvent:n}));return i&&a}function Q(e,t,r){var n,i,a;if(l(e)){n=m().body;i="all.hx";a=e}else if(l(t)){n=m().body;i=e;a=t}else{n=e;i=t;a=r}return n.addEventListener(i,a)}function Z(){var e=m().querySelector(".hx-history-element");return e||m().body}function $(e,r){var e=e.sort(function(e,t){return r[t]-r[e]});var t=0;S(e,function(e){t++;if(t>20){delete r[e];localStorage.removeItem(e)}})}function ee(e){var t=JSON.parse(localStorage.getItem("hx-history-timestamps"))||{};t[e]=Date.now;var r=Object.keys(t);if(r.length>20){$(r,t)}localStorage.setItem("hx-history-timestamps",JSON.stringify(t))}function te(){var e=Z();var t=location.pathname+location.search;K(m().body,"historyUpdate.hx",{path:t,historyElement:e});history.replaceState({},m().title,window.location.href);localStorage.setItem("hx-history-content-"+t,e.innerHTML);ee(t)}function re(e){history.pushState({},"",e)}function ne(e){S(e,function(e){e.call()})}function ie(t){K(m().body,"historyCacheMiss.hx",{path:t});var e=new XMLHttpRequest;e.open("GET",t,true);e.onload=function(){K(m().body,"historyCacheMissLoad.hx",{path:t});if(this.status>=200&&this.status<400){var e=s(this.response);e=e.querySelector(".hx-history-element")||e;ne(q(Z(),e))}}}function ae(){var e=location.pathname+location.search;K(m().body,"historyUpdate.hx",{path:e});var t=localStorage.getItem("hx-history-content-"+e);if(t){ee(e);ne(q(Z(),s(t)))}else{ie(e)}}function oe(e){return x(e,"hx-push-url")==="true"||e.tagName==="A"&&y(e).boosted}function ue(e){le(e,"add")}function se(e){le(e,"remove")}function le(e,t){var r=x(e,"hx-indicator");if(r){var n=m().querySelectorAll(r)}else{n=[e]}S(n,function(e){e.classList[t].call(e.classList,"hx-show-indicator")})}function fe(e,t){for(var r=0;r=200&&this.status<400){if(this.status!==204){var i=this.response;if(!K(a,"beforeSwap.hx",{xhr:f,target:u}))return;if(e){te()}u.classList.add("hx-swapping");var t=function(){try{var e=N(u,a,i);u.classList.remove("hx-swapping");u.classList.add("hx-settling");K(a,"afterSwap.hx",{xhr:f,target:u});var t=function(){S(e,function(e){e.call()});u.classList.remove("hx-settling");re(n||v);te();K(a,"afterSettle.hx",{xhr:f,target:u})};var r=g(a,"hx-settle-delay")||"100ms";if(r){setTimeout(t,d(r))}else{t()}}catch(e){K(a,"swapError.hx",{xhr:f,response:f.response,status:f.status,target:u});throw e}};var r=g(a,"hx-swap-delay")||"100ms";if(r){setTimeout(t,d(r))}else{t()}}}else{K(a,"responseError.hx",{xhr:f,response:f.response,status:f.status,target:u})}}catch(e){K(a,"onLoadError.hx",{xhr:f,response:f.response,status:f.status,target:u});throw e}finally{se(a);o();K(a,"afterOnLoad.hx",{xhr:f,response:f.response,status:f.status,target:u})}};f.onerror=function(){se(a);K(a,"loadError.hx",{xhr:f});o()};if(!K(a,"beforeRequest.hx",{xhr:f,values:c,target:u}))return o();ue(a);f.send(e==="get"?null:de(c))}function me(e){if(m().readyState!=="loading"){e()}else{m().addEventListener("DOMContentLoaded",e)}}me(function(){_(m().body);window.onpopstate=function(e){ae()}});function xe(e){return eval(e)}return{processElement:_,on:Q,version:"0.0.2",_:xe}}(); \ No newline at end of file diff --git a/dist/htmx.min.js.gz b/dist/htmx.min.js.gz index 85b2e4f9..7595db85 100644 Binary files a/dist/htmx.min.js.gz and b/dist/htmx.min.js.gz differ diff --git a/src/htmx.js b/src/htmx.js index 39d9b791..676acb29 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -117,6 +117,10 @@ var HTMx = HTMx || (function () { return getDocument().body.contains(elt); } + function concat(arr1, arr2) { + return arr1.concat(arr2); + } + //==================================================================== // Node processing //==================================================================== @@ -152,19 +156,21 @@ var HTMx = HTMx || (function () { } function handleOutOfBandSwaps(fragment) { + var settleTasks = []; forEach(fragment.children, function(child){ if (getAttributeValue(child, "hx-swap-oob") === "true") { var target = getDocument().getElementById(child.id); if (target) { var fragment = new DocumentFragment() fragment.append(child); - swapOuterHTML(target, fragment); + settleTasks = settleTasks.concat(swapOuterHTML(target, fragment)); } else { child.parentNode.removeChild(child); triggerEvent(getDocument().body, "oobErrorNoTarget.hx", {id:child.id, content:child}) } } }) + return settleTasks; } function handleAttributes(parentNode, fragment) { @@ -179,15 +185,11 @@ var HTMx = HTMx || (function () { }); } }); - setTimeout(function () { - forEach(attributeSwaps, function (swap) { - swap.call(); - }); - }, 100); + return attributeSwaps; } function insertNodesBefore(parentNode, insertBefore, fragment) { - handleAttributes(parentNode, fragment); + var settleTasks = handleAttributes(parentNode, fragment); while(fragment.childNodes.length > 0){ var child = fragment.firstChild; parentNode.insertBefore(child, insertBefore); @@ -196,36 +198,45 @@ var HTMx = HTMx || (function () { processNode(child); } } + return settleTasks; } function swapOuterHTML(target, fragment) { if (target.tagName === "BODY") { - swapInnerHTML(target, fragment); + return swapInnerHTML(target, fragment); } else { - insertNodesBefore(parentElt(target), target, fragment); + var settleTasks = insertNodesBefore(parentElt(target), target, fragment); parentElt(target).removeChild(target); + return settleTasks; } } function swapPrepend(target, fragment) { - insertNodesBefore(target, target.firstChild, fragment); + return insertNodesBefore(target, target.firstChild, fragment); } function swapPrependBefore(target, fragment) { - insertNodesBefore(parentElt(target), target, fragment); + return insertNodesBefore(parentElt(target), target, fragment); } function swapAppend(target, fragment) { - insertNodesBefore(target, null, fragment); + return insertNodesBefore(target, null, fragment); } function swapAppendAfter(target, fragment) { - insertNodesBefore(parentElt(target), target.nextSibling, fragment); + return insertNodesBefore(parentElt(target), target.nextSibling, fragment); } function swapInnerHTML(target, fragment) { - target.innerHTML = ""; - insertNodesBefore(target, null, fragment); + var firstChild = target.firstChild; + var settleTasks = insertNodesBefore(target, firstChild, fragment); + if (firstChild) { + while (firstChild.nextSibling) { + target.removeChild(firstChild.nextSibling); + } + target.removeChild(firstChild); + } + return settleTasks; } function maybeSelectFromResponse(elt, fragment) { @@ -240,30 +251,20 @@ var HTMx = HTMx || (function () { return fragment; } - function swapResponse(target, elt, responseText, callBack) { - + function swapResponse(target, elt, responseText) { var fragment = makeFragment(responseText); - handleOutOfBandSwaps(fragment); + var settleTasks = handleOutOfBandSwaps(fragment); fragment = maybeSelectFromResponse(elt, fragment); var swapStyle = getClosestAttributeValue(elt, "hx-swap"); - if (swapStyle === "outerHTML") { - swapOuterHTML(target, fragment); - } else if (swapStyle === "prepend") { - swapPrepend(target, fragment); - } else if (swapStyle === "prependBefore") { - swapPrependBefore(target, fragment); - } else if (swapStyle === "append") { - swapAppend(target, fragment); - } else if (swapStyle === "appendAfter") { - swapAppendAfter(target, fragment); - } else { - swapInnerHTML(target, fragment); - } - - if(callBack) { - callBack.call(); + switch(swapStyle) { + case "outerHTML": return concat(settleTasks, swapOuterHTML(target, fragment)); + case "prepend": return concat(settleTasks, swapPrepend(target, fragment)); + case "prependBefore": return concat(settleTasks, swapPrependBefore(target, fragment)); + case "append": return concat(settleTasks, swapAppend(target, fragment)); + case "appendAfter": return concat(settleTasks, swapAppendAfter(target, fragment)); + default: return concat(settleTasks, swapInnerHTML(target, fragment)); } } @@ -604,7 +605,7 @@ var HTMx = HTMx || (function () { function bumpHistoryAccessDate(pathAndSearch) { var historyTimestamps = JSON.parse(localStorage.getItem("hx-history-timestamps")) || {}; - historyTimestamps[pathAndSearch] = Date.now; + historyTimestamps[pathAndSearch] = Date.now(); var paths = Object.keys(historyTimestamps); if (paths.length > 20) { purgeOldestPaths(paths, historyTimestamps); @@ -612,20 +613,23 @@ var HTMx = HTMx || (function () { localStorage.setItem("hx-history-timestamps", JSON.stringify(historyTimestamps)); } - function saveForHistory() { + function saveHistory() { var elt = getHistoryElement(); var pathAndSearch = location.pathname+location.search; - triggerEvent(getDocument().body, "historyUpdate.hx", {path:pathAndSearch}); + triggerEvent(getDocument().body, "historyUpdate.hx", {path:pathAndSearch, historyElement:elt}); history.replaceState({}, getDocument().title, window.location.href); - localStorage.setItem('hx-history-content-' + pathAndSearch, elt.innerHTML); + localStorage.setItem('hx-history:' + pathAndSearch, elt.innerHTML); bumpHistoryAccessDate(pathAndSearch); } - function initNewHistoryEntry(elt, url) { - if (shouldPush(elt)) { - history.pushState({}, "", url ); - saveForHistory(); - } + function pushUrlIntoHistory(url) { + history.pushState({}, "", url ); + } + + function settleImmediately(settleTasks) { + forEach(settleTasks, function (task) { + task.call(); + }); } function loadHistoryFromServer(pathAndSearch) { @@ -637,18 +641,18 @@ var HTMx = HTMx || (function () { if (this.status >= 200 && this.status < 400) { var fragment = makeFragment(this.response); fragment = fragment.querySelector('.hx-history-element') || fragment; - swapInnerHTML(getHistoryElement(), fragment); + settleImmediately(swapInnerHTML(getHistoryElement(), fragment)); } }; } function restoreHistory() { var pathAndSearch = location.pathname+location.search; - triggerEvent(getDocument().body, "historyUpdate.hx", {path:pathAndSearch}); - var content = localStorage.getItem('hx-history-content-' + pathAndSearch); + triggerEvent(getDocument().body, "historyRestore.hx", {path:pathAndSearch}); + var content = localStorage.getItem('hx-history:' + pathAndSearch); if (content) { bumpHistoryAccessDate(pathAndSearch); - swapInnerHTML(getHistoryElement(), makeFragment(content)); + settleImmediately(swapInnerHTML(getHistoryElement(), makeFragment(content))); } else { loadHistoryFromServer(pathAndSearch); } @@ -659,14 +663,6 @@ var HTMx = HTMx || (function () { (elt.tagName === "A" && getInternalData(elt).boosted); } - function snapshotForCurrentHistoryEntry(elt) { - if (shouldPush(elt)) { - // TODO event to allow de-initialization of HTML elements in target - saveForHistory(); - } - } - - function addRequestIndicatorClasses(elt) { mutateRequestIndicatorClasses(elt, "add"); } @@ -810,11 +806,14 @@ var HTMx = HTMx || (function () { if(!triggerEvent(elt, 'values.hx', {values: inputValues, target:target})) return endRequestLock(); // request type + var requestURL; if (verb === 'get') { var noValues = Object.keys(inputValues).length === 0; - xhr.open('GET', path + (noValues ? "" : "?" + urlEncode(inputValues)), true); + requestURL = path + (noValues ? "" : "?" + urlEncode(inputValues)); + xhr.open('GET', requestURL, true); } else { - xhr.open('POST', path, true); + requestURL = path; + xhr.open('POST', requestURL, true); setHeader(xhr,'Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8', true); if (verb !== 'post') { setHeader(xhr, 'X-HTTP-Method-Override', verb.toUpperCase(), true); @@ -847,29 +846,57 @@ var HTMx = HTMx || (function () { xhr.onload = function () { try { if (!triggerEvent(elt, 'beforeOnLoad.hx', {xhr: xhr, target: target})) return; - snapshotForCurrentHistoryEntry(elt, path); - var trigger = this.getResponseHeader("X-HX-Trigger"); - handleTrigger(elt, trigger); - initNewHistoryEntry(elt, path); + + handleTrigger(elt, this.getResponseHeader("X-HX-Trigger")); + var pushedUrl = this.getResponseHeader("X-HX-Push") + + var shouldSaveHistory = shouldPush(elt) || pushedUrl; + if (this.status >= 200 && this.status < 400) { // don't process 'No Content' response if (this.status !== 204) { // Success! var resp = this.response; if (!triggerEvent(elt, 'beforeSwap.hx', {xhr: xhr, target: target})) return; + + // Save current page + if (shouldSaveHistory) { + saveHistory(); + } + target.classList.add("hx-swapping"); var doSwap = function () { try { - swapResponse(target, elt, resp, function () { - target.classList.remove("hx-swapping"); - setTimeout(saveForHistory, 200); - triggerEvent(elt, 'afterSwap.hx', {xhr: xhr, target: target}); - }); + var settleTasks = swapResponse(target, elt, resp); + target.classList.remove("hx-swapping"); + target.classList.add("hx-settling"); + triggerEvent(elt, 'afterSwap.hx', {xhr: xhr, target: target}); + + var doSettle = function(){ + forEach(settleTasks, function (settleTask) { + settleTask.call(); + }); + target.classList.remove("hx-settling"); + // push URL and save new page + if (shouldSaveHistory) { + pushUrlIntoHistory(pushedUrl || requestURL ); + saveHistory(); + } + triggerEvent(elt, 'afterSettle.hx', {xhr: xhr, target: target}); + } + + var settleDelayStr = getAttributeValue(elt, "hx-settle-delay") || "100ms"; + if (settleDelayStr) { + setTimeout(doSettle, parseInterval(settleDelayStr)) + } else { + doSettle(); + } } catch (e) { triggerEvent(elt, 'swapError.hx', {xhr: xhr, response: xhr.response, status: xhr.status, target: target}); throw e; } }; + var swapDelayStr = getAttributeValue(elt, "hx-swap-delay"); if (swapDelayStr) { setTimeout(doSwap, parseInterval(swapDelayStr)) diff --git a/test/scratch.html b/test/scratch.html index 81c03932..5cd61d7e 100644 --- a/test/scratch.html +++ b/test/scratch.html @@ -24,11 +24,13 @@