diff --git a/dist/htmx.js b/dist/htmx.js index ba8ec346..0de78c93 100644 --- a/dist/htmx.js +++ b/dist/htmx.js @@ -123,7 +123,6 @@ var HTMx = HTMx || (function () { function getTarget(elt) { var explicitTarget = getClosestMatch(elt, function(e){return getRawAttribute(e,"hx-target") !== null}); - if (explicitTarget) { var targetStr = getRawAttribute(explicitTarget, "hx-target"); if (targetStr === "this") { @@ -141,59 +140,6 @@ var HTMx = HTMx || (function () { } } - function directSwap(child) { - var swapDirect = getAttributeValue(child, 'hx-swap-direct'); - if (swapDirect) { - var target = getDocument().getElementById(getRawAttribute(child,'id')); - if (target) { - if (swapDirect === "merge") { - mergeInto(target, child); - } else { - var newParent = parentElt(target); - newParent.insertBefore(child, target); - newParent.removeChild(target); - return true; - } - } - } - return false; - } - - function processResponseNodes(parentNode, insertBefore, text, executeAfter, selector) { - var fragment = makeFragment(text); - var nodesToProcess; - if (selector) { - nodesToProcess = toArray(fragment.querySelectorAll(selector)); - } else { - nodesToProcess = toArray(fragment.childNodes); - } - forEach(nodesToProcess, function(child){ - if (!directSwap(child)) { - parentNode.insertBefore(child, insertBefore); - } - if (child.nodeType !== Node.TEXT_NODE) { - triggerEvent(child, 'load.hx', {parent:parentElt(child)}); - processNode(child); - } - }); - if(executeAfter) { - executeAfter.call(); - } - } - - function findMatch(elt, possible) { - for (var i = 0; i < possible.length; i++) { - var candidate = possible[i]; - if (elt.hasAttribute("id") && elt.id === candidate.id) { - return candidate; - } - if (!candidate.hasAttribute("id") && elt.tagName === candidate.tagName) { - return candidate; - } - } - return null; - } - function cloneAttributes(mergeTo, mergeFrom) { forEach(mergeTo.attributes, function (attr) { if (!mergeFrom.hasAttribute(attr.name)) { @@ -205,58 +151,119 @@ var HTMx = HTMx || (function () { }); } - function mergeChildren(mergeTo, mergeFrom) { - var oldChildren = toArray(mergeTo.children); - var marker = getDocument().createElement("span"); - mergeTo.insertBefore(marker, mergeTo.firstChild); - forEach(mergeFrom.childNodes, function (newChild) { - var match = findMatch(newChild, oldChildren); - if (match) { - while (marker.nextSibling && marker.nextSibling !== match) { - mergeTo.removeChild(marker.nextSibling); + function handleOutOfBandSwaps(fragment) { + 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); + } else { + child.parentNode.removeChild(child); + triggerEvent(getDocument().body, "oobErrorNoTarget.hx", {id:child.id, content:child}) } - mergeTo.insertBefore(marker, match.nextSibling); - mergeInto(match, newChild); - } else { - mergeTo.insertBefore(newChild, marker); + } + }) + } + + function handleAttributes(parentNode, fragment) { + var attributeSwaps = []; + forEach(fragment.querySelectorAll("[id]"), function (newNode) { + var oldNode = parentNode.querySelector(newNode.tagName + "[id=" + newNode.id + "]") + if (oldNode) { + var newAttributes = newNode.cloneNode(); + cloneAttributes(newNode, oldNode); + attributeSwaps.push(function () { + cloneAttributes(newNode, newAttributes); + }); } }); - while (marker.nextSibling) { - mergeTo.removeChild(marker.nextSibling); + setTimeout(function () { + forEach(attributeSwaps, function (swap) { + swap.call(); + }); + }, 100); + } + + function insertNodesBefore(parentNode, insertBefore, fragment) { + handleAttributes(parentNode, fragment); + while(fragment.childNodes.length > 0){ + var child = fragment.firstChild; + parentNode.insertBefore(child, insertBefore); + if (child.nodeType !== Node.TEXT_NODE) { + triggerEvent(child, 'load.hx', {elt:child, parent:parentElt(child)}); + processNode(child); + } } - mergeTo.removeChild(marker); } - function mergeInto(mergeTo, mergeFrom) { - cloneAttributes(mergeTo, mergeFrom); - mergeChildren(mergeTo, mergeFrom); + function swapOuterHTML(target, fragment) { + if (target.tagName === "BODY") { + swapInnerHTML(target, fragment); + } else { + insertNodesBefore(parentElt(target), target, fragment); + parentElt(target).removeChild(target); + } } - function mergeResponse(target, resp, selector) { - var fragment = makeFragment(resp); - mergeInto(target, selector ? fragment.querySelector(selector) : fragment.firstElementChild); + function swapPrepend(target, fragment) { + insertNodesBefore(target, target.firstChild, fragment); } - function swapResponse(target, elt, resp, after) { + function swapPrependBefore(target, fragment) { + insertNodesBefore(parentElt(target), target, fragment); + } + + function swapAppend(target, fragment) { + insertNodesBefore(target, null, fragment); + } + + function swapAppendAfter(target, fragment) { + insertNodesBefore(parentElt(target), target.nextSibling, fragment); + } + + function swapInnerHTML(target, fragment) { + target.innerHTML = ""; + insertNodesBefore(target, null, fragment); + } + + function maybeSelectFromResponse(elt, fragment) { + var selector = getClosestAttributeValue(elt, "hx-select"); + if (selector) { + var newFragment = new DocumentFragment(); + forEach(fragment.querySelectorAll(selector), function (node) { + newFragment.append(node); + }); + fragment = newFragment; + } + return fragment; + } + + function swapResponse(target, elt, responseText, callBack) { + + var fragment = makeFragment(responseText); + handleOutOfBandSwaps(fragment); + + fragment = maybeSelectFromResponse(elt, fragment); var swapStyle = getClosestAttributeValue(elt, "hx-swap"); - var selector = getClosestAttributeValue(elt, "hx-select"); - if (swapStyle === "merge") { - mergeResponse(target, resp, selector); - } else if (swapStyle === "outerHTML") { - processResponseNodes(parentElt(target), target, resp, after, selector); - parentElt(target).removeChild(target); + if (swapStyle === "outerHTML") { + swapOuterHTML(target, fragment); } else if (swapStyle === "prepend") { - processResponseNodes(target, target.firstChild, resp, after, selector); + swapPrepend(target, fragment); } else if (swapStyle === "prependBefore") { - processResponseNodes(parentElt(target), target, resp, after, selector); + swapPrependBefore(target, fragment); } else if (swapStyle === "append") { - processResponseNodes(target, null, resp, after, selector); + swapAppend(target, fragment); } else if (swapStyle === "appendAfter") { - processResponseNodes(parentElt(target), target.nextSibling, resp, after, selector); + swapAppendAfter(target, fragment); } else { - target.innerHTML = ""; - processResponseNodes(target, null, resp, after, selector); + swapInnerHTML(target, fragment); + } + + if(callBack) { + callBack.call(); } } @@ -423,9 +430,12 @@ var HTMx = HTMx || (function () { } function initSSESource(elt, sseSrc) { - var config = {withCredentials: true}; - triggerEvent(elt, "initSSE.mx", config) - var source = new EventSource(sseSrc); + var details = { + initializer: function() { new EventSource(sseSrc, details.config) }, + config:{withCredentials: true} + }; + triggerEvent(elt, "initSSE.mx", {config:details}) + var source = details.initializer(); source.onerror = function (e) { triggerEvent(elt, "sseError.mx", {error:e, source:source}); maybeCloseSSESource(elt); @@ -433,51 +443,67 @@ var HTMx = HTMx || (function () { getInternalData(elt).sseSource = source; } + function processSSETrigger(sseEventName, elt, verb, path) { + var sseSource = getClosestMatch(elt, function (parent) { + return parent.sseSource; + }); + if (sseSource) { + var sseListener = function () { + if (!maybeCloseSSESource(sseSource)) { + if (bodyContains(elt)) { + issueAjaxRequest(elt, verb, path); + } else { + sseSource.sseSource.removeEventListener(sseEventName, sseListener); + } + } + }; + sseSource.sseSource.addEventListener(sseEventName, sseListener); + } else { + triggerEvent(elt, "noSSESourceError.mx") + } + } + + function loadImmediately(nodeData, elt, verb, path) { + if (!nodeData.loaded) { + nodeData.loaded = true; + issueAjaxRequest(elt, verb, path); + } + } + + function processVerbs(elt, nodeData, trigger) { + var explicitAction = false; + forEach(VERBS, function (verb) { + var path = getAttributeValue(elt, 'hx-' + verb); + if (path) { + explicitAction = true; + nodeData.path = path; + nodeData.verb = verb; + if (trigger.indexOf("sse:") === 0) { + processSSETrigger(trigger.substr(4), elt, verb, path); + } else if (trigger === 'revealed') { + initScrollHandler(); + maybeReveal(elt); + } else if (trigger === 'load') { + loadImmediately(nodeData, elt, verb, path); + } else if (trigger.trim().indexOf('every ') === 0) { + nodeData.polling = true; + processPolling(elt, verb, path); + } else { + addEventListener(elt, verb, path, nodeData, trigger); + } + } + }); + return explicitAction; + } + function processNode(elt) { var nodeData = getInternalData(elt); if (!nodeData.processed) { nodeData.processed = true; + var trigger = getTrigger(elt); - var explicitAction = false; - forEach(VERBS, function(verb){ - var path = getAttributeValue(elt, 'hx-' + verb); - if (path) { - nodeData.path = path; - nodeData.verb = verb; - explicitAction = true; - if (trigger.indexOf("sse:") === 0) { - var sseEventName = trigger.substr(4); - var sseSource = getClosestMatch(elt, function(parent) {return parent.sseSource;}); - if (sseSource) { - var sseListener = function () { - if (!maybeCloseSSESource(sseSource)) { - if (bodyContains(elt)) { - issueAjaxRequest(elt, verb, path); - } else { - sseSource.sseSource.removeEventListener(sseEventName, sseListener); - } - } - }; - sseSource.sseSource.addEventListener(sseEventName, sseListener); - } else { - triggerEvent(elt, "noSSESourceError.mx") - } - } if (trigger === 'revealed') { - initScrollHandler(); - maybeReveal(elt); - } else if (trigger === 'load') { - if (!nodeData.loaded) { - nodeData.loaded = true; - issueAjaxRequest(elt, verb, path); - } - } else if (trigger.trim().indexOf('every ') === 0) { - nodeData.polling = true; - processPolling(elt, verb, path); - } else { - addEventListener(elt, verb, path, nodeData, trigger); - } - } - }); + var explicitAction = processVerbs(elt, nodeData, trigger); + if (!explicitAction && getClosestAttributeValue(elt, "hx-boost") === "true") { boostElement(elt, nodeData, trigger); } @@ -612,8 +638,7 @@ var HTMx = HTMx || (function () { var historyKey = data['hx-history-key']; var content = localStorage.getItem('hx-history-' + historyKey); var elt = getHistoryElement(); - elt.innerHTML = ""; - processResponseNodes(elt, null, content); + swapInnerHTML(elt, makeFragment(content)); } function shouldPush(elt) { @@ -895,7 +920,7 @@ var HTMx = HTMx || (function () { return { processElement: processNode, on: addHTMxEventListener, - version: "0.0.1", + version: "0.0.2", _:internalEval } } diff --git a/dist/htmx.min.js b/dist/htmx.min.js index 79dc00d1..1f67df9d 100644 --- a/dist/htmx.min.js +++ b/dist/htmx.min.js @@ -1 +1 @@ -var HTMx=HTMx||function(){"use strict";var n=["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 s(e){return e.parentElement}function p(){return document}function l(e,t){if(t(e)){return e}else if(s(e)){return l(s(e),t)}else{return null}}function m(e,t){var r=null;l(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 a(e,t){do{if(e==null||u(e,t))return e}while(e=e&&s(e))}function f(e){var t=p().createRange();return t.createContextualFragment(e)}function t(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function o(e){return t(e,"Function")}function c(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 v(e){var t=[];y(e,function(e){t.push(e)});return t}function y(e,t){for(var r=0;r=0}function S(e){return p().body.contains(e)}function b(e){var t=l(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 w(e){var t=g(e,"hx-swap-direct");if(t){var r=p().getElementById(d(e,"id"));if(r){if(t==="merge"){C(r,e)}else{var n=s(r);n.insertBefore(e,r);n.removeChild(r);return true}}}return false}function E(t,r,e,n,i){var a=f(e);var o;if(i){o=v(a.querySelectorAll(i))}else{o=v(a.childNodes)}y(o,function(e){if(!w(e)){t.insertBefore(e,r)}if(e.nodeType!==Node.TEXT_NODE){J(e,"load.hx",{parent:s(e)});U(e)}});if(n){n.call()}}function T(e,t){for(var r=0;r0){var i=n.split(":");t=i[0];r=h(i[1])}else{t=n}setTimeout(function(){a.classList[o].call(a.classList,t)},r)})}function q(e,t,r){var n=M(e);var i=x(e);if(n.trim().indexOf("every ")===0){var a=n.split(/\s+/);var o=a[1];if(o){var s=h(o);i.timeout=setTimeout(function(){if(S(e)){le(e,t,r);q(e,t,g(e,"hx-"+t))}},s)}}}function I(e){return location.hostname===e.hostname&&d(e,"href")&&!d(e,"href").startsWith("#")}function R(e,t,r){if(e.tagName==="A"&&I(e)||e.tagName==="FORM"){t.boosted=true;var n,i;if(e.tagName==="A"){n="get";i=d(e,"href")}else{var a=d(e,"method");n=a?a.toLowerCase():"get";i=d(e,"action")}F(e,n,i,t,r,true)}}function F(a,o,s,e,t,u){var r=function(e){if(u)e.preventDefault();var t=x(e);var r=x(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(){le(a,o,s,e.target)};if(n){r.delayed=setTimeout(i,h(n))}else{i()}}};e.trigger=t;e.eventListener=r;a.addEventListener(t,r)}function B(){if(!window["hxScrollHandler"]){var e=function(){y(p().querySelectorAll("[hx-trigger='reveal']"),function(e){X(e)})};window["hxScrollHandler"]=e;window.addEventListener("scroll",e)}}function X(e){var t=x(e);if(!t.revealed&&r(e)){t.revealed=true;le(e,t.verb,t.path)}}function k(e){if(!S(e)){e.sseSource.close();return true}}function P(t,e){var r={withCredentials:true};J(t,"initSSE.mx",r);var n=new EventSource(e);n.onerror=function(e){J(t,"sseError.mx",{error:e,source:n});k(t)};x(t).sseSource=n}function U(a){var o=x(a);if(!o.processed){o.processed=true;var s=M(a);var u=false;y(n,function(e){var t=g(a,"hx-"+e);if(t){o.path=t;o.verb=e;u=true;if(s.indexOf("sse:")===0){var r=s.substr(4);var n=l(a,function(e){return e.sseSource});if(n){var i=function(){if(!k(n)){if(S(a)){le(a,e,t)}else{n.sseSource.removeEventListener(r,i)}}};n.sseSource.addEventListener(r,i)}else{J(a,"noSSESourceError.mx")}}if(s==="revealed"){B();X(a)}else if(s==="load"){if(!o.loaded){o.loaded=true;le(a,e,t)}}else if(s.trim().indexOf("every ")===0){o.polling=true;q(a,e,t)}else{F(a,e,t,o,s)}}});if(!u&&m(a,"hx-boost")==="true"){R(a,o,s)}var e=g(a,"hx-sse-source");if(e){P(a,e)}var t=g(a,"hx-add-class");if(t){N(a,t,"add")}var r=g(a,"hx-remove-class");if(r){N(a,r,"remove")}}y(a.children,function(e){U(e)})}function j(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 D(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 J(e,t,r){r["elt"]=e;var n=D(t,r);if(HTMx.logger){HTMx.logger(e,t,r);if(t.indexOf("Error")>0){j(e,t,r)}}var i=e.dispatchEvent(n);var a=e.dispatchEvent(D("all.hx",{elt:e,originalDetails:r,originalEvent:n}));return i&&a}function e(e,t,r){var n,i,a;if(o(e)){n=p().body;i="all.hx";a=e}else if(o(t)){n=p().body;i=e;a=t}else{n=e;i=t;a=r}return n.addEventListener(i,a)}function V(){return Math.random().toString(36).substr(3,9)}function _(){var e=p().getElementsByClassName("hx-history-element");if(e.length>0){return e[0]}else{return p().body}}function z(e){localStorage.setItem("hx-history",JSON.stringify(e))}function G(){var e=localStorage.getItem("hx-history");var t;if(e){t=JSON.parse(e)}else{var r=V();t={current:r,slots:[r]};z(t)}return t}function W(){var e=G();var t=V();var r=e.slots;if(r.length>20){var n=r.shift();localStorage.removeItem("hx-history-"+n)}r.push(t);e.current=t;z(e)}function K(){var e=_();var t=G();history.replaceState({"hx-history-key":t.current},p().title,window.location.href);localStorage.setItem("hx-history-"+t.current,e.innerHTML)}function Q(e){var t=e["hx-history-key"];var r=localStorage.getItem("hx-history-"+t);var n=_();n.innerHTML="";E(n,null,r)}function Y(e){return m(e,"hx-push-url")==="true"||e.tagName==="A"&&x(e).boosted}function Z(e){if(Y(e)){K()}}function $(e,t){if(Y(e)){W();history.pushState({},"",t);K()}}function ee(e){re(e,"add")}function te(e){re(e,"remove")}function re(e,t){var r=m(e,"hx-indicator");if(r){var n=p().querySelectorAll(r)}else{n=[e]}y(n,function(e){e.classList[t].call(e.classList,"hx-show-indicator")})}function ne(e,t){for(var r=0;r=200&&this.status<400){if(this.status!==204){var t=this.response;if(!J(i,"beforeSwap.hx",{xhr:f,target:s}))return;s.classList.add("hx-swapping");var r=function(){try{A(s,i,t,function(){s.classList.remove("hx-swapping");K();J(i,"afterSwap.hx",{xhr:f,target:s})})}catch(e){J(i,"swapError.hx",{xhr:f,response:f.response,status:f.status,target:s});throw e}};var n=g(i,"hx-swap-delay");if(n){setTimeout(r,h(n))}else{r()}}}else{J(i,"responseError.hx",{xhr:f,response:f.response,status:f.status,target:s})}}catch(e){J(i,"onLoadError.hx",{xhr:f,response:f.response,status:f.status,target:s});throw e}finally{te(i);o();J(i,"afterOnLoad.hx",{xhr:f,response:f.response,status:f.status,target:s})}};f.onerror=function(){te(i);J(i,"loadError.hx",{xhr:f});o()};if(!J(i,"beforeRequest.hx",{xhr:f,values:c,target:s}))return o();ee(i);f.send(e==="get"?null:se(c))}function fe(e){if(p().readyState!=="loading"){e()}else{p().addEventListener("DOMContentLoaded",e)}}fe(function(){U(p().body);window.onpopstate=function(e){Q(e.state)}});function ce(e){return eval(e)}return{processElement:U,on:e,version:"0.0.1",_:ce}}(); \ No newline at end of file +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 diff --git a/dist/htmx.min.js.gz b/dist/htmx.min.js.gz new file mode 100644 index 00000000..85b2e4f9 Binary files /dev/null and b/dist/htmx.min.js.gz differ diff --git a/package.json b/package.json index 9a2c1952..39a16bdd 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "unpkg": "dist/htmx.min.js", "scripts": { "test": "mocha-chrome test/index.html", - "dist": "cp src/htmx.js dist/ && npm run-script uglify && exit", + "dist": "cp src/htmx.js dist/ && npm run-script uglify && gzip -k -f dist/htmx.min.js > dist/htmx.min.js.gz && exit", "www": "node scripts/www.js", "uglify": "uglifyjs -m eval -o dist/htmx.min.js dist/htmx.js" }, diff --git a/src/htmx.js b/src/htmx.js index 92c1ddad..52b28c1e 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -123,7 +123,6 @@ var HTMx = HTMx || (function () { function getTarget(elt) { var explicitTarget = getClosestMatch(elt, function(e){return getRawAttribute(e,"hx-target") !== null}); - if (explicitTarget) { var targetStr = getRawAttribute(explicitTarget, "hx-target"); if (targetStr === "this") { @@ -141,59 +140,6 @@ var HTMx = HTMx || (function () { } } - function directSwap(child) { - var swapDirect = getAttributeValue(child, 'hx-swap-direct'); - if (swapDirect) { - var target = getDocument().getElementById(getRawAttribute(child,'id')); - if (target) { - if (swapDirect === "merge") { - mergeInto(target, child); - } else { - var newParent = parentElt(target); - newParent.insertBefore(child, target); - newParent.removeChild(target); - return true; - } - } - } - return false; - } - - function processResponseNodes(parentNode, insertBefore, text, executeAfter, selector) { - var fragment = makeFragment(text); - var nodesToProcess; - if (selector) { - nodesToProcess = toArray(fragment.querySelectorAll(selector)); - } else { - nodesToProcess = toArray(fragment.childNodes); - } - forEach(nodesToProcess, function(child){ - if (!directSwap(child)) { - parentNode.insertBefore(child, insertBefore); - } - if (child.nodeType !== Node.TEXT_NODE) { - triggerEvent(child, 'load.hx', {parent:parentElt(child)}); - processNode(child); - } - }); - if(executeAfter) { - executeAfter.call(); - } - } - - function findMatch(elt, possible) { - for (var i = 0; i < possible.length; i++) { - var candidate = possible[i]; - if (elt.hasAttribute("id") && elt.id === candidate.id) { - return candidate; - } - if (!candidate.hasAttribute("id") && elt.tagName === candidate.tagName) { - return candidate; - } - } - return null; - } - function cloneAttributes(mergeTo, mergeFrom) { forEach(mergeTo.attributes, function (attr) { if (!mergeFrom.hasAttribute(attr.name)) { @@ -205,58 +151,119 @@ var HTMx = HTMx || (function () { }); } - function mergeChildren(mergeTo, mergeFrom) { - var oldChildren = toArray(mergeTo.children); - var marker = getDocument().createElement("span"); - mergeTo.insertBefore(marker, mergeTo.firstChild); - forEach(mergeFrom.childNodes, function (newChild) { - var match = findMatch(newChild, oldChildren); - if (match) { - while (marker.nextSibling && marker.nextSibling !== match) { - mergeTo.removeChild(marker.nextSibling); + function handleOutOfBandSwaps(fragment) { + 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); + } else { + child.parentNode.removeChild(child); + triggerEvent(getDocument().body, "oobErrorNoTarget.hx", {id:child.id, content:child}) } - mergeTo.insertBefore(marker, match.nextSibling); - mergeInto(match, newChild); - } else { - mergeTo.insertBefore(newChild, marker); + } + }) + } + + function handleAttributes(parentNode, fragment) { + var attributeSwaps = []; + forEach(fragment.querySelectorAll("[id]"), function (newNode) { + var oldNode = parentNode.querySelector(newNode.tagName + "[id=" + newNode.id + "]") + if (oldNode) { + var newAttributes = newNode.cloneNode(); + cloneAttributes(newNode, oldNode); + attributeSwaps.push(function () { + cloneAttributes(newNode, newAttributes); + }); } }); - while (marker.nextSibling) { - mergeTo.removeChild(marker.nextSibling); + setTimeout(function () { + forEach(attributeSwaps, function (swap) { + swap.call(); + }); + }, 100); + } + + function insertNodesBefore(parentNode, insertBefore, fragment) { + handleAttributes(parentNode, fragment); + while(fragment.childNodes.length > 0){ + var child = fragment.firstChild; + parentNode.insertBefore(child, insertBefore); + if (child.nodeType !== Node.TEXT_NODE) { + triggerEvent(child, 'load.hx', {elt:child, parent:parentElt(child)}); + processNode(child); + } } - mergeTo.removeChild(marker); } - function mergeInto(mergeTo, mergeFrom) { - cloneAttributes(mergeTo, mergeFrom); - mergeChildren(mergeTo, mergeFrom); + function swapOuterHTML(target, fragment) { + if (target.tagName === "BODY") { + swapInnerHTML(target, fragment); + } else { + insertNodesBefore(parentElt(target), target, fragment); + parentElt(target).removeChild(target); + } } - function mergeResponse(target, resp, selector) { - var fragment = makeFragment(resp); - mergeInto(target, selector ? fragment.querySelector(selector) : fragment.firstElementChild); + function swapPrepend(target, fragment) { + insertNodesBefore(target, target.firstChild, fragment); } - function swapResponse(target, elt, resp, after) { + function swapPrependBefore(target, fragment) { + insertNodesBefore(parentElt(target), target, fragment); + } + + function swapAppend(target, fragment) { + insertNodesBefore(target, null, fragment); + } + + function swapAppendAfter(target, fragment) { + insertNodesBefore(parentElt(target), target.nextSibling, fragment); + } + + function swapInnerHTML(target, fragment) { + target.innerHTML = ""; + insertNodesBefore(target, null, fragment); + } + + function maybeSelectFromResponse(elt, fragment) { + var selector = getClosestAttributeValue(elt, "hx-select"); + if (selector) { + var newFragment = new DocumentFragment(); + forEach(fragment.querySelectorAll(selector), function (node) { + newFragment.append(node); + }); + fragment = newFragment; + } + return fragment; + } + + function swapResponse(target, elt, responseText, callBack) { + + var fragment = makeFragment(responseText); + handleOutOfBandSwaps(fragment); + + fragment = maybeSelectFromResponse(elt, fragment); var swapStyle = getClosestAttributeValue(elt, "hx-swap"); - var selector = getClosestAttributeValue(elt, "hx-select"); - if (swapStyle === "merge") { - mergeResponse(target, resp, selector); - } else if (swapStyle === "outerHTML") { - processResponseNodes(parentElt(target), target, resp, after, selector); - parentElt(target).removeChild(target); + if (swapStyle === "outerHTML") { + swapOuterHTML(target, fragment); } else if (swapStyle === "prepend") { - processResponseNodes(target, target.firstChild, resp, after, selector); + swapPrepend(target, fragment); } else if (swapStyle === "prependBefore") { - processResponseNodes(parentElt(target), target, resp, after, selector); + swapPrependBefore(target, fragment); } else if (swapStyle === "append") { - processResponseNodes(target, null, resp, after, selector); + swapAppend(target, fragment); } else if (swapStyle === "appendAfter") { - processResponseNodes(parentElt(target), target.nextSibling, resp, after, selector); + swapAppendAfter(target, fragment); } else { - target.innerHTML = ""; - processResponseNodes(target, null, resp, after, selector); + swapInnerHTML(target, fragment); + } + + if(callBack) { + callBack.call(); } } @@ -436,51 +443,67 @@ var HTMx = HTMx || (function () { getInternalData(elt).sseSource = source; } + function processSSETrigger(sseEventName, elt, verb, path) { + var sseSource = getClosestMatch(elt, function (parent) { + return parent.sseSource; + }); + if (sseSource) { + var sseListener = function () { + if (!maybeCloseSSESource(sseSource)) { + if (bodyContains(elt)) { + issueAjaxRequest(elt, verb, path); + } else { + sseSource.sseSource.removeEventListener(sseEventName, sseListener); + } + } + }; + sseSource.sseSource.addEventListener(sseEventName, sseListener); + } else { + triggerEvent(elt, "noSSESourceError.mx") + } + } + + function loadImmediately(nodeData, elt, verb, path) { + if (!nodeData.loaded) { + nodeData.loaded = true; + issueAjaxRequest(elt, verb, path); + } + } + + function processVerbs(elt, nodeData, trigger) { + var explicitAction = false; + forEach(VERBS, function (verb) { + var path = getAttributeValue(elt, 'hx-' + verb); + if (path) { + explicitAction = true; + nodeData.path = path; + nodeData.verb = verb; + if (trigger.indexOf("sse:") === 0) { + processSSETrigger(trigger.substr(4), elt, verb, path); + } else if (trigger === 'revealed') { + initScrollHandler(); + maybeReveal(elt); + } else if (trigger === 'load') { + loadImmediately(nodeData, elt, verb, path); + } else if (trigger.trim().indexOf('every ') === 0) { + nodeData.polling = true; + processPolling(elt, verb, path); + } else { + addEventListener(elt, verb, path, nodeData, trigger); + } + } + }); + return explicitAction; + } + function processNode(elt) { var nodeData = getInternalData(elt); if (!nodeData.processed) { nodeData.processed = true; + var trigger = getTrigger(elt); - var explicitAction = false; - forEach(VERBS, function(verb){ - var path = getAttributeValue(elt, 'hx-' + verb); - if (path) { - nodeData.path = path; - nodeData.verb = verb; - explicitAction = true; - if (trigger.indexOf("sse:") === 0) { - var sseEventName = trigger.substr(4); - var sseSource = getClosestMatch(elt, function(parent) {return parent.sseSource;}); - if (sseSource) { - var sseListener = function () { - if (!maybeCloseSSESource(sseSource)) { - if (bodyContains(elt)) { - issueAjaxRequest(elt, verb, path); - } else { - sseSource.sseSource.removeEventListener(sseEventName, sseListener); - } - } - }; - sseSource.sseSource.addEventListener(sseEventName, sseListener); - } else { - triggerEvent(elt, "noSSESourceError.mx") - } - } if (trigger === 'revealed') { - initScrollHandler(); - maybeReveal(elt); - } else if (trigger === 'load') { - if (!nodeData.loaded) { - nodeData.loaded = true; - issueAjaxRequest(elt, verb, path); - } - } else if (trigger.trim().indexOf('every ') === 0) { - nodeData.polling = true; - processPolling(elt, verb, path); - } else { - addEventListener(elt, verb, path, nodeData, trigger); - } - } - }); + var explicitAction = processVerbs(elt, nodeData, trigger); + if (!explicitAction && getClosestAttributeValue(elt, "hx-boost") === "true") { boostElement(elt, nodeData, trigger); } @@ -575,10 +598,11 @@ var HTMx = HTMx || (function () { } function saveLocalHistoryData(historyData) { + triggerEvent(getDocument().body, "historySave.hx", {data:historyData}); localStorage.setItem('hx-history', JSON.stringify(historyData)); } - function getLocalHistoryData() { + function getHistoryMetadata() { var historyEntry = localStorage.getItem('hx-history'); var historyData; if (historyEntry) { @@ -592,9 +616,10 @@ var HTMx = HTMx || (function () { } function newHistoryData() { - var historyData = getLocalHistoryData(); + var historyData = getHistoryMetadata(); var newId = makeHistoryId(); var slots = historyData.slots; + triggerEvent(getDocument().body, "historyNew.hx", {data:historyData}); if (slots.length > 20) { var toEvict = slots.shift(); localStorage.removeItem('hx-history-' + toEvict); @@ -606,17 +631,19 @@ var HTMx = HTMx || (function () { function updateCurrentHistoryContent() { var elt = getHistoryElement(); - var historyData = getLocalHistoryData(); + var historyData = getHistoryMetadata(); + triggerEvent(getDocument().body, "historyUpdate.hx", {data:historyData}); history.replaceState({"hx-history-key": historyData.current}, getDocument().title, window.location.href); localStorage.setItem('hx-history-' + historyData.current, elt.innerHTML); } function restoreHistory(data) { + updateCurrentHistoryContent(); var historyKey = data['hx-history-key']; + triggerEvent(getDocument().body, "historyUpdate.hx", {data:historyKey}); var content = localStorage.getItem('hx-history-' + historyKey); var elt = getHistoryElement(); - elt.innerHTML = ""; - processResponseNodes(elt, null, content); + swapInnerHTML(elt, makeFragment(content)); } function shouldPush(elt) { diff --git a/test/index.html b/test/index.html index 3ef57bb6..2246088a 100644 --- a/test/index.html +++ b/test/index.html @@ -25,7 +25,7 @@ - + diff --git a/test/swap_direct.js b/test/oob.js similarity index 85% rename from test/swap_direct.js rename to test/oob.js index acc66c7a..e9c2c035 100644 --- a/test/swap_direct.js +++ b/test/oob.js @@ -9,7 +9,7 @@ describe("HTMx Direct Swap", function () { }); it('handles basic response properly', function () { - this.server.respondWith("GET", "/test", "Clicked
Swapped
"); + this.server.respondWith("GET", "/test", "Clicked
Swapped
"); var div = make('
click me
'); make('
'); div.click(); @@ -19,11 +19,11 @@ describe("HTMx Direct Swap", function () { }) it('handles no id match properly', function () { - this.server.respondWith("GET", "/test", "Clicked
Swapped
"); + this.server.respondWith("GET", "/test", "Clicked
Swapped
"); var div = make('
click me
'); div.click(); this.server.respond(); - div.innerText.should.equal("Clicked\nSwapped"); + div.innerText.should.equal("Clicked"); }) diff --git a/test/scratch.html b/test/scratch.html index 33f53a8f..81c03932 100644 --- a/test/scratch.html +++ b/test/scratch.html @@ -1,23 +1,18 @@ @@ -29,9 +24,11 @@ @@ -45,6 +42,7 @@ Autorespond: + diff --git a/www/_includes/layout.html b/www/_includes/layout.html deleted file mode 100644 index d5a3afdf..00000000 --- a/www/_includes/layout.html +++ /dev/null @@ -1,36 +0,0 @@ - - - HTMx - Teaching HTML new tricks - - - - - -

</> HTMx

- -
-{{ content | safe }} -
- - \ No newline at end of file diff --git a/www/_includes/layout.njk b/www/_includes/layout.njk new file mode 100644 index 00000000..f76fd4ec --- /dev/null +++ b/www/_includes/layout.njk @@ -0,0 +1,38 @@ + + + HTMx - Teaching HTML new tricks + + + + + +
+
+

</> HTMx

+
+
+ home +
+
+ docs +
+ +
+ events +
+
+ headers +
+
+ github +
+
+
+
+ {{ content | safe }} +
+
+ + \ No newline at end of file diff --git a/www/css/site.css b/www/css/site.css index fd695980..11f5c599 100644 --- a/www/css/site.css +++ b/www/css/site.css @@ -1,30 +1,53 @@ body { - margin: 40px auto; - max-width: 740px; + margin: 0px; line-height: 1.6; font-size: 18px; color: #333; - padding: 0 10px; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", + "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif !important; } -h1, h2, h3 { - line-height: 1.2 -} -h1 { -} -.flair { + +h2 { + border-bottom: 2px solid whitesmoke; color: rgb(52, 101, 164); } +#content { + margin-top: 32px +} + +p { + margin: 16px; +} + +pre[class*="language-"] { + font-size: 16px; + margin-top: 24px !important; + margin-bottom: 24px !important; + margin-left: 48px !important; + margin-right: 48px !important; +} + +blockquote { + border-left: 4px solid whitesmoke; + margin-left: 32px; + margin-right: 32px; +} + .hero { text-align: center; - font-size: 4em; + font-size: 5em; + margin: 0; + line-height: 1em; +} + +.root .hero { opacity: 5%; position: relative; top: -20px; } -.hero.settle { +.root .hero.settle { top: 0px; opacity: 100%; transition: 500ms ease-in; @@ -32,16 +55,125 @@ h1 { .nav { margin: 12px; - position: absolute; + /*position: absolute;*/ top: 180px; left: 10px; } a { text-decoration: none; + color:rgb(52, 101, 164) +} + +.center { + text-align: center; } .nav ul { - padding-left: 12px; list-style: none; + padding-left: 12px; +} + +/* customized version of lit.css */ +* + *{ + box-sizing: border-box; + margin: .5em 0; +} + +@media(max-width:45em) { + .nav { + text-align: center; + } + .nav ul { + padding: 0; + font-size: 22px; + } + .nav li { + + } +} + +@media(min-width:45em) { + .col { + display: table-cell; + } + .\31 { + width: 5%; + } + .\33 { + width: 22%; + } + .\34 { + width: 30%; + } + .\35 { + width: 40%; + } + .\32 { + width: 15%; + } + .row { + display: table; + border-spacing: 1em 0; + } +} + +.w-100, +.row { + width: 100%; +} + +.card:focus { + outline: 0; + border: solid rgb(52, 101, 164); +} + +hr { + outline: 0; +} + +.card { + padding: 1em; + border: solid #eee; +} + +a[href]:hover, .btn:hover { + opacity: .6; +} + +.c { + max-width: 55em; + padding: 1em; + margin: auto; +} + +.btn.primary { + color: white; + background: rgb(52, 101, 164); + border: solid rgb(52, 101, 164); +} + +td { + padding: 1em; + text-align: left; + border-bottom: solid #eee; +} + +th { + padding: 1em; + text-align: left; + border-bottom: solid #eee; +} + +.btn { + padding: 1em; + letter-spacing: .1em; + text-transform: uppercase; + background: white; + border: solid; + cursor:pointer; +} + +pre { + overflow: auto; } \ No newline at end of file diff --git a/www/docs.md b/www/docs.md new file mode 100644 index 00000000..72645c44 --- /dev/null +++ b/www/docs.md @@ -0,0 +1,244 @@ +--- +layout: layout.njk +title: HTMx - HTML Extensions +--- +
+ +
+ +## [HTMx in a Nutshell](#introduction) +HTMx is a set of attributes in HTML that allow you to access modern browser features directly +from the browser. To understand how HTMx works, first lets take a look at an anchor tag: + +``` html + Blog +``` + +This anchor tag tells a browser: +targest +> "When a user clicks on this link, issue an HTTP GET request to '/blog' and load the response content +> into the browser window". + +Now consider some HTMx code: + +``` html + Click Me! +``` + +This tells a browser: + +> "When a user clicks on this link, issue an HTTP GET request to '/blog' and load the response content into the inner +> html of this element" + +So the difference is that with HTMx: + +* A different HTTP action is used +* The request is done via AJAX +* The response replaces the content of the element, rather than the entire page + +HTMx expects responses to the AJAX calls that it makes to be *HTML* rather than *JSON*, as is more typical with AJAX +requests. + +If you would prefer, you can use the `data-` prefix when using HTMx: + +``` html + Click Me! +``` + +## [Installing](#installing) + +Intercooler is a dependency-free library, written in javascript. + +It can be loaded via NPM as "`htmx.org`" or included from [unpkg](https://unpkg.com/browse/htmx.org/): + +``` html + +``` + +## [AJAX](#ajax) + +HTMx provides attributes to allow you to issue AJAX requests directly from HTML. The main attributes are: + +* [hx-get](/attributes/hx-get) - Issues a `GET` request to the given URL +* [hx-post](/attributes/hx-post) - Issues a `POST` request to the given URL +* [hx-put](/attributes/hx-put) - Issues a `PUT` request to the given URL (see [details](#htmx-request-details)) +* [hx-patch](/attributes/hx-patch) - Issues a `PATCH` request to the given URL (see [details](#htmx-request-details)) +* [hx-delete](/attributes/hx-delete) - Issues a `GET` request to the given URL (see [details](#htmx-request-details)) + +Each of these attributes takes a URL to issue an AJAX request to. The element will issue a request of the specified +type to the given URL when the element is triggered. + +### [Triggering Requests](#triggers) + +By default, elements issue a request on the "natural" event: + +* `input`, `textarea` & `select`: the `change` event +* `form`: the `submit` event +* everything else: the `click` event + +You might not want to use the default event. In this case you can use the [hx-trigger](/attributes/hx-trigger) +attribute to specify the event you want the element to respond to. Here is a `div` that posts to `/mouse_entered` +when a mouse enters it: + +```html +
+ [Here Mouse, Mouse!] +
+``` + +If you want a request to only happen once, you can use the [hx-trigger-once](/attributes/hx-trigger-once) attribute: + +```html +
+ [Here Mouse, Mouse!] +
+``` + +If the element is an input, and you only want the request to happen when the value changes, you can use the +[hx-trigger-changed-only](/attributes/hx-trigger-changed-only) attribute. + +This can be paired with the [hx-trigger-delay](/attributes/hx-trigger-delay) attribute to implement a common UX +pattern, [Live Search](/demo/live-search): + +```html + +
+``` + +This input will issue a request 500 milliseconds after a key up event if the input has been changed and puts the results +into the `div#search-results`. + +#### [Special Events](#special-events) + +HTMx provides a few special events for use in [hx-trigger](/attributes/hx-trigger): + +* `load` - fires once when the element is first loaded +* `revealed` - fires once when an element first scrolls into the viewport + +You can also use custom events to trigger requests if you have an advanced use case. + +### [Targets](#targets) + +If you want the response to be loaded into a different element other than the one that made the request, you can +use the [hx-target](/attributes/hx-target) attribute, which takes a CSS selector. Looking back at our Live Search example: + +```html + +
+``` + +You can see that the results from the search are going to be loaded into `div#search-results`. + +### [Forms & Input Values](#forms) + +By default, an element will include its value if it has one. Additionally, if the element is in a form, all values +in the form will be included in the response. + +If you wish to include the values of other elements, you can use the [hx-include](/attributes/hx-include) attribute +with a CSS selector of all the elements whose values you want to include in the request. + +Finally, if you want to programatically modify the arguments, you can use the [values.hx](/events/values.hx) event to +do so. + +### [Swapping](#swapping) + +HTMx offers a few different ways to swap the HTML returned into the DOM. By default, the content replaces the +`innerHTML` of the target element. You can modify this by using the [hx-swap](/attributes/hx-swap) attribute +with any of the following values: + +* `innerHTML` - the default, puts the content inside the target element +* `outerHTML` - replaces the target element with the returned content +* `prepend` - prepends the content before the first child inside the target +* `prependBefore` - prepends the content before the target in the targets parent element +* `append` - appends the content after the last child inside the target +* `appendAfter` - appends the content after the target in the targets parent element +* `merge` - attempts to merge the response content into the target, reusing matching elements in the existing DOM + +#### Out of Band Swaps + +If you want to swap content from a response directly into the DOM by using the `id` attribute you can use the +[hx-swap-directly](/attributes/hx-swap-directly) attribute in the *response* html: + +```html +
Swap me directly!
+ Additional Content +``` + +In this response, `div#message` would be swapped directly into the matching DOM element, while the additional content +would be swapped into the target in the normal manner. + +You can use this technique to "piggy-back" updates on other requests. + +If you want the out of band content merged you can use the value `merge` for this attribute. + +#### Selecting Content To Swap + +If you want to select a subset of the response HTML to swap into the target, you can use the [hx-select](/attributes/hx-select) +attribute, which takes a CSS selector and selects the matching elements from the response. + +## [History Support](#history) + +HTMx provides a simple mechanism for interacting with the [browser history API](https://developer.mozilla.org/en-US/docs/Web/API/History_API): + +If you want a given element to push its request into the browser navigation bar and add the current state of the page +to the browsers history, include the [hx-push](/attributes/hx-push) attribute: + +```html + Blog +``` + +When a user clicks on this link, HTMx will snapshot the current DOM and store it before it makes a request to /blog. +It then does the swap and pushes a new location onto the history stack. + +When a user hits the back button, HTMx will retrieve the old content from storage and swap it back into the target, + simulating "going back" to the previous state. + +### Specifying History Snapshot Element + +By default, HTMx will use the `body` to take and restore the history snapshop from. This is usually good enough but +if you want to use a narrower element for snapshotting you can use the [hx-history-element](/attributes/hx-history-element) +attribute to specify a different one. Careful: this element will need to be on all pages or restoring from history +won't work reliably. + +## [Requests & Responses](#requests) + +## Miscellaneous Attributes + +### Class Swapping + +### Timed Removal + +### Boosting + +## [Events & Logging](#events) + +
+
\ No newline at end of file diff --git a/www/docs/attributes.md b/www/docs/attributes.md index dd440954..47d78d98 100644 --- a/www/docs/attributes.md +++ b/www/docs/attributes.md @@ -1,5 +1,5 @@ --- -layout: layout.html +layout: layout.njk title: HTMx - HTML Extensions / Attributes --- diff --git a/www/docs/events.md b/www/docs/events.md index 7d0f6167..c6adbbbc 100644 --- a/www/docs/events.md +++ b/www/docs/events.md @@ -1,5 +1,5 @@ --- -layout: layout.html +layout: layout.njk title: HTMx - HTML Extensions / Attributes --- diff --git a/www/docs/headers.md b/www/docs/headers.md index 2906ed32..a52801e1 100644 --- a/www/docs/headers.md +++ b/www/docs/headers.md @@ -1,5 +1,5 @@ --- -layout: layout.html +layout: layout.njk title: HTMx - HTML Extensions / Attributes --- diff --git a/www/index.md b/www/index.md index 642a4d94..e1f3810a 100644 --- a/www/index.md +++ b/www/index.md @@ -1,22 +1,29 @@ --- -layout: layout.html +layout: layout.njk title: HTMx - HTML Extensions --- -HTMx is a set of extensions to HTML that bring many of the useful features of modern web browsers directly -into HTML. It fills gaps in functionality found in standard HTML, dramatically expanding its expressiveness while -retaining the fundamental simplicity of declarative hypertext.

+## Introduction -Here is a simple example of HTMx in action: +HTMx is a small (<12Kb) & dependency-free library that surfaces the features of modern browsers using HTML +attributes. Using HTMx you can implement many [UX patterns](/demo) that would typically require writing javascript. + +HTMx is unobtrusive, plays well with other tools, can be adopted incrementally with no up-front rewrites. + +## Quick Start ``` html - + + + + + ``` -This example issues an AJAX request to /example when a user clicks on it, and swaps the response -HTML into the element with the id `myDiv` +This code tells HTMx that: + +> "When a user clicks on this button, issue an AJAX request to /example, and load the content into the body +> of the button" + +HTMx is based on [intercooler.js](http://intercoolerjs.org) and is the successor to that project. -HTMx is based on [intercooler.js](http://intercoolerjs.org), and aims to be a minimalist & -dependency free successor to that project. diff --git a/www/js/htmx.js b/www/js/htmx.js index 7b3a7a60..7adfda52 100644 --- a/www/js/htmx.js +++ b/www/js/htmx.js @@ -1,233 +1,905 @@ -var HTMx = HTMx || (function() -{ - 'use strict'; +// noinspection JSUnusedAssignment +var HTMx = HTMx || (function () { + 'use strict'; - function parseInterval(str) { - if (str === "null" || str === "false" || str === "") { - return null; - } else if (str.lastIndexOf("ms") === str.length - 2) { - return parseFloat(str.substr(0, str.length - 2)); - } else if (str.lastIndexOf("s") === str.length - 1) { - return parseFloat(str.substr(0, str.length - 1)) * 1000; - } else { - return 1000; - } - } + var VERBS = ['get', 'post', 'put', 'delete', 'patch'] - // resolve with both hx and data-hx prefixes - function getAttributeValue(elt, qualifiedName) { - return elt.getAttribute(qualifiedName) || elt.getAttribute("data-" + qualifiedName); - } + //==================================================================== + // Utilities + //==================================================================== - function getClosestAttributeValue(elt, attributeName) - { - var attribute = getAttributeValue(elt, attributeName); - if(attribute) - { - return attribute; - } - else if (elt.parentElement) - { - return getClosestAttributeValue(elt.parentElement, attributeName); - } - else - { - return null; - } - } - - function getTarget(elt) { - var targetVal = getClosestAttributeValue(elt, "hx-target"); - if (targetVal) { - return document.querySelector(targetVal); - } else { - return elt; - } - } - - function makeFragment(resp) { - var range = document.createRange(); - return range.createContextualFragment(resp); - } - - function processResponseNodes(parent, target, text) { - var fragment = makeFragment(text); - for (var i = fragment.childNodes.length - 1; i >= 0; i--) { - var child = fragment.childNodes[i]; - parent.insertBefore(child, target); - if (child.nodeType != Node.TEXT_NODE) { - processElement(child); + function parseInterval(str) { + if (str === "null" || str === "false" || str === "") { + return null; + } else if (str.lastIndexOf("ms") === str.length - 2) { + return parseFloat(str.substr(0, str.length - 2)); + } else if (str.lastIndexOf("s") === str.length - 1) { + return parseFloat(str.substr(0, str.length - 1)) * 1000; + } else { + return parseFloat(str); } } - } - function swapResponse(elt, resp) { - var target = getTarget(elt); - var swapStyle = getClosestAttributeValue(elt, "hx-swap"); - if (swapStyle === "outerHTML") { - processResponseNodes(target.parentElement, target, resp); - target.parentElement.removeChild(target); - } else if (swapStyle === "prepend") { - processResponseNodes(target, target.firstChild, resp); - } else if (swapStyle === "prependBefore") { - processResponseNodes(target.parentElement, target, resp); - } else if (swapStyle === "append") { - processResponseNodes(target, null, resp); - } else if (swapStyle === "appendAfter") { - processResponseNodes(target.parentElement, target.nextSibling, resp); - } else { - target.innerHTML = ""; - processResponseNodes(target, null, resp); + function getRawAttribute(elt, name) { + return elt.getAttribute && elt.getAttribute(name); } - } - function triggerEvent(elt, eventName, details) { - details["elt"] = elt; - if (window.CustomEvent && typeof window.CustomEvent === 'function') { - var event = new CustomEvent(eventName, {detail: details}); - } else { - var event = document.createEvent('CustomEvent'); - event.initCustomEvent(eventName, true, true, details); + // resolve with both hx and data-hx prefixes + function getAttributeValue(elt, qualifiedName) { + return getRawAttribute(elt, qualifiedName) || getRawAttribute(elt, "data-" + qualifiedName); } - elt.dispatchEvent(event); - } - function isRawObject(o){ - return Object.prototype.toString.call(o) === "[object Object]"; - } + function parentElt(elt) { + return elt.parentElement; + } - function handleTrigger(elt, trigger) { - if (trigger) { - if (trigger.indexOf("{") === 0) { - var triggers = JSON.parse(trigger); - for (var eventName in triggers) { - if (triggers.hasOwnProperty(eventName)) { - var details = triggers[eventName]; - if (!isRawObject(details)) { - details = {"value": details} - } - triggerEvent(elt, eventName, details); + function getDocument() { + return document; + } + + function getClosestMatch(elt, condition) { + if (condition(elt)) { + return elt; + } else if (parentElt(elt)) { + return getClosestMatch(parentElt(elt), condition); + } else { + return null; + } + } + + function getClosestAttributeValue(elt, attributeName) { + var closestAttr = null; + getClosestMatch(elt, function (e) { + return closestAttr = getRawAttribute(e, attributeName); + }); + return closestAttr; + } + + function matches(elt, selector) { + // noinspection JSUnresolvedVariable + return (elt != null) &&(elt.matches || elt.matchesSelector || elt.msMatchesSelector || elt.mozMatchesSelector + || elt.webkitMatchesSelector || elt.oMatchesSelector).call(elt, selector); + } + + function closest (elt, selector) { + do if (elt == null || matches(elt, selector)) return elt; + while (elt = elt && parentElt(elt)); + } + + function makeFragment(resp) { + var range = getDocument().createRange(); + return range.createContextualFragment(resp); + } + + function isType(o, type) { + return Object.prototype.toString.call(o) === "[object " + type + "]"; + } + + function isFunction(o) { + return isType(o, "Function"); + } + + function isRawObject(o) { + return isType(o, "Object"); + } + + function getInternalData(elt) { + var dataProp = 'hx-data-internal'; + var data = elt[dataProp]; + if (!data) { + data = elt[dataProp] = {}; + } + return data; + } + + function toArray(object) { + var arr = []; + forEach(object, function(elt) { + arr.push(elt) + }); + return arr; + } + + function forEach(arr, func) { + for (var i = 0; i < arr.length; i++) { + func(arr[i]); + } + } + + function isScrolledIntoView(el) { + var rect = el.getBoundingClientRect(); + var elemTop = rect.top; + var elemBottom = rect.bottom; + return elemTop < window.innerHeight && elemBottom >= 0; + } + + function bodyContains(elt) { + return getDocument().body.contains(elt); + } + + //==================================================================== + // Node processing + //==================================================================== + + function getTarget(elt) { + var explicitTarget = getClosestMatch(elt, function(e){return getRawAttribute(e,"hx-target") !== null}); + + if (explicitTarget) { + var targetStr = getRawAttribute(explicitTarget, "hx-target"); + if (targetStr === "this") { + return explicitTarget; + } else { + return getDocument().querySelector(targetStr); + } + } else { + var data = getInternalData(elt); + if (data.boosted) { + return getDocument().body; + } else { + return elt; + } + } + } + + function directSwap(child) { + var swapDirect = getAttributeValue(child, 'hx-swap-direct'); + if (swapDirect) { + var target = getDocument().getElementById(getRawAttribute(child,'id')); + if (target) { + if (swapDirect === "merge") { + mergeInto(target, child); + } else { + var newParent = parentElt(target); + newParent.insertBefore(child, target); + newParent.removeChild(target); + return true; } } + } + return false; + } + + function processResponseNodes(parentNode, insertBefore, text, executeAfter, selector) { + var fragment = makeFragment(text); + var nodesToProcess; + if (selector) { + nodesToProcess = toArray(fragment.querySelectorAll(selector)); } else { - triggerEvent(elt, trigger, []); + nodesToProcess = toArray(fragment.childNodes); + } + forEach(nodesToProcess, function(child){ + if (!directSwap(child)) { + parentNode.insertBefore(child, insertBefore); + } + if (child.nodeType !== Node.TEXT_NODE) { + triggerEvent(child, 'load.hx', {parent:parentElt(child)}); + processNode(child); + } + }); + if(executeAfter) { + executeAfter.call(); } } - } - // core ajax request - function issueAjaxRequest(elt, url) - { - var request = new XMLHttpRequest(); - // TODO - support more request types POST, PUT, DELETE, etc. - request.open('GET', url, true); - request.onload = function() - { - var trigger = this.getResponseHeader("X-HX-Trigger"); - handleTrigger(elt, trigger); - if (this.status >= 200 && this.status < 400) - { - // don't process 'No Content' response - if (this.status != 204) { - // Success! - var resp = this.response; - swapResponse(elt, resp); + function findMatch(elt, possible) { + for (var i = 0; i < possible.length; i++) { + var candidate = possible[i]; + if (elt.hasAttribute("id") && elt.id === candidate.id) { + return candidate; + } + if (!candidate.hasAttribute("id") && elt.tagName === candidate.tagName) { + return candidate; } } - else - { - // TODO error handling - elt.innerHTML = "ERROR"; + return null; + } + + function cloneAttributes(mergeTo, mergeFrom) { + forEach(mergeTo.attributes, function (attr) { + if (!mergeFrom.hasAttribute(attr.name)) { + mergeTo.removeAttribute(attr.name) + } + }); + forEach(mergeFrom.attributes, function (attr) { + mergeTo.setAttribute(attr.name, attr.value); + }); + } + + function mergeChildren(mergeTo, mergeFrom) { + var oldChildren = toArray(mergeTo.children); + var marker = getDocument().createElement("span"); + mergeTo.insertBefore(marker, mergeTo.firstChild); + forEach(mergeFrom.childNodes, function (newChild) { + var match = findMatch(newChild, oldChildren); + if (match) { + while (marker.nextSibling && marker.nextSibling !== match) { + mergeTo.removeChild(marker.nextSibling); + } + mergeTo.insertBefore(marker, match.nextSibling); + mergeInto(match, newChild); + } else { + mergeTo.insertBefore(newChild, marker); + } + }); + while (marker.nextSibling) { + mergeTo.removeChild(marker.nextSibling); } - }; - request.onerror = function () { - // TODO error handling - // There was a connection error of some sort - }; - request.send(); - } + mergeTo.removeChild(marker); + } - function matches(el, selector) { - return (el.matches || el.matchesSelector || el.msMatchesSelector || el.mozMatchesSelector || el.webkitMatchesSelector || el.oMatchesSelector).call(el, selector); - } + function mergeInto(mergeTo, mergeFrom) { + cloneAttributes(mergeTo, mergeFrom); + mergeChildren(mergeTo, mergeFrom); + } + function mergeResponse(target, resp, selector) { + var fragment = makeFragment(resp); + mergeInto(target, selector ? fragment.querySelector(selector) : fragment.firstElementChild); + } - function getTrigger(elt) { - var explicitTrigger = getClosestAttributeValue(elt, 'hx-trigger'); - if (explicitTrigger) { - return explicitTrigger; - } else { - if (matches(elt, 'button')) { - return 'click'; - } else if (matches(elt, 'form')) { - return 'submit'; - } else if (matches(elt, 'input, textarea, select')) { - return 'change'; + function swapResponse(target, elt, resp, after) { + + var swapStyle = getClosestAttributeValue(elt, "hx-swap"); + var selector = getClosestAttributeValue(elt, "hx-select"); + if (swapStyle === "merge") { + mergeResponse(target, resp, selector); + } else if (swapStyle === "outerHTML") { + processResponseNodes(parentElt(target), target, resp, after, selector); + parentElt(target).removeChild(target); + } else if (swapStyle === "prepend") { + processResponseNodes(target, target.firstChild, resp, after, selector); + } else if (swapStyle === "prependBefore") { + processResponseNodes(parentElt(target), target, resp, after, selector); + } else if (swapStyle === "append") { + processResponseNodes(target, null, resp, after, selector); + } else if (swapStyle === "appendAfter") { + processResponseNodes(parentElt(target), target.nextSibling, resp, after, selector); } else { - return 'click'; + target.innerHTML = ""; + processResponseNodes(target, null, resp, after, selector); } } - } -// DOM element processing - function processClassList(elt, classList, operation) { - var values = classList.split(","); - for (var i = 0; i < values.length; i++) { - var cssClass = ""; - var delay = 50; - if (values[i].trim().indexOf(":") > 0) { - var split = values[i].trim().split(':'); - cssClass = split[0]; - delay = parseInterval(split[1]); - } else { - cssClass = values[i].trim(); + function handleTrigger(elt, trigger) { + if (trigger) { + if (trigger.indexOf("{") === 0) { + var triggers = JSON.parse(trigger); + for (var eventName in triggers) { + if (triggers.hasOwnProperty(eventName)) { + var details = triggers[eventName]; + if (!isRawObject(details)) { + details = {"value": details} + } + triggerEvent(elt, eventName, details); + } + } + } else { + triggerEvent(elt, trigger, []); + } } - setTimeout(function () { - elt.classList[operation].call(elt.classList, cssClass); - }, delay); } - } - function processElement(elt) { - if(getAttributeValue(elt,'hx-get')) { + function getTrigger(elt) { + var explicitTrigger = getClosestAttributeValue(elt, 'hx-trigger'); + if (explicitTrigger) { + return explicitTrigger; + } else { + if (matches(elt, 'button')) { + return 'click'; + } else if (matches(elt, 'form')) { + return 'submit'; + } else if (matches(elt, 'input, textarea, select')) { + return 'change'; + } else { + return 'click'; + } + } + } + + function processClassList(elt, classList, operation) { + var values = classList.split(","); + forEach(values, function(value){ + var cssClass = ""; + var delay = 50; + var trimmedValue = value.trim(); + if (trimmedValue.indexOf(":") > 0) { + var split = trimmedValue.split(':'); + cssClass = split[0]; + delay = parseInterval(split[1]); + } else { + cssClass = trimmedValue; + } + setTimeout(function () { + elt.classList[operation].call(elt.classList, cssClass); + }, delay); + }); + } + + function processPolling(elt, verb, path) { var trigger = getTrigger(elt); - if (trigger === 'load') { - issueAjaxRequest(elt, getAttributeValue(elt, 'hx-get')); + var nodeData = getInternalData(elt); + if (trigger.trim().indexOf("every ") === 0) { + var args = trigger.split(/\s+/); + var intervalStr = args[1]; + if (intervalStr) { + var interval = parseInterval(intervalStr); + nodeData.timeout = setTimeout(function () { + if (bodyContains(elt)) { + issueAjaxRequest(elt, verb, path); + processPolling(elt, verb, getAttributeValue(elt, "hx-" + verb)); + } + }, interval); + } + } + } + + function isLocalLink(elt) { + return location.hostname === elt.hostname && + getRawAttribute(elt,'href') && + !getRawAttribute(elt,'href').startsWith("#") + } + + function boostElement(elt, nodeData, trigger) { + if ((elt.tagName === "A" && isLocalLink(elt)) || elt.tagName === "FORM") { + nodeData.boosted = true; + var verb, path; + if (elt.tagName === "A") { + verb = "get"; + path = getRawAttribute(elt, 'href'); + } else { + var rawAttribute = getRawAttribute(elt, "method"); + verb = rawAttribute ? rawAttribute.toLowerCase() : "get"; + path = getRawAttribute(elt, 'action'); + } + addEventListener(elt, verb, path, nodeData, trigger, true); + } + } + + function addEventListener(elt, verb, path, nodeData, trigger, cancel) { + var eventListener = function (evt) { + if(cancel) evt.preventDefault(); + var eventData = getInternalData(evt); + var elementData = getInternalData(elt); + if (!eventData.handled) { + eventData.handled = true; + if (getAttributeValue(elt, "hx-trigger-once") === "true") { + if (elementData.triggeredOnce) { + return; + } else { + elementData.triggeredOnce = true; + } + } + if (getAttributeValue(elt, "hx-trigger-changed-only") === "true") { + if (elementData.lastValue === elt.value) { + return; + } else { + elementData.lastValue = elt.value; + } + } + if (elementData.delayed) { + clearTimeout(elementData.delayed); + } + var eventDelay = getAttributeValue(elt, "hx-trigger-delay"); + var issueRequest = function(){ + issueAjaxRequest(elt, verb, path, evt.target); + } + if (eventDelay) { + elementData.delayed = setTimeout(issueRequest, parseInterval(eventDelay)); + } else { + issueRequest(); + } + } + }; + nodeData.trigger = trigger; + nodeData.eventListener = eventListener; + elt.addEventListener(trigger, eventListener); + } + + function initScrollHandler() { + if (!window['hxScrollHandler']) { + var scrollHandler = function() { + forEach(getDocument().querySelectorAll("[hx-trigger='reveal']"), function (elt) { + maybeReveal(elt); + }); + }; + window['hxScrollHandler'] = scrollHandler; + window.addEventListener("scroll", scrollHandler) + } + } + + function maybeReveal(elt) { + var nodeData = getInternalData(elt); + if (!nodeData.revealed && isScrolledIntoView(elt)) { + nodeData.revealed = true; + issueAjaxRequest(elt, nodeData.verb, nodeData.path); + } + } + + function maybeCloseSSESource(elt) { + if (!bodyContains(elt)) { + elt.sseSource.close(); + return true; + } + } + + function initSSESource(elt, sseSrc) { + var details = { + initializer: function() { new EventSource(sseSrc, details.config) }, + config:{withCredentials: true} + }; + triggerEvent(elt, "initSSE.mx", {config:details}) + var source = details.initializer(); + source.onerror = function (e) { + triggerEvent(elt, "sseError.mx", {error:e, source:source}); + maybeCloseSSESource(elt); + }; + getInternalData(elt).sseSource = source; + } + + function processNode(elt) { + var nodeData = getInternalData(elt); + if (!nodeData.processed) { + nodeData.processed = true; + var trigger = getTrigger(elt); + var explicitAction = false; + forEach(VERBS, function(verb){ + var path = getAttributeValue(elt, 'hx-' + verb); + if (path) { + nodeData.path = path; + nodeData.verb = verb; + explicitAction = true; + if (trigger.indexOf("sse:") === 0) { + var sseEventName = trigger.substr(4); + var sseSource = getClosestMatch(elt, function(parent) {return parent.sseSource;}); + if (sseSource) { + var sseListener = function () { + if (!maybeCloseSSESource(sseSource)) { + if (bodyContains(elt)) { + issueAjaxRequest(elt, verb, path); + } else { + sseSource.sseSource.removeEventListener(sseEventName, sseListener); + } + } + }; + sseSource.sseSource.addEventListener(sseEventName, sseListener); + } else { + triggerEvent(elt, "noSSESourceError.mx") + } + } if (trigger === 'revealed') { + initScrollHandler(); + maybeReveal(elt); + } else if (trigger === 'load') { + if (!nodeData.loaded) { + nodeData.loaded = true; + issueAjaxRequest(elt, verb, path); + } + } else if (trigger.trim().indexOf('every ') === 0) { + nodeData.polling = true; + processPolling(elt, verb, path); + } else { + addEventListener(elt, verb, path, nodeData, trigger); + } + } + }); + if (!explicitAction && getClosestAttributeValue(elt, "hx-boost") === "true") { + boostElement(elt, nodeData, trigger); + } + var sseSrc = getAttributeValue(elt, 'hx-sse-source'); + if (sseSrc) { + initSSESource(elt, sseSrc); + } + var addClass = getAttributeValue(elt, 'hx-add-class'); + if (addClass) { + processClassList(elt, addClass, "add"); + } + var removeClass = getAttributeValue(elt, 'hx-remove-class'); + if (removeClass) { + processClassList(elt, removeClass, "remove"); + } + } + forEach(elt.children, function(child) { processNode(child) }); + } + + //==================================================================== + // Event/Log Support + //==================================================================== + + function sendError(elt, eventName, details) { + var errorURL = getClosestAttributeValue(elt, "hx-error-url"); + if (errorURL) { + var xhr = new XMLHttpRequest(); + xhr.open("POST", errorURL); + xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); + xhr.send(JSON.stringify({ "elt": elt.id, "event": eventName, "details" : details })); + } + } + + function makeEvent(eventName, details) { + var evt; + if (window.CustomEvent && typeof window.CustomEvent === 'function') { + evt = new CustomEvent(eventName, {detail: details}); } else { - elt.addEventListener(trigger, function(evt){ - issueAjaxRequest(elt, getAttributeValue(elt, 'hx-get')); - evt.stopPropagation(); + evt = getDocument().createEvent('CustomEvent'); + evt.initCustomEvent(eventName, true, true, details); + } + return evt; + } + + function triggerEvent(elt, eventName, details) { + details["elt"] = elt; + var event = makeEvent(eventName, details); + if (HTMx.logger) { + HTMx.logger(elt, eventName, details); + if (eventName.indexOf("Error") > 0) { + sendError(elt, eventName, details); + } + } + var eventResult = elt.dispatchEvent(event); + var allResult = elt.dispatchEvent(makeEvent("all.hx", {elt:elt, originalDetails:details, originalEvent: event})); + return eventResult && allResult; + } + + function addHTMxEventListener(arg1, arg2, arg3) { + var target, event, listener; + if (isFunction(arg1)) { + target = getDocument().body; + event = "all.hx"; + listener = arg1; + } else if (isFunction(arg2)) { + target = getDocument().body; + event = arg1; + listener = arg2; + } else { + target = arg1; + event = arg2; + listener = arg3; + } + return target.addEventListener(event, listener); + } + + //==================================================================== + // 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; + } + } + + 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() { + 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); + } + + function restoreHistory(data) { + var historyKey = data['hx-history-key']; + var content = localStorage.getItem('hx-history-' + historyKey); + var elt = getHistoryElement(); + elt.innerHTML = ""; + processResponseNodes(elt, null, content); + } + + function shouldPush(elt) { + return getClosestAttributeValue(elt, "hx-push-url") === "true" || + (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"); + } + + function removeRequestIndicatorClasses(elt) { + mutateRequestIndicatorClasses(elt, "remove"); + } + + function mutateRequestIndicatorClasses(elt, action) { + var indicator = getClosestAttributeValue(elt, 'hx-indicator'); + if (indicator) { + var indicators = getDocument().querySelectorAll(indicator); + } else { + indicators = [elt]; + } + forEach(indicators, function(ic) { + ic.classList[action].call(ic.classList, "hx-show-indicator"); + }); + } + + //==================================================================== + // Input Value Processing + //==================================================================== + + function haveSeenNode(processed, elt) { + for (var i = 0; i < processed.length; i++) { + var node = processed[i]; + if (node.isSameNode(elt)) { + return true; + } + } + return false; + } + + function processInputValue(processed, values, elt) { + if (elt == null || haveSeenNode(processed, elt)) { + return; + } else { + processed.push(elt); + } + var name = getRawAttribute(elt,"name"); + var value = elt.value; + if (name && value) { + var current = values[name]; + if(current) { + if (Array.isArray(current)) { + current.push(value); + } else { + values[name] = [current, value]; + } + } else { + values[name] = value; + } + } + if (matches(elt, 'form')) { + var inputs = elt.elements; + forEach(inputs, function(input) { + processInputValue(processed, values, input); }); } } - if (getAttributeValue(elt, 'hx-add-class')) { - processClassList(elt, getAttributeValue(elt,'hx-add-class'), "add"); + + function getInputValues(elt) { + var processed = []; + var values = {}; + // include the element itself + processInputValue(processed, values, elt); + + // include any explicit includes + var includes = getAttributeValue(elt, "hx-include"); + if (includes) { + var nodes = getDocument().querySelectorAll(includes); + forEach(nodes, function(node) { + processInputValue(processed, values, node); + }); + } + + // include the closest form + processInputValue(processed, values, closest(elt, 'form')); + return values; } - if (getAttributeValue(elt, 'hx-remove-class')) { - processClassList(elt, getAttributeValue(elt,'hx-remove-class'), "remove"); + + function appendParam(returnStr, name, realValue) { + if (returnStr !== "") { + returnStr += "&"; + } + returnStr += encodeURIComponent(name) + "=" + encodeURIComponent(realValue); + return returnStr; } - for (var i = 0; i < elt.children.length; i++) { - var child = elt.children[i]; - processElement(child); + + function urlEncode(values) { + var returnStr = ""; + for (var name in values) { + if (values.hasOwnProperty(name)) { + var value = values[name]; + if (Array.isArray(value)) { + forEach(value, function(v) { + returnStr = appendParam(returnStr, name, v); + }); + } else { + returnStr = appendParam(returnStr, name, value); + } + } + } + return returnStr; + } + + //==================================================================== + // Ajax + //==================================================================== + + function setHeader(xhr, name, value, noPrefix) { + xhr.setRequestHeader((noPrefix ? "" : "X-HX-") + name, value || ""); + } + + function issueAjaxRequest(elt, verb, path, eventTarget) { + var eltData = getInternalData(elt); + if (eltData.requestInFlight) { + return; + } else { + eltData.requestInFlight = true; + } + var endRequestLock = function(){ + eltData.requestInFlight = false + } + var target = getTarget(elt); + var promptQuestion = getClosestAttributeValue(elt, "hx-prompt"); + if (promptQuestion) { + var prompt = prompt(promptQuestion); + if(!triggerEvent(elt, 'prompt.hx', {prompt: prompt, target:target})) return endRequestLock(); + } + + var confirmQuestion = getClosestAttributeValue(elt, "hx-confirm"); + if (confirmQuestion) { + if(!confirm(confirmQuestion)) return endRequestLock(); + } + + var xhr = new XMLHttpRequest(); + + var inputValues = getInputValues(elt); + if(!triggerEvent(elt, 'values.hx', {values: inputValues, target:target})) return endRequestLock(); + + // request type + if (verb === 'get') { + var noValues = Object.keys(inputValues).length === 0; + xhr.open('GET', path + (noValues ? "" : "?" + urlEncode(inputValues)), true); + } else { + xhr.open('POST', path, 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); + } + } + + // TODO IE10 compatibility? + xhr.overrideMimeType("text/html"); + + // request headers + setHeader(xhr, "Request", "true"); + setHeader(xhr,"Trigger-Id", getRawAttribute(elt,"id")); + setHeader(xhr,"Trigger-Name", getRawAttribute(elt, "name")); + setHeader(xhr,"Target-Id", getRawAttribute(target,"id")); + setHeader(xhr,"Current-URL", getDocument().location.href); + if (prompt) { + setHeader(xhr,"Prompt", prompt); + } + if (eventTarget) { + setHeader(xhr,"Event-Target", getRawAttribute(eventTarget,"id")); + } + if (getDocument().activeElement) { + setHeader(xhr,"Active-Element", getRawAttribute(getDocument().activeElement,"id")); + // noinspection JSUnresolvedVariable + if (getDocument().activeElement.value) { + setHeader(xhr,"Active-Element-Value", getDocument().activeElement.value); + } + } + + 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); + 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; + 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}); + }); + } 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)) + } else { + doSwap(); + } + } + } else { + triggerEvent(elt, 'responseError.hx', {xhr: xhr, response: xhr.response, status: xhr.status, target: target}); + } + } catch (e) { + triggerEvent(elt, 'onLoadError.hx', {xhr: xhr, response: xhr.response, status: xhr.status, target: target}); + throw e; + } finally { + removeRequestIndicatorClasses(elt); + endRequestLock(); + triggerEvent(elt, 'afterOnLoad.hx', {xhr: xhr, response: xhr.response, status: xhr.status, target: target}); + } + } + xhr.onerror = function () { + removeRequestIndicatorClasses(elt);triggerEvent(elt, 'loadError.hx', {xhr:xhr}); + endRequestLock(); + } + if(!triggerEvent(elt, 'beforeRequest.hx', {xhr:xhr, values: inputValues, target:target})) return endRequestLock(); + addRequestIndicatorClasses(elt); + xhr.send(verb === 'get' ? null : urlEncode(inputValues)); + } + + //==================================================================== + // Initialization + //==================================================================== + + function ready(fn) { + if (getDocument().readyState !== 'loading') { + fn(); + } else { + getDocument().addEventListener('DOMContentLoaded', fn); + } + } + + // initialize the document + ready(function () { + processNode(getDocument().body); + window.onpopstate = function (event) { + restoreHistory(event.state); + }; + }) + + function internalEval(str){ + return eval(str); + } + + // Public API + return { + processElement: processNode, + on: addHTMxEventListener, + version: "0.0.2", + _:internalEval } } - - function ready(fn) { - if (document.readyState !== 'loading'){ - fn(); - } else { - document.addEventListener('DOMContentLoaded', fn); - } - } - - // initialize the document - ready(function () { - processElement(document.body); - }) - - // Public API - return { - processElement : processElement, - version : "0.0.1" - } -})(); \ No newline at end of file +)(); \ No newline at end of file