mirror of
https://github.com/bigskysoftware/htmx.git
synced 2025-10-02 23:35:13 +00:00
lots of work:
* reworked the swap code did away with merge in favor of general attributes swapping * updated docs * changed hx-swap-direct to hx-swap-oob * updated build script to create a .gz so I can know what size that is * cleaned up code so it's easier to follow
This commit is contained in:
parent
8be1f10636
commit
fe9dbb8b3e
255
dist/htmx.js
vendored
255
dist/htmx.js
vendored
@ -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);
|
||||
}
|
||||
mergeTo.insertBefore(marker, match.nextSibling);
|
||||
mergeInto(match, newChild);
|
||||
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 {
|
||||
mergeTo.insertBefore(newChild, marker);
|
||||
child.parentNode.removeChild(child);
|
||||
triggerEvent(getDocument().body, "oobErrorNoTarget.hx", {id:child.id, content:child})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
mergeTo.removeChild(marker);
|
||||
setTimeout(function () {
|
||||
forEach(attributeSwaps, function (swap) {
|
||||
swap.call();
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function mergeInto(mergeTo, mergeFrom) {
|
||||
cloneAttributes(mergeTo, mergeFrom);
|
||||
mergeChildren(mergeTo, mergeFrom);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function mergeResponse(target, resp, selector) {
|
||||
var fragment = makeFragment(resp);
|
||||
mergeInto(target, selector ? fragment.querySelector(selector) : fragment.firstElementChild);
|
||||
function swapOuterHTML(target, fragment) {
|
||||
if (target.tagName === "BODY") {
|
||||
swapInnerHTML(target, fragment);
|
||||
} else {
|
||||
insertNodesBefore(parentElt(target), target, fragment);
|
||||
parentElt(target).removeChild(target);
|
||||
}
|
||||
}
|
||||
|
||||
function swapResponse(target, elt, resp, after) {
|
||||
function swapPrepend(target, fragment) {
|
||||
insertNodesBefore(target, target.firstChild, fragment);
|
||||
}
|
||||
|
||||
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,21 +443,10 @@ var HTMx = HTMx || (function () {
|
||||
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;});
|
||||
function processSSETrigger(sseEventName, elt, verb, path) {
|
||||
var sseSource = getClosestMatch(elt, function (parent) {
|
||||
return parent.sseSource;
|
||||
});
|
||||
if (sseSource) {
|
||||
var sseListener = function () {
|
||||
if (!maybeCloseSSESource(sseSource)) {
|
||||
@ -462,14 +461,30 @@ var HTMx = HTMx || (function () {
|
||||
} else {
|
||||
triggerEvent(elt, "noSSESourceError.mx")
|
||||
}
|
||||
} if (trigger === 'revealed') {
|
||||
initScrollHandler();
|
||||
maybeReveal(elt);
|
||||
} else if (trigger === 'load') {
|
||||
}
|
||||
|
||||
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);
|
||||
@ -478,6 +493,17 @@ var HTMx = HTMx || (function () {
|
||||
}
|
||||
}
|
||||
});
|
||||
return explicitAction;
|
||||
}
|
||||
|
||||
function processNode(elt) {
|
||||
var nodeData = getInternalData(elt);
|
||||
if (!nodeData.processed) {
|
||||
nodeData.processed = true;
|
||||
|
||||
var trigger = getTrigger(elt);
|
||||
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
|
||||
}
|
||||
}
|
||||
|
2
dist/htmx.min.js
vendored
2
dist/htmx.min.js
vendored
File diff suppressed because one or more lines are too long
BIN
dist/htmx.min.js.gz
vendored
Normal file
BIN
dist/htmx.min.js.gz
vendored
Normal file
Binary file not shown.
@ -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"
|
||||
},
|
||||
|
255
src/htmx.js
255
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);
|
||||
}
|
||||
mergeTo.insertBefore(marker, match.nextSibling);
|
||||
mergeInto(match, newChild);
|
||||
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 {
|
||||
mergeTo.insertBefore(newChild, marker);
|
||||
child.parentNode.removeChild(child);
|
||||
triggerEvent(getDocument().body, "oobErrorNoTarget.hx", {id:child.id, content:child})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
mergeTo.removeChild(marker);
|
||||
setTimeout(function () {
|
||||
forEach(attributeSwaps, function (swap) {
|
||||
swap.call();
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function mergeInto(mergeTo, mergeFrom) {
|
||||
cloneAttributes(mergeTo, mergeFrom);
|
||||
mergeChildren(mergeTo, mergeFrom);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function mergeResponse(target, resp, selector) {
|
||||
var fragment = makeFragment(resp);
|
||||
mergeInto(target, selector ? fragment.querySelector(selector) : fragment.firstElementChild);
|
||||
function swapOuterHTML(target, fragment) {
|
||||
if (target.tagName === "BODY") {
|
||||
swapInnerHTML(target, fragment);
|
||||
} else {
|
||||
insertNodesBefore(parentElt(target), target, fragment);
|
||||
parentElt(target).removeChild(target);
|
||||
}
|
||||
}
|
||||
|
||||
function swapResponse(target, elt, resp, after) {
|
||||
function swapPrepend(target, fragment) {
|
||||
insertNodesBefore(target, target.firstChild, fragment);
|
||||
}
|
||||
|
||||
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,21 +443,10 @@ var HTMx = HTMx || (function () {
|
||||
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;});
|
||||
function processSSETrigger(sseEventName, elt, verb, path) {
|
||||
var sseSource = getClosestMatch(elt, function (parent) {
|
||||
return parent.sseSource;
|
||||
});
|
||||
if (sseSource) {
|
||||
var sseListener = function () {
|
||||
if (!maybeCloseSSESource(sseSource)) {
|
||||
@ -465,14 +461,30 @@ var HTMx = HTMx || (function () {
|
||||
} else {
|
||||
triggerEvent(elt, "noSSESourceError.mx")
|
||||
}
|
||||
} if (trigger === 'revealed') {
|
||||
initScrollHandler();
|
||||
maybeReveal(elt);
|
||||
} else if (trigger === 'load') {
|
||||
}
|
||||
|
||||
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);
|
||||
@ -481,6 +493,17 @@ var HTMx = HTMx || (function () {
|
||||
}
|
||||
}
|
||||
});
|
||||
return explicitAction;
|
||||
}
|
||||
|
||||
function processNode(elt) {
|
||||
var nodeData = getInternalData(elt);
|
||||
if (!nodeData.processed) {
|
||||
nodeData.processed = true;
|
||||
|
||||
var trigger = getTrigger(elt);
|
||||
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) {
|
||||
|
@ -25,7 +25,7 @@
|
||||
<script src="indicators.js"></script>
|
||||
<script src="values.js"></script>
|
||||
<script src="events.js"></script>
|
||||
<script src="swap_direct.js"></script>
|
||||
<script src="oob.js"></script>
|
||||
|
||||
<!--TODO figure out how to test stuff w/ history involved-->
|
||||
<!--<script src="history.js"></script>-->
|
||||
|
@ -9,7 +9,7 @@ describe("HTMx Direct Swap", function () {
|
||||
});
|
||||
|
||||
it('handles basic response properly', function () {
|
||||
this.server.respondWith("GET", "/test", "Clicked<div id='d1' hx-swap-direct='true'>Swapped</div>");
|
||||
this.server.respondWith("GET", "/test", "Clicked<div id='d1' hx-swap-oob='true'>Swapped</div>");
|
||||
var div = make('<div hx-get="/test">click me</div>');
|
||||
make('<div id="d1"></div>');
|
||||
div.click();
|
||||
@ -19,11 +19,11 @@ describe("HTMx Direct Swap", function () {
|
||||
})
|
||||
|
||||
it('handles no id match properly', function () {
|
||||
this.server.respondWith("GET", "/test", "Clicked<div id='d1' hx-swap-direct='true'>Swapped</div>");
|
||||
this.server.respondWith("GET", "/test", "Clicked<div id='d1' hx-swap-oob='true'>Swapped</div>");
|
||||
var div = make('<div hx-get="/test">click me</div>');
|
||||
div.click();
|
||||
this.server.respond();
|
||||
div.innerText.should.equal("Clicked\nSwapped");
|
||||
div.innerText.should.equal("Clicked");
|
||||
})
|
||||
|
||||
|
@ -1,23 +1,18 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<style>
|
||||
div {
|
||||
transition: all 1000ms ease-in;
|
||||
}
|
||||
|
||||
.indicator {
|
||||
opacity: 0;
|
||||
transition: all 200ms ease-in;
|
||||
}
|
||||
|
||||
.hx-show-indicator .indicator {
|
||||
opacity: 100%;
|
||||
}
|
||||
|
||||
div {
|
||||
transition: all 1000ms ease-in;
|
||||
}
|
||||
|
||||
div.foo {
|
||||
color: red;
|
||||
transition: all 1000ms ease-in;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css">
|
||||
|
||||
@ -29,9 +24,11 @@
|
||||
<script src="scratch_server.js"></script>
|
||||
|
||||
<script>
|
||||
this.server.respondWith("POST", "/test", "Boosted");
|
||||
var div = make('<div hx-target="this" hx-boost="true"><form id="f1" action="/test" method="post"><button id="b1">Submit</button></form></div>');
|
||||
var btn = byId('b1');
|
||||
// this.server.respondWith("GET", "/test", "Clicked!");
|
||||
// var btn = make('<button hx-get="/test">Click Me!</button>')
|
||||
|
||||
this.server.respondWith("GET", "/test", '<div id="d1" style="color: red; margin: 100px">Foo</div>');
|
||||
make('<div hx-swap="outerHTML" hx-get="/test" hx-push-url="true" id="d1">Foo</div>');
|
||||
</script>
|
||||
|
||||
|
||||
@ -45,6 +42,7 @@ Autorespond: <input id="autorespond" type="checkbox" onclick="toggleAutoRespond(
|
||||
<hr/>
|
||||
|
||||
<div id="work-area" class="hx-history-element">
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
@ -1,36 +0,0 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>HTMx - Teaching HTML new tricks</title>
|
||||
<link rel="stylesheet" href="/css/site.css"/>
|
||||
<link rel="stylesheet" href="/css/prism-theme.css"/>
|
||||
<script src="/js/htmx.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1 class="hero" hx-add-class="settle"><<span class="flair">/</span>> HTM<sub class="flair">x</sub></h1>
|
||||
<div class="nav">
|
||||
<h3>Navigation</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/">Home</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/docs">Docs</a>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/docs/attributes">Attributes</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/docs/attributes">Events</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/docs/attributes">Headers</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="content">
|
||||
{{ content | safe }}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
38
www/_includes/layout.njk
Normal file
38
www/_includes/layout.njk
Normal file
@ -0,0 +1,38 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>HTMx - Teaching HTML new tricks</title>
|
||||
<link rel="stylesheet" href="/css/site.css"/>
|
||||
<link rel="stylesheet" href="/css/prism-theme.css"/>
|
||||
<script src="/js/htmx.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="c">
|
||||
<div class="{% if page.url == '/' %}root{% endif %} top-nav">
|
||||
<h1 class="hero" hx-add-class="settle"><<a>/</a>> HTM<sub><a>x</a></sub></h1>
|
||||
<div class="row center">
|
||||
<div class="1 col">
|
||||
<a href="/">home</a>
|
||||
</div>
|
||||
<div class="1 col">
|
||||
<a href="/docs">docs</a>
|
||||
</div>
|
||||
<div class="1 col">
|
||||
<a href="/docs/attributes">attributes</a>
|
||||
</div>
|
||||
<div class="1 col">
|
||||
<a href="/docs/attributes">events</a>
|
||||
</div>
|
||||
<div class="1 col">
|
||||
<a href="/docs/attributes">headers</a>
|
||||
</div>
|
||||
<div class="1 col">
|
||||
<a href="https://github.com/bigskysoftware/htmx">github</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="content">
|
||||
{{ content | safe }}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
160
www/css/site.css
160
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;
|
||||
}
|
244
www/docs.md
Normal file
244
www/docs.md
Normal file
@ -0,0 +1,244 @@
|
||||
---
|
||||
layout: layout.njk
|
||||
title: HTMx - HTML Extensions
|
||||
---
|
||||
<div class="row">
|
||||
<div class="2 col nav">
|
||||
|
||||
**Contents**
|
||||
|
||||
* [introduction](#introduction)
|
||||
* [installing](#installing)
|
||||
* [ajax](#ajax)
|
||||
* [triggers](#triggers)
|
||||
* [special events](#special-events)
|
||||
* [polling](#polling)
|
||||
* [SSE](#sse)
|
||||
* [targets](#targets)
|
||||
* [forms](#forms)
|
||||
* [swapping](#swapping)
|
||||
* [history](#history)
|
||||
* [requests & responses](#requests)
|
||||
* [misc](#misc)
|
||||
* [events & logging](#events)
|
||||
|
||||
</div>
|
||||
<div class="10 col">
|
||||
|
||||
## <a name="introduction"></a>[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
|
||||
<a href="/blog">Blog</a>
|
||||
```
|
||||
|
||||
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
|
||||
<a hx-post="/click">Click Me!</a>
|
||||
```
|
||||
|
||||
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
|
||||
<a data-hx-post="/click">Click Me!</a>
|
||||
```
|
||||
|
||||
## <a name="installing"></a> [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
|
||||
<script src="https://unpkg.com/htmx.org@0.0.1"></script>
|
||||
```
|
||||
|
||||
## <a name="ajax"></a> [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.
|
||||
|
||||
### <a name="triggers"></a> [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
|
||||
<div hx-post="/mouse_entered" hx-trigger="mouseenter">
|
||||
[Here Mouse, Mouse!]
|
||||
</div>
|
||||
```
|
||||
|
||||
If you want a request to only happen once, you can use the [hx-trigger-once](/attributes/hx-trigger-once) attribute:
|
||||
|
||||
```html
|
||||
<div hx-post="/mouse_entered" hx-trigger="mouseenter"
|
||||
hx-trigger-once="true">
|
||||
[Here Mouse, Mouse!]
|
||||
</div>
|
||||
```
|
||||
|
||||
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
|
||||
<input type="text" name="q"
|
||||
hx-get="/trigger_delay"
|
||||
hx-trigger="keyup"
|
||||
hx-target="#search-results"
|
||||
hx-trigger-delay="500ms" placeholder="Search..."/>
|
||||
<div id="search-results"></div>
|
||||
```
|
||||
|
||||
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`.
|
||||
|
||||
#### <a name="special-events"></a> [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.
|
||||
|
||||
### <a name="targets"></a> [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
|
||||
<input type="text" name="q"
|
||||
hx-get="/trigger_delay"
|
||||
hx-trigger="keyup"
|
||||
hx-target="#search-results"
|
||||
hx-trigger-delay="500ms" placeholder="Search..."/>
|
||||
<div id="search-results"></div>
|
||||
```
|
||||
|
||||
You can see that the results from the search are going to be loaded into `div#search-results`.
|
||||
|
||||
### <a name="forms"></a> [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.
|
||||
|
||||
### <a name="swapping"></a> [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
|
||||
<div id="message" hx-swap-directly="true">Swap me directly!</div>
|
||||
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.
|
||||
|
||||
## <a name="history"></a> [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
|
||||
<a hx-get="/Blog" hx-push="true">Blog</a>
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
## <a name="requests">[Requests & Responses](#requests)
|
||||
|
||||
## Miscellaneous Attributes
|
||||
|
||||
### Class Swapping
|
||||
|
||||
### Timed Removal
|
||||
|
||||
### Boosting
|
||||
|
||||
## <a name="events"></a> [Events & Logging](#events)
|
||||
|
||||
</div>
|
||||
</div>
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
layout: layout.html
|
||||
layout: layout.njk
|
||||
title: HTMx - HTML Extensions / Attributes
|
||||
---
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
layout: layout.html
|
||||
layout: layout.njk
|
||||
title: HTMx - HTML Extensions / Attributes
|
||||
---
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
layout: layout.html
|
||||
layout: layout.njk
|
||||
title: HTMx - HTML Extensions / Attributes
|
||||
---
|
||||
|
||||
|
31
www/index.md
31
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.</p>
|
||||
## 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
|
||||
<button hx-get="/example" hx-target="#myDiv">
|
||||
Click Me
|
||||
</button>
|
||||
<!-- Load from unpkg -->
|
||||
<script src="https://unpkg.com/htmx.org@0.0.1"></script>
|
||||
|
||||
<!-- enhance a button -->
|
||||
<button hx-get="/example">Click Me</button>
|
||||
```
|
||||
|
||||
This example issues an AJAX request to <code>/example</code> 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.
|
||||
|
904
www/js/htmx.js
904
www/js/htmx.js
@ -1,7 +1,13 @@
|
||||
var HTMx = HTMx || (function()
|
||||
{
|
||||
// noinspection JSUnusedAssignment
|
||||
var HTMx = HTMx || (function () {
|
||||
'use strict';
|
||||
|
||||
var VERBS = ['get', 'post', 'put', 'delete', 'patch']
|
||||
|
||||
//====================================================================
|
||||
// Utilities
|
||||
//====================================================================
|
||||
|
||||
function parseInterval(str) {
|
||||
if (str === "null" || str === "false" || str === "") {
|
||||
return null;
|
||||
@ -10,92 +16,250 @@ var HTMx = HTMx || (function()
|
||||
} else if (str.lastIndexOf("s") === str.length - 1) {
|
||||
return parseFloat(str.substr(0, str.length - 1)) * 1000;
|
||||
} else {
|
||||
return 1000;
|
||||
return parseFloat(str);
|
||||
}
|
||||
}
|
||||
|
||||
function getRawAttribute(elt, name) {
|
||||
return elt.getAttribute && elt.getAttribute(name);
|
||||
}
|
||||
|
||||
// resolve with both hx and data-hx prefixes
|
||||
function getAttributeValue(elt, qualifiedName) {
|
||||
return elt.getAttribute(qualifiedName) || elt.getAttribute("data-" + qualifiedName);
|
||||
return getRawAttribute(elt, qualifiedName) || getRawAttribute(elt, "data-" + qualifiedName);
|
||||
}
|
||||
|
||||
function getClosestAttributeValue(elt, attributeName)
|
||||
{
|
||||
var attribute = getAttributeValue(elt, attributeName);
|
||||
if(attribute)
|
||||
{
|
||||
return attribute;
|
||||
function parentElt(elt) {
|
||||
return elt.parentElement;
|
||||
}
|
||||
else if (elt.parentElement)
|
||||
{
|
||||
return getClosestAttributeValue(elt.parentElement, attributeName);
|
||||
|
||||
function getDocument() {
|
||||
return document;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
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 targetVal = getClosestAttributeValue(elt, "hx-target");
|
||||
if (targetVal) {
|
||||
return document.querySelector(targetVal);
|
||||
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 makeFragment(resp) {
|
||||
var range = document.createRange();
|
||||
return range.createContextualFragment(resp);
|
||||
}
|
||||
|
||||
function processResponseNodes(parent, target, text) {
|
||||
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);
|
||||
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);
|
||||
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 swapResponse(elt, resp) {
|
||||
var target = getTarget(elt);
|
||||
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)) {
|
||||
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);
|
||||
}
|
||||
mergeTo.removeChild(marker);
|
||||
}
|
||||
|
||||
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 swapResponse(target, elt, resp, after) {
|
||||
|
||||
var swapStyle = getClosestAttributeValue(elt, "hx-swap");
|
||||
if (swapStyle === "outerHTML") {
|
||||
processResponseNodes(target.parentElement, target, resp);
|
||||
target.parentElement.removeChild(target);
|
||||
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);
|
||||
processResponseNodes(target, target.firstChild, resp, after, selector);
|
||||
} else if (swapStyle === "prependBefore") {
|
||||
processResponseNodes(target.parentElement, target, resp);
|
||||
processResponseNodes(parentElt(target), target, resp, after, selector);
|
||||
} else if (swapStyle === "append") {
|
||||
processResponseNodes(target, null, resp);
|
||||
processResponseNodes(target, null, resp, after, selector);
|
||||
} else if (swapStyle === "appendAfter") {
|
||||
processResponseNodes(target.parentElement, target.nextSibling, resp);
|
||||
processResponseNodes(parentElt(target), target.nextSibling, resp, after, selector);
|
||||
} else {
|
||||
target.innerHTML = "";
|
||||
processResponseNodes(target, null, resp);
|
||||
processResponseNodes(target, null, resp, after, selector);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
elt.dispatchEvent(event);
|
||||
}
|
||||
|
||||
function isRawObject(o){
|
||||
return Object.prototype.toString.call(o) === "[object Object]";
|
||||
}
|
||||
|
||||
function handleTrigger(elt, trigger) {
|
||||
if (trigger) {
|
||||
if (trigger.indexOf("{") === 0) {
|
||||
@ -115,43 +279,6 @@ var HTMx = HTMx || (function()
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO error handling
|
||||
elt.innerHTML = "ERROR";
|
||||
}
|
||||
};
|
||||
request.onerror = function () {
|
||||
// TODO error handling
|
||||
// There was a connection error of some sort
|
||||
};
|
||||
request.send();
|
||||
}
|
||||
|
||||
function matches(el, selector) {
|
||||
return (el.matches || el.matchesSelector || el.msMatchesSelector || el.mozMatchesSelector || el.webkitMatchesSelector || el.oMatchesSelector).call(el, selector);
|
||||
}
|
||||
|
||||
|
||||
function getTrigger(elt) {
|
||||
var explicitTrigger = getClosestAttributeValue(elt, 'hx-trigger');
|
||||
if (explicitTrigger) {
|
||||
@ -169,65 +296,610 @@ var HTMx = HTMx || (function()
|
||||
}
|
||||
}
|
||||
|
||||
// DOM element processing
|
||||
function processClassList(elt, classList, operation) {
|
||||
var values = classList.split(",");
|
||||
for (var i = 0; i < values.length; i++) {
|
||||
forEach(values, function(value){
|
||||
var cssClass = "";
|
||||
var delay = 50;
|
||||
if (values[i].trim().indexOf(":") > 0) {
|
||||
var split = values[i].trim().split(':');
|
||||
var trimmedValue = value.trim();
|
||||
if (trimmedValue.indexOf(":") > 0) {
|
||||
var split = trimmedValue.split(':');
|
||||
cssClass = split[0];
|
||||
delay = parseInterval(split[1]);
|
||||
} else {
|
||||
cssClass = values[i].trim();
|
||||
cssClass = trimmedValue;
|
||||
}
|
||||
setTimeout(function () {
|
||||
elt.classList[operation].call(elt.classList, cssClass);
|
||||
}, delay);
|
||||
});
|
||||
}
|
||||
|
||||
function processPolling(elt, verb, path) {
|
||||
var trigger = getTrigger(elt);
|
||||
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 processElement(elt) {
|
||||
if(getAttributeValue(elt,'hx-get')) {
|
||||
var trigger = getTrigger(elt);
|
||||
if (trigger === 'load') {
|
||||
issueAjaxRequest(elt, getAttributeValue(elt, 'hx-get'));
|
||||
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 {
|
||||
elt.addEventListener(trigger, function(evt){
|
||||
issueAjaxRequest(elt, getAttributeValue(elt, 'hx-get'));
|
||||
evt.stopPropagation();
|
||||
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 {
|
||||
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);
|
||||
});
|
||||
}
|
||||
if (getAttributeValue(elt, 'hx-remove-class')) {
|
||||
processClassList(elt, getAttributeValue(elt,'hx-remove-class'), "remove");
|
||||
|
||||
// include the closest form
|
||||
processInputValue(processed, values, closest(elt, 'form'));
|
||||
return values;
|
||||
}
|
||||
for (var i = 0; i < elt.children.length; i++) {
|
||||
var child = elt.children[i];
|
||||
processElement(child);
|
||||
|
||||
function appendParam(returnStr, name, realValue) {
|
||||
if (returnStr !== "") {
|
||||
returnStr += "&";
|
||||
}
|
||||
returnStr += encodeURIComponent(name) + "=" + encodeURIComponent(realValue);
|
||||
return returnStr;
|
||||
}
|
||||
|
||||
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 (document.readyState !== 'loading'){
|
||||
if (getDocument().readyState !== 'loading') {
|
||||
fn();
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', fn);
|
||||
getDocument().addEventListener('DOMContentLoaded', fn);
|
||||
}
|
||||
}
|
||||
|
||||
// initialize the document
|
||||
ready(function () {
|
||||
processElement(document.body);
|
||||
processNode(getDocument().body);
|
||||
window.onpopstate = function (event) {
|
||||
restoreHistory(event.state);
|
||||
};
|
||||
})
|
||||
|
||||
function internalEval(str){
|
||||
return eval(str);
|
||||
}
|
||||
|
||||
// Public API
|
||||
return {
|
||||
processElement : processElement,
|
||||
version : "0.0.1"
|
||||
processElement: processNode,
|
||||
on: addHTMxEventListener,
|
||||
version: "0.0.2",
|
||||
_:internalEval
|
||||
}
|
||||
})();
|
||||
}
|
||||
)();
|
Loading…
x
Reference in New Issue
Block a user