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 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, function (parent) {
return getInternalData(parent).sseEventSource != null;
});
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 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, function (parent) {
return getInternalData(parent).sseEventSource != null;
});
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 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, function (parent) {
return getInternalData(parent).sseEventSource != null;
});
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

@ -47,6 +47,18 @@ describe("hx-target attribute", function(){
this.server.respond();
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()
{

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,23 +191,56 @@ 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>

File diff suppressed because it is too large Load Diff

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>