release 0.1 prep

This commit is contained in:
carson 2020-09-17 13:09:07 -06:00
parent 325cb29bf8
commit 1dbabdb4c4
20 changed files with 2029 additions and 1140 deletions

184
dist/htmx.js vendored
View File

@ -41,6 +41,7 @@ return (function () {
requestClass:'htmx-request',
settlingClass:'htmx-settling',
swappingClass:'htmx-swapping',
attributesToSwizzle:["class", "style", "width", "height"]
},
parseInterval:parseInterval,
_:internalEval,
@ -57,6 +58,8 @@ return (function () {
return "[hx-" + verb + "], [data-hx-" + verb + "]"
}).join(", ");
var windowIsScrolling = false // used by initScrollHandler
//====================================================================
// Utilities
//====================================================================
@ -216,7 +219,7 @@ return (function () {
}
function splitOnWhitespace(trigger) {
return trigger.split(/\s+/);
return trigger.trim().split(/\s+/);
}
function mergeObjects(obj1, obj2) {
@ -362,6 +365,8 @@ return (function () {
return explicitTarget;
} else if (targetStr.indexOf("closest ") === 0) {
return closest(elt, targetStr.substr(8));
} else if (targetStr.indexOf("find ") === 0) {
return find(elt, targetStr.substr(5));
} else {
return getDocument().querySelector(targetStr);
}
@ -375,15 +380,24 @@ return (function () {
}
}
var EXCLUDED_ATTRIBUTES = ['id', 'value'];
function shouldSettleAttribute(name) {
var attributesToSwizzle = htmx.config.attributesToSwizzle;
for (var i = 0; i < attributesToSwizzle.length; i++) {
if (name === attributesToSwizzle[i]) {
return true;
}
}
return false;
}
function cloneAttributes(mergeTo, mergeFrom) {
forEach(mergeTo.attributes, function (attr) {
if (!mergeFrom.hasAttribute(attr.name) && EXCLUDED_ATTRIBUTES.indexOf(attr.name) === -1) {
if (!mergeFrom.hasAttribute(attr.name) && shouldSettleAttribute(attr.name)) {
mergeTo.removeAttribute(attr.name)
}
});
forEach(mergeFrom.attributes, function (attr) {
if (EXCLUDED_ATTRIBUTES.indexOf(attr.name) === -1) {
if (shouldSettleAttribute(attr.name)) {
mergeTo.setAttribute(attr.name, attr.value);
}
});
@ -450,12 +464,21 @@ return (function () {
function makeAjaxLoadTask(child) {
return function () {
processNode(child, true);
processNode(child);
processScripts(child);
triggerEvent(child, 'htmx:load', {});
processFocus(child)
triggerEvent(child, 'htmx:load');
};
}
function processFocus(child) {
var autofocus = "[autofocus]";
var autoFocusedElt = matches(child, autofocus) ? child : child.querySelector(autofocus)
if (autoFocusedElt != null) {
autoFocusedElt.focus();
}
}
function insertNodesBefore(parentNode, insertBefore, fragment, settleInfo) {
handleAttributes(parentNode, fragment, settleInfo);
while(fragment.childNodes.length > 0){
@ -491,6 +514,7 @@ return (function () {
} else {
var newElt = eltBeforeNewContent.nextSibling;
}
getInternalData(target).replacedWith = newElt; // tuck away so we can fire events on it later
while(newElt && newElt !== target) {
settleInfo.elts.push(newElt);
newElt = newElt.nextSibling;
@ -521,8 +545,10 @@ return (function () {
insertNodesBefore(target, firstChild, fragment, settleInfo);
if (firstChild) {
while (firstChild.nextSibling) {
closeConnections(firstChild.nextSibling)
target.removeChild(firstChild.nextSibling);
}
closeConnections(firstChild)
target.removeChild(firstChild);
}
}
@ -760,12 +786,18 @@ return (function () {
function initScrollHandler() {
if (!window['htmxScrollHandler']) {
var scrollHandler = function() {
forEach(getDocument().querySelectorAll("[hx-trigger='revealed'],[data-hx-trigger='revealed']"), function (elt) {
maybeReveal(elt);
});
windowIsScrolling = true
};
window['htmxScrollHandler'] = scrollHandler;
window.addEventListener("scroll", scrollHandler)
setInterval(function() {
if (windowIsScrolling) {
windowIsScrolling = false;
forEach(getDocument().querySelectorAll("[hx-trigger='revealed'],[data-hx-trigger='revealed']"), function (elt) {
maybeReveal(elt);
})
}
}, 200);
}
}
@ -778,9 +810,9 @@ return (function () {
}
function processWebSocketInfo(elt, nodeData, info) {
var values = info.split(",");
var values = splitOnWhitespace(info);
for (var i = 0; i < values.length; i++) {
var value = splitOnWhitespace(values[i]);
var value = values[i].split(/:(.+)/);
if (value[0] === "connect") {
processWebSocketSource(elt, value[1]);
}
@ -791,6 +823,9 @@ return (function () {
}
function processWebSocketSource(elt, wssSource) {
if (wssSource.indexOf("ws:") !== 0 && wssSource.indexOf("wss:") !== 0) {
wssSource = "wss:" + wssSource;
}
var socket = htmx.createWebSocket(wssSource);
socket.onerror = function (e) {
triggerErrorEvent(elt, "htmx:wsError", {error:e, socket:socket});
@ -847,20 +882,21 @@ return (function () {
}
}
function maybeCloseSSESource(elt) {
if (!bodyContains(elt)) {
getInternalData(elt).sseEventSource.close();
return true;
}
}
//====================================================================
// Server Sent Events
//====================================================================
function processSSEInfo(elt, nodeData, info) {
var values = info.split(",");
var values = splitOnWhitespace(info);
for (var i = 0; i < values.length; i++) {
var value = splitOnWhitespace(values[i]);
var value = values[i].split(/:(.+)/);
if (value[0] === "connect") {
processSSESource(elt, value[1]);
}
if ((value[0] === "swap")) {
processSSESwap(elt, value[1])
}
}
}
@ -873,10 +909,41 @@ return (function () {
getInternalData(elt).sseEventSource = source;
}
function processSSETrigger(elt, verb, path, sseEventName) {
var sseSourceElt = getClosestMatch(elt, function (parent) {
return getInternalData(parent).sseEventSource != null;
function processSSESwap(elt, sseEventName) {
var sseSourceElt = getClosestMatch(elt, hasEventSource);
if (sseSourceElt) {
var sseEventSource = getInternalData(sseSourceElt).sseEventSource;
var sseListener = function (event) {
if (maybeCloseSSESource(sseSourceElt)) {
sseEventSource.removeEventListener(sseEventName, sseListener);
return;
}
///////////////////////////
// TODO: merge this code with AJAX and WebSockets code in the future.
var response = event.data;
withExtensions(elt, function(extension){
response = extension.transformResponse(response, null, elt);
});
var swapSpec = getSwapSpecification(elt)
var target = getTarget(elt)
var settleInfo = makeSettleInfo(elt);
selectAndSwap(swapSpec.swapStyle, elt, target, response, settleInfo)
triggerEvent(elt, "htmx:sseMessage", event)
};
getInternalData(elt).sseListener = sseListener;
sseEventSource.addEventListener(sseEventName, sseListener);
} else {
triggerErrorEvent(elt, "htmx:noSSESourceError");
}
}
function processSSETrigger(elt, verb, path, sseEventName) {
var sseSourceElt = getClosestMatch(elt, hasEventSource);
if (sseSourceElt) {
var sseEventSource = getInternalData(sseSourceElt).sseEventSource;
var sseListener = function () {
@ -895,6 +962,19 @@ return (function () {
}
}
function maybeCloseSSESource(elt) {
if (!bodyContains(elt)) {
getInternalData(elt).sseEventSource.close();
return true;
}
}
function hasEventSource(node) {
return getInternalData(node).sseEventSource != null;
}
//====================================================================
function loadImmediately(elt, verb, path, nodeData, delay) {
var load = function(){
if (!nodeData.loaded) {
@ -956,13 +1036,10 @@ return (function () {
});
}
function isHyperScriptAvailable() {
return typeof _hyperscript !== "undefined";
}
function findElementsToProcess(elt) {
if (elt.querySelectorAll) {
var results = elt.querySelectorAll(VERB_SELECTOR + ", a, form, [hx-sse], [data-hx-sse], [hx-ws], [data-hx-ws]");
var results = elt.querySelectorAll(VERB_SELECTOR + ", a, form, [hx-sse], [data-hx-sse], [hx-ws]," +
" [data-hx-ws]");
return results;
} else {
return [];
@ -974,10 +1051,6 @@ return (function () {
if (!nodeData.initialized) {
nodeData.initialized = true;
if (isHyperScriptAvailable()) {
_hyperscript.init(elt);
}
if (elt.value) {
nodeData.lastValue = elt.value;
}
@ -1011,6 +1084,10 @@ return (function () {
// Event/Log Support
//====================================================================
function kebabEventName(str) {
return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
}
function makeEvent(eventName, detail) {
var evt;
if (window.CustomEvent && typeof window.CustomEvent === 'function') {
@ -1062,6 +1139,10 @@ return (function () {
triggerEvent(elt, "htmx:error", {errorInfo:detail})
}
var eventResult = elt.dispatchEvent(event);
if (eventResult) {
var kebabedEvent = makeEvent(kebabEventName(eventName), event.detail);
eventResult = eventResult && elt.dispatchEvent(kebabedEvent)
}
withExtensions(elt, function (extension) {
eventResult = eventResult && (extension.onEvent(eventName, event) !== false)
});
@ -1231,6 +1312,9 @@ return (function () {
if (shouldInclude(elt)) {
var name = getRawAttribute(elt,"name");
var value = elt.value;
if (!!getRawAttribute(elt, 'multiple')) {
value = toArray(elt.querySelectorAll("option:checked")).map(function (e) { return e.value });
}
if (name != null && value != null) {
var current = values[name];
if(current) {
@ -1442,6 +1526,30 @@ return (function () {
addExpressionVars(parentElt(elt), rawParameters);
}
function safelySetHeaderValue(xhr, header, headerValue) {
if (headerValue !== null) {
try {
xhr.setRequestHeader(header, headerValue);
} catch (e) {
// On an exception, try to set the header URI encoded instead
xhr.setRequestHeader(header, encodeURIComponent(headerValue));
xhr.setRequestHeader(header + "-URI-AutoEncoded", "true");
}
}
}
function getResponseURL(xhr) {
// NB: IE11 does not support this stuff
if (xhr.responseURL && typeof(URL) !== "undefined") {
try {
var url = new URL(xhr.responseURL);
return url.pathname + url.search;
} catch (e) {
triggerErrorEvent(getDocument().body, "htmx:badResponseUrl", {url: xhr.responseURL});
}
}
}
function issueAjaxRequest(elt, verb, path, eventTarget) {
var target = getTarget(elt);
if (target == null) {
@ -1535,7 +1643,8 @@ return (function () {
// request headers
for (var header in headers) {
if (headers.hasOwnProperty(header)) {
if (headers[header] !== null) xhr.setRequestHeader(header, headers[header]);
var headerValue = headers[header];
safelySetHeaderValue(xhr, header, headerValue);
}
}
@ -1553,7 +1662,7 @@ return (function () {
if (this.status === 286) {
cancelPolling(elt);
}
// don't process 'No Content' response
// don't process 'No Content'
if (this.status !== 204) {
if (!triggerEvent(target, 'htmx:beforeSwap', eventDetail)) return;
@ -1613,7 +1722,7 @@ return (function () {
});
// push URL and save new page
if (shouldSaveHistory) {
var pathToPush = pushedUrl || getPushUrl(elt) || finalPathForGet || path;
var pathToPush = pushedUrl || getPushUrl(elt) || getResponseURL(xhr) || finalPathForGet || path;
pushUrlIntoHistory(pathToPush);
triggerEvent(getDocument().body, 'htmx:pushedIntoHistory', {path:pathToPush});
}
@ -1645,8 +1754,9 @@ return (function () {
throw e;
} finally {
removeRequestIndicatorClasses(elt);
triggerEvent(elt, 'htmx:afterRequest', eventDetail);
triggerEvent(elt, 'htmx:afterOnLoad', eventDetail);
var finalElt = getInternalData(elt).replacedWith || elt;
triggerEvent(finalElt, 'htmx:afterRequest', eventDetail);
triggerEvent(finalElt, 'htmx:afterOnLoad', eventDetail);
endRequestLock();
}
}
@ -1747,7 +1857,7 @@ return (function () {
mergeMetaConfig();
insertIndicatorStyles();
var body = getDocument().body;
processNode(body, true);
processNode(body);
triggerEvent(body, 'htmx:load', {});
window.onpopstate = function (event) {
if (event.state && event.state.htmx) {

2
dist/htmx.min.js vendored

File diff suppressed because one or more lines are too long

BIN
dist/htmx.min.js.gz vendored

Binary file not shown.

View File

@ -41,6 +41,7 @@ return (function () {
requestClass:'htmx-request',
settlingClass:'htmx-settling',
swappingClass:'htmx-swapping',
attributesToSwizzle:["class", "style", "width", "height"]
},
parseInterval:parseInterval,
_:internalEval,
@ -57,6 +58,8 @@ return (function () {
return "[hx-" + verb + "], [data-hx-" + verb + "]"
}).join(", ");
var windowIsScrolling = false // used by initScrollHandler
//====================================================================
// Utilities
//====================================================================
@ -216,7 +219,7 @@ return (function () {
}
function splitOnWhitespace(trigger) {
return trigger.split(/\s+/);
return trigger.trim().split(/\s+/);
}
function mergeObjects(obj1, obj2) {
@ -362,6 +365,8 @@ return (function () {
return explicitTarget;
} else if (targetStr.indexOf("closest ") === 0) {
return closest(elt, targetStr.substr(8));
} else if (targetStr.indexOf("find ") === 0) {
return find(elt, targetStr.substr(5));
} else {
return getDocument().querySelector(targetStr);
}
@ -375,15 +380,24 @@ return (function () {
}
}
var EXCLUDED_ATTRIBUTES = ['id', 'value'];
function shouldSettleAttribute(name) {
var attributesToSwizzle = htmx.config.attributesToSwizzle;
for (var i = 0; i < attributesToSwizzle.length; i++) {
if (name === attributesToSwizzle[i]) {
return true;
}
}
return false;
}
function cloneAttributes(mergeTo, mergeFrom) {
forEach(mergeTo.attributes, function (attr) {
if (!mergeFrom.hasAttribute(attr.name) && EXCLUDED_ATTRIBUTES.indexOf(attr.name) === -1) {
if (!mergeFrom.hasAttribute(attr.name) && shouldSettleAttribute(attr.name)) {
mergeTo.removeAttribute(attr.name)
}
});
forEach(mergeFrom.attributes, function (attr) {
if (EXCLUDED_ATTRIBUTES.indexOf(attr.name) === -1) {
if (shouldSettleAttribute(attr.name)) {
mergeTo.setAttribute(attr.name, attr.value);
}
});
@ -450,12 +464,21 @@ return (function () {
function makeAjaxLoadTask(child) {
return function () {
processNode(child, true);
processNode(child);
processScripts(child);
triggerEvent(child, 'htmx:load', {});
processFocus(child)
triggerEvent(child, 'htmx:load');
};
}
function processFocus(child) {
var autofocus = "[autofocus]";
var autoFocusedElt = matches(child, autofocus) ? child : child.querySelector(autofocus)
if (autoFocusedElt != null) {
autoFocusedElt.focus();
}
}
function insertNodesBefore(parentNode, insertBefore, fragment, settleInfo) {
handleAttributes(parentNode, fragment, settleInfo);
while(fragment.childNodes.length > 0){
@ -491,6 +514,7 @@ return (function () {
} else {
var newElt = eltBeforeNewContent.nextSibling;
}
getInternalData(target).replacedWith = newElt; // tuck away so we can fire events on it later
while(newElt && newElt !== target) {
settleInfo.elts.push(newElt);
newElt = newElt.nextSibling;
@ -521,8 +545,10 @@ return (function () {
insertNodesBefore(target, firstChild, fragment, settleInfo);
if (firstChild) {
while (firstChild.nextSibling) {
closeConnections(firstChild.nextSibling)
target.removeChild(firstChild.nextSibling);
}
closeConnections(firstChild)
target.removeChild(firstChild);
}
}
@ -760,12 +786,18 @@ return (function () {
function initScrollHandler() {
if (!window['htmxScrollHandler']) {
var scrollHandler = function() {
forEach(getDocument().querySelectorAll("[hx-trigger='revealed'],[data-hx-trigger='revealed']"), function (elt) {
maybeReveal(elt);
});
windowIsScrolling = true
};
window['htmxScrollHandler'] = scrollHandler;
window.addEventListener("scroll", scrollHandler)
setInterval(function() {
if (windowIsScrolling) {
windowIsScrolling = false;
forEach(getDocument().querySelectorAll("[hx-trigger='revealed'],[data-hx-trigger='revealed']"), function (elt) {
maybeReveal(elt);
})
}
}, 200);
}
}
@ -778,9 +810,9 @@ return (function () {
}
function processWebSocketInfo(elt, nodeData, info) {
var values = info.split(",");
var values = splitOnWhitespace(info);
for (var i = 0; i < values.length; i++) {
var value = splitOnWhitespace(values[i]);
var value = values[i].split(/:(.+)/);
if (value[0] === "connect") {
processWebSocketSource(elt, value[1]);
}
@ -791,6 +823,9 @@ return (function () {
}
function processWebSocketSource(elt, wssSource) {
if (wssSource.indexOf("ws:") !== 0 && wssSource.indexOf("wss:") !== 0) {
wssSource = "wss:" + wssSource;
}
var socket = htmx.createWebSocket(wssSource);
socket.onerror = function (e) {
triggerErrorEvent(elt, "htmx:wsError", {error:e, socket:socket});
@ -847,20 +882,21 @@ return (function () {
}
}
function maybeCloseSSESource(elt) {
if (!bodyContains(elt)) {
getInternalData(elt).sseEventSource.close();
return true;
}
}
//====================================================================
// Server Sent Events
//====================================================================
function processSSEInfo(elt, nodeData, info) {
var values = info.split(",");
var values = splitOnWhitespace(info);
for (var i = 0; i < values.length; i++) {
var value = splitOnWhitespace(values[i]);
var value = values[i].split(/:(.+)/);
if (value[0] === "connect") {
processSSESource(elt, value[1]);
}
if ((value[0] === "swap")) {
processSSESwap(elt, value[1])
}
}
}
@ -873,10 +909,41 @@ return (function () {
getInternalData(elt).sseEventSource = source;
}
function processSSETrigger(elt, verb, path, sseEventName) {
var sseSourceElt = getClosestMatch(elt, function (parent) {
return getInternalData(parent).sseEventSource != null;
function processSSESwap(elt, sseEventName) {
var sseSourceElt = getClosestMatch(elt, hasEventSource);
if (sseSourceElt) {
var sseEventSource = getInternalData(sseSourceElt).sseEventSource;
var sseListener = function (event) {
if (maybeCloseSSESource(sseSourceElt)) {
sseEventSource.removeEventListener(sseEventName, sseListener);
return;
}
///////////////////////////
// TODO: merge this code with AJAX and WebSockets code in the future.
var response = event.data;
withExtensions(elt, function(extension){
response = extension.transformResponse(response, null, elt);
});
var swapSpec = getSwapSpecification(elt)
var target = getTarget(elt)
var settleInfo = makeSettleInfo(elt);
selectAndSwap(swapSpec.swapStyle, elt, target, response, settleInfo)
triggerEvent(elt, "htmx:sseMessage", event)
};
getInternalData(elt).sseListener = sseListener;
sseEventSource.addEventListener(sseEventName, sseListener);
} else {
triggerErrorEvent(elt, "htmx:noSSESourceError");
}
}
function processSSETrigger(elt, verb, path, sseEventName) {
var sseSourceElt = getClosestMatch(elt, hasEventSource);
if (sseSourceElt) {
var sseEventSource = getInternalData(sseSourceElt).sseEventSource;
var sseListener = function () {
@ -895,6 +962,19 @@ return (function () {
}
}
function maybeCloseSSESource(elt) {
if (!bodyContains(elt)) {
getInternalData(elt).sseEventSource.close();
return true;
}
}
function hasEventSource(node) {
return getInternalData(node).sseEventSource != null;
}
//====================================================================
function loadImmediately(elt, verb, path, nodeData, delay) {
var load = function(){
if (!nodeData.loaded) {
@ -956,13 +1036,10 @@ return (function () {
});
}
function isHyperScriptAvailable() {
return typeof _hyperscript !== "undefined";
}
function findElementsToProcess(elt) {
if (elt.querySelectorAll) {
var results = elt.querySelectorAll(VERB_SELECTOR + ", a, form, [hx-sse], [data-hx-sse], [hx-ws], [data-hx-ws]");
var results = elt.querySelectorAll(VERB_SELECTOR + ", a, form, [hx-sse], [data-hx-sse], [hx-ws]," +
" [data-hx-ws]");
return results;
} else {
return [];
@ -974,10 +1051,6 @@ return (function () {
if (!nodeData.initialized) {
nodeData.initialized = true;
if (isHyperScriptAvailable()) {
_hyperscript.init(elt);
}
if (elt.value) {
nodeData.lastValue = elt.value;
}
@ -1011,6 +1084,10 @@ return (function () {
// Event/Log Support
//====================================================================
function kebabEventName(str) {
return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
}
function makeEvent(eventName, detail) {
var evt;
if (window.CustomEvent && typeof window.CustomEvent === 'function') {
@ -1062,6 +1139,10 @@ return (function () {
triggerEvent(elt, "htmx:error", {errorInfo:detail})
}
var eventResult = elt.dispatchEvent(event);
if (eventResult) {
var kebabedEvent = makeEvent(kebabEventName(eventName), event.detail);
eventResult = eventResult && elt.dispatchEvent(kebabedEvent)
}
withExtensions(elt, function (extension) {
eventResult = eventResult && (extension.onEvent(eventName, event) !== false)
});
@ -1231,6 +1312,9 @@ return (function () {
if (shouldInclude(elt)) {
var name = getRawAttribute(elt,"name");
var value = elt.value;
if (!!getRawAttribute(elt, 'multiple')) {
value = toArray(elt.querySelectorAll("option:checked")).map(function (e) { return e.value });
}
if (name != null && value != null) {
var current = values[name];
if(current) {
@ -1442,6 +1526,30 @@ return (function () {
addExpressionVars(parentElt(elt), rawParameters);
}
function safelySetHeaderValue(xhr, header, headerValue) {
if (headerValue !== null) {
try {
xhr.setRequestHeader(header, headerValue);
} catch (e) {
// On an exception, try to set the header URI encoded instead
xhr.setRequestHeader(header, encodeURIComponent(headerValue));
xhr.setRequestHeader(header + "-URI-AutoEncoded", "true");
}
}
}
function getResponseURL(xhr) {
// NB: IE11 does not support this stuff
if (xhr.responseURL && typeof(URL) !== "undefined") {
try {
var url = new URL(xhr.responseURL);
return url.pathname + url.search;
} catch (e) {
triggerErrorEvent(getDocument().body, "htmx:badResponseUrl", {url: xhr.responseURL});
}
}
}
function issueAjaxRequest(elt, verb, path, eventTarget) {
var target = getTarget(elt);
if (target == null) {
@ -1535,7 +1643,8 @@ return (function () {
// request headers
for (var header in headers) {
if (headers.hasOwnProperty(header)) {
if (headers[header] !== null) xhr.setRequestHeader(header, headers[header]);
var headerValue = headers[header];
safelySetHeaderValue(xhr, header, headerValue);
}
}
@ -1553,7 +1662,7 @@ return (function () {
if (this.status === 286) {
cancelPolling(elt);
}
// don't process 'No Content' response
// don't process 'No Content'
if (this.status !== 204) {
if (!triggerEvent(target, 'htmx:beforeSwap', eventDetail)) return;
@ -1613,7 +1722,7 @@ return (function () {
});
// push URL and save new page
if (shouldSaveHistory) {
var pathToPush = pushedUrl || getPushUrl(elt) || finalPathForGet || path;
var pathToPush = pushedUrl || getPushUrl(elt) || getResponseURL(xhr) || finalPathForGet || path;
pushUrlIntoHistory(pathToPush);
triggerEvent(getDocument().body, 'htmx:pushedIntoHistory', {path:pathToPush});
}
@ -1645,8 +1754,9 @@ return (function () {
throw e;
} finally {
removeRequestIndicatorClasses(elt);
triggerEvent(elt, 'htmx:afterRequest', eventDetail);
triggerEvent(elt, 'htmx:afterOnLoad', eventDetail);
var finalElt = getInternalData(elt).replacedWith || elt;
triggerEvent(finalElt, 'htmx:afterRequest', eventDetail);
triggerEvent(finalElt, 'htmx:afterOnLoad', eventDetail);
endRequestLock();
}
}
@ -1747,7 +1857,7 @@ return (function () {
mergeMetaConfig();
insertIndicatorStyles();
var body = getDocument().body;
processNode(body, true);
processNode(body);
triggerEvent(body, 'htmx:load', {});
window.onpopstate = function (event) {
if (event.state && event.state.htmx) {

View File

@ -41,6 +41,7 @@ return (function () {
requestClass:'htmx-request',
settlingClass:'htmx-settling',
swappingClass:'htmx-swapping',
attributesToSwizzle:["class", "style", "width", "height"]
},
parseInterval:parseInterval,
_:internalEval,
@ -57,6 +58,8 @@ return (function () {
return "[hx-" + verb + "], [data-hx-" + verb + "]"
}).join(", ");
var windowIsScrolling = false // used by initScrollHandler
//====================================================================
// Utilities
//====================================================================
@ -216,7 +219,7 @@ return (function () {
}
function splitOnWhitespace(trigger) {
return trigger.split(/\s+/);
return trigger.trim().split(/\s+/);
}
function mergeObjects(obj1, obj2) {
@ -362,6 +365,8 @@ return (function () {
return explicitTarget;
} else if (targetStr.indexOf("closest ") === 0) {
return closest(elt, targetStr.substr(8));
} else if (targetStr.indexOf("find ") === 0) {
return find(elt, targetStr.substr(5));
} else {
return getDocument().querySelector(targetStr);
}
@ -375,15 +380,24 @@ return (function () {
}
}
var EXCLUDED_ATTRIBUTES = ['id', 'value'];
function shouldSettleAttribute(name) {
var attributesToSwizzle = htmx.config.attributesToSwizzle;
for (var i = 0; i < attributesToSwizzle.length; i++) {
if (name === attributesToSwizzle[i]) {
return true;
}
}
return false;
}
function cloneAttributes(mergeTo, mergeFrom) {
forEach(mergeTo.attributes, function (attr) {
if (!mergeFrom.hasAttribute(attr.name) && EXCLUDED_ATTRIBUTES.indexOf(attr.name) === -1) {
if (!mergeFrom.hasAttribute(attr.name) && shouldSettleAttribute(attr.name)) {
mergeTo.removeAttribute(attr.name)
}
});
forEach(mergeFrom.attributes, function (attr) {
if (EXCLUDED_ATTRIBUTES.indexOf(attr.name) === -1) {
if (shouldSettleAttribute(attr.name)) {
mergeTo.setAttribute(attr.name, attr.value);
}
});
@ -450,12 +464,21 @@ return (function () {
function makeAjaxLoadTask(child) {
return function () {
processNode(child, true);
processNode(child);
processScripts(child);
triggerEvent(child, 'htmx:load', {});
processFocus(child)
triggerEvent(child, 'htmx:load');
};
}
function processFocus(child) {
var autofocus = "[autofocus]";
var autoFocusedElt = matches(child, autofocus) ? child : child.querySelector(autofocus)
if (autoFocusedElt != null) {
autoFocusedElt.focus();
}
}
function insertNodesBefore(parentNode, insertBefore, fragment, settleInfo) {
handleAttributes(parentNode, fragment, settleInfo);
while(fragment.childNodes.length > 0){
@ -491,6 +514,7 @@ return (function () {
} else {
var newElt = eltBeforeNewContent.nextSibling;
}
getInternalData(target).replacedWith = newElt; // tuck away so we can fire events on it later
while(newElt && newElt !== target) {
settleInfo.elts.push(newElt);
newElt = newElt.nextSibling;
@ -521,8 +545,10 @@ return (function () {
insertNodesBefore(target, firstChild, fragment, settleInfo);
if (firstChild) {
while (firstChild.nextSibling) {
closeConnections(firstChild.nextSibling)
target.removeChild(firstChild.nextSibling);
}
closeConnections(firstChild)
target.removeChild(firstChild);
}
}
@ -760,12 +786,18 @@ return (function () {
function initScrollHandler() {
if (!window['htmxScrollHandler']) {
var scrollHandler = function() {
forEach(getDocument().querySelectorAll("[hx-trigger='revealed'],[data-hx-trigger='revealed']"), function (elt) {
maybeReveal(elt);
});
windowIsScrolling = true
};
window['htmxScrollHandler'] = scrollHandler;
window.addEventListener("scroll", scrollHandler)
setInterval(function() {
if (windowIsScrolling) {
windowIsScrolling = false;
forEach(getDocument().querySelectorAll("[hx-trigger='revealed'],[data-hx-trigger='revealed']"), function (elt) {
maybeReveal(elt);
})
}
}, 200);
}
}
@ -778,9 +810,9 @@ return (function () {
}
function processWebSocketInfo(elt, nodeData, info) {
var values = info.split(",");
var values = splitOnWhitespace(info);
for (var i = 0; i < values.length; i++) {
var value = splitOnWhitespace(values[i]);
var value = values[i].split(/:(.+)/);
if (value[0] === "connect") {
processWebSocketSource(elt, value[1]);
}
@ -791,6 +823,9 @@ return (function () {
}
function processWebSocketSource(elt, wssSource) {
if (wssSource.indexOf("ws:") !== 0 && wssSource.indexOf("wss:") !== 0) {
wssSource = "wss:" + wssSource;
}
var socket = htmx.createWebSocket(wssSource);
socket.onerror = function (e) {
triggerErrorEvent(elt, "htmx:wsError", {error:e, socket:socket});
@ -847,20 +882,21 @@ return (function () {
}
}
function maybeCloseSSESource(elt) {
if (!bodyContains(elt)) {
getInternalData(elt).sseEventSource.close();
return true;
}
}
//====================================================================
// Server Sent Events
//====================================================================
function processSSEInfo(elt, nodeData, info) {
var values = info.split(",");
var values = splitOnWhitespace(info);
for (var i = 0; i < values.length; i++) {
var value = splitOnWhitespace(values[i]);
var value = values[i].split(/:(.+)/);
if (value[0] === "connect") {
processSSESource(elt, value[1]);
}
if ((value[0] === "swap")) {
processSSESwap(elt, value[1])
}
}
}
@ -873,10 +909,41 @@ return (function () {
getInternalData(elt).sseEventSource = source;
}
function processSSETrigger(elt, verb, path, sseEventName) {
var sseSourceElt = getClosestMatch(elt, function (parent) {
return getInternalData(parent).sseEventSource != null;
function processSSESwap(elt, sseEventName) {
var sseSourceElt = getClosestMatch(elt, hasEventSource);
if (sseSourceElt) {
var sseEventSource = getInternalData(sseSourceElt).sseEventSource;
var sseListener = function (event) {
if (maybeCloseSSESource(sseSourceElt)) {
sseEventSource.removeEventListener(sseEventName, sseListener);
return;
}
///////////////////////////
// TODO: merge this code with AJAX and WebSockets code in the future.
var response = event.data;
withExtensions(elt, function(extension){
response = extension.transformResponse(response, null, elt);
});
var swapSpec = getSwapSpecification(elt)
var target = getTarget(elt)
var settleInfo = makeSettleInfo(elt);
selectAndSwap(swapSpec.swapStyle, elt, target, response, settleInfo)
triggerEvent(elt, "htmx:sseMessage", event)
};
getInternalData(elt).sseListener = sseListener;
sseEventSource.addEventListener(sseEventName, sseListener);
} else {
triggerErrorEvent(elt, "htmx:noSSESourceError");
}
}
function processSSETrigger(elt, verb, path, sseEventName) {
var sseSourceElt = getClosestMatch(elt, hasEventSource);
if (sseSourceElt) {
var sseEventSource = getInternalData(sseSourceElt).sseEventSource;
var sseListener = function () {
@ -895,6 +962,19 @@ return (function () {
}
}
function maybeCloseSSESource(elt) {
if (!bodyContains(elt)) {
getInternalData(elt).sseEventSource.close();
return true;
}
}
function hasEventSource(node) {
return getInternalData(node).sseEventSource != null;
}
//====================================================================
function loadImmediately(elt, verb, path, nodeData, delay) {
var load = function(){
if (!nodeData.loaded) {
@ -956,13 +1036,10 @@ return (function () {
});
}
function isHyperScriptAvailable() {
return typeof _hyperscript !== "undefined";
}
function findElementsToProcess(elt) {
if (elt.querySelectorAll) {
var results = elt.querySelectorAll(VERB_SELECTOR + ", a, form, [hx-sse], [data-hx-sse], [hx-ws], [data-hx-ws]");
var results = elt.querySelectorAll(VERB_SELECTOR + ", a, form, [hx-sse], [data-hx-sse], [hx-ws]," +
" [data-hx-ws]");
return results;
} else {
return [];
@ -974,10 +1051,6 @@ return (function () {
if (!nodeData.initialized) {
nodeData.initialized = true;
if (isHyperScriptAvailable()) {
_hyperscript.init(elt);
}
if (elt.value) {
nodeData.lastValue = elt.value;
}
@ -1011,6 +1084,10 @@ return (function () {
// Event/Log Support
//====================================================================
function kebabEventName(str) {
return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
}
function makeEvent(eventName, detail) {
var evt;
if (window.CustomEvent && typeof window.CustomEvent === 'function') {
@ -1062,6 +1139,10 @@ return (function () {
triggerEvent(elt, "htmx:error", {errorInfo:detail})
}
var eventResult = elt.dispatchEvent(event);
if (eventResult) {
var kebabedEvent = makeEvent(kebabEventName(eventName), event.detail);
eventResult = eventResult && elt.dispatchEvent(kebabedEvent)
}
withExtensions(elt, function (extension) {
eventResult = eventResult && (extension.onEvent(eventName, event) !== false)
});
@ -1231,6 +1312,9 @@ return (function () {
if (shouldInclude(elt)) {
var name = getRawAttribute(elt,"name");
var value = elt.value;
if (!!getRawAttribute(elt, 'multiple')) {
value = toArray(elt.querySelectorAll("option:checked")).map(function (e) { return e.value });
}
if (name != null && value != null) {
var current = values[name];
if(current) {
@ -1442,6 +1526,30 @@ return (function () {
addExpressionVars(parentElt(elt), rawParameters);
}
function safelySetHeaderValue(xhr, header, headerValue) {
if (headerValue !== null) {
try {
xhr.setRequestHeader(header, headerValue);
} catch (e) {
// On an exception, try to set the header URI encoded instead
xhr.setRequestHeader(header, encodeURIComponent(headerValue));
xhr.setRequestHeader(header + "-URI-AutoEncoded", "true");
}
}
}
function getResponseURL(xhr) {
// NB: IE11 does not support this stuff
if (xhr.responseURL && typeof(URL) !== "undefined") {
try {
var url = new URL(xhr.responseURL);
return url.pathname + url.search;
} catch (e) {
triggerErrorEvent(getDocument().body, "htmx:badResponseUrl", {url: xhr.responseURL});
}
}
}
function issueAjaxRequest(elt, verb, path, eventTarget) {
var target = getTarget(elt);
if (target == null) {
@ -1535,7 +1643,8 @@ return (function () {
// request headers
for (var header in headers) {
if (headers.hasOwnProperty(header)) {
if (headers[header] !== null) xhr.setRequestHeader(header, headers[header]);
var headerValue = headers[header];
safelySetHeaderValue(xhr, header, headerValue);
}
}
@ -1553,7 +1662,7 @@ return (function () {
if (this.status === 286) {
cancelPolling(elt);
}
// don't process 'No Content' response
// don't process 'No Content'
if (this.status !== 204) {
if (!triggerEvent(target, 'htmx:beforeSwap', eventDetail)) return;
@ -1613,7 +1722,7 @@ return (function () {
});
// push URL and save new page
if (shouldSaveHistory) {
var pathToPush = pushedUrl || getPushUrl(elt) || finalPathForGet || path;
var pathToPush = pushedUrl || getPushUrl(elt) || getResponseURL(xhr) || finalPathForGet || path;
pushUrlIntoHistory(pathToPush);
triggerEvent(getDocument().body, 'htmx:pushedIntoHistory', {path:pathToPush});
}
@ -1645,8 +1754,9 @@ return (function () {
throw e;
} finally {
removeRequestIndicatorClasses(elt);
triggerEvent(elt, 'htmx:afterRequest', eventDetail);
triggerEvent(elt, 'htmx:afterOnLoad', eventDetail);
var finalElt = getInternalData(elt).replacedWith || elt;
triggerEvent(finalElt, 'htmx:afterRequest', eventDetail);
triggerEvent(finalElt, 'htmx:afterOnLoad', eventDetail);
endRequestLock();
}
}
@ -1747,7 +1857,7 @@ return (function () {
mergeMetaConfig();
insertIndicatorStyles();
var body = getDocument().body;
processNode(body, true);
processNode(body);
triggerEvent(body, 'htmx:load', {});
window.onpopstate = function (event) {
if (event.state && event.state.htmx) {

View File

@ -40,7 +40,7 @@ describe("hx-sse attribute", function() {
this.server.respondWith("GET", "/d1", "div1 updated");
this.server.respondWith("GET", "/d2", "div2 updated");
var div = make('<div hx-sse="connect /foo">' +
var div = make('<div hx-sse="connect:/foo">' +
'<div id="d1" hx-trigger="sse:e1" hx-get="/d1">div1</div>' +
'<div id="d2" hx-trigger="sse:e2" hx-get="/d2">div2</div>' +
'</div>');
@ -60,7 +60,7 @@ describe("hx-sse attribute", function() {
this.server.respondWith("GET", "/d1", "div1 updated");
var div = make('<div hx-sse="connect /foo">' +
var div = make('<div hx-sse="connect:/foo">' +
'<div id="d1" hx-trigger="sse:e1" hx-get="/d1">div1</div>' +
'</div>');
@ -81,7 +81,7 @@ describe("hx-sse attribute", function() {
this.server.respondWith("GET", "/d1", "div1 updated");
var div = make('<div hx-sse="connect /foo"></div>' +
var div = make('<div hx-sse="connect:/foo"></div>' +
'<div id="d1" hx-trigger="sse:e1" hx-get="/d1">div1</div>');
this.eventSource.sendEvent("foo");
@ -99,7 +99,7 @@ describe("hx-sse attribute", function() {
it('is closed after removal', function () {
this.server.respondWith("GET", "/test", "Clicked!");
var div = make('<div hx-get="/test" hx-swap="outerHTML" hx-sse="connect /foo">' +
var div = make('<div hx-get="/test" hx-swap="outerHTML" hx-sse="connect:/foo">' +
'<div id="d1" hx-trigger="sse:e1" hx-get="/d1">div1</div>' +
'</div>');
div.click();
@ -108,7 +108,7 @@ describe("hx-sse attribute", function() {
})
it('is closed after removal with no close and activity', function () {
var div = make('<div hx-get="/test" hx-swap="outerHTML" hx-sse="connect /foo">' +
var div = make('<div hx-get="/test" hx-swap="outerHTML" hx-sse="connect:/foo">' +
'<div id="d1" hx-trigger="sse:e1" hx-get="/d1">div1</div>' +
'</div>');
div.parentElement.removeChild(div);

View File

@ -48,6 +48,18 @@ describe("hx-target attribute", function(){
div1.innerHTML.should.equal("Clicked!");
});
it('targets a `find` element properly', function()
{
this.server.respondWith("GET", "/test", "Clicked!");
var div1 = make('<div hx-target="find span" hx-get="/test">Click Me! <div><span id="s1"></span><span id="s2"></span></div></div>')
div1.click();
this.server.respond();
var span1 = byId("s1")
var span2 = byId("s2")
span1.innerHTML.should.equal("Clicked!");
span2.innerHTML.should.equal("");
});
it('targets an inner element properly', function()
{
this.server.respondWith("GET", "/test", "Clicked!");

View File

@ -40,14 +40,14 @@ describe("hx-ws attribute", function() {
});
it('handles a basic call back', function () {
var div = make('<div hx-ws="connect wss:/foo"><div id="d1">div1</div><div id="d2">div2</div></div>');
var div = make('<div hx-ws="connect:/foo"><div id="d1">div1</div><div id="d2">div2</div></div>');
this.socket.write("<div id=\"d1\">replaced</div>")
byId("d1").innerHTML.should.equal("replaced");
byId("d2").innerHTML.should.equal("div2");
})
it('handles a basic send', function () {
var div = make('<div hx-ws="connect wss:/foo"><div hx-ws="send" id="d1">div1</div></div>');
var div = make('<div hx-ws="connect:/foo"><div hx-ws="send" id="d1">div1</div></div>');
byId("d1").click();
var lastSent = this.socket.getLastSent();
var data = JSON.parse(lastSent);
@ -56,14 +56,14 @@ describe("hx-ws attribute", function() {
it('is closed after removal', function () {
this.server.respondWith("GET", "/test", "Clicked!");
var div = make('<div hx-get="/test" hx-swap="outerHTML" hx-ws="connect wss:/foo"></div>');
var div = make('<div hx-get="/test" hx-swap="outerHTML" hx-ws="connect:wss:/foo"></div>');
div.click();
this.server.respond();
this.socket.wasClosed().should.equal(true)
})
it('is closed after removal with no close and activity', function () {
var div = make('<div hx-ws="connect wss:/foo"></div>');
var div = make('<div hx-ws="connect:/foo"></div>');
div.parentElement.removeChild(div);
this.socket.write("<div id=\"d1\">replaced</div>")
this.socket.wasClosed().should.equal(true)

View File

@ -240,6 +240,36 @@ describe("Core htmx AJAX Tests", function(){
btn.innerHTML.should.equal("Click Me!");
});
it('handles 304 NOT MODIFIED responses properly', function()
{
this.server.respondWith("GET", "/test-1", [200, {}, "Content for Tab 1"]);
this.server.respondWith("GET", "/test-2", [200, {}, "Content for Tab 2"]);
var target = make('<div id="target"></div>')
var btn1 = make('<button hx-get="/test-1" hx-target="#target">Tab 1</button>');
var btn2 = make('<button hx-get="/test-2" hx-target="#target">Tab 2</button>');
btn1.click();
target.innerHTML.should.equal("");
this.server.respond();
target.innerHTML.should.equal("Content for Tab 1");
btn2.click();
this.server.respond();
target.innerHTML.should.equal("Content for Tab 2");
this.server.respondWith("GET", "/test-1", [304, {}, "Content for Tab 1"]);
this.server.respondWith("GET", "/test-2", [304, {}, "Content for Tab 2"]);
btn1.click();
this.server.respond();
target.innerHTML.should.equal("Content for Tab 1");
btn2.click();
this.server.respond();
target.innerHTML.should.equal("Content for Tab 2");
});
it('handles hx-trigger with non-default value', function()
{
this.server.respondWith("GET", "/test", "Clicked!");
@ -323,17 +353,49 @@ describe("Core htmx AJAX Tests", function(){
it('properly settles attributes on interior elements', function(done)
{
this.server.respondWith("GET", "/test", "<div hx-get='/test'><div foo='bar' id='d1'></div></div>");
this.server.respondWith("GET", "/test", "<div hx-get='/test'><div width='bar' id='d1'></div></div>");
var div = make("<div hx-get='/test' hx-swap='outerHTML settle:10ms'><div id='d1'></div></div>");
div.click();
this.server.respond();
should.equal(byId("d1").getAttribute("foo"), null);
should.equal(byId("d1").getAttribute("width"), null);
setTimeout(function () {
should.equal(byId("d1").getAttribute("foo"), "bar");
should.equal(byId("d1").getAttribute("width"), "bar");
done();
}, 20);
});
it('properly handles multiple select input', function()
{
var values;
this.server.respondWith("Post", "/test", function (xhr) {
values = getParameters(xhr);
xhr.respond(204, {}, "");
});
var form = make('<form hx-post="/test" hx-trigger="click">' +
'<select id="multiSelect" name="multiSelect" multiple="multiple">'+
'<option id="m1" value="m1">m1</option>'+
'<option id="m2" value="m2">m2</option>'+
'<option id="m3" value="m3">m3</option>'+
'<option id="m4" value="m4">m4</option>'+
'</form>');
form.click();
this.server.respond();
values.should.deep.equal({});
byId("m1").selected = true;
form.click();
this.server.respond();
values.should.deep.equal({multiSelect:"m1"});
byId("m1").selected = true;
byId("m3").selected = true;
form.click();
this.server.respond();
values.should.deep.equal({multiSelect:["m1", "m3"]});
});
it('properly handles checkbox inputs', function()
{
var values;
@ -487,5 +549,41 @@ describe("Core htmx AJAX Tests", function(){
input.value.should.equal('bar');
});
it('autofocus attribute works properly', function()
{
this.server.respondWith("GET", "/test", "<input id='i2' value='bar' autofocus/>");
var input = make("<input id='i1' hx-get='/test' value='foo' hx-swap='afterend' hx-trigger='click'/>");
input.focus();
input.click();
document.activeElement.should.equal(input);
this.server.respond();
var input2 = byId('i2');
document.activeElement.should.equal(input2);
});
it('autofocus attribute works properly w/ child', function()
{
this.server.respondWith("GET", "/test", "<div><input id='i2' value='bar' autofocus/></div>");
var input = make("<input id='i1' hx-get='/test' value='foo' hx-swap='afterend' hx-trigger='click'/>");
input.focus();
input.click();
document.activeElement.should.equal(input);
this.server.respond();
var input2 = byId('i2');
document.activeElement.should.equal(input2);
});
it('autofocus attribute works properly w/ true value', function()
{
this.server.respondWith("GET", "/test", "<div><input id='i2' value='bar' autofocus='true'/></div>");
var input = make("<input id='i1' hx-get='/test' value='foo' hx-swap='afterend' hx-trigger='click'/>");
input.focus();
input.click();
document.activeElement.should.equal(input);
this.server.respond();
var input2 = byId('i2');
document.activeElement.should.equal(input2);
});
})

View File

@ -44,6 +44,25 @@ describe("Core htmx Events", function() {
}
});
it("htmx:configRequest is also dispatched in kebab-case", function () {
var handler = htmx.on("htmx:config-request", function (evt) {
evt.detail.parameters['param'] = "true";
});
try {
var param = null;
this.server.respondWith("POST", "/test", function (xhr) {
param = getParameters(xhr)['param'];
xhr.respond(200, {}, "");
});
var div = make("<div hx-post='/test'></div>");
div.click();
this.server.respond();
param.should.equal("true");
} finally {
htmx.off("htmx:config-request", handler);
}
});
it("htmx:configRequest allows attribute removal", function () {
var param = "foo";
var handler = htmx.on("htmx:configRequest", function (evt) {
@ -172,21 +191,54 @@ describe("Core htmx Events", function() {
}
});
it("htmx:sendError is called after a failed request", function () {
it("htmx:sendError is called after a failed request", function (done) {
var called = false;
var handler = htmx.on("htmx:sendError", function (evt) {
called = true;
});
this.server.restore(); // turn off server mock so connection doesn't work
var div = make("<button hx-post='file://foo'>Foo</button>");
div.click();
setTimeout(function () {
htmx.off("htmx:sendError", handler);
should.equal(called, true);
done();
}, 30);
});
it("htmx:afterRequest is called when replacing outerHTML", function () {
var called = false;
var handler = htmx.on("htmx:afterRequest", function (evt) {
called = true;
});
try {
this.server.respondWith("POST", "/test", function (xhr) {
xhr.respond(200, {}, "");
xhr.respond(200, {}, "<button>Bar</button>");
});
var div = make("<button hx-post='/test'>Foo</button>");
var div = make("<button hx-post='/test' hx-swap='outerHTML'>Foo</button>");
div.click();
this.server.respond();
should.equal(called, true);
} finally {
htmx.off("htmx:sendError", handler);
htmx.off("htmx:afterRequest", handler);
}
});
it("htmx:afterOnLoad is called when replacing outerHTML", function () {
var called = false;
var handler = htmx.on("htmx:afterOnLoad", function (evt) {
called = true;
});
try {
this.server.respondWith("POST", "/test", function (xhr) {
xhr.respond(200, {}, "<button>Bar</button>");
});
var div = make("<button hx-post='/test' hx-swap='outerHTML'>Foo</button>");
div.click();
this.server.respond();
should.equal(called, true);
} finally {
htmx.off("htmx:afterOnLoad", handler);
}
});

View File

@ -1,12 +1,4 @@
describe("Core htmx internals Tests", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it("makeFragment works with janky stuff", function(){
htmx._("makeFragment")("<html></html>").tagName.should.equal("BODY");
@ -20,4 +12,12 @@ describe("Core htmx internals Tests", function() {
htmx._("makeFragment")("<tr></tr>").tagName.should.equal("TBODY");
})
it("set header works with non-ASCII values", function(){
var xhr = new XMLHttpRequest();
xhr.open("GET", "/dummy");
htmx._("safelySetHeaderValue")(xhr, "Example", "привет");
// unfortunately I can't test the value :/
// https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest
})
});

View File

@ -73,4 +73,20 @@ describe("Core htmx Regression Tests", function(){
div.innerText.should.contain("Foo");
});
it ('@ symbol in attributes does not break requests', function(){
this.server.respondWith("GET", "/test", "<div id='d1' @foo='bar'>Foo</div>");
var div = make('<div hx-get="/test">Get It</div>');
div.click();
this.server.respond();
byId("d1").getAttribute('@foo').should.equal('bar');
});
it ('@ symbol in attributes does not break attribute swizzling requests', function(){
this.server.respondWith("GET", "/test", "<div id='d1' @foo='bar'>Foo</div>");
var div = make('<div hx-get="/test"><div id="d1">Foo</div></div>');
div.click();
this.server.respond();
byId("d1").getAttribute('@foo').should.equal('bar');
});
})

View File

@ -11,6 +11,7 @@ describe("hyperscript integration", function() {
it('can trigger with a custom event', function () {
this.server.respondWith("GET", "/test", "Custom Event Sent!");
var btn = make('<button _="on click send customEvent" hx-trigger="customEvent" hx-get="/test">Click Me!</button>')
htmx.trigger(btn, "htmx:load"); // have to manually trigger the load event for non-AJAX dynamic content
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Custom Event Sent!");
@ -19,6 +20,7 @@ describe("hyperscript integration", function() {
it('can handle htmx driven events', function () {
this.server.respondWith("GET", "/test", "Clicked!");
var btn = make('<button _="on htmx:afterSettle add .afterSettle" hx-get="/test">Click Me!</button>')
htmx.trigger(btn, "htmx:load");
btn.classList.contains("afterSettle").should.equal(false);
btn.click();
this.server.respond();
@ -29,9 +31,20 @@ describe("hyperscript integration", function() {
this.server.respondWith("GET", "/test", [404, {}, "Bad request"]);
var div = make('<div id="d1"></div>')
var btn = make('<button _="on htmx:error(errorInfo) put errorInfo.error into #d1.innerHTML" hx-get="/test">Click Me!</button>')
htmx.trigger(btn, "htmx:load");
btn.click();
this.server.respond();
div.innerHTML.should.equal("Response Status Error Code 404 from /test");
});
it('hyperscript in non-htmx annotated nodes is evaluated', function () {
this.server.respondWith("GET", "/test", "<div><div><div id='d1' _='on click put \"Clicked...\" into my.innerHTML'></div></div></div>");
var btn = make('<button hx-get="/test">Click Me!</button>')
btn.click();
this.server.respond();
var newDiv = byId("d1");
newDiv.click();
newDiv.innerText.should.equal("Clicked...");
});
});

View File

@ -37,7 +37,19 @@
<a href="manual/confirm-and-prompt.html">Confirm & Prompt Test</a>
</li>
<li>
<a href="manual/scroll-tests.html">Scroll Test</a>
<a href="manual/scroll-test-startEnd.html">Scroll Test 1 - Start/End</a>
</li>
<li>
<a href="manual/scroll-test-eventHandler.html">Scroll Test 2 - Event Handler</a>
</li>
<li>
<a href="manual/sse.html">SSE - Multiple Event Sources - Single Event Name</a>
</li>
<li>
<a href="manual/sse-multichannel.html">SSE - Single Event Source - Multiple Event Names</a>
</li>
<li>
<a href="manual/sse-settle.html">SSE - Settling</a>
</li>
</ul>
@ -89,9 +101,6 @@
<!-- hyperscript integration -->
<script src="lib/_hyperscript.js"></script>
<script src="ext/hyperscript.js"></script>
<script>
_hyperscript.start();
</script>
<!-- extension tests -->
<script src="ext/extension-swap.js"></script>

View File

@ -11,9 +11,39 @@
return (function () {
'use strict';
//-----------------------------------------------
//====================================================================
// Utilities
//====================================================================
function mergeObjects(obj1, obj2) {
for (var key in obj2) {
if (obj2.hasOwnProperty(key)) {
obj1[key] = obj2[key];
}
}
return obj1;
}
function parseJSON(jString) {
try {
return JSON.parse(jString);
} catch(error) {
logError(error);
return null;
}
}
function logError(msg) {
if(console.error) {
console.error(msg);
} else if (console.log) {
console.log("ERROR: ", msg);
}
}
//====================================================================
// Lexer
//-----------------------------------------------
//====================================================================
var _lexer = function () {
var OP_TABLE = {
'+': 'PLUS',
@ -77,6 +107,10 @@
(c >= 'A' && c <= 'Z');
}
function isIdentifierChar(c) {
return (c === "_" || c === "$");
}
function makeTokensObject(tokens, consumed, source) {
@ -205,7 +239,7 @@
tokens.push(consumeClassReference());
} else if (!possiblePrecedingSymbol() && currentChar() === "#" && isAlpha(nextChar())) {
tokens.push(consumeIdReference());
} else if (isAlpha(currentChar())) {
} else if (isAlpha(currentChar()) || isIdentifierChar(currentChar())) {
tokens.push(consumeIdentifier());
} else if (isNumeric(currentChar())) {
tokens.push(consumeNumber());
@ -273,7 +307,7 @@
function consumeIdentifier() {
var identifier = makeToken("IDENTIFIER");
var value = consumeChar();
while (isAlpha(currentChar())) {
while (isAlpha(currentChar()) || isIdentifierChar(currentChar())) {
value += consumeChar();
}
identifier.value = value;
@ -369,9 +403,9 @@
}
}();
//-----------------------------------------------
//====================================================================
// Parser
//-----------------------------------------------
//====================================================================
var _parser = function () {
var GRAMMAR = {}
@ -440,11 +474,10 @@
}
}();
//-----------------------------------------------
//====================================================================
// Runtime
//-----------------------------------------------
//====================================================================
var _runtime = function () {
var SCRIPT_ATTRIBUTES = ["_", "script", "data-script"];
function matchesSelector(elt, selector) {
// noinspection JSUnresolvedVariable
@ -509,9 +542,17 @@
return last;
}
var _scriptAttrs = null;
function getScriptAttributes() {
if (_scriptAttrs == null) {
_scriptAttrs = _hyperscript.config.attributes.replace(/ /g,'').split(",")
}
return _scriptAttrs;
}
function getScript(elt) {
for (var i = 0; i < SCRIPT_ATTRIBUTES.length; i++) {
var scriptAttribute = SCRIPT_ATTRIBUTES[i];
for (var i = 0; i < getScriptAttributes().length; i++) {
var scriptAttribute = getScriptAttributes()[i];
if (elt.hasAttribute && elt.hasAttribute(scriptAttribute)) {
return elt.getAttribute(scriptAttribute)
}
@ -525,12 +566,8 @@
});
}
function setScriptAttrs(values) {
SCRIPT_ATTRIBUTES = values;
}
function getScriptSelector() {
return SCRIPT_ATTRIBUTES.map(function (attribute) {
return getScriptAttributes().map(function (attribute) {
return "[" + attribute + "]";
}).join(", ");
}
@ -562,9 +599,23 @@
return eval(evalString).apply(null, args);
}
function processNode(elt) {
var selector = _runtime.getScriptSelector();
if (matchesSelector(elt, selector)) {
initElement(elt);
}
forEach(elt.querySelectorAll(selector), function (elt) {
initElement(elt);
})
}
function initElement(elt) {
var internalData = getInternalData(elt);
if (!internalData.initialized) {
var src = getScript(elt);
if (src) {
internalData.initialized = true;
internalData.script = src;
var tokens = _lexer.tokenize(src);
var hyperScript = _parser.parseHyperScript(tokens);
var transpiled = _parser.transpile(hyperScript);
@ -575,6 +626,17 @@
hyperscriptObj.applyEventListenersTo(elt);
}
}
}
function getInternalData(elt) {
var dataProp = 'hyperscript-internal-data';
var data = elt[dataProp];
if (!data) {
data = elt[dataProp] = {};
}
return data;
}
function ajax(method, url, callback, data) {
var xhr = new XMLHttpRequest();
@ -606,18 +668,17 @@
matchesSelector: matchesSelector,
getScript: getScript,
applyEventListeners: applyEventListeners,
setScriptAttrs: setScriptAttrs,
initElement: initElement,
processNode: processNode,
evaluate: evaluate,
getScriptSelector: getScriptSelector,
ajax: ajax,
}
}();
//-----------------------------------------------
// Expressions
//-----------------------------------------------
//====================================================================
// Grammar
//====================================================================
{
_parser.addGrammarElement("parenthesized", function (parser, tokens) {
if (tokens.matchOpToken('(')) {
var expr = parser.parseElement("expression", tokens);
@ -657,7 +718,9 @@
type: "nakedString",
tokens: tokenArr,
transpile: function () {
return "'" + tokenArr.map(function(t){return t.value}).join("") + "'";
return "'" + tokenArr.map(function (t) {
return t.value
}).join("") + "'";
}
}
}
@ -865,14 +928,16 @@
parser.raiseParseError(tokens, "Expected an expression");
}
values.push(expr);
} while(tokens.matchOpToken(","))
} while (tokens.matchOpToken(","))
tokens.requireOpToken("]");
}
return {
type: "arrayLiteral",
values:values,
values: values,
transpile: function () {
return "[" + values.map(function(v){ return parser.transpile(v) }).join(", ") + "]";
return "[" + values.map(function (v) {
return parser.transpile(v)
}).join(", ") + "]";
}
}
}
@ -931,10 +996,12 @@
_parser.addGrammarElement("functionCall", function (parser, tokens, root) {
if (tokens.matchOpToken("(")) {
var args = [];
if (!tokens.matchOpToken(')')) {
do {
args.push(parser.parseElement("expression", tokens));
} while (tokens.matchOpToken(","))
tokens.requireOpToken(")");
}
var functionCall = {
type: "functionCall",
root: root,
@ -1026,7 +1093,7 @@
mathOp = tokens.matchAnyOpToken("+", "-", "*", "/", "%")
while (mathOp) {
initialMathOp = initialMathOp || mathOp;
if(initialMathOp.value !== mathOp.value) {
if (initialMathOp.value !== mathOp.value) {
parser.raiseParseError(tokens, "You must parenthesize math operations with different operators")
}
var rhs = parser.parseElement("unaryExpression", tokens);
@ -1054,7 +1121,7 @@
comparisonOp = tokens.matchAnyOpToken("<", ">", "<=", ">=", "==", "===", "!=", "!==")
while (comparisonOp) {
initialComparisonOp = initialComparisonOp || comparisonOp;
if(initialComparisonOp.value !== comparisonOp.value) {
if (initialComparisonOp.value !== comparisonOp.value) {
parser.raiseParseError(tokens, "You must parenthesize comparison operations with different operators")
}
var rhs = parser.parseElement("mathExpression", tokens);
@ -1082,7 +1149,7 @@
logicalOp = tokens.matchToken("and") || tokens.matchToken("or");
while (logicalOp) {
initialLogicalOp = initialLogicalOp || logicalOp;
if(initialLogicalOp.value !== logicalOp.value) {
if (initialLogicalOp.value !== logicalOp.value) {
parser.raiseParseError(tokens, "You must parenthesize logical operations with different operators")
}
var rhs = parser.parseElement("comparisonExpression", tokens);
@ -1169,7 +1236,6 @@
};
})
_parser.addGrammarElement("eventListener", function (parser, tokens) {
tokens.requireToken("on");
var on = parser.parseElement("dotOrColonPath", tokens);
@ -1204,7 +1270,9 @@
"var my = me;\n" +
"_hyperscript.runtime.forEach( " + parser.transpile(from) + ", function(target){\n" +
" target.addEventListener('" + parser.transpile(on) + "', function(event){\n" +
args.map(function(arg){return "var " + arg.value + " = event.detail." + arg.value + ";"}).join("\n") + "\n" +
args.map(function (arg) {
return "var " + arg.value + " = event.detail." + arg.value + ";"
}).join("\n") + "\n" +
parser.transpile(start) +
" })\n" +
"})\n" +
@ -1645,45 +1713,67 @@
};
}
})
}
//-----------------------------------------------
//====================================================================
// API
//-----------------------------------------------
//====================================================================
function start(scriptAttrs) {
if (scriptAttrs) {
_runtime.setScriptAttrs(scriptAttrs);
}
var fn = function () {
var elements = document.querySelectorAll(_runtime.getScriptSelector());
_runtime.forEach(elements, function (elt) {
init(elt);
})
};
if (document.readyState !== 'loading') {
fn();
} else {
document.addEventListener('DOMContentLoaded', fn);
}
return true;
}
function init(elt) {
_runtime.initElement(elt);
function processNode(elt) {
_runtime.processNode(elt);
}
function evaluate(str) {
return _runtime.evaluate(str);
}
return {
//====================================================================
// Initialization
//====================================================================
function ready(fn) {
if (document.readyState !== 'loading') {
fn();
} else {
document.addEventListener('DOMContentLoaded', fn);
}
}
function getMetaConfig() {
var element = document.querySelector('meta[name="htmx-config"]');
if (element) {
return parseJSON(element.content);
} else {
return null;
}
}
function mergeMetaConfig() {
var metaConfig = getMetaConfig();
if (metaConfig) {
_hyperscript.config = mergeObjects(_hyperscript.config , metaConfig)
}
}
var _hyperscript = {
lexer: _lexer,
parser: _parser,
runtime: _runtime,
evaluate: evaluate,
init: init,
start: start
processNode: processNode,
config: {
attributes : "_, script, data-script"
}
};
ready(function () {
mergeMetaConfig();
processNode(document.body);
document.addEventListener("htmx:load", function(evt){
processNode(evt.detail.elt);
})
})
return _hyperscript;
}
)()
}));

View File

@ -0,0 +1,107 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Test Scroll Event Handler</title>
<script src="../../node_modules/sinon/pkg/sinon.js"></script>
<script src="../util/util.js"></script>
<script src="../../src/htmx.js"></script>
<script>
server = makeServer();
server.autoRespond = true;
server.respondWith("GET", "/more_content", "Here is more content for this page, loaded 'remotely'.");
</script>
<style>
.panel {
height:200px;
background-color:#eee;
margin-bottom:20px;
padding:20px;
}
</style>
</head>
<body style="padding:20px;font-family: sans-serif">
<h1>Scrolling Event Handler Tests</h1>
<p>You should be able to scroll this page at any speed and see HTML fragments loaded into the DIVs "remotely" without any hiccups.</p>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
<div class="panel" hx-get="/more_content" hx-trigger="revealed"></div>
</body>
</html>

View File

@ -0,0 +1,29 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Test Scroll Behavior</title>
<script src="../../src/htmx.js"></script>
</head>
<body style="padding:20px;font-family: sans-serif">
<script src="../../node_modules/sinon/pkg/sinon.js"></script>
<script src="../../src/htmx.js"></script>
<script src="../util/util.js"></script>
<script>
server = makeServer();
server.autoRespond = true;
server.respondWith("GET", "/more_divs", "<div>More Content</div>");
</script>
<h1>Scrolling Start/End Tests</h1>
<h3>End</h3>
<div hx-get="/more_divs" hx-swap="beforeend scroll:bottom" style="height: 100px; overflow: scroll">
Click To Add Content...
</div>
<hr/>
<h3>Start</h3>
<div hx-get="/more_divs" hx-swap="beforeend scroll:top" style="height: 100px; overflow: scroll">
Click To Add Content...
</div>
</body>
</html>

View File

@ -0,0 +1,41 @@
<html>
<head>
<script src="../../src/htmx.js"></script>
<script>
htmx.createEventSource = function(url){
return new EventSource(url, {withCredentials:false})
}
</script>
<style>
*{
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
body {
background-color: #eeeeee;
}
.container {
padding: 10px;
border: solid 1px gray;
margin-bottom: 20px;
background-color:white;
}
.bold {
font-weight:bold;
}
</style>
</head>
<body>
<div id="page" hx-sse="connect:http://sseplaceholder.openfollow.org/posts.html?types=Event1%2cEvent2%2cEvent3%2cEvent4">
<h3>Multiple Listeners. message only</h3>
<div class="container" hx-sse="swap:Event1">Waiting for Posts in Event1 channel...</div>
<div class="container" hx-sse="swap:Event2">Waiting for Posts in Event2 channel...</div>
<div class="container" hx-sse="swap:Event3">Waiting for Posts in Event3 channel...</div>
<div class="container" hx-sse="swap:Event4">Waiting for Posts in Event4 channel...</div>
</div>
</body>
</html>

View File

@ -0,0 +1,49 @@
<html>
<head>
<script src="../../src/htmx.js"></script>
<script>
htmx.createEventSource = function(url){
return new EventSource(url, {withCredentials:false})
}
</script>
<style>
*{
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
body {
background-color: #eeeeee;
}
.container {
padding: 10px;
border: solid 1px gray;
margin-bottom: 20px;
background-color:white;
}
.htmx-settling {
border:solid 3px red!important;
padding:8px!important;
}
.bold {
font-weight:bold;
}
</style>
</head>
<body>
<div id="page">
<h3>Settling Options</h3>
<div hx-sse="connect:http://sseplaceholder.openfollow.org/comments.html">
<div class="container" hx-sse="swap:message" hx-swap="innerHTML settle:100ms">Waiting for Comments...</div>
<div class="container" hx-sse="swap:message" hx-swap="innerHTML settle:200ms">Waiting for Comments...</div>
<div class="container" hx-sse="swap:message" hx-swap="innerHTML settle:300ms">Waiting for Comments...</div>
<div class="container" hx-sse="swap:message" hx-swap="innerHTML settle:400ms">Waiting for Comments...</div>
<div class="container" hx-sse="swap:message" hx-swap="innerHTML settle:500ms">Waiting for Comments...</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,43 @@
<html>
<head>
<script src="../../src/htmx.js"></script>
<script>
// "withCredentials:false" is necessary to circumvent CORS restrictions
htmx.createEventSource = function(url){
return new EventSource(url, {withCredentials:false})
}
</script>
<style>
*{
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
body {
background-color: #eeeeee;
}
.container {
padding: 10px;
border: solid 1px gray;
margin-bottom: 20px;
background-color:white;
}
.bold {
font-weight:bold;
}
</style>
</head>
<body>
<div id="page">
<h3>Multiple Listeners. message only</h3>
<div class="container" hx-sse="connect:http://sseplaceholder.openfollow.org/posts.html swap:message">Waiting for Posts...</div>
<div class="container" hx-sse="connect:http://sseplaceholder.openfollow.org/comments.html swap:message">Waiting for Comments...</div>
<div class="container" hx-sse="connect:http://sseplaceholder.openfollow.org/albums.html swap:message">Waiting for Albums...</div>
<div class="container" hx-sse="connect:http://sseplaceholder.openfollow.org/todos.html swap:message">Waiting for ToDos...</div>
<div class="container" hx-sse="connect:http://sseplaceholder.openfollow.org/users.html swap:message">Waiting for Users...</div>
</div>
</body>
</html>