mirror of
https://github.com/bigskysoftware/htmx.git
synced 2025-09-27 13:01:03 +00:00
prep web for release
This commit is contained in:
parent
b1e15c08cc
commit
6489f1bfef
11
www/static/src/ext/path-params.js
Normal file
11
www/static/src/ext/path-params.js
Normal file
@ -0,0 +1,11 @@
|
||||
htmx.defineExtension('path-params', {
|
||||
onEvent: function(name, evt) {
|
||||
if (name === "htmx:configRequest") {
|
||||
evt.detail.path = evt.detail.path.replace(/{([^}]+)}/g, function (_, param) {
|
||||
var val = evt.detail.parameters[param];
|
||||
delete evt.detail.parameters[param];
|
||||
return val === undefined ? "{" + param + "}" : encodeURIComponent(val);
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
@ -5,7 +5,7 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
|
||||
|
||||
*/
|
||||
|
||||
(function(){
|
||||
(function() {
|
||||
|
||||
/** @type {import("../htmx").HtmxInternalApi} */
|
||||
var api;
|
||||
@ -39,17 +39,19 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
|
||||
|
||||
switch (name) {
|
||||
|
||||
// Try to remove remove an EventSource when elements are removed
|
||||
case "htmx:beforeCleanupElement":
|
||||
var internalData = api.getInternalData(evt.target)
|
||||
if (internalData.sseEventSource) {
|
||||
internalData.sseEventSource.close();
|
||||
}
|
||||
return;
|
||||
case "htmx:beforeCleanupElement":
|
||||
var internalData = api.getInternalData(evt.target)
|
||||
// Try to remove remove an EventSource when elements are removed
|
||||
if (internalData.sseEventSource) {
|
||||
internalData.sseEventSource.close();
|
||||
}
|
||||
|
||||
// Try to create EventSources when elements are processed
|
||||
case "htmx:afterProcessNode":
|
||||
createEventSourceOnElement(evt.target);
|
||||
return;
|
||||
|
||||
// Try to create EventSources when elements are processed
|
||||
case "htmx:afterProcessNode":
|
||||
ensureEventSourceOnElement(evt.target);
|
||||
registerSSE(evt.target);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -66,8 +68,8 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
|
||||
* @param {string} url
|
||||
* @returns EventSource
|
||||
*/
|
||||
function createEventSource(url) {
|
||||
return new EventSource(url, {withCredentials:true});
|
||||
function createEventSource(url) {
|
||||
return new EventSource(url, { withCredentials: true });
|
||||
}
|
||||
|
||||
function splitOnWhitespace(trigger) {
|
||||
@ -90,7 +92,7 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
|
||||
function getLegacySSESwaps(elt) {
|
||||
var legacySSEValue = api.getAttributeValue(elt, "hx-sse");
|
||||
var returnArr = [];
|
||||
if (legacySSEValue) {
|
||||
if (legacySSEValue != null) {
|
||||
var values = splitOnWhitespace(legacySSEValue);
|
||||
for (var i = 0; i < values.length; i++) {
|
||||
var value = values[i].split(/:(.+)/);
|
||||
@ -103,63 +105,24 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
|
||||
}
|
||||
|
||||
/**
|
||||
* createEventSourceOnElement creates a new EventSource connection on the provided element.
|
||||
* If a usable EventSource already exists, then it is returned. If not, then a new EventSource
|
||||
* is created and stored in the element's internalData.
|
||||
* registerSSE looks for attributes that can contain sse events, right
|
||||
* now hx-trigger and sse-swap and adds listeners based on these attributes too
|
||||
* the closest event source
|
||||
*
|
||||
* @param {HTMLElement} elt
|
||||
* @param {number} retryCount
|
||||
* @returns {EventSource | null}
|
||||
*/
|
||||
function createEventSourceOnElement(elt, retryCount) {
|
||||
|
||||
if (elt == null) {
|
||||
return null;
|
||||
function registerSSE(elt) {
|
||||
// Find closest existing event source
|
||||
var sourceElement = api.getClosestMatch(elt, hasEventSource);
|
||||
if (sourceElement == null) {
|
||||
// api.triggerErrorEvent(elt, "htmx:noSSESourceError")
|
||||
return null; // no eventsource in parentage, orphaned element
|
||||
}
|
||||
|
||||
var internalData = api.getInternalData(elt);
|
||||
// Set internalData and source
|
||||
var internalData = api.getInternalData(sourceElement);
|
||||
var source = internalData.sseEventSource;
|
||||
|
||||
// get URL from element's attribute
|
||||
var sseURL = api.getAttributeValue(elt, "sse-connect");
|
||||
|
||||
|
||||
if (sseURL == undefined) {
|
||||
var legacyURL = getLegacySSEURL(elt)
|
||||
if (legacyURL) {
|
||||
sseURL = legacyURL;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Connect to the EventSource
|
||||
var source = htmx.createEventSource(sseURL);
|
||||
internalData.sseEventSource = source;
|
||||
|
||||
// Create event handlers
|
||||
source.onerror = function (err) {
|
||||
|
||||
// Log an error event
|
||||
api.triggerErrorEvent(elt, "htmx:sseError", {error:err, source:source});
|
||||
|
||||
// If parent no longer exists in the document, then clean up this EventSource
|
||||
if (maybeCloseSSESource(elt)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, try to reconnect the EventSource
|
||||
if (source.readyState === EventSource.CLOSED) {
|
||||
retryCount = retryCount || 0;
|
||||
var timeout = Math.random() * (2 ^ retryCount) * 500;
|
||||
window.setTimeout(function() {
|
||||
createEventSourceOnElement(elt, Math.min(7, retryCount+1));
|
||||
}, timeout);
|
||||
}
|
||||
};
|
||||
|
||||
source.onopen = function (evt) {
|
||||
api.triggerEvent(elt, "htmx:sseOpen", {source: source});
|
||||
}
|
||||
|
||||
// Add message handlers for every `sse-swap` attribute
|
||||
queryAttributeOnThisOrChildren(elt, "sse-swap").forEach(function(child) {
|
||||
|
||||
@ -170,23 +133,27 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
|
||||
var sseEventNames = getLegacySSESwaps(child);
|
||||
}
|
||||
|
||||
for (var i = 0 ; i < sseEventNames.length ; i++) {
|
||||
for (var i = 0; i < sseEventNames.length; i++) {
|
||||
var sseEventName = sseEventNames[i].trim();
|
||||
var listener = function(event) {
|
||||
|
||||
// If the parent is missing then close SSE and remove listener
|
||||
if (maybeCloseSSESource(elt)) {
|
||||
source.removeEventListener(sseEventName, listener);
|
||||
// If the source is missing then close SSE
|
||||
if (maybeCloseSSESource(sourceElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the body no longer contains the element, remove the listener
|
||||
if (!api.bodyContains(child)) {
|
||||
source.removeEventListener(sseEventName, listener);
|
||||
}
|
||||
|
||||
// swap the response into the DOM and trigger a notification
|
||||
swap(child, event.data);
|
||||
api.triggerEvent(elt, "htmx:sseMessage", event);
|
||||
};
|
||||
|
||||
// Register the new listener
|
||||
api.getInternalData(elt).sseEventListener = listener;
|
||||
api.getInternalData(child).sseEventListener = listener;
|
||||
source.addEventListener(sseEventName, listener);
|
||||
}
|
||||
});
|
||||
@ -203,24 +170,86 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
|
||||
if (sseEventName.slice(0, 4) != "sse:") {
|
||||
return;
|
||||
}
|
||||
|
||||
// remove the sse: prefix from here on out
|
||||
sseEventName = sseEventName.substr(4);
|
||||
|
||||
var listener = function(event) {
|
||||
|
||||
// If parent is missing, then close SSE and remove listener
|
||||
if (maybeCloseSSESource(elt)) {
|
||||
source.removeEventListener(sseEventName, listener);
|
||||
return;
|
||||
var listener = function() {
|
||||
if (maybeCloseSSESource(sourceElement)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Trigger events to be handled by the rest of htmx
|
||||
htmx.trigger(child, sseEventName, event);
|
||||
htmx.trigger(child, "htmx:sseMessage", event);
|
||||
if (!api.bodyContains(child)) {
|
||||
source.removeEventListener(sseEventName, listener);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ensureEventSourceOnElement creates a new EventSource connection on the provided element.
|
||||
* If a usable EventSource already exists, then it is returned. If not, then a new EventSource
|
||||
* is created and stored in the element's internalData.
|
||||
* @param {HTMLElement} elt
|
||||
* @param {number} retryCount
|
||||
* @returns {EventSource | null}
|
||||
*/
|
||||
function ensureEventSourceOnElement(elt, retryCount) {
|
||||
|
||||
if (elt == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// handle extension source creation attribute
|
||||
queryAttributeOnThisOrChildren(elt, "sse-connect").forEach(function(child) {
|
||||
var sseURL = api.getAttributeValue(child, "sse-connect");
|
||||
if (sseURL == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Register the new listener
|
||||
api.getInternalData(elt).sseEventListener = listener;
|
||||
source.addEventListener(sseEventName.slice(4), listener);
|
||||
ensureEventSource(child, sseURL, retryCount);
|
||||
});
|
||||
|
||||
// handle legacy sse, remove for HTMX2
|
||||
queryAttributeOnThisOrChildren(elt, "hx-sse").forEach(function(child) {
|
||||
var sseURL = getLegacySSEURL(child);
|
||||
if (sseURL == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ensureEventSource(child, sseURL, retryCount);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function ensureEventSource(elt, url, retryCount) {
|
||||
var source = htmx.createEventSource(url);
|
||||
|
||||
source.onerror = function(err) {
|
||||
|
||||
// Log an error event
|
||||
api.triggerErrorEvent(elt, "htmx:sseError", { error: err, source: source });
|
||||
|
||||
// If parent no longer exists in the document, then clean up this EventSource
|
||||
if (maybeCloseSSESource(elt)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, try to reconnect the EventSource
|
||||
if (source.readyState === EventSource.CLOSED) {
|
||||
retryCount = retryCount || 0;
|
||||
var timeout = Math.random() * (2 ^ retryCount) * 500;
|
||||
window.setTimeout(function() {
|
||||
ensureEventSourceOnElement(elt, Math.min(7, retryCount + 1));
|
||||
}, timeout);
|
||||
}
|
||||
};
|
||||
|
||||
source.onopen = function(evt) {
|
||||
api.triggerEvent(elt, "htmx:sseOpen", { source: source });
|
||||
}
|
||||
|
||||
api.getInternalData(elt).sseEventSource = source;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -253,12 +282,12 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
|
||||
var result = [];
|
||||
|
||||
// If the parent element also contains the requested attribute, then add it to the results too.
|
||||
if (api.hasAttribute(elt, attributeName) || api.hasAttribute(elt, "hx-sse")) {
|
||||
if (api.hasAttribute(elt, attributeName)) {
|
||||
result.push(elt);
|
||||
}
|
||||
|
||||
// Search all child nodes that match the requested attribute
|
||||
elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "], [hx-sse], [data-hx-sse]").forEach(function(node) {
|
||||
elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "]").forEach(function(node) {
|
||||
result.push(node);
|
||||
});
|
||||
|
||||
@ -281,7 +310,7 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
|
||||
|
||||
api.selectAndSwap(swapSpec.swapStyle, target, elt, content, settleInfo);
|
||||
|
||||
settleInfo.elts.forEach(function (elt) {
|
||||
settleInfo.elts.forEach(function(elt) {
|
||||
if (elt.classList) {
|
||||
elt.classList.add(htmx.config.settlingClass);
|
||||
}
|
||||
@ -306,11 +335,11 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
|
||||
function doSettle(settleInfo) {
|
||||
|
||||
return function() {
|
||||
settleInfo.tasks.forEach(function (task) {
|
||||
settleInfo.tasks.forEach(function(task) {
|
||||
task.call();
|
||||
});
|
||||
|
||||
settleInfo.elts.forEach(function (elt) {
|
||||
settleInfo.elts.forEach(function(elt) {
|
||||
if (elt.classList) {
|
||||
elt.classList.remove(htmx.config.settlingClass);
|
||||
}
|
||||
@ -319,4 +348,8 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
|
||||
}
|
||||
}
|
||||
|
||||
})();
|
||||
function hasEventSource(node) {
|
||||
return api.getInternalData(node).sseEventSource != null;
|
||||
}
|
||||
|
||||
})();
|
||||
|
36
www/static/src/htmx.d.ts
vendored
36
www/static/src/htmx.d.ts
vendored
@ -400,6 +400,42 @@ export interface HtmxConfig {
|
||||
* @default true
|
||||
*/
|
||||
scrollIntoViewOnBoost?: boolean;
|
||||
/**
|
||||
* If set, the nonce will be added to inline scripts.
|
||||
* @default ''
|
||||
*/
|
||||
inlineScriptNonce?: string;
|
||||
/**
|
||||
* The type of binary data being received over the WebSocket connection
|
||||
* @default 'blob'
|
||||
*/
|
||||
wsBinaryType?: 'blob' | 'arraybuffer';
|
||||
/**
|
||||
* If set to true htmx will include a cache-busting parameter in GET requests to avoid caching partial responses by the browser
|
||||
* @default false
|
||||
*/
|
||||
getCacheBusterParam?: boolean;
|
||||
/**
|
||||
* If set to true, htmx will use the View Transition API when swapping in new content.
|
||||
* @default false
|
||||
*/
|
||||
globalViewTransitions?: boolean;
|
||||
/**
|
||||
* htmx will format requests with these methods by encoding their parameters in the URL, not the request body
|
||||
* @default ["get"]
|
||||
*/
|
||||
methodsThatUseUrlParams?: ('get' | 'head' | 'post' | 'put' | 'delete' | 'connect' | 'options' | 'trace' | 'patch' )[];
|
||||
/**
|
||||
* If set to true htmx will not update the title of the document when a title tag is found in new content
|
||||
* @default false
|
||||
*/
|
||||
ignoreTitle:? boolean;
|
||||
/**
|
||||
* The cache to store evaluated trigger specifications into.
|
||||
* You may define a simple object to use a never-clearing cache, or implement your own system using a [proxy object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Proxy)
|
||||
* @default null
|
||||
*/
|
||||
triggerSpecsCache?: {[trigger: string]: HtmxTriggerSpecification[]};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -76,7 +76,8 @@ return (function () {
|
||||
methodsThatUseUrlParams: ["get"],
|
||||
selfRequestsOnly: false,
|
||||
ignoreTitle: false,
|
||||
scrollIntoViewOnBoost: true
|
||||
scrollIntoViewOnBoost: true,
|
||||
triggerSpecsCache: null,
|
||||
},
|
||||
parseInterval:parseInterval,
|
||||
_:internalEval,
|
||||
@ -127,24 +128,40 @@ return (function () {
|
||||
return "[hx-" + verb + "], [data-hx-" + verb + "]"
|
||||
}).join(", ");
|
||||
|
||||
var HEAD_TAG_REGEX = makeTagRegEx('head'),
|
||||
TITLE_TAG_REGEX = makeTagRegEx('title'),
|
||||
SVG_TAGS_REGEX = makeTagRegEx('svg', true);
|
||||
|
||||
//====================================================================
|
||||
// Utilities
|
||||
//====================================================================
|
||||
|
||||
/**
|
||||
* @param {string} tag
|
||||
* @param {boolean} global
|
||||
* @returns {RegExp}
|
||||
*/
|
||||
function makeTagRegEx(tag, global = false) {
|
||||
return new RegExp(`<${tag}(\\s[^>]*>|>)([\\s\\S]*?)<\\/${tag}>`,
|
||||
global ? 'gim' : 'im');
|
||||
}
|
||||
|
||||
function parseInterval(str) {
|
||||
if (str == undefined) {
|
||||
return undefined
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let interval = NaN;
|
||||
if (str.slice(-2) == "ms") {
|
||||
return parseFloat(str.slice(0,-2)) || undefined
|
||||
interval = parseFloat(str.slice(0, -2));
|
||||
} else if (str.slice(-1) == "s") {
|
||||
interval = parseFloat(str.slice(0, -1)) * 1000;
|
||||
} else if (str.slice(-1) == "m") {
|
||||
interval = parseFloat(str.slice(0, -1)) * 1000 * 60;
|
||||
} else {
|
||||
interval = parseFloat(str);
|
||||
}
|
||||
if (str.slice(-1) == "s") {
|
||||
return (parseFloat(str.slice(0,-1)) * 1000) || undefined
|
||||
}
|
||||
if (str.slice(-1) == "m") {
|
||||
return (parseFloat(str.slice(0,-1)) * 1000 * 60) || undefined
|
||||
}
|
||||
return parseFloat(str) || undefined
|
||||
return isNaN(interval) ? undefined : interval;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -276,43 +293,46 @@ return (function () {
|
||||
}
|
||||
|
||||
function aFullPageResponse(resp) {
|
||||
return resp.match(/<body/);
|
||||
return /<body/.test(resp)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} resp
|
||||
* @param {string} response
|
||||
* @returns {Element}
|
||||
*/
|
||||
function makeFragment(resp) {
|
||||
var partialResponse = !aFullPageResponse(resp);
|
||||
function makeFragment(response) {
|
||||
var partialResponse = !aFullPageResponse(response);
|
||||
var startTag = getStartTag(response);
|
||||
var content = response;
|
||||
if (startTag === 'head') {
|
||||
content = content.replace(HEAD_TAG_REGEX, '');
|
||||
}
|
||||
if (htmx.config.useTemplateFragments && partialResponse) {
|
||||
var documentFragment = parseHTML("<body><template>" + resp + "</template></body>", 0);
|
||||
var documentFragment = parseHTML("<body><template>" + content + "</template></body>", 0);
|
||||
// @ts-ignore type mismatch between DocumentFragment and Element.
|
||||
// TODO: Are these close enough for htmx to use interchangeably?
|
||||
return documentFragment.querySelector('template').content;
|
||||
} else {
|
||||
var startTag = getStartTag(resp);
|
||||
switch (startTag) {
|
||||
case "thead":
|
||||
case "tbody":
|
||||
case "tfoot":
|
||||
case "colgroup":
|
||||
case "caption":
|
||||
return parseHTML("<table>" + resp + "</table>", 1);
|
||||
case "col":
|
||||
return parseHTML("<table><colgroup>" + resp + "</colgroup></table>", 2);
|
||||
case "tr":
|
||||
return parseHTML("<table><tbody>" + resp + "</tbody></table>", 2);
|
||||
case "td":
|
||||
case "th":
|
||||
return parseHTML("<table><tbody><tr>" + resp + "</tr></tbody></table>", 3);
|
||||
case "script":
|
||||
case "style":
|
||||
return parseHTML("<div>" + resp + "</div>", 1);
|
||||
default:
|
||||
return parseHTML(resp, 0);
|
||||
}
|
||||
}
|
||||
switch (startTag) {
|
||||
case "thead":
|
||||
case "tbody":
|
||||
case "tfoot":
|
||||
case "colgroup":
|
||||
case "caption":
|
||||
return parseHTML("<table>" + content + "</table>", 1);
|
||||
case "col":
|
||||
return parseHTML("<table><colgroup>" + content + "</colgroup></table>", 2);
|
||||
case "tr":
|
||||
return parseHTML("<table><tbody>" + content + "</tbody></table>", 2);
|
||||
case "td":
|
||||
case "th":
|
||||
return parseHTML("<table><tbody><tr>" + content + "</tr></tbody></table>", 3);
|
||||
case "script":
|
||||
case "style":
|
||||
return parseHTML("<div>" + content + "</div>", 1);
|
||||
default:
|
||||
return parseHTML(content, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@ -450,7 +470,7 @@ return (function () {
|
||||
path = url.pathname + url.search;
|
||||
}
|
||||
// remove trailing slash, unless index page
|
||||
if (!path.match('^/$')) {
|
||||
if (!(/^\/$/.test(path))) {
|
||||
path = path.replace(/\/+$/, '');
|
||||
}
|
||||
return path;
|
||||
@ -827,7 +847,7 @@ return (function () {
|
||||
var oobSelects = getClosestAttributeValue(elt, "hx-select-oob");
|
||||
if (oobSelects) {
|
||||
var oobSelectValues = oobSelects.split(",");
|
||||
for (let i = 0; i < oobSelectValues.length; i++) {
|
||||
for (var i = 0; i < oobSelectValues.length; i++) {
|
||||
var oobSelectValue = oobSelectValues[i].split(":", 2);
|
||||
var id = oobSelectValue[0].trim();
|
||||
if (id.indexOf("#") === 0) {
|
||||
@ -934,7 +954,7 @@ return (function () {
|
||||
function deInitOnHandlers(elt) {
|
||||
var internalData = getInternalData(elt);
|
||||
if (internalData.onHandlers) {
|
||||
for (let i = 0; i < internalData.onHandlers.length; i++) {
|
||||
for (var i = 0; i < internalData.onHandlers.length; i++) {
|
||||
const handlerInfo = internalData.onHandlers[i];
|
||||
elt.removeEventListener(handlerInfo.event, handlerInfo.listener);
|
||||
}
|
||||
@ -960,10 +980,8 @@ return (function () {
|
||||
}
|
||||
});
|
||||
}
|
||||
if (internalData.initHash) {
|
||||
internalData.initHash = null
|
||||
}
|
||||
deInitOnHandlers(element);
|
||||
forEach(Object.keys(internalData), function(key) { delete internalData[key] });
|
||||
}
|
||||
|
||||
function cleanUpElement(element) {
|
||||
@ -987,7 +1005,6 @@ return (function () {
|
||||
} else {
|
||||
newElt = eltBeforeNewContent.nextSibling;
|
||||
}
|
||||
getInternalData(target).replacedWith = newElt; // tuck away so we can fire events on it later
|
||||
settleInfo.elts = settleInfo.elts.filter(function(e) { return e != target });
|
||||
while(newElt && newElt !== target) {
|
||||
if (newElt.nodeType === Node.ELEMENT_NODE) {
|
||||
@ -1099,9 +1116,8 @@ return (function () {
|
||||
|
||||
function findTitle(content) {
|
||||
if (content.indexOf('<title') > -1) {
|
||||
var contentWithSvgsRemoved = content.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, '');
|
||||
var result = contentWithSvgsRemoved.match(/<title(\s[^>]*>|>)([\s\S]*?)<\/title>/im);
|
||||
|
||||
var contentWithSvgsRemoved = content.replace(SVG_TAGS_REGEX, '');
|
||||
var result = contentWithSvgsRemoved.match(TITLE_TAG_REGEX);
|
||||
if (result) {
|
||||
return result[2];
|
||||
}
|
||||
@ -1230,7 +1246,7 @@ return (function () {
|
||||
|
||||
function consumeUntil(tokens, match) {
|
||||
var result = "";
|
||||
while (tokens.length > 0 && !tokens[0].match(match)) {
|
||||
while (tokens.length > 0 && !match.test(tokens[0])) {
|
||||
result += tokens.shift();
|
||||
}
|
||||
return result;
|
||||
@ -1250,6 +1266,99 @@ return (function () {
|
||||
|
||||
var INPUT_SELECTOR = 'input, textarea, select';
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} elt
|
||||
* @param {string} explicitTrigger
|
||||
* @param {cache} cache for trigger specs
|
||||
* @returns {import("./htmx").HtmxTriggerSpecification[]}
|
||||
*/
|
||||
function parseAndCacheTrigger(elt, explicitTrigger, cache) {
|
||||
var triggerSpecs = [];
|
||||
var tokens = tokenizeString(explicitTrigger);
|
||||
do {
|
||||
consumeUntil(tokens, NOT_WHITESPACE);
|
||||
var initialLength = tokens.length;
|
||||
var trigger = consumeUntil(tokens, /[,\[\s]/);
|
||||
if (trigger !== "") {
|
||||
if (trigger === "every") {
|
||||
var every = {trigger: 'every'};
|
||||
consumeUntil(tokens, NOT_WHITESPACE);
|
||||
every.pollInterval = parseInterval(consumeUntil(tokens, /[,\[\s]/));
|
||||
consumeUntil(tokens, NOT_WHITESPACE);
|
||||
var eventFilter = maybeGenerateConditional(elt, tokens, "event");
|
||||
if (eventFilter) {
|
||||
every.eventFilter = eventFilter;
|
||||
}
|
||||
triggerSpecs.push(every);
|
||||
} else if (trigger.indexOf("sse:") === 0) {
|
||||
triggerSpecs.push({trigger: 'sse', sseEvent: trigger.substr(4)});
|
||||
} else {
|
||||
var triggerSpec = {trigger: trigger};
|
||||
var eventFilter = maybeGenerateConditional(elt, tokens, "event");
|
||||
if (eventFilter) {
|
||||
triggerSpec.eventFilter = eventFilter;
|
||||
}
|
||||
while (tokens.length > 0 && tokens[0] !== ",") {
|
||||
consumeUntil(tokens, NOT_WHITESPACE)
|
||||
var token = tokens.shift();
|
||||
if (token === "changed") {
|
||||
triggerSpec.changed = true;
|
||||
} else if (token === "once") {
|
||||
triggerSpec.once = true;
|
||||
} else if (token === "consume") {
|
||||
triggerSpec.consume = true;
|
||||
} else if (token === "delay" && tokens[0] === ":") {
|
||||
tokens.shift();
|
||||
triggerSpec.delay = parseInterval(consumeUntil(tokens, WHITESPACE_OR_COMMA));
|
||||
} else if (token === "from" && tokens[0] === ":") {
|
||||
tokens.shift();
|
||||
if (COMBINED_SELECTOR_START.test(tokens[0])) {
|
||||
var from_arg = consumeCSSSelector(tokens);
|
||||
} else {
|
||||
var from_arg = consumeUntil(tokens, WHITESPACE_OR_COMMA);
|
||||
if (from_arg === "closest" || from_arg === "find" || from_arg === "next" || from_arg === "previous") {
|
||||
tokens.shift();
|
||||
var selector = consumeCSSSelector(tokens);
|
||||
// `next` and `previous` allow a selector-less syntax
|
||||
if (selector.length > 0) {
|
||||
from_arg += " " + selector;
|
||||
}
|
||||
}
|
||||
}
|
||||
triggerSpec.from = from_arg;
|
||||
} else if (token === "target" && tokens[0] === ":") {
|
||||
tokens.shift();
|
||||
triggerSpec.target = consumeCSSSelector(tokens);
|
||||
} else if (token === "throttle" && tokens[0] === ":") {
|
||||
tokens.shift();
|
||||
triggerSpec.throttle = parseInterval(consumeUntil(tokens, WHITESPACE_OR_COMMA));
|
||||
} else if (token === "queue" && tokens[0] === ":") {
|
||||
tokens.shift();
|
||||
triggerSpec.queue = consumeUntil(tokens, WHITESPACE_OR_COMMA);
|
||||
} else if (token === "root" && tokens[0] === ":") {
|
||||
tokens.shift();
|
||||
triggerSpec[token] = consumeCSSSelector(tokens);
|
||||
} else if (token === "threshold" && tokens[0] === ":") {
|
||||
tokens.shift();
|
||||
triggerSpec[token] = consumeUntil(tokens, WHITESPACE_OR_COMMA);
|
||||
} else {
|
||||
triggerErrorEvent(elt, "htmx:syntax:error", {token:tokens.shift()});
|
||||
}
|
||||
}
|
||||
triggerSpecs.push(triggerSpec);
|
||||
}
|
||||
}
|
||||
if (tokens.length === initialLength) {
|
||||
triggerErrorEvent(elt, "htmx:syntax:error", {token:tokens.shift()});
|
||||
}
|
||||
consumeUntil(tokens, NOT_WHITESPACE);
|
||||
} while (tokens[0] === "," && tokens.shift())
|
||||
if (cache) {
|
||||
cache[explicitTrigger] = triggerSpecs
|
||||
}
|
||||
return triggerSpecs
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} elt
|
||||
* @returns {import("./htmx").HtmxTriggerSpecification[]}
|
||||
@ -1258,85 +1367,8 @@ return (function () {
|
||||
var explicitTrigger = getAttributeValue(elt, 'hx-trigger');
|
||||
var triggerSpecs = [];
|
||||
if (explicitTrigger) {
|
||||
var tokens = tokenizeString(explicitTrigger);
|
||||
do {
|
||||
consumeUntil(tokens, NOT_WHITESPACE);
|
||||
var initialLength = tokens.length;
|
||||
var trigger = consumeUntil(tokens, /[,\[\s]/);
|
||||
if (trigger !== "") {
|
||||
if (trigger === "every") {
|
||||
var every = {trigger: 'every'};
|
||||
consumeUntil(tokens, NOT_WHITESPACE);
|
||||
every.pollInterval = parseInterval(consumeUntil(tokens, /[,\[\s]/));
|
||||
consumeUntil(tokens, NOT_WHITESPACE);
|
||||
var eventFilter = maybeGenerateConditional(elt, tokens, "event");
|
||||
if (eventFilter) {
|
||||
every.eventFilter = eventFilter;
|
||||
}
|
||||
triggerSpecs.push(every);
|
||||
} else if (trigger.indexOf("sse:") === 0) {
|
||||
triggerSpecs.push({trigger: 'sse', sseEvent: trigger.substr(4)});
|
||||
} else {
|
||||
var triggerSpec = {trigger: trigger};
|
||||
var eventFilter = maybeGenerateConditional(elt, tokens, "event");
|
||||
if (eventFilter) {
|
||||
triggerSpec.eventFilter = eventFilter;
|
||||
}
|
||||
while (tokens.length > 0 && tokens[0] !== ",") {
|
||||
consumeUntil(tokens, NOT_WHITESPACE)
|
||||
var token = tokens.shift();
|
||||
if (token === "changed") {
|
||||
triggerSpec.changed = true;
|
||||
} else if (token === "once") {
|
||||
triggerSpec.once = true;
|
||||
} else if (token === "consume") {
|
||||
triggerSpec.consume = true;
|
||||
} else if (token === "delay" && tokens[0] === ":") {
|
||||
tokens.shift();
|
||||
triggerSpec.delay = parseInterval(consumeUntil(tokens, WHITESPACE_OR_COMMA));
|
||||
} else if (token === "from" && tokens[0] === ":") {
|
||||
tokens.shift();
|
||||
if (COMBINED_SELECTOR_START.test(tokens[0])) {
|
||||
var from_arg = consumeCSSSelector(tokens);
|
||||
} else {
|
||||
var from_arg = consumeUntil(tokens, WHITESPACE_OR_COMMA);
|
||||
if (from_arg === "closest" || from_arg === "find" || from_arg === "next" || from_arg === "previous") {
|
||||
tokens.shift();
|
||||
var selector = consumeCSSSelector(tokens);
|
||||
// `next` and `previous` allow a selector-less syntax
|
||||
if (selector.length > 0) {
|
||||
from_arg += " " + selector;
|
||||
}
|
||||
}
|
||||
}
|
||||
triggerSpec.from = from_arg;
|
||||
} else if (token === "target" && tokens[0] === ":") {
|
||||
tokens.shift();
|
||||
triggerSpec.target = consumeCSSSelector(tokens);
|
||||
} else if (token === "throttle" && tokens[0] === ":") {
|
||||
tokens.shift();
|
||||
triggerSpec.throttle = parseInterval(consumeUntil(tokens, WHITESPACE_OR_COMMA));
|
||||
} else if (token === "queue" && tokens[0] === ":") {
|
||||
tokens.shift();
|
||||
triggerSpec.queue = consumeUntil(tokens, WHITESPACE_OR_COMMA);
|
||||
} else if (token === "root" && tokens[0] === ":") {
|
||||
tokens.shift();
|
||||
triggerSpec[token] = consumeCSSSelector(tokens);
|
||||
} else if (token === "threshold" && tokens[0] === ":") {
|
||||
tokens.shift();
|
||||
triggerSpec[token] = consumeUntil(tokens, WHITESPACE_OR_COMMA);
|
||||
} else {
|
||||
triggerErrorEvent(elt, "htmx:syntax:error", {token:tokens.shift()});
|
||||
}
|
||||
}
|
||||
triggerSpecs.push(triggerSpec);
|
||||
}
|
||||
}
|
||||
if (tokens.length === initialLength) {
|
||||
triggerErrorEvent(elt, "htmx:syntax:error", {token:tokens.shift()});
|
||||
}
|
||||
consumeUntil(tokens, NOT_WHITESPACE);
|
||||
} while (tokens[0] === "," && tokens.shift())
|
||||
var cache = htmx.config.triggerSpecsCache
|
||||
triggerSpecs = (cache && cache[explicitTrigger]) || parseAndCacheTrigger(elt, explicitTrigger, cache)
|
||||
}
|
||||
|
||||
if (triggerSpecs.length > 0) {
|
||||
@ -1508,14 +1540,14 @@ return (function () {
|
||||
return;
|
||||
}
|
||||
|
||||
if (triggerSpec.throttle) {
|
||||
if (triggerSpec.throttle > 0) {
|
||||
if (!elementData.throttle) {
|
||||
handler(elt, evt);
|
||||
elementData.throttle = setTimeout(function () {
|
||||
elementData.throttle = null;
|
||||
}, triggerSpec.throttle);
|
||||
}
|
||||
} else if (triggerSpec.delay) {
|
||||
} else if (triggerSpec.delay > 0) {
|
||||
elementData.delayed = setTimeout(function() { handler(elt, evt) }, triggerSpec.delay);
|
||||
} else {
|
||||
triggerEvent(elt, 'htmx:trigger')
|
||||
@ -1792,7 +1824,7 @@ return (function () {
|
||||
handler(elt);
|
||||
}
|
||||
}
|
||||
if (delay) {
|
||||
if (delay > 0) {
|
||||
setTimeout(load, delay);
|
||||
} else {
|
||||
load();
|
||||
@ -1851,7 +1883,7 @@ return (function () {
|
||||
if (!maybeFilterEvent(triggerSpec, elt, makeEvent("load", {elt: elt}))) {
|
||||
loadImmediately(elt, handler, nodeData, triggerSpec.delay);
|
||||
}
|
||||
} else if (triggerSpec.pollInterval) {
|
||||
} else if (triggerSpec.pollInterval > 0) {
|
||||
nodeData.polling = true;
|
||||
processPolling(elt, handler, triggerSpec);
|
||||
} else {
|
||||
@ -1894,26 +1926,35 @@ return (function () {
|
||||
});
|
||||
}
|
||||
|
||||
function hasChanceOfBeingBoosted() {
|
||||
return document.querySelector("[hx-boost], [data-hx-boost]");
|
||||
function shouldProcessHxOn(elt) {
|
||||
var attributes = elt.attributes
|
||||
for (var j = 0; j < attributes.length; j++) {
|
||||
var attrName = attributes[j].name
|
||||
if (startsWith(attrName, "hx-on:") || startsWith(attrName, "data-hx-on:") ||
|
||||
startsWith(attrName, "hx-on-") || startsWith(attrName, "data-hx-on-")) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function findHxOnWildcardElements(elt) {
|
||||
var node = null
|
||||
var elements = []
|
||||
|
||||
if (shouldProcessHxOn(elt)) {
|
||||
elements.push(elt)
|
||||
}
|
||||
|
||||
if (document.evaluate) {
|
||||
var iter = document.evaluate('//*[@*[ starts-with(name(), "hx-on:") or starts-with(name(), "data-hx-on:") ]]', elt)
|
||||
var iter = document.evaluate('.//*[@*[ starts-with(name(), "hx-on:") or starts-with(name(), "data-hx-on:") or' +
|
||||
' starts-with(name(), "hx-on-") or starts-with(name(), "data-hx-on-") ]]', elt)
|
||||
while (node = iter.iterateNext()) elements.push(node)
|
||||
} else {
|
||||
var allElements = document.getElementsByTagName("*")
|
||||
var allElements = elt.getElementsByTagName("*")
|
||||
for (var i = 0; i < allElements.length; i++) {
|
||||
var attributes = allElements[i].attributes
|
||||
for (var j = 0; j < attributes.length; j++) {
|
||||
var attrName = attributes[j].name
|
||||
if (startsWith(attrName, "hx-on:") || startsWith(attrName, "data-hx-on:")) {
|
||||
elements.push(allElements[i])
|
||||
}
|
||||
if (shouldProcessHxOn(allElements[i])) {
|
||||
elements.push(allElements[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1923,8 +1964,8 @@ return (function () {
|
||||
|
||||
function findElementsToProcess(elt) {
|
||||
if (elt.querySelectorAll) {
|
||||
var boostedElts = hasChanceOfBeingBoosted() ? ", a" : "";
|
||||
var results = elt.querySelectorAll(VERB_SELECTOR + boostedElts + ", form, [type='submit'], [hx-sse], [data-hx-sse], [hx-ws]," +
|
||||
var boostedSelector = ", [hx-boost] a, [data-hx-boost] a, a[hx-boost], a[data-hx-boost]";
|
||||
var results = elt.querySelectorAll(VERB_SELECTOR + boostedSelector + ", form, [type='submit'], [hx-sse], [data-hx-sse], [hx-ws]," +
|
||||
" [data-hx-ws], [hx-ext], [data-hx-ext], [hx-trigger], [data-hx-trigger], [hx-on], [data-hx-on]");
|
||||
return results;
|
||||
} else {
|
||||
@ -1970,7 +2011,7 @@ return (function () {
|
||||
function countCurlies(line) {
|
||||
var tokens = tokenizeString(line);
|
||||
var netCurlies = 0;
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
for (var i = 0; i < tokens.length; i++) {
|
||||
const token = tokens[i];
|
||||
if (token === "{") {
|
||||
netCurlies++;
|
||||
@ -2032,12 +2073,22 @@ return (function () {
|
||||
for (var i = 0; i < elt.attributes.length; i++) {
|
||||
var name = elt.attributes[i].name
|
||||
var value = elt.attributes[i].value
|
||||
if (startsWith(name, "hx-on:") || startsWith(name, "data-hx-on:")) {
|
||||
let eventName = name.slice(name.indexOf(":") + 1)
|
||||
// if the eventName starts with a colon, prepend "htmx" for shorthand support
|
||||
if (startsWith(eventName, ":")) eventName = "htmx" + eventName
|
||||
if (startsWith(name, "hx-on") || startsWith(name, "data-hx-on")) {
|
||||
var afterOnPosition = name.indexOf("-on") + 3;
|
||||
var nextChar = name.slice(afterOnPosition, afterOnPosition + 1);
|
||||
if (nextChar === "-" || nextChar === ":") {
|
||||
var eventName = name.slice(afterOnPosition + 1);
|
||||
// if the eventName starts with a colon or dash, prepend "htmx" for shorthand support
|
||||
if (startsWith(eventName, ":")) {
|
||||
eventName = "htmx" + eventName
|
||||
} else if (startsWith(eventName, "-")) {
|
||||
eventName = "htmx:" + eventName.slice(1);
|
||||
} else if (startsWith(eventName, "htmx-")) {
|
||||
eventName = "htmx:" + eventName.slice(5);
|
||||
}
|
||||
|
||||
addHxOnEventHandler(elt, eventName, value)
|
||||
addHxOnEventHandler(elt, eventName, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2315,7 +2366,9 @@ return (function () {
|
||||
var details = {path: path, xhr:request};
|
||||
triggerEvent(getDocument().body, "htmx:historyCacheMiss", details);
|
||||
request.open('GET', path, true);
|
||||
request.setRequestHeader("HX-Request", "true");
|
||||
request.setRequestHeader("HX-History-Restore-Request", "true");
|
||||
request.setRequestHeader("HX-Current-URL", getDocument().location.href);
|
||||
request.onload = function () {
|
||||
if (this.status >= 200 && this.status < 400) {
|
||||
triggerEvent(getDocument().body, "htmx:historyCacheMissLoad", details);
|
||||
@ -2430,7 +2483,7 @@ return (function () {
|
||||
}
|
||||
|
||||
function shouldInclude(elt) {
|
||||
if(elt.name === "" || elt.name == null || elt.disabled) {
|
||||
if(elt.name === "" || elt.name == null || elt.disabled || closest(elt, "fieldset[disabled]")) {
|
||||
return false;
|
||||
}
|
||||
// ignore "submitter" types (see jQuery src/serialize.js)
|
||||
@ -2906,7 +2959,7 @@ return (function () {
|
||||
}
|
||||
|
||||
function hasHeader(xhr, regexp) {
|
||||
return xhr.getAllResponseHeaders().match(regexp);
|
||||
return regexp.test(xhr.getAllResponseHeaders())
|
||||
}
|
||||
|
||||
function ajaxHelper(verb, path, context) {
|
||||
@ -3453,7 +3506,11 @@ return (function () {
|
||||
}
|
||||
|
||||
if (hasHeader(xhr,/HX-Retarget:/i)) {
|
||||
responseInfo.target = getDocument().querySelector(xhr.getResponseHeader("HX-Retarget"));
|
||||
if (xhr.getResponseHeader("HX-Retarget") === "this") {
|
||||
responseInfo.target = elt;
|
||||
} else {
|
||||
responseInfo.target = querySelectorExt(elt, xhr.getResponseHeader("HX-Retarget"));
|
||||
}
|
||||
}
|
||||
|
||||
var historyUpdate = determineHistoryUpdates(elt, responseInfo);
|
||||
@ -3752,34 +3809,25 @@ return (function () {
|
||||
//====================================================================
|
||||
// Initialization
|
||||
//====================================================================
|
||||
/**
|
||||
* We want to initialize the page elements after DOMContentLoaded
|
||||
* fires, but there isn't always a good way to tell whether
|
||||
* it has already fired when we get here or not.
|
||||
*/
|
||||
function ready(functionToCall) {
|
||||
// call the function exactly once no matter how many times this is called
|
||||
var callReadyFunction = function() {
|
||||
if (!functionToCall) return;
|
||||
functionToCall();
|
||||
functionToCall = null;
|
||||
};
|
||||
var isReady = false
|
||||
getDocument().addEventListener('DOMContentLoaded', function() {
|
||||
isReady = true
|
||||
})
|
||||
|
||||
if (getDocument().readyState === "complete") {
|
||||
// DOMContentLoaded definitely fired, we can initialize the page
|
||||
callReadyFunction();
|
||||
}
|
||||
else {
|
||||
/* DOMContentLoaded *maybe* already fired, wait for
|
||||
* the next DOMContentLoaded or readystatechange event
|
||||
*/
|
||||
getDocument().addEventListener("DOMContentLoaded", function() {
|
||||
callReadyFunction();
|
||||
});
|
||||
getDocument().addEventListener("readystatechange", function() {
|
||||
if (getDocument().readyState !== "complete") return;
|
||||
callReadyFunction();
|
||||
});
|
||||
/**
|
||||
* Execute a function now if DOMContentLoaded has fired, otherwise listen for it.
|
||||
*
|
||||
* This function uses isReady because there is no realiable way to ask the browswer whether
|
||||
* the DOMContentLoaded event has already been fired; there's a gap between DOMContentLoaded
|
||||
* firing and readystate=complete.
|
||||
*/
|
||||
function ready(fn) {
|
||||
// Checking readyState here is a failsafe in case the htmx script tag entered the DOM by
|
||||
// some means other than the initial page load.
|
||||
if (isReady || getDocument().readyState === 'complete') {
|
||||
fn();
|
||||
} else {
|
||||
getDocument().addEventListener('DOMContentLoaded', fn);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3827,7 +3875,9 @@ return (function () {
|
||||
internalData.xhr.abort();
|
||||
}
|
||||
});
|
||||
var originalPopstate = window.onpopstate;
|
||||
/** @type {(ev: PopStateEvent) => any} */
|
||||
const originalPopstate = window.onpopstate ? window.onpopstate.bind(window) : null;
|
||||
/** @type {(ev: PopStateEvent) => any} */
|
||||
window.onpopstate = function (event) {
|
||||
if (event.state && event.state.htmx) {
|
||||
restoreHistory();
|
||||
|
@ -15,6 +15,14 @@ describe("hx-on:* attribute", function() {
|
||||
delete window.foo;
|
||||
});
|
||||
|
||||
it("can use dashes rather than colons", function () {
|
||||
var btn = make("<button hx-on-click='window.foo = true'>Foo</button>");
|
||||
btn.click();
|
||||
window.foo.should.equal(true);
|
||||
delete window.foo;
|
||||
});
|
||||
|
||||
|
||||
it("can modify a parameter via htmx:configRequest", function () {
|
||||
this.server.respondWith("POST", "/test", function (xhr) {
|
||||
var params = parseParams(xhr.requestBody);
|
||||
@ -26,6 +34,17 @@ describe("hx-on:* attribute", function() {
|
||||
btn.innerText.should.equal("bar");
|
||||
});
|
||||
|
||||
it("can modify a parameter via htmx:configRequest with dashes", function () {
|
||||
this.server.respondWith("POST", "/test", function (xhr) {
|
||||
var params = parseParams(xhr.requestBody);
|
||||
xhr.respond(200, {}, params.foo);
|
||||
});
|
||||
var btn = make("<button hx-on-htmx-config-request='event.detail.parameters.foo = \"bar\"' hx-post='/test'>Foo</button>");
|
||||
btn.click();
|
||||
this.server.respond();
|
||||
btn.innerText.should.equal("bar");
|
||||
});
|
||||
|
||||
it("expands :: shorthand into htmx:", function () {
|
||||
this.server.respondWith("POST", "/test", function (xhr) {
|
||||
var params = parseParams(xhr.requestBody);
|
||||
@ -37,6 +56,17 @@ describe("hx-on:* attribute", function() {
|
||||
btn.innerText.should.equal("bar");
|
||||
});
|
||||
|
||||
it("expands -- shorthand into htmx:", function () {
|
||||
this.server.respondWith("POST", "/test", function (xhr) {
|
||||
var params = parseParams(xhr.requestBody);
|
||||
xhr.respond(200, {}, params.foo);
|
||||
});
|
||||
var btn = make("<button hx-on--config-request='event.detail.parameters.foo = \"bar\"' hx-post='/test'>Foo</button>");
|
||||
btn.click();
|
||||
this.server.respond();
|
||||
btn.innerText.should.equal("bar");
|
||||
});
|
||||
|
||||
it("can cancel an event via preventDefault for htmx:config-request", function () {
|
||||
this.server.respondWith("POST", "/test", function (xhr) {
|
||||
xhr.respond(200, {}, "<button>Bar</button>");
|
||||
@ -185,7 +215,7 @@ describe("hx-on:* attribute", function() {
|
||||
|
||||
// check there is just one handler against each event
|
||||
htmx.trigger(div, "increment-foo");
|
||||
htmx.trigger(div, "increment-bar");
|
||||
htmx.trigger(div, "increment-bar");
|
||||
window.foo.should.equal(1);
|
||||
window.bar.should.equal(1);
|
||||
|
||||
|
@ -4,43 +4,53 @@ describe("hx-sse attribute", function() {
|
||||
var listeners = {};
|
||||
var wasClosed = false;
|
||||
var mockEventSource = {
|
||||
removeEventListener: function(name) {
|
||||
delete listeners[name];
|
||||
removeEventListener: function(name, l) {
|
||||
listeners[name] = listeners[name].filter(function(elt, idx, arr) {
|
||||
if (arr[idx] === l) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
},
|
||||
addEventListener: function (message, l) {
|
||||
listeners[message] = l;
|
||||
addEventListener: function(message, l) {
|
||||
if (listeners == undefined) {
|
||||
listeners[message] = [];
|
||||
}
|
||||
listeners[message].push(l)
|
||||
},
|
||||
sendEvent: function (eventName, data) {
|
||||
var listener = listeners[eventName];
|
||||
if (listener) {
|
||||
var event = htmx._("makeEvent")(eventName);
|
||||
event.data = data;
|
||||
listener(event);
|
||||
sendEvent: function(eventName, data) {
|
||||
var listeners = listeners[eventName];
|
||||
if (listeners) {
|
||||
listeners.forEach(function(listener) {
|
||||
var event = htmx._("makeEvent")(eventName);
|
||||
event.data = data;
|
||||
listener(event);
|
||||
}
|
||||
}
|
||||
},
|
||||
close: function () {
|
||||
close: function() {
|
||||
wasClosed = true;
|
||||
},
|
||||
wasClosed: function () {
|
||||
wasClosed: function() {
|
||||
return wasClosed;
|
||||
}
|
||||
};
|
||||
return mockEventSource;
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
beforeEach(function() {
|
||||
this.server = makeServer();
|
||||
var eventSource = mockEventSource();
|
||||
this.eventSource = eventSource;
|
||||
clearWorkArea();
|
||||
htmx.createEventSource = function(){ return eventSource };
|
||||
htmx.createEventSource = function() { return eventSource };
|
||||
});
|
||||
afterEach(function () {
|
||||
afterEach(function() {
|
||||
this.server.restore();
|
||||
clearWorkArea();
|
||||
});
|
||||
|
||||
it('handles basic sse triggering', function () {
|
||||
it('handles basic sse triggering', function() {
|
||||
|
||||
this.server.respondWith("GET", "/d1", "div1 updated");
|
||||
this.server.respondWith("GET", "/d2", "div2 updated");
|
||||
@ -61,7 +71,7 @@ describe("hx-sse attribute", function() {
|
||||
byId("d2").innerHTML.should.equal("div2 updated");
|
||||
})
|
||||
|
||||
it('does not trigger events that arent named', function () {
|
||||
it('does not trigger events that arent named', function() {
|
||||
|
||||
this.server.respondWith("GET", "/d1", "div1 updated");
|
||||
|
||||
@ -82,7 +92,7 @@ describe("hx-sse attribute", function() {
|
||||
byId("d1").innerHTML.should.equal("div1 updated");
|
||||
})
|
||||
|
||||
it('does not trigger events not on descendents', function () {
|
||||
it('does not trigger events not on descendents', function() {
|
||||
|
||||
this.server.respondWith("GET", "/d1", "div1 updated");
|
||||
|
||||
@ -102,7 +112,7 @@ describe("hx-sse attribute", function() {
|
||||
byId("d1").innerHTML.should.equal("div1");
|
||||
})
|
||||
|
||||
it('is closed after removal', 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">' +
|
||||
'<div id="d1" hx-trigger="sse:e1" hx-get="/d1">div1</div>' +
|
||||
@ -112,7 +122,7 @@ describe("hx-sse attribute", function() {
|
||||
this.eventSource.wasClosed().should.equal(true)
|
||||
})
|
||||
|
||||
it('is closed after removal with no close and activity', 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">' +
|
||||
'<div id="d1" hx-trigger="sse:e1" hx-get="/d1">div1</div>' +
|
||||
'</div>');
|
||||
@ -121,7 +131,7 @@ describe("hx-sse attribute", function() {
|
||||
this.eventSource.wasClosed().should.equal(true)
|
||||
})
|
||||
|
||||
it('swaps content properly on SSE swap', function () {
|
||||
it('swaps content properly on SSE swap', function() {
|
||||
var div = make('<div hx-sse="connect:/event_stream">\n' +
|
||||
' <div id="d1" hx-sse="swap:e1"></div>\n' +
|
||||
' <div id="d2" hx-sse="swap:e2"></div>\n' +
|
||||
@ -136,5 +146,15 @@ describe("hx-sse attribute", function() {
|
||||
byId("d2").innerText.should.equal("Event 2")
|
||||
})
|
||||
|
||||
it('swaps swapped in content', function() {
|
||||
var div = make('<div hx-sse="connect:/event_stream">\n' +
|
||||
'<div id="d1" hx-sse="swap:e1" hx-swap="outerHTML"></div>\n' +
|
||||
'</div>\n'
|
||||
)
|
||||
|
||||
this.eventSource.sendEvent("e1", '<div id="d2" hx-sse="swap:e2"></div>')
|
||||
this.eventSource.sendEvent("e2", 'Event 2')
|
||||
byId("d2").innerText.should.equal("Event 2")
|
||||
})
|
||||
});
|
||||
|
||||
|
@ -197,27 +197,43 @@ describe("hx-swap attribute", function(){
|
||||
swapSpec(make("<div hx-swap='innerHTML'/>")).swapDelay.should.equal(0)
|
||||
swapSpec(make("<div hx-swap='innerHTML'/>")).settleDelay.should.equal(0) // set to 0 in tests
|
||||
swapSpec(make("<div hx-swap='innerHTML swap:10'/>")).swapDelay.should.equal(10)
|
||||
swapSpec(make("<div hx-swap='innerHTML swap:0'/>")).swapDelay.should.equal(0)
|
||||
swapSpec(make("<div hx-swap='innerHTML swap:0ms'/>")).swapDelay.should.equal(0)
|
||||
swapSpec(make("<div hx-swap='innerHTML settle:10'/>")).settleDelay.should.equal(10)
|
||||
swapSpec(make("<div hx-swap='innerHTML settle:0'/>")).settleDelay.should.equal(0)
|
||||
swapSpec(make("<div hx-swap='innerHTML settle:0s'/>")).settleDelay.should.equal(0)
|
||||
swapSpec(make("<div hx-swap='innerHTML swap:10 settle:11'/>")).swapDelay.should.equal(10)
|
||||
swapSpec(make("<div hx-swap='innerHTML swap:10 settle:11'/>")).settleDelay.should.equal(11)
|
||||
swapSpec(make("<div hx-swap='innerHTML settle:11 swap:10'/>")).swapDelay.should.equal(10)
|
||||
swapSpec(make("<div hx-swap='innerHTML settle:11 swap:10'/>")).settleDelay.should.equal(11)
|
||||
swapSpec(make("<div hx-swap='innerHTML settle:0 swap:0'/>")).settleDelay.should.equal(0)
|
||||
swapSpec(make("<div hx-swap='innerHTML settle:0 swap:0'/>")).settleDelay.should.equal(0)
|
||||
swapSpec(make("<div hx-swap='innerHTML settle:0s swap:0ms'/>")).settleDelay.should.equal(0)
|
||||
swapSpec(make("<div hx-swap='innerHTML settle:0s swap:0ms'/>")).settleDelay.should.equal(0)
|
||||
swapSpec(make("<div hx-swap='innerHTML nonsense settle:11 swap:10'/>")).settleDelay.should.equal(11)
|
||||
swapSpec(make("<div hx-swap='innerHTML nonsense settle:11 swap:10 '/>")).settleDelay.should.equal(11)
|
||||
|
||||
|
||||
swapSpec(make("<div hx-swap='swap:10'/>")).swapStyle.should.equal("innerHTML")
|
||||
swapSpec(make("<div hx-swap='swap:10'/>")).swapDelay.should.equal(10)
|
||||
swapSpec(make("<div hx-swap='swap:0'/>")).swapDelay.should.equal(0);
|
||||
swapSpec(make("<div hx-swap='swap:0s'/>")).swapDelay.should.equal(0);
|
||||
|
||||
swapSpec(make("<div hx-swap='settle:10'/>")).swapStyle.should.equal("innerHTML")
|
||||
swapSpec(make("<div hx-swap='settle:10'/>")).settleDelay.should.equal(10)
|
||||
|
||||
swapSpec(make("<div hx-swap='settle:0'/>")).settleDelay.should.equal(0)
|
||||
swapSpec(make("<div hx-swap='settle:0s'/>")).settleDelay.should.equal(0)
|
||||
|
||||
swapSpec(make("<div hx-swap='swap:10 settle:11'/>")).swapStyle.should.equal("innerHTML")
|
||||
swapSpec(make("<div hx-swap='swap:10 settle:11'/>")).swapDelay.should.equal(10)
|
||||
swapSpec(make("<div hx-swap='swap:10 settle:11'/>")).settleDelay.should.equal(11)
|
||||
swapSpec(make("<div hx-swap='swap:0s settle:0'/>")).swapDelay.should.equal(0)
|
||||
swapSpec(make("<div hx-swap='swap:0s settle:0'/>")).settleDelay.should.equal(0)
|
||||
|
||||
swapSpec(make("<div hx-swap='settle:11 swap:10'/>")).swapStyle.should.equal("innerHTML")
|
||||
swapSpec(make("<div hx-swap='settle:11 swap:10'/>")).swapDelay.should.equal(10)
|
||||
swapSpec(make("<div hx-swap='settle:11 swap:10'/>")).settleDelay.should.equal(11)
|
||||
swapSpec(make("<div hx-swap='settle:0s swap:10'/>")).swapDelay.should.equal(10)
|
||||
swapSpec(make("<div hx-swap='settle:0s swap:10'/>")).settleDelay.should.equal(0)
|
||||
|
||||
swapSpec(make("<div hx-swap='customstyle settle:11 swap:10'/>")).swapStyle.should.equal("customstyle")
|
||||
})
|
||||
@ -234,6 +250,17 @@ describe("hx-swap attribute", function(){
|
||||
}, 30);
|
||||
});
|
||||
|
||||
it("works immediately with no swap delay", function (done) {
|
||||
this.server.respondWith("GET", "/test", "Clicked!");
|
||||
var div = make(
|
||||
"<div hx-get='/test' hx-swap='innerHTML swap:0ms'></div>"
|
||||
);
|
||||
div.click();
|
||||
this.server.respond();
|
||||
div.innerText.should.equal("Clicked!");
|
||||
done();
|
||||
});
|
||||
|
||||
it('works with a settle delay', function(done) {
|
||||
this.server.respondWith("GET", "/test", "<div id='d1' class='foo' hx-get='/test' hx-swap='outerHTML settle:10ms'></div>");
|
||||
var div = make("<div id='d1' hx-get='/test' hx-swap='outerHTML settle:10ms'></div>");
|
||||
@ -246,6 +273,24 @@ describe("hx-swap attribute", function(){
|
||||
}, 30);
|
||||
});
|
||||
|
||||
it("works with no settle delay", function (done) {
|
||||
this.server.respondWith(
|
||||
"GET",
|
||||
"/test",
|
||||
"<div id='d1' class='foo' hx-get='/test' hx-swap='outerHTML settle:0ms'></div>"
|
||||
);
|
||||
var div = make(
|
||||
"<div id='d1' hx-get='/test' hx-swap='outerHTML settle:0ms'></div>"
|
||||
);
|
||||
div.click();
|
||||
this.server.respond();
|
||||
div.classList.contains("foo").should.equal(false);
|
||||
setTimeout(function () {
|
||||
byId("d1").classList.contains("foo").should.equal(true);
|
||||
done();
|
||||
}, 30);
|
||||
});
|
||||
|
||||
it('swap outerHTML properly w/ data-* prefix', function()
|
||||
{
|
||||
this.server.respondWith("GET", "/test", '<a id="a1" data-hx-get="/test2">Click Me</a>');
|
||||
|
@ -211,14 +211,20 @@ describe("hx-trigger attribute", function(){
|
||||
var specExamples = {
|
||||
"": [{trigger: 'click'}],
|
||||
"every 1s": [{trigger: 'every', pollInterval: 1000}],
|
||||
"every 0s": [{trigger: 'every', pollInterval: 0}],
|
||||
"every 0ms": [{trigger: 'every', pollInterval: 0}],
|
||||
"click": [{trigger: 'click'}],
|
||||
"customEvent": [{trigger: 'customEvent'}],
|
||||
"event changed": [{trigger: 'event', changed: true}],
|
||||
"event once": [{trigger: 'event', once: true}],
|
||||
"event delay:1s": [{trigger: 'event', delay: 1000}],
|
||||
"event throttle:1s": [{trigger: 'event', throttle: 1000}],
|
||||
"event delay:1s, foo": [{trigger: 'event', delay: 1000}, {trigger: 'foo'}],
|
||||
"event throttle:0s": [{trigger: 'event', throttle: 0}],
|
||||
"event throttle:0ms": [{trigger: 'event', throttle: 0}],
|
||||
"event throttle:1s, foo": [{trigger: 'event', throttle: 1000}, {trigger: 'foo'}],
|
||||
"event delay:1s": [{trigger: 'event', delay: 1000}],
|
||||
"event delay:1s, foo": [{trigger: 'event', delay: 1000}, {trigger: 'foo'}],
|
||||
"event delay:0s, foo": [{trigger: 'event', delay: 0}, {trigger: 'foo'}],
|
||||
"event delay:0ms, foo": [{trigger: 'event', delay: 0}, {trigger: 'foo'}],
|
||||
"event changed once delay:1s": [{trigger: 'event', changed: true, once: true, delay: 1000}],
|
||||
"event1,event2": [{trigger: 'event1'}, {trigger: 'event2'}],
|
||||
"event1, event2": [{trigger: 'event1'}, {trigger: 'event2'}],
|
||||
@ -678,6 +684,37 @@ describe("hx-trigger attribute", function(){
|
||||
}, 50);
|
||||
});
|
||||
|
||||
it("A throttle of 0 does not multiple requests from happening", function (done) {
|
||||
var requests = 0;
|
||||
var server = this.server;
|
||||
server.respondWith("GET", "/test", function (xhr) {
|
||||
requests++;
|
||||
xhr.respond(200, {}, "Requests: " + requests);
|
||||
});
|
||||
server.respondWith("GET", "/bar", "bar");
|
||||
var div = make(
|
||||
"<div hx-trigger='click throttle:0ms' hx-get='/test'></div>"
|
||||
);
|
||||
|
||||
div.click();
|
||||
server.respond();
|
||||
div.innerText.should.equal("Requests: 1");
|
||||
|
||||
div.click();
|
||||
server.respond();
|
||||
div.innerText.should.equal("Requests: 2");
|
||||
|
||||
div.click();
|
||||
server.respond();
|
||||
div.innerText.should.equal("Requests: 3");
|
||||
|
||||
div.click();
|
||||
server.respond();
|
||||
div.innerText.should.equal("Requests: 4");
|
||||
|
||||
done()
|
||||
});
|
||||
|
||||
it('delay delays the request', function(done)
|
||||
{
|
||||
var requests = 0;
|
||||
@ -714,6 +751,37 @@ describe("hx-trigger attribute", function(){
|
||||
}, 50);
|
||||
});
|
||||
|
||||
it("A 0 delay does not delay the request", function (done) {
|
||||
var requests = 0;
|
||||
this.server.respondWith("GET", "/test", function (xhr) {
|
||||
requests++;
|
||||
xhr.respond(200, {}, "Requests: " + requests);
|
||||
});
|
||||
this.server.respondWith("GET", "/bar", "bar");
|
||||
var div = make(
|
||||
"<div hx-trigger='click delay:0ms' hx-get='/test'></div>"
|
||||
);
|
||||
|
||||
div.click();
|
||||
this.server.respond();
|
||||
div.innerText.should.equal("Requests: 1");
|
||||
|
||||
div.click();
|
||||
this.server.respond();
|
||||
div.innerText.should.equal("Requests: 2");
|
||||
|
||||
div.click();
|
||||
this.server.respond();
|
||||
div.innerText.should.equal("Requests: 3");
|
||||
|
||||
div.click();
|
||||
this.server.respond();
|
||||
div.innerText.should.equal("Requests: 4");
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
|
||||
it('requests are queued with last one winning by default', function()
|
||||
{
|
||||
var requests = 0;
|
||||
@ -954,5 +1022,56 @@ describe("hx-trigger attribute", function(){
|
||||
make('<div hx-trigger="intersect root:{form input}" hx-get="/test">Not Called</div>');
|
||||
});
|
||||
|
||||
it("uses trigger specs cache if defined", function () {
|
||||
var initialCacheConfig = htmx.config.triggerSpecsCache
|
||||
htmx.config.triggerSpecsCache = {}
|
||||
|
||||
var specExamples = {
|
||||
"every 1s": [{trigger: 'every', pollInterval: 1000}],
|
||||
"click": [{trigger: 'click'}],
|
||||
"customEvent": [{trigger: 'customEvent'}],
|
||||
"event changed": [{trigger: 'event', changed: true}],
|
||||
"event once": [{trigger: 'event', once: true}],
|
||||
"event delay:1s": [{trigger: 'event', delay: 1000}],
|
||||
"event throttle:1s": [{trigger: 'event', throttle: 1000}],
|
||||
"event delay:1s, foo": [{trigger: 'event', delay: 1000}, {trigger: 'foo'}],
|
||||
"event throttle:1s, foo": [{trigger: 'event', throttle: 1000}, {trigger: 'foo'}],
|
||||
"event changed once delay:1s": [{trigger: 'event', changed: true, once: true, delay: 1000}],
|
||||
"event1,event2": [{trigger: 'event1'}, {trigger: 'event2'}],
|
||||
"event1, event2": [{trigger: 'event1'}, {trigger: 'event2'}],
|
||||
"event1 once, event2 changed": [{trigger: 'event1', once: true}, {trigger: 'event2', changed: true}],
|
||||
}
|
||||
|
||||
for (var specString in specExamples) {
|
||||
var div = make("<div hx-trigger='" + specString + "'></div>");
|
||||
var spec = htmx._('getTriggerSpecs')(div);
|
||||
spec.should.deep.equal(specExamples[specString], "Found : " + JSON.stringify(spec) + ", expected : " + JSON.stringify(specExamples[specString]) + " for spec: " + specString);
|
||||
}
|
||||
|
||||
Object.keys(htmx.config.triggerSpecsCache).length.should.greaterThan(0)
|
||||
Object.keys(htmx.config.triggerSpecsCache).length.should.equal(Object.keys(specExamples).length)
|
||||
|
||||
htmx.config.triggerSpecsCache = initialCacheConfig
|
||||
})
|
||||
|
||||
it("correctly reuses trigger specs from the cache if defined", function () {
|
||||
var initialCacheConfig = htmx.config.triggerSpecsCache
|
||||
htmx.config.triggerSpecsCache = {}
|
||||
|
||||
var triggerStr = "event changed once delay:1s"
|
||||
var expectedSpec = [{trigger: 'event', changed: true, once: true, delay: 1000}]
|
||||
|
||||
var div = make("<div hx-trigger='event changed once delay:1s'></div>");
|
||||
var spec = htmx._('getTriggerSpecs')(div);
|
||||
spec.should.deep.equal(expectedSpec, "Found : " + JSON.stringify(spec) + ", expected : " + JSON.stringify(expectedSpec) + " for spec: " + triggerStr);
|
||||
spec.push("This should be carried to further identical specs thanks to the cache")
|
||||
|
||||
var div2 = make("<div hx-trigger='event changed once delay:1s'></div>");
|
||||
var spec2 = htmx._('getTriggerSpecs')(div2);
|
||||
spec2.should.deep.equal(spec, "Found : " + JSON.stringify(spec) + ", expected : " + JSON.stringify(spec2) + " for cached spec: " + triggerStr);
|
||||
|
||||
Object.keys(htmx.config.triggerSpecsCache).length.should.equal(1)
|
||||
|
||||
htmx.config.triggerSpecsCache = initialCacheConfig
|
||||
})
|
||||
})
|
||||
|
@ -348,4 +348,31 @@ describe("Core htmx AJAX headers", function () {
|
||||
this.server.respond();
|
||||
div.innerHTML.should.equal('<div>Yay! Welcome</div>');
|
||||
})
|
||||
|
||||
it('request to restore history should include the HX-Request header', function () {
|
||||
this.server.respondWith('GET', '/test', function (xhr) {
|
||||
xhr.requestHeaders['HX-Request'].should.be.equal('true');
|
||||
xhr.respond(200, {}, '');
|
||||
});
|
||||
htmx._('loadHistoryFromServer')('/test');
|
||||
this.server.respond();
|
||||
});
|
||||
|
||||
it('request to restore history should include the HX-History-Restore-Request header', function () {
|
||||
this.server.respondWith('GET', '/test', function (xhr) {
|
||||
xhr.requestHeaders['HX-History-Restore-Request'].should.be.equal('true');
|
||||
xhr.respond(200, {}, '');
|
||||
});
|
||||
htmx._('loadHistoryFromServer')('/test');
|
||||
this.server.respond();
|
||||
});
|
||||
|
||||
it('request to restore history should include the HX-Current-URL header', function () {
|
||||
this.server.respondWith('GET', '/test', function (xhr) {
|
||||
chai.assert(typeof xhr.requestHeaders['HX-Current-URL'] !== 'undefined', 'HX-Current-URL should not be undefined');
|
||||
xhr.respond(200, {}, '');
|
||||
});
|
||||
htmx._('loadHistoryFromServer')('/test');
|
||||
this.server.respond();
|
||||
});
|
||||
});
|
||||
|
@ -76,9 +76,14 @@ describe("Core htmx internals Tests", function() {
|
||||
it("handles parseInterval correctly", function() {
|
||||
chai.expect(htmx.parseInterval("1ms")).to.be.equal(1);
|
||||
chai.expect(htmx.parseInterval("300ms")).to.be.equal(300);
|
||||
chai.expect(htmx.parseInterval("1s")).to.be.equal(1000)
|
||||
chai.expect(htmx.parseInterval("1.5s")).to.be.equal(1500)
|
||||
chai.expect(htmx.parseInterval("2s")).to.be.equal(2000)
|
||||
chai.expect(htmx.parseInterval("1s")).to.be.equal(1000);
|
||||
chai.expect(htmx.parseInterval("1.5s")).to.be.equal(1500);
|
||||
chai.expect(htmx.parseInterval("2s")).to.be.equal(2000);
|
||||
chai.expect(htmx.parseInterval("0ms")).to.be.equal(0);
|
||||
chai.expect(htmx.parseInterval("0s")).to.be.equal(0);
|
||||
chai.expect(htmx.parseInterval("0m")).to.be.equal(0);
|
||||
chai.expect(htmx.parseInterval("0")).to.be.equal(0);
|
||||
chai.expect(htmx.parseInterval("5")).to.be.equal(5);
|
||||
|
||||
chai.expect(htmx.parseInterval(null)).to.be.undefined
|
||||
chai.expect(htmx.parseInterval("")).to.be.undefined
|
||||
|
@ -239,5 +239,12 @@ describe("Core htmx Parameter Handling", function() {
|
||||
}
|
||||
});
|
||||
|
||||
it('Input within disabled fieldset is excluded', function () {
|
||||
var input = make('<form><input name="foo" value="bar"/><fieldset disabled><input name="do" value="rey"/></fieldset></form>');
|
||||
var vals = htmx._('getInputValues')(input, "get").values;
|
||||
vals['foo'].should.equal('bar');
|
||||
should.equal(vals["do"], undefined);
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
|
51
www/static/test/ext/path-params.js
Normal file
51
www/static/test/ext/path-params.js
Normal file
@ -0,0 +1,51 @@
|
||||
describe("path-params extension", function() {
|
||||
beforeEach(function () {
|
||||
this.server = makeServer();
|
||||
clearWorkArea();
|
||||
});
|
||||
afterEach(function () {
|
||||
this.server.restore();
|
||||
clearWorkArea();
|
||||
});
|
||||
|
||||
it('uses parameter to populate path variable', function () {
|
||||
var request;
|
||||
htmx.on("htmx:beforeRequest", function (evt) {
|
||||
request = evt;
|
||||
});
|
||||
var div = make("<div hx-ext='path-params' hx-get='/items/{itemId}' hx-vals='{\"itemId\":42}'></div>")
|
||||
div.click();
|
||||
should.equal(request.detail.requestConfig.path, '/items/42');
|
||||
});
|
||||
|
||||
it('parameter is removed when used', function () {
|
||||
var request;
|
||||
htmx.on("htmx:beforeRequest", function (evt) {
|
||||
request = evt;
|
||||
});
|
||||
var div = make("<div hx-ext='path-params' hx-get='/items/{itemId}' hx-vals='{\"itemId\":42, \"other\":43}'></div>")
|
||||
div.click();
|
||||
should.equal(request.detail.requestConfig.parameters.other, 43);
|
||||
should.equal(request.detail.requestConfig.parameters.itemId, undefined);
|
||||
});
|
||||
|
||||
it('parameter value is encoded', function () {
|
||||
var request;
|
||||
htmx.on("htmx:beforeRequest", function (evt) {
|
||||
request = evt;
|
||||
});
|
||||
var div = make("<div hx-ext='path-params' hx-get='/items/{itemId}' hx-vals='{\"itemId\":\"a/b\"}'></div>")
|
||||
div.click();
|
||||
should.equal(request.detail.requestConfig.path, '/items/a%2Fb');
|
||||
});
|
||||
|
||||
it('missing variables are ignored', function () {
|
||||
var request;
|
||||
htmx.on("htmx:beforeRequest", function (evt) {
|
||||
request = evt;
|
||||
});
|
||||
var div = make("<div hx-ext='path-params' hx-get='/items/{itemId}/{subitem}' hx-vals='{\"itemId\":42}'></div>")
|
||||
div.click();
|
||||
should.equal(request.detail.requestConfig.path, '/items/42/{subitem}');
|
||||
});
|
||||
});
|
232
www/static/test/ext/sse.js
Normal file
232
www/static/test/ext/sse.js
Normal file
@ -0,0 +1,232 @@
|
||||
const { assert } = require("chai");
|
||||
|
||||
describe("sse extension", function() {
|
||||
|
||||
function mockEventSource() {
|
||||
var listeners = {};
|
||||
var wasClosed = false;
|
||||
var url;
|
||||
var mockEventSource = {
|
||||
removeEventListener: function(name, l) {
|
||||
listeners[name] = listeners[name].filter(function(elt, idx, arr) {
|
||||
if (arr[idx] === l) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
},
|
||||
addEventListener: function(message, l) {
|
||||
if (listeners == undefined) {
|
||||
listeners[message] = [];
|
||||
}
|
||||
listeners[message].push(l)
|
||||
},
|
||||
sendEvent: function(eventName, data) {
|
||||
var listeners = listeners[eventName];
|
||||
if (listeners) {
|
||||
listeners.forEach(function(listener) {
|
||||
var event = htmx._("makeEvent")(eventName);
|
||||
event.data = data;
|
||||
listener(event);
|
||||
}
|
||||
}
|
||||
},
|
||||
close: function() {
|
||||
wasClosed = true;
|
||||
},
|
||||
wasClosed: function() {
|
||||
return wasClosed;
|
||||
},
|
||||
connect: function(url) {
|
||||
this.url = url
|
||||
}
|
||||
};
|
||||
return mockEventSource;
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
this.server = makeServer();
|
||||
var eventSource = mockEventSource();
|
||||
this.eventSource = eventSource;
|
||||
clearWorkArea();
|
||||
htmx.createEventSource = function(url) {
|
||||
eventSource.connect(url);
|
||||
return eventSource;
|
||||
};
|
||||
});
|
||||
afterEach(function() {
|
||||
this.server.restore();
|
||||
clearWorkArea();
|
||||
});
|
||||
|
||||
it('handles basic sse triggering', function() {
|
||||
|
||||
this.server.respondWith("GET", "/d1", "div1 updated");
|
||||
this.server.respondWith("GET", "/d2", "div2 updated");
|
||||
|
||||
var div = make('<div hx-ext="sse" 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>');
|
||||
|
||||
this.eventSource.sendEvent("e1");
|
||||
this.server.respond();
|
||||
byId("d1").innerHTML.should.equal("div1 updated");
|
||||
byId("d2").innerHTML.should.equal("div2");
|
||||
|
||||
this.eventSource.sendEvent("e2");
|
||||
this.server.respond();
|
||||
byId("d1").innerHTML.should.equal("div1 updated");
|
||||
byId("d2").innerHTML.should.equal("div2 updated");
|
||||
})
|
||||
|
||||
it('does not trigger events that arent named', function() {
|
||||
|
||||
this.server.respondWith("GET", "/d1", "div1 updated");
|
||||
|
||||
var div = make('<div hx-ext="sse" sse-connect="/foo">' +
|
||||
'<div id="d1" hx-trigger="sse:e1" hx-get="/d1">div1</div>' +
|
||||
'</div>');
|
||||
|
||||
this.eventSource.sendEvent("foo");
|
||||
this.server.respond();
|
||||
byId("d1").innerHTML.should.equal("div1");
|
||||
|
||||
this.eventSource.sendEvent("e2");
|
||||
this.server.respond();
|
||||
byId("d1").innerHTML.should.equal("div1");
|
||||
|
||||
this.eventSource.sendEvent("e1");
|
||||
this.server.respond();
|
||||
byId("d1").innerHTML.should.equal("div1 updated");
|
||||
})
|
||||
|
||||
it('does not trigger events not on descendents', function() {
|
||||
|
||||
this.server.respondWith("GET", "/d1", "div1 updated");
|
||||
|
||||
var div = make('<div hx-ext="sse" sse-connect="/foo"></div>' +
|
||||
'<div id="d1" hx-trigger="sse:e1" hx-get="/d1">div1</div>');
|
||||
|
||||
this.eventSource.sendEvent("foo");
|
||||
this.server.respond();
|
||||
byId("d1").innerHTML.should.equal("div1");
|
||||
|
||||
this.eventSource.sendEvent("e2");
|
||||
this.server.respond();
|
||||
byId("d1").innerHTML.should.equal("div1");
|
||||
|
||||
this.eventSource.sendEvent("e1");
|
||||
this.server.respond();
|
||||
byId("d1").innerHTML.should.equal("div1");
|
||||
})
|
||||
|
||||
it('is closed after removal, hx-trigger', function() {
|
||||
this.server.respondWith("GET", "/test", "Clicked!");
|
||||
var div = make('<div hx-get="/test" hx-swap="outerHTML" hx-ext="sse" sse-connect="/foo">' +
|
||||
'<div id="d1" hx-trigger="sse:e1" hx-get="/d1">div1</div>' +
|
||||
'</div>');
|
||||
div.click();
|
||||
this.server.respond();
|
||||
this.eventSource.wasClosed().should.equal(true)
|
||||
})
|
||||
|
||||
it('is closed after removal, hx-swap', function() {
|
||||
this.server.respondWith("GET", "/test", "Clicked!");
|
||||
var div = make('<div hx-get="/test" hx-swap="outerHTML" hx-ext="sse" sse-connect="/foo">' +
|
||||
'<div id="d1" hx-swap="e1" hx-get="/d1">div1</div>' +
|
||||
'</div>');
|
||||
div.click();
|
||||
this.server.respond();
|
||||
this.eventSource.wasClosed().should.equal(true)
|
||||
})
|
||||
|
||||
it('is closed after removal with no close and activity, hx-trigger', function() {
|
||||
var div = make('<div hx-get="/test" hx-swap="outerHTML" hx-ext="sse" sse-connect="/foo">' +
|
||||
'<div id="d1" hx-trigger="sse:e1" hx-get="/d1">div1</div>' +
|
||||
'</div>');
|
||||
div.parentElement.removeChild(div);
|
||||
this.eventSource.sendEvent("e1")
|
||||
this.eventSource.wasClosed().should.equal(true)
|
||||
})
|
||||
|
||||
// sse and hx-trigger handlers are distinct
|
||||
it('is closed after removal with no close and activity, sse-swap', function() {
|
||||
var div = make('<div hx-get="/test" hx-swap="outerHTML" hx-ext="sse" sse-connect="/foo">' +
|
||||
'<div id="d1" sse-swap="e1" hx-get="/d1">div1</div>' +
|
||||
'</div>');
|
||||
div.parentElement.removeChild(div);
|
||||
this.eventSource.sendEvent("e1")
|
||||
this.eventSource.wasClosed().should.equal(true)
|
||||
})
|
||||
|
||||
it('swaps content properly on SSE swap', function() {
|
||||
var div = make('<div hx-ext="sse" sse-connect="/event_stream">\n' +
|
||||
' <div id="d1" sse-swap="e1"></div>\n' +
|
||||
' <div id="d2" sse-swap="e2"></div>\n' +
|
||||
'</div>\n');
|
||||
byId("d1").innerText.should.equal("")
|
||||
byId("d2").innerText.should.equal("")
|
||||
this.eventSource.sendEvent("e1", "Event 1")
|
||||
byId("d1").innerText.should.equal("Event 1")
|
||||
byId("d2").innerText.should.equal("")
|
||||
this.eventSource.sendEvent("e2", "Event 2")
|
||||
byId("d1").innerText.should.equal("Event 1")
|
||||
byId("d2").innerText.should.equal("Event 2")
|
||||
})
|
||||
|
||||
it('swaps swapped in content', function() {
|
||||
var div = make('<div hx-ext="sse" sse-connect="/event_stream">\n' +
|
||||
'<div id="d1" sse-swap="e1" hx-swap="outerHTML"></div>\n' +
|
||||
'</div>\n'
|
||||
)
|
||||
|
||||
this.eventSource.sendEvent("e1", '<div id="d2" sse-swap="e2"></div>')
|
||||
this.eventSource.sendEvent("e2", 'Event 2')
|
||||
byId("d2").innerText.should.equal("Event 2")
|
||||
})
|
||||
|
||||
it('works in a child of an hx-ext="sse" element', function() {
|
||||
var div = make('<div hx-ext="sse">\n' +
|
||||
'<div id="d1" sse-connect="/event_stream" sse-swap="e1">div1</div>\n' +
|
||||
'</div>\n'
|
||||
)
|
||||
this.eventSource.url = "/event_stream"
|
||||
})
|
||||
|
||||
it('only adds sseEventSource to elements with sse-connect', function() {
|
||||
var div = make('<div hx-ext="sse" sse-connect="/event_stream" >\n' +
|
||||
'<div id="d1" sse-swap="e1"></div>\n' +
|
||||
'</div>');
|
||||
|
||||
(byId('d1')["htmx-internal-data"].sseEventSource == undefined).should.be.true
|
||||
|
||||
// Even when content is swapped in
|
||||
this.eventSource.sendEvent("e1", '<div id="d2" sse-swap="e2"></div>');
|
||||
|
||||
(byId('d2')["htmx-internal-data"].sseEventSource == undefined).should.be.true
|
||||
})
|
||||
|
||||
it('initializes connections in swapped content', function() {
|
||||
this.server.respondWith("GET", "/d1", '<div><div sse-connect="/foo"><div id="d2" hx-trigger="sse:e2" hx-get="/d2">div2</div></div></div>');
|
||||
this.server.respondWith("GET", "/d2", "div2 updated");
|
||||
|
||||
var div = make('<div hx-ext="sse" hx-get="/d1"></div>');
|
||||
div.click();
|
||||
|
||||
this.server.respond();
|
||||
this.eventSource.sendEvent("e2");
|
||||
this.server.respond();
|
||||
|
||||
byId("d2").innerHTML.should.equal("div2 updated");
|
||||
})
|
||||
|
||||
it('creates an eventsource on elements with sse-connect', function() {
|
||||
var div = make('<div hx-ext="sse"><div id="d1"sse-connect="/event_stream"></div></div>');
|
||||
|
||||
(byId("d1")['htmx-internal-data'].sseEventSource == undefined).should.be.false;
|
||||
|
||||
})
|
||||
|
||||
});
|
||||
|
@ -158,9 +158,15 @@
|
||||
<script src="../src/ext/ws.js"></script>
|
||||
<script src="ext/ws.js"></script>
|
||||
|
||||
<script src="../src/ext/sse.js"></script>
|
||||
<script src="ext/sse.js"></script>
|
||||
|
||||
<script src="../src/ext/response-targets.js"></script>
|
||||
<script src="ext/response-targets.js"></script>
|
||||
|
||||
<script src="../src/ext/path-params.js"></script>
|
||||
<script src="ext/path-params.js"></script>
|
||||
|
||||
<!-- events last so they don't screw up other tests -->
|
||||
<script src="core/events.js"></script>
|
||||
|
||||
@ -174,7 +180,7 @@
|
||||
</script>
|
||||
<em>Work Area</em>
|
||||
<hr/>
|
||||
<div id="work-area" hx-history-elt hx-ext="sse">
|
||||
<div id="work-area" hx-history-elt>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -0,0 +1,8 @@
|
||||
<head>
|
||||
<title>Index content</title>
|
||||
</head>
|
||||
<h1># Index</h1>
|
||||
<ul>
|
||||
<li><code><title></code> <b>should not</b> spawn inside <code><main></code></li>
|
||||
<li><code><title></code> <b>should be</b> <em>index content</em></li>
|
||||
</ul>
|
@ -0,0 +1,41 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" hx-preserve="true">
|
||||
<meta name="htmx-config" content='{"useTemplateFragments": true}' hx-preserve="true">
|
||||
|
||||
<title>Index content</title>
|
||||
|
||||
<style hx-preserve="true">
|
||||
* {
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 24px;
|
||||
}
|
||||
code {
|
||||
font-family: monospace;
|
||||
font-size: 20px;
|
||||
}
|
||||
hr { border: 1px solid black; }
|
||||
</style>
|
||||
|
||||
<script src="../../../src/htmx.js" hx-preserve="true"></script>
|
||||
<script src="../../../src/ext/head-support.js" hx-preserve="true"></script>
|
||||
</head>
|
||||
<body hx-ext="head-support" hx-boost="true">
|
||||
<header hx-push-url="false" hx-target="main" hx-swap="innerHTML">
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="./index-partial.html">Index</a></li>
|
||||
<li><a href="./other-content.html">See other content</a></li>
|
||||
</ul>
|
||||
</nav><hr>
|
||||
</header>
|
||||
<main>
|
||||
<h1># Index</h1>
|
||||
<ul>
|
||||
<li><code><title></code> <b>should not</b> spawn inside <code><main></code></li>
|
||||
<li><code><title></code> <b>should be</b> <em>index content</em></li>
|
||||
</ul>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,16 @@
|
||||
<head>
|
||||
<title>Other content</title>
|
||||
<style>
|
||||
body {
|
||||
background: lightgreen;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<h1># Swapped content</h1>
|
||||
<ul>
|
||||
<li><code><title></code> and <style>
|
||||
<b>should not</b> spawn inside <code><main></code></li>
|
||||
<li><code><title></code> <b>should be</b> <em>other content</em></li>
|
||||
<li>Background <b>should be</b> green</li>
|
||||
</ul>
|
||||
|
@ -41,6 +41,7 @@
|
||||
<ul>
|
||||
<li><a href="hxboost_relative_resources">Relative Resources</a></li>
|
||||
<li><a href="hxboost_template_parsing">Template Parsing</a></li>
|
||||
<li><a href="hxboost_partial_template_parsing">Partial Template Parsing</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -76,7 +76,8 @@ return (function () {
|
||||
methodsThatUseUrlParams: ["get"],
|
||||
selfRequestsOnly: false,
|
||||
ignoreTitle: false,
|
||||
scrollIntoViewOnBoost: true
|
||||
scrollIntoViewOnBoost: true,
|
||||
triggerSpecsCache: null,
|
||||
},
|
||||
parseInterval:parseInterval,
|
||||
_:internalEval,
|
||||
@ -88,7 +89,7 @@ return (function () {
|
||||
sock.binaryType = htmx.config.wsBinaryType;
|
||||
return sock;
|
||||
},
|
||||
version: "1.9.9"
|
||||
version: "1.9.10"
|
||||
};
|
||||
|
||||
/** @type {import("./htmx").HtmxInternalApi} */
|
||||
@ -127,24 +128,40 @@ return (function () {
|
||||
return "[hx-" + verb + "], [data-hx-" + verb + "]"
|
||||
}).join(", ");
|
||||
|
||||
var HEAD_TAG_REGEX = makeTagRegEx('head'),
|
||||
TITLE_TAG_REGEX = makeTagRegEx('title'),
|
||||
SVG_TAGS_REGEX = makeTagRegEx('svg', true);
|
||||
|
||||
//====================================================================
|
||||
// Utilities
|
||||
//====================================================================
|
||||
|
||||
/**
|
||||
* @param {string} tag
|
||||
* @param {boolean} global
|
||||
* @returns {RegExp}
|
||||
*/
|
||||
function makeTagRegEx(tag, global = false) {
|
||||
return new RegExp(`<${tag}(\\s[^>]*>|>)([\\s\\S]*?)<\\/${tag}>`,
|
||||
global ? 'gim' : 'im');
|
||||
}
|
||||
|
||||
function parseInterval(str) {
|
||||
if (str == undefined) {
|
||||
return undefined
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let interval = NaN;
|
||||
if (str.slice(-2) == "ms") {
|
||||
return parseFloat(str.slice(0,-2)) || undefined
|
||||
interval = parseFloat(str.slice(0, -2));
|
||||
} else if (str.slice(-1) == "s") {
|
||||
interval = parseFloat(str.slice(0, -1)) * 1000;
|
||||
} else if (str.slice(-1) == "m") {
|
||||
interval = parseFloat(str.slice(0, -1)) * 1000 * 60;
|
||||
} else {
|
||||
interval = parseFloat(str);
|
||||
}
|
||||
if (str.slice(-1) == "s") {
|
||||
return (parseFloat(str.slice(0,-1)) * 1000) || undefined
|
||||
}
|
||||
if (str.slice(-1) == "m") {
|
||||
return (parseFloat(str.slice(0,-1)) * 1000 * 60) || undefined
|
||||
}
|
||||
return parseFloat(str) || undefined
|
||||
return isNaN(interval) ? undefined : interval;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -276,43 +293,46 @@ return (function () {
|
||||
}
|
||||
|
||||
function aFullPageResponse(resp) {
|
||||
return resp.match(/<body/);
|
||||
return /<body/.test(resp)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} resp
|
||||
* @param {string} response
|
||||
* @returns {Element}
|
||||
*/
|
||||
function makeFragment(resp) {
|
||||
var partialResponse = !aFullPageResponse(resp);
|
||||
function makeFragment(response) {
|
||||
var partialResponse = !aFullPageResponse(response);
|
||||
var startTag = getStartTag(response);
|
||||
var content = response;
|
||||
if (startTag === 'head') {
|
||||
content = content.replace(HEAD_TAG_REGEX, '');
|
||||
}
|
||||
if (htmx.config.useTemplateFragments && partialResponse) {
|
||||
var documentFragment = parseHTML("<body><template>" + resp + "</template></body>", 0);
|
||||
var documentFragment = parseHTML("<body><template>" + content + "</template></body>", 0);
|
||||
// @ts-ignore type mismatch between DocumentFragment and Element.
|
||||
// TODO: Are these close enough for htmx to use interchangeably?
|
||||
return documentFragment.querySelector('template').content;
|
||||
} else {
|
||||
var startTag = getStartTag(resp);
|
||||
switch (startTag) {
|
||||
case "thead":
|
||||
case "tbody":
|
||||
case "tfoot":
|
||||
case "colgroup":
|
||||
case "caption":
|
||||
return parseHTML("<table>" + resp + "</table>", 1);
|
||||
case "col":
|
||||
return parseHTML("<table><colgroup>" + resp + "</colgroup></table>", 2);
|
||||
case "tr":
|
||||
return parseHTML("<table><tbody>" + resp + "</tbody></table>", 2);
|
||||
case "td":
|
||||
case "th":
|
||||
return parseHTML("<table><tbody><tr>" + resp + "</tr></tbody></table>", 3);
|
||||
case "script":
|
||||
case "style":
|
||||
return parseHTML("<div>" + resp + "</div>", 1);
|
||||
default:
|
||||
return parseHTML(resp, 0);
|
||||
}
|
||||
}
|
||||
switch (startTag) {
|
||||
case "thead":
|
||||
case "tbody":
|
||||
case "tfoot":
|
||||
case "colgroup":
|
||||
case "caption":
|
||||
return parseHTML("<table>" + content + "</table>", 1);
|
||||
case "col":
|
||||
return parseHTML("<table><colgroup>" + content + "</colgroup></table>", 2);
|
||||
case "tr":
|
||||
return parseHTML("<table><tbody>" + content + "</tbody></table>", 2);
|
||||
case "td":
|
||||
case "th":
|
||||
return parseHTML("<table><tbody><tr>" + content + "</tr></tbody></table>", 3);
|
||||
case "script":
|
||||
case "style":
|
||||
return parseHTML("<div>" + content + "</div>", 1);
|
||||
default:
|
||||
return parseHTML(content, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@ -450,7 +470,7 @@ return (function () {
|
||||
path = url.pathname + url.search;
|
||||
}
|
||||
// remove trailing slash, unless index page
|
||||
if (!path.match('^/$')) {
|
||||
if (!(/^\/$/.test(path))) {
|
||||
path = path.replace(/\/+$/, '');
|
||||
}
|
||||
return path;
|
||||
@ -827,7 +847,7 @@ return (function () {
|
||||
var oobSelects = getClosestAttributeValue(elt, "hx-select-oob");
|
||||
if (oobSelects) {
|
||||
var oobSelectValues = oobSelects.split(",");
|
||||
for (let i = 0; i < oobSelectValues.length; i++) {
|
||||
for (var i = 0; i < oobSelectValues.length; i++) {
|
||||
var oobSelectValue = oobSelectValues[i].split(":", 2);
|
||||
var id = oobSelectValue[0].trim();
|
||||
if (id.indexOf("#") === 0) {
|
||||
@ -934,7 +954,7 @@ return (function () {
|
||||
function deInitOnHandlers(elt) {
|
||||
var internalData = getInternalData(elt);
|
||||
if (internalData.onHandlers) {
|
||||
for (let i = 0; i < internalData.onHandlers.length; i++) {
|
||||
for (var i = 0; i < internalData.onHandlers.length; i++) {
|
||||
const handlerInfo = internalData.onHandlers[i];
|
||||
elt.removeEventListener(handlerInfo.event, handlerInfo.listener);
|
||||
}
|
||||
@ -960,10 +980,8 @@ return (function () {
|
||||
}
|
||||
});
|
||||
}
|
||||
if (internalData.initHash) {
|
||||
internalData.initHash = null
|
||||
}
|
||||
deInitOnHandlers(element);
|
||||
forEach(Object.keys(internalData), function(key) { delete internalData[key] });
|
||||
}
|
||||
|
||||
function cleanUpElement(element) {
|
||||
@ -987,7 +1005,6 @@ return (function () {
|
||||
} else {
|
||||
newElt = eltBeforeNewContent.nextSibling;
|
||||
}
|
||||
getInternalData(target).replacedWith = newElt; // tuck away so we can fire events on it later
|
||||
settleInfo.elts = settleInfo.elts.filter(function(e) { return e != target });
|
||||
while(newElt && newElt !== target) {
|
||||
if (newElt.nodeType === Node.ELEMENT_NODE) {
|
||||
@ -1099,9 +1116,8 @@ return (function () {
|
||||
|
||||
function findTitle(content) {
|
||||
if (content.indexOf('<title') > -1) {
|
||||
var contentWithSvgsRemoved = content.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, '');
|
||||
var result = contentWithSvgsRemoved.match(/<title(\s[^>]*>|>)([\s\S]*?)<\/title>/im);
|
||||
|
||||
var contentWithSvgsRemoved = content.replace(SVG_TAGS_REGEX, '');
|
||||
var result = contentWithSvgsRemoved.match(TITLE_TAG_REGEX);
|
||||
if (result) {
|
||||
return result[2];
|
||||
}
|
||||
@ -1230,7 +1246,7 @@ return (function () {
|
||||
|
||||
function consumeUntil(tokens, match) {
|
||||
var result = "";
|
||||
while (tokens.length > 0 && !tokens[0].match(match)) {
|
||||
while (tokens.length > 0 && !match.test(tokens[0])) {
|
||||
result += tokens.shift();
|
||||
}
|
||||
return result;
|
||||
@ -1250,6 +1266,99 @@ return (function () {
|
||||
|
||||
var INPUT_SELECTOR = 'input, textarea, select';
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} elt
|
||||
* @param {string} explicitTrigger
|
||||
* @param {cache} cache for trigger specs
|
||||
* @returns {import("./htmx").HtmxTriggerSpecification[]}
|
||||
*/
|
||||
function parseAndCacheTrigger(elt, explicitTrigger, cache) {
|
||||
var triggerSpecs = [];
|
||||
var tokens = tokenizeString(explicitTrigger);
|
||||
do {
|
||||
consumeUntil(tokens, NOT_WHITESPACE);
|
||||
var initialLength = tokens.length;
|
||||
var trigger = consumeUntil(tokens, /[,\[\s]/);
|
||||
if (trigger !== "") {
|
||||
if (trigger === "every") {
|
||||
var every = {trigger: 'every'};
|
||||
consumeUntil(tokens, NOT_WHITESPACE);
|
||||
every.pollInterval = parseInterval(consumeUntil(tokens, /[,\[\s]/));
|
||||
consumeUntil(tokens, NOT_WHITESPACE);
|
||||
var eventFilter = maybeGenerateConditional(elt, tokens, "event");
|
||||
if (eventFilter) {
|
||||
every.eventFilter = eventFilter;
|
||||
}
|
||||
triggerSpecs.push(every);
|
||||
} else if (trigger.indexOf("sse:") === 0) {
|
||||
triggerSpecs.push({trigger: 'sse', sseEvent: trigger.substr(4)});
|
||||
} else {
|
||||
var triggerSpec = {trigger: trigger};
|
||||
var eventFilter = maybeGenerateConditional(elt, tokens, "event");
|
||||
if (eventFilter) {
|
||||
triggerSpec.eventFilter = eventFilter;
|
||||
}
|
||||
while (tokens.length > 0 && tokens[0] !== ",") {
|
||||
consumeUntil(tokens, NOT_WHITESPACE)
|
||||
var token = tokens.shift();
|
||||
if (token === "changed") {
|
||||
triggerSpec.changed = true;
|
||||
} else if (token === "once") {
|
||||
triggerSpec.once = true;
|
||||
} else if (token === "consume") {
|
||||
triggerSpec.consume = true;
|
||||
} else if (token === "delay" && tokens[0] === ":") {
|
||||
tokens.shift();
|
||||
triggerSpec.delay = parseInterval(consumeUntil(tokens, WHITESPACE_OR_COMMA));
|
||||
} else if (token === "from" && tokens[0] === ":") {
|
||||
tokens.shift();
|
||||
if (COMBINED_SELECTOR_START.test(tokens[0])) {
|
||||
var from_arg = consumeCSSSelector(tokens);
|
||||
} else {
|
||||
var from_arg = consumeUntil(tokens, WHITESPACE_OR_COMMA);
|
||||
if (from_arg === "closest" || from_arg === "find" || from_arg === "next" || from_arg === "previous") {
|
||||
tokens.shift();
|
||||
var selector = consumeCSSSelector(tokens);
|
||||
// `next` and `previous` allow a selector-less syntax
|
||||
if (selector.length > 0) {
|
||||
from_arg += " " + selector;
|
||||
}
|
||||
}
|
||||
}
|
||||
triggerSpec.from = from_arg;
|
||||
} else if (token === "target" && tokens[0] === ":") {
|
||||
tokens.shift();
|
||||
triggerSpec.target = consumeCSSSelector(tokens);
|
||||
} else if (token === "throttle" && tokens[0] === ":") {
|
||||
tokens.shift();
|
||||
triggerSpec.throttle = parseInterval(consumeUntil(tokens, WHITESPACE_OR_COMMA));
|
||||
} else if (token === "queue" && tokens[0] === ":") {
|
||||
tokens.shift();
|
||||
triggerSpec.queue = consumeUntil(tokens, WHITESPACE_OR_COMMA);
|
||||
} else if (token === "root" && tokens[0] === ":") {
|
||||
tokens.shift();
|
||||
triggerSpec[token] = consumeCSSSelector(tokens);
|
||||
} else if (token === "threshold" && tokens[0] === ":") {
|
||||
tokens.shift();
|
||||
triggerSpec[token] = consumeUntil(tokens, WHITESPACE_OR_COMMA);
|
||||
} else {
|
||||
triggerErrorEvent(elt, "htmx:syntax:error", {token:tokens.shift()});
|
||||
}
|
||||
}
|
||||
triggerSpecs.push(triggerSpec);
|
||||
}
|
||||
}
|
||||
if (tokens.length === initialLength) {
|
||||
triggerErrorEvent(elt, "htmx:syntax:error", {token:tokens.shift()});
|
||||
}
|
||||
consumeUntil(tokens, NOT_WHITESPACE);
|
||||
} while (tokens[0] === "," && tokens.shift())
|
||||
if (cache) {
|
||||
cache[explicitTrigger] = triggerSpecs
|
||||
}
|
||||
return triggerSpecs
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} elt
|
||||
* @returns {import("./htmx").HtmxTriggerSpecification[]}
|
||||
@ -1258,85 +1367,8 @@ return (function () {
|
||||
var explicitTrigger = getAttributeValue(elt, 'hx-trigger');
|
||||
var triggerSpecs = [];
|
||||
if (explicitTrigger) {
|
||||
var tokens = tokenizeString(explicitTrigger);
|
||||
do {
|
||||
consumeUntil(tokens, NOT_WHITESPACE);
|
||||
var initialLength = tokens.length;
|
||||
var trigger = consumeUntil(tokens, /[,\[\s]/);
|
||||
if (trigger !== "") {
|
||||
if (trigger === "every") {
|
||||
var every = {trigger: 'every'};
|
||||
consumeUntil(tokens, NOT_WHITESPACE);
|
||||
every.pollInterval = parseInterval(consumeUntil(tokens, /[,\[\s]/));
|
||||
consumeUntil(tokens, NOT_WHITESPACE);
|
||||
var eventFilter = maybeGenerateConditional(elt, tokens, "event");
|
||||
if (eventFilter) {
|
||||
every.eventFilter = eventFilter;
|
||||
}
|
||||
triggerSpecs.push(every);
|
||||
} else if (trigger.indexOf("sse:") === 0) {
|
||||
triggerSpecs.push({trigger: 'sse', sseEvent: trigger.substr(4)});
|
||||
} else {
|
||||
var triggerSpec = {trigger: trigger};
|
||||
var eventFilter = maybeGenerateConditional(elt, tokens, "event");
|
||||
if (eventFilter) {
|
||||
triggerSpec.eventFilter = eventFilter;
|
||||
}
|
||||
while (tokens.length > 0 && tokens[0] !== ",") {
|
||||
consumeUntil(tokens, NOT_WHITESPACE)
|
||||
var token = tokens.shift();
|
||||
if (token === "changed") {
|
||||
triggerSpec.changed = true;
|
||||
} else if (token === "once") {
|
||||
triggerSpec.once = true;
|
||||
} else if (token === "consume") {
|
||||
triggerSpec.consume = true;
|
||||
} else if (token === "delay" && tokens[0] === ":") {
|
||||
tokens.shift();
|
||||
triggerSpec.delay = parseInterval(consumeUntil(tokens, WHITESPACE_OR_COMMA));
|
||||
} else if (token === "from" && tokens[0] === ":") {
|
||||
tokens.shift();
|
||||
if (COMBINED_SELECTOR_START.test(tokens[0])) {
|
||||
var from_arg = consumeCSSSelector(tokens);
|
||||
} else {
|
||||
var from_arg = consumeUntil(tokens, WHITESPACE_OR_COMMA);
|
||||
if (from_arg === "closest" || from_arg === "find" || from_arg === "next" || from_arg === "previous") {
|
||||
tokens.shift();
|
||||
var selector = consumeCSSSelector(tokens);
|
||||
// `next` and `previous` allow a selector-less syntax
|
||||
if (selector.length > 0) {
|
||||
from_arg += " " + selector;
|
||||
}
|
||||
}
|
||||
}
|
||||
triggerSpec.from = from_arg;
|
||||
} else if (token === "target" && tokens[0] === ":") {
|
||||
tokens.shift();
|
||||
triggerSpec.target = consumeCSSSelector(tokens);
|
||||
} else if (token === "throttle" && tokens[0] === ":") {
|
||||
tokens.shift();
|
||||
triggerSpec.throttle = parseInterval(consumeUntil(tokens, WHITESPACE_OR_COMMA));
|
||||
} else if (token === "queue" && tokens[0] === ":") {
|
||||
tokens.shift();
|
||||
triggerSpec.queue = consumeUntil(tokens, WHITESPACE_OR_COMMA);
|
||||
} else if (token === "root" && tokens[0] === ":") {
|
||||
tokens.shift();
|
||||
triggerSpec[token] = consumeCSSSelector(tokens);
|
||||
} else if (token === "threshold" && tokens[0] === ":") {
|
||||
tokens.shift();
|
||||
triggerSpec[token] = consumeUntil(tokens, WHITESPACE_OR_COMMA);
|
||||
} else {
|
||||
triggerErrorEvent(elt, "htmx:syntax:error", {token:tokens.shift()});
|
||||
}
|
||||
}
|
||||
triggerSpecs.push(triggerSpec);
|
||||
}
|
||||
}
|
||||
if (tokens.length === initialLength) {
|
||||
triggerErrorEvent(elt, "htmx:syntax:error", {token:tokens.shift()});
|
||||
}
|
||||
consumeUntil(tokens, NOT_WHITESPACE);
|
||||
} while (tokens[0] === "," && tokens.shift())
|
||||
var cache = htmx.config.triggerSpecsCache
|
||||
triggerSpecs = (cache && cache[explicitTrigger]) || parseAndCacheTrigger(elt, explicitTrigger, cache)
|
||||
}
|
||||
|
||||
if (triggerSpecs.length > 0) {
|
||||
@ -1508,14 +1540,14 @@ return (function () {
|
||||
return;
|
||||
}
|
||||
|
||||
if (triggerSpec.throttle) {
|
||||
if (triggerSpec.throttle > 0) {
|
||||
if (!elementData.throttle) {
|
||||
handler(elt, evt);
|
||||
elementData.throttle = setTimeout(function () {
|
||||
elementData.throttle = null;
|
||||
}, triggerSpec.throttle);
|
||||
}
|
||||
} else if (triggerSpec.delay) {
|
||||
} else if (triggerSpec.delay > 0) {
|
||||
elementData.delayed = setTimeout(function() { handler(elt, evt) }, triggerSpec.delay);
|
||||
} else {
|
||||
triggerEvent(elt, 'htmx:trigger')
|
||||
@ -1792,7 +1824,7 @@ return (function () {
|
||||
handler(elt);
|
||||
}
|
||||
}
|
||||
if (delay) {
|
||||
if (delay > 0) {
|
||||
setTimeout(load, delay);
|
||||
} else {
|
||||
load();
|
||||
@ -1851,7 +1883,7 @@ return (function () {
|
||||
if (!maybeFilterEvent(triggerSpec, elt, makeEvent("load", {elt: elt}))) {
|
||||
loadImmediately(elt, handler, nodeData, triggerSpec.delay);
|
||||
}
|
||||
} else if (triggerSpec.pollInterval) {
|
||||
} else if (triggerSpec.pollInterval > 0) {
|
||||
nodeData.polling = true;
|
||||
processPolling(elt, handler, triggerSpec);
|
||||
} else {
|
||||
@ -1894,26 +1926,35 @@ return (function () {
|
||||
});
|
||||
}
|
||||
|
||||
function hasChanceOfBeingBoosted() {
|
||||
return document.querySelector("[hx-boost], [data-hx-boost]");
|
||||
function shouldProcessHxOn(elt) {
|
||||
var attributes = elt.attributes
|
||||
for (var j = 0; j < attributes.length; j++) {
|
||||
var attrName = attributes[j].name
|
||||
if (startsWith(attrName, "hx-on:") || startsWith(attrName, "data-hx-on:") ||
|
||||
startsWith(attrName, "hx-on-") || startsWith(attrName, "data-hx-on-")) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function findHxOnWildcardElements(elt) {
|
||||
var node = null
|
||||
var elements = []
|
||||
|
||||
if (shouldProcessHxOn(elt)) {
|
||||
elements.push(elt)
|
||||
}
|
||||
|
||||
if (document.evaluate) {
|
||||
var iter = document.evaluate('//*[@*[ starts-with(name(), "hx-on:") or starts-with(name(), "data-hx-on:") ]]', elt)
|
||||
var iter = document.evaluate('.//*[@*[ starts-with(name(), "hx-on:") or starts-with(name(), "data-hx-on:") or' +
|
||||
' starts-with(name(), "hx-on-") or starts-with(name(), "data-hx-on-") ]]', elt)
|
||||
while (node = iter.iterateNext()) elements.push(node)
|
||||
} else {
|
||||
var allElements = document.getElementsByTagName("*")
|
||||
var allElements = elt.getElementsByTagName("*")
|
||||
for (var i = 0; i < allElements.length; i++) {
|
||||
var attributes = allElements[i].attributes
|
||||
for (var j = 0; j < attributes.length; j++) {
|
||||
var attrName = attributes[j].name
|
||||
if (startsWith(attrName, "hx-on:") || startsWith(attrName, "data-hx-on:")) {
|
||||
elements.push(allElements[i])
|
||||
}
|
||||
if (shouldProcessHxOn(allElements[i])) {
|
||||
elements.push(allElements[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1923,8 +1964,8 @@ return (function () {
|
||||
|
||||
function findElementsToProcess(elt) {
|
||||
if (elt.querySelectorAll) {
|
||||
var boostedElts = hasChanceOfBeingBoosted() ? ", a" : "";
|
||||
var results = elt.querySelectorAll(VERB_SELECTOR + boostedElts + ", form, [type='submit'], [hx-sse], [data-hx-sse], [hx-ws]," +
|
||||
var boostedSelector = ", [hx-boost] a, [data-hx-boost] a, a[hx-boost], a[data-hx-boost]";
|
||||
var results = elt.querySelectorAll(VERB_SELECTOR + boostedSelector + ", form, [type='submit'], [hx-sse], [data-hx-sse], [hx-ws]," +
|
||||
" [data-hx-ws], [hx-ext], [data-hx-ext], [hx-trigger], [data-hx-trigger], [hx-on], [data-hx-on]");
|
||||
return results;
|
||||
} else {
|
||||
@ -1970,7 +2011,7 @@ return (function () {
|
||||
function countCurlies(line) {
|
||||
var tokens = tokenizeString(line);
|
||||
var netCurlies = 0;
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
for (var i = 0; i < tokens.length; i++) {
|
||||
const token = tokens[i];
|
||||
if (token === "{") {
|
||||
netCurlies++;
|
||||
@ -2032,12 +2073,22 @@ return (function () {
|
||||
for (var i = 0; i < elt.attributes.length; i++) {
|
||||
var name = elt.attributes[i].name
|
||||
var value = elt.attributes[i].value
|
||||
if (startsWith(name, "hx-on:") || startsWith(name, "data-hx-on:")) {
|
||||
let eventName = name.slice(name.indexOf(":") + 1)
|
||||
// if the eventName starts with a colon, prepend "htmx" for shorthand support
|
||||
if (startsWith(eventName, ":")) eventName = "htmx" + eventName
|
||||
if (startsWith(name, "hx-on") || startsWith(name, "data-hx-on")) {
|
||||
var afterOnPosition = name.indexOf("-on") + 3;
|
||||
var nextChar = name.slice(afterOnPosition, afterOnPosition + 1);
|
||||
if (nextChar === "-" || nextChar === ":") {
|
||||
var eventName = name.slice(afterOnPosition + 1);
|
||||
// if the eventName starts with a colon or dash, prepend "htmx" for shorthand support
|
||||
if (startsWith(eventName, ":")) {
|
||||
eventName = "htmx" + eventName
|
||||
} else if (startsWith(eventName, "-")) {
|
||||
eventName = "htmx:" + eventName.slice(1);
|
||||
} else if (startsWith(eventName, "htmx-")) {
|
||||
eventName = "htmx:" + eventName.slice(5);
|
||||
}
|
||||
|
||||
addHxOnEventHandler(elt, eventName, value)
|
||||
addHxOnEventHandler(elt, eventName, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2315,7 +2366,9 @@ return (function () {
|
||||
var details = {path: path, xhr:request};
|
||||
triggerEvent(getDocument().body, "htmx:historyCacheMiss", details);
|
||||
request.open('GET', path, true);
|
||||
request.setRequestHeader("HX-Request", "true");
|
||||
request.setRequestHeader("HX-History-Restore-Request", "true");
|
||||
request.setRequestHeader("HX-Current-URL", getDocument().location.href);
|
||||
request.onload = function () {
|
||||
if (this.status >= 200 && this.status < 400) {
|
||||
triggerEvent(getDocument().body, "htmx:historyCacheMissLoad", details);
|
||||
@ -2430,7 +2483,7 @@ return (function () {
|
||||
}
|
||||
|
||||
function shouldInclude(elt) {
|
||||
if(elt.name === "" || elt.name == null || elt.disabled) {
|
||||
if(elt.name === "" || elt.name == null || elt.disabled || closest(elt, "fieldset[disabled]")) {
|
||||
return false;
|
||||
}
|
||||
// ignore "submitter" types (see jQuery src/serialize.js)
|
||||
@ -2906,7 +2959,7 @@ return (function () {
|
||||
}
|
||||
|
||||
function hasHeader(xhr, regexp) {
|
||||
return xhr.getAllResponseHeaders().match(regexp);
|
||||
return regexp.test(xhr.getAllResponseHeaders())
|
||||
}
|
||||
|
||||
function ajaxHelper(verb, path, context) {
|
||||
@ -3453,7 +3506,11 @@ return (function () {
|
||||
}
|
||||
|
||||
if (hasHeader(xhr,/HX-Retarget:/i)) {
|
||||
responseInfo.target = getDocument().querySelector(xhr.getResponseHeader("HX-Retarget"));
|
||||
if (xhr.getResponseHeader("HX-Retarget") === "this") {
|
||||
responseInfo.target = elt;
|
||||
} else {
|
||||
responseInfo.target = querySelectorExt(elt, xhr.getResponseHeader("HX-Retarget"));
|
||||
}
|
||||
}
|
||||
|
||||
var historyUpdate = determineHistoryUpdates(elt, responseInfo);
|
||||
@ -3752,34 +3809,25 @@ return (function () {
|
||||
//====================================================================
|
||||
// Initialization
|
||||
//====================================================================
|
||||
/**
|
||||
* We want to initialize the page elements after DOMContentLoaded
|
||||
* fires, but there isn't always a good way to tell whether
|
||||
* it has already fired when we get here or not.
|
||||
*/
|
||||
function ready(functionToCall) {
|
||||
// call the function exactly once no matter how many times this is called
|
||||
var callReadyFunction = function() {
|
||||
if (!functionToCall) return;
|
||||
functionToCall();
|
||||
functionToCall = null;
|
||||
};
|
||||
var isReady = false
|
||||
getDocument().addEventListener('DOMContentLoaded', function() {
|
||||
isReady = true
|
||||
})
|
||||
|
||||
if (getDocument().readyState === "complete") {
|
||||
// DOMContentLoaded definitely fired, we can initialize the page
|
||||
callReadyFunction();
|
||||
}
|
||||
else {
|
||||
/* DOMContentLoaded *maybe* already fired, wait for
|
||||
* the next DOMContentLoaded or readystatechange event
|
||||
*/
|
||||
getDocument().addEventListener("DOMContentLoaded", function() {
|
||||
callReadyFunction();
|
||||
});
|
||||
getDocument().addEventListener("readystatechange", function() {
|
||||
if (getDocument().readyState !== "complete") return;
|
||||
callReadyFunction();
|
||||
});
|
||||
/**
|
||||
* Execute a function now if DOMContentLoaded has fired, otherwise listen for it.
|
||||
*
|
||||
* This function uses isReady because there is no realiable way to ask the browswer whether
|
||||
* the DOMContentLoaded event has already been fired; there's a gap between DOMContentLoaded
|
||||
* firing and readystate=complete.
|
||||
*/
|
||||
function ready(fn) {
|
||||
// Checking readyState here is a failsafe in case the htmx script tag entered the DOM by
|
||||
// some means other than the initial page load.
|
||||
if (isReady || getDocument().readyState === 'complete') {
|
||||
fn();
|
||||
} else {
|
||||
getDocument().addEventListener('DOMContentLoaded', fn);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3827,7 +3875,9 @@ return (function () {
|
||||
internalData.xhr.abort();
|
||||
}
|
||||
});
|
||||
var originalPopstate = window.onpopstate;
|
||||
/** @type {(ev: PopStateEvent) => any} */
|
||||
const originalPopstate = window.onpopstate ? window.onpopstate.bind(window) : null;
|
||||
/** @type {(ev: PopStateEvent) => any} */
|
||||
window.onpopstate = function (event) {
|
||||
if (event.state && event.state.htmx) {
|
||||
restoreHistory();
|
||||
|
Loading…
x
Reference in New Issue
Block a user