mirror of
https://github.com/bigskysoftware/htmx.git
synced 2026-01-25 05:06:13 +00:00
test reorg and docs
This commit is contained in:
@@ -1,35 +1,46 @@
|
||||
# htmx Coding Standards
|
||||
|
||||
* Prefer `for` loop to `forEach` (easier to debug, compresses better)
|
||||
* Assign complex expressions to a local variable rather than using them directly in statements (easier to debug)
|
||||
* Private methods should be prefixed with `__`. The `dist` task will replace double underscore with `#` when it builds
|
||||
the final script. This allows us to unit test private methods.
|
||||
* "Internal" methods should be prefixed with a `_`. These methods are _not_ guaranteed to never change, but may be useful
|
||||
for special cases (e.g. the `quirks` htmx 2.0 compatibility extension)
|
||||
* Public methods are forever, be very careful with them
|
||||
* Use `let` rather than `const`
|
||||
* Publicly surfaced properties should not be shortened, _except_ "Configuration" which can be shortened to "Config"
|
||||
* Local variables should have descriptive names in most cases. `ctx` and `elt` are acceptable.
|
||||
* Terser does a good job of minimizing names, so there is no benefit from a size perspective to using short variable names.
|
||||
* Generally all state in the trigger -> request -> swap life cycle should be stored on `ctx`. Try to avoid overwriting
|
||||
an existing property, pick a new property name. These properties are part of the public API and *must* be documented.
|
||||
* There is size code benefit to naked if statements, use curlies to make debugging easier:
|
||||
```js
|
||||
// terser turns these two forms into the same compressed code
|
||||
if(bool) return;
|
||||
if(bool) {
|
||||
return;
|
||||
}
|
||||
```
|
||||
* Please lets keep this thing under 10k please please please
|
||||
* General Code Style
|
||||
* Prefer `for` loop to `forEach` (easier to debug, compresses better)
|
||||
* Assign complex expressions to a local variable rather than using them directly in statements (easier to debug)
|
||||
* Use `let` rather than `const`
|
||||
* Local variables should have descriptive names in most cases. `ctx` and `elt` are acceptable.
|
||||
* Terser does a good job of minimizing names, so there is no benefit from a size perspective to using short variable names.
|
||||
* There is no size code benefit to naked if statements, use curlies to make debugging easier:
|
||||
```js
|
||||
// terser turns these two forms into the same compressed code
|
||||
if(bool) return;
|
||||
if(bool) {
|
||||
return;
|
||||
}
|
||||
```
|
||||
* Method/Field Conventions
|
||||
* Private methods should be prefixed with `__`. The `dist` task will replace double underscore with `#` when it builds
|
||||
the final script. This allows us to unit test private methods.
|
||||
* "Internal" methods should be prefixed with a `_`. These methods are _not_ guaranteed to never change, but may be useful
|
||||
for special cases (e.g. the `quirks` htmx 2.0 compatibility extension)
|
||||
* Public methods are forever, be very careful with them
|
||||
* Publicly surfaced properties should not be shortened, _except_ "Configuration" which can be shortened to "Config"
|
||||
* Architectural Style
|
||||
* Generally all state in the trigger -> request -> swap life cycle should be stored on `ctx`. Try to avoid overwriting
|
||||
an existing property, pick a new property name. These properties are part of the public API and *must* be documented.
|
||||
|
||||
## Testing
|
||||
|
||||
* TODO - outline testing standards
|
||||
Tests for htmx are organized in the following manner:
|
||||
|
||||
* `/test/unit` - These tests should for the most part *directly* exercise public and private methods. Because in
|
||||
dev private methods are just public methods that start with `__` this is easy to do. Unit tests should be created
|
||||
after a method has stabilized and the behavior is reasonably well understood.
|
||||
* `/test/attributes` - These are integration tests that test the full behavior of a given attribute and should do things
|
||||
like set up a response mock using `mockResponse()`, create a live HTML button with the `createProcessedHTML` method,
|
||||
invoke `click()` on the button, await the `"htmx:finally:request" event, and assert something about the updated DOM.
|
||||
* `/test/end2end` - These are end-to-end tests that do not fit in the other two categories
|
||||
|
||||
|
||||
## AI Policy
|
||||
|
||||
AI may not be used to generate any significant amount of code that is added to htmx.js. It may be used to _suggest_ code,
|
||||
AI may _not_ be used to generate any significant amount of code that is added to htmx.js. It may be used to _suggest_ code,
|
||||
but that code must be audited and every line understood by the author.
|
||||
|
||||
AI _may_ be used to generate tests for htmx. These tests should follow the existing standards as much as possible and
|
||||
|
||||
178
dist/htmx.esm.js
vendored
178
dist/htmx.esm.js
vendored
@@ -72,7 +72,7 @@ var htmx = (() => {
|
||||
this.#initHtmxConfig();
|
||||
this.#initRequestIndicatorCss();
|
||||
this.#actionSelector = `[${this.#prefix("hx-action")}],[${this.#prefix("hx-get")}],[${this.#prefix("hx-post")}],[${this.#prefix("hx-put")}],[${this.#prefix("hx-patch")}],[${this.#prefix("hx-delete")}]`;
|
||||
this.#hxOnQuery = new XPathEvaluator().createExpression(`.//*[@*[ starts-with(name(), "${this.#prefix("hx-on:")}")]]`);
|
||||
this.#hxOnQuery = new XPathEvaluator().createExpression(`.//*[@*[ starts-with(name(), "${this.#prefix("hx-on")}")]]`);
|
||||
this.#internalAPI = {
|
||||
attributeValue: this.#attributeValue.bind(this),
|
||||
parseTriggerSpecs: this.#parseTriggerSpecs.bind(this),
|
||||
@@ -110,6 +110,7 @@ var htmx = (() => {
|
||||
},
|
||||
morphIgnore: ["data-htmx-powered"],
|
||||
noSwap: [204],
|
||||
implicitInheritance: false
|
||||
}
|
||||
let metaConfig = document.querySelector('meta[name="htmx:config"]');
|
||||
if (metaConfig) {
|
||||
@@ -176,29 +177,41 @@ var htmx = (() => {
|
||||
|
||||
#attributeValue(elt, name, defaultVal) {
|
||||
name = this.#prefix(name);
|
||||
let appendName = name + ":append";
|
||||
let inheritName = name + ":inherited";
|
||||
let inheritAppendName = name + ":inherited:append";
|
||||
let appendName = name + this.#maybeAdjustMetaCharacter(":append");
|
||||
let inheritName = name + (this.config.implicitInheritance ? "" : this.#maybeAdjustMetaCharacter(":inherited"));
|
||||
let inheritAppendName = name + this.#maybeAdjustMetaCharacter(":inherited:append");
|
||||
|
||||
if (elt.hasAttribute(name) || elt.hasAttribute(inheritName)) {
|
||||
return elt.getAttribute(name) || elt.getAttribute(inheritName);
|
||||
if (elt.hasAttribute(name)) {
|
||||
return{ val: elt.getAttribute(name), src: elt };
|
||||
}
|
||||
|
||||
if (elt.hasAttribute(inheritName)) {
|
||||
return{ val: elt.getAttribute(inheritName), src: elt };
|
||||
}
|
||||
|
||||
if (elt.hasAttribute(appendName) || elt.hasAttribute(inheritAppendName)) {
|
||||
let appendValue = elt.getAttribute(appendName) || elt.getAttribute(inheritAppendName);
|
||||
let parent = elt.parentNode?.closest?.(`[${CSS.escape(inheritName)}],[${CSS.escape(inheritAppendName)}]`);
|
||||
if (parent) {
|
||||
let inheritedValue = this.#attributeValue(parent, name);
|
||||
return inheritedValue ? inheritedValue + "," + appendValue : appendValue;
|
||||
let inherited = this.#attributeValue(parent, name);
|
||||
return {
|
||||
val: inherited.val ? inherited.val + "," + appendValue : appendValue,
|
||||
src: elt
|
||||
};
|
||||
} else {
|
||||
return {val: appendValue, src: elt};
|
||||
}
|
||||
return appendValue;
|
||||
}
|
||||
|
||||
let parent = elt.parentNode?.closest?.(`[${CSS.escape(inheritName)}],[${CSS.escape(inheritAppendName)}]`);
|
||||
if (parent) {
|
||||
return this.#attributeValue(parent, name);
|
||||
let valAndSrc = this.#attributeValue(parent, name);
|
||||
if (valAndSrc && this.config.implicitInheritance) {
|
||||
this.#trigger(elt, "htmx:after:implicitInheritance", {elt, parent})
|
||||
}
|
||||
return valAndSrc;
|
||||
}
|
||||
return defaultVal;
|
||||
return{ val: defaultVal, src: elt };
|
||||
}
|
||||
|
||||
#tokenize(str) {
|
||||
@@ -263,13 +276,14 @@ var htmx = (() => {
|
||||
if (this.#isBoosted(elt)) {
|
||||
return this.#boostedMethodAndAction(elt, evt)
|
||||
} else {
|
||||
let method = this.#attributeValue(elt, "hx-method") || "get";
|
||||
let action = this.#attributeValue(elt, "hx-action");
|
||||
let valueAndElt = this.#attributeValue(elt, "hx-method")
|
||||
let method = valueAndElt.val || "GET"
|
||||
let {val: action} = this.#attributeValue(elt, "hx-action") || {};
|
||||
if (!action) {
|
||||
for (let verb of this.#verbs) {
|
||||
let verbAttribute = this.#attributeValue(elt, "hx-" + verb);
|
||||
if (verbAttribute) {
|
||||
action = verbAttribute;
|
||||
let {val: verbAction} = this.#attributeValue(elt, "hx-" + verb) || {};
|
||||
if (verbAction) {
|
||||
action = verbAction;
|
||||
method = verb;
|
||||
break;
|
||||
}
|
||||
@@ -319,15 +333,15 @@ var htmx = (() => {
|
||||
sourceElement,
|
||||
sourceEvent,
|
||||
status: "created",
|
||||
select: this.#attributeValue(sourceElement, "hx-select"),
|
||||
selectOOB: this.#attributeValue(sourceElement, "hx-select-oob"),
|
||||
target: this.#attributeValue(sourceElement, "hx-target"),
|
||||
swap: this.#attributeValue(sourceElement, "hx-swap", this.config.defaultSwap),
|
||||
push: this.#attributeValue(sourceElement, "hx-push-url"),
|
||||
replace: this.#attributeValue(sourceElement, "hx-replace-url"),
|
||||
select: this.#attributeValue(sourceElement, "hx-select")?.val,
|
||||
selectOOB: this.#attributeValue(sourceElement, "hx-select-oob")?.val,
|
||||
target: this.#attributeValue(sourceElement, "hx-target")?.val,
|
||||
swap: this.#attributeValue(sourceElement, "hx-swap", this.config.defaultSwap)?.val,
|
||||
push: this.#attributeValue(sourceElement, "hx-push-url")?.val,
|
||||
replace: this.#attributeValue(sourceElement, "hx-replace-url")?.val,
|
||||
transition: this.config.transitions,
|
||||
request: {
|
||||
validate: "true" === this.#attributeValue(sourceElement, "hx-validate", sourceElement.matches('form') ? "true" : "false"),
|
||||
validate: "true" === this.#attributeValue(sourceElement, "hx-validate", sourceElement.matches('form') ? "true" : "false")?.val,
|
||||
action,
|
||||
method,
|
||||
headers: this.#determineHeaders(sourceElement)
|
||||
@@ -335,7 +349,7 @@ var htmx = (() => {
|
||||
};
|
||||
|
||||
// Apply hx-config overrides
|
||||
let configAttr = this.#attributeValue(sourceElement, "hx-config");
|
||||
let {val: configAttr} = this.#attributeValue(sourceElement, "hx-config") || {};
|
||||
if (configAttr) {
|
||||
let configOverrides = JSON.parse(configAttr);
|
||||
let requestConfig = ctx.request;
|
||||
@@ -364,7 +378,7 @@ var htmx = (() => {
|
||||
if (this.#isBoosted(elt)) {
|
||||
headers["HX-Boosted"] = "true"
|
||||
}
|
||||
let headersAttribute = this.#attributeValue(elt, "hx-headers");
|
||||
let {val: headersAttribute} = this.#attributeValue(elt, "hx-headers") || {};
|
||||
if (headersAttribute) {
|
||||
Object.assign(headers, JSON.parse(headersAttribute));
|
||||
}
|
||||
@@ -375,11 +389,7 @@ var htmx = (() => {
|
||||
if (selector instanceof Element) {
|
||||
return selector;
|
||||
} else if (selector === 'this') {
|
||||
if (elt.hasAttribute(this.#prefix("hx-target"))) {
|
||||
return elt;
|
||||
} else {
|
||||
return elt.closest(`[${this.#prefix("hx-target")}\\:inherited='this']`)
|
||||
}
|
||||
return this.#attributeValue(elt, "hx-target").src
|
||||
} else if (selector != null) {
|
||||
return this.find(elt, selector);
|
||||
} else if (this.#isBoosted(elt)) {
|
||||
@@ -393,7 +403,7 @@ var htmx = (() => {
|
||||
return elt._htmx?.boosted;
|
||||
}
|
||||
|
||||
async __handleTriggerEvent(ctx) {
|
||||
async #handleTriggerEvent(ctx) {
|
||||
let elt = ctx.sourceElement
|
||||
let evt = ctx.sourceEvent
|
||||
if (!elt.isConnected) return
|
||||
@@ -419,7 +429,7 @@ var htmx = (() => {
|
||||
// Setup abort controller and action
|
||||
let ac = new AbortController()
|
||||
let action = ctx.request.action.replace?.(/#.*$/, '')
|
||||
// TODO - consider how this works with hx-config, move most to __createRequestContext?
|
||||
// TODO - consider how this works with hx-config, move most to #createRequestContext?
|
||||
Object.assign(ctx.request, {
|
||||
originalAction: ctx.request.action,
|
||||
action,
|
||||
@@ -445,14 +455,14 @@ var htmx = (() => {
|
||||
let params = new URLSearchParams(ctx.request.body);
|
||||
if (params.size) ctx.request.action += (/\?/.test(ctx.request.action) ? "&" : "?") + params
|
||||
ctx.request.body = null
|
||||
} else if (this.#attributeValue(elt, "hx-encoding") !== "multipart/form-data") {
|
||||
} else if (this.#attributeValue(elt, "hx-encoding")?.val !== "multipart/form-data") {
|
||||
ctx.request.body = new URLSearchParams(ctx.request.body);
|
||||
}
|
||||
|
||||
await this.#issueRequest(ctx);
|
||||
}
|
||||
|
||||
async __issueRequest(ctx) {
|
||||
async #issueRequest(ctx) {
|
||||
let elt = ctx.sourceElement
|
||||
let syncStrategy = this.#determineSyncStrategy(elt);
|
||||
let requestQueue = this.#getRequestQueue(elt);
|
||||
@@ -462,14 +472,14 @@ var htmx = (() => {
|
||||
ctx.status = "issuing"
|
||||
this.#initTimeout(ctx);
|
||||
|
||||
let indicatorsSelector = this.#attributeValue(elt, "hx-indicator");
|
||||
let {val: indicatorsSelector} = this.#attributeValue(elt, "hx-indicator") || {};
|
||||
let indicators = this.#showIndicators(elt, indicatorsSelector);
|
||||
let disableSelector = this.#attributeValue(elt, "hx-disable");
|
||||
let {val: disableSelector} = this.#attributeValue(elt, "hx-disable") || {};
|
||||
let disableElements = this.#disableElements(elt, disableSelector);
|
||||
|
||||
try {
|
||||
// Confirm dialog
|
||||
let confirmVal = this.#attributeValue(elt, 'hx-confirm')
|
||||
let {val: confirmVal} = this.#attributeValue(elt, 'hx-confirm') || {};
|
||||
if (confirmVal) {
|
||||
let js = this.#extractJavascriptContent(confirmVal);
|
||||
if (js) {
|
||||
@@ -572,7 +582,7 @@ var htmx = (() => {
|
||||
}
|
||||
}
|
||||
|
||||
async __handleSSE(ctx, elt, response) {
|
||||
async #handleSSE(ctx, elt, response) {
|
||||
let config = elt._htmx?.streamConfig || {...this.config.streams};
|
||||
|
||||
let waitForVisible = () => new Promise(r => {
|
||||
@@ -669,7 +679,7 @@ var htmx = (() => {
|
||||
}
|
||||
}
|
||||
|
||||
async* __parseSSE(res) {
|
||||
async* #parseSSE(res) {
|
||||
let r = res.body.getReader(), d = new TextDecoder(), b = '', m = {data: '', event: '', id: '', retry: null},
|
||||
ls, i, n, f, v;
|
||||
try {
|
||||
@@ -700,12 +710,12 @@ var htmx = (() => {
|
||||
}
|
||||
|
||||
#determineSyncStrategy(elt) {
|
||||
let syncValue = this.#attributeValue(elt, "hx-sync");
|
||||
let {val: syncValue} = this.#attributeValue(elt, "hx-sync") || {};
|
||||
return syncValue?.split(":")[1] || "queue first";
|
||||
}
|
||||
|
||||
#getRequestQueue(elt) {
|
||||
let syncValue = this.#attributeValue(elt, "hx-sync");
|
||||
let {val: syncValue} = this.#attributeValue(elt, "hx-sync") || {};
|
||||
let syncElt = elt
|
||||
if (syncValue && syncValue.includes(":")) {
|
||||
let strings = syncValue.split(":");
|
||||
@@ -742,7 +752,7 @@ var htmx = (() => {
|
||||
}
|
||||
|
||||
#initializeTriggers(elt, initialHandler = elt._htmx.eventHandler) {
|
||||
let specString = this.#attributeValue(elt, "hx-trigger");
|
||||
let {val: specString} = this.#attributeValue(elt, "hx-trigger") || {};
|
||||
if (!specString) {
|
||||
specString = elt.matches("form") ? "submit" :
|
||||
elt.matches("input:not([type=button]),select,textarea") ? "change" :
|
||||
@@ -892,7 +902,7 @@ var htmx = (() => {
|
||||
}
|
||||
|
||||
#initializeStreamConfig(elt) {
|
||||
let streamSpec = this.#attributeValue(elt, 'hx-stream');
|
||||
let {val: streamSpec} = this.#attributeValue(elt, 'hx-stream') || {};
|
||||
if (!streamSpec) return;
|
||||
|
||||
// Start with global defaults
|
||||
@@ -961,7 +971,7 @@ var htmx = (() => {
|
||||
return bound;
|
||||
}
|
||||
|
||||
async __executeJavaScriptAsync(thisArg, obj, code, expression = true) {
|
||||
async #executeJavaScriptAsync(thisArg, obj, code, expression = true) {
|
||||
let args = {}
|
||||
Object.assign(args, this.#apiMethods(thisArg))
|
||||
Object.assign(args, obj)
|
||||
@@ -1002,7 +1012,7 @@ var htmx = (() => {
|
||||
}
|
||||
|
||||
#maybeBoost(elt) {
|
||||
if (this.#attributeValue(elt, "hx-boost") === "true") {
|
||||
if (this.#attributeValue(elt, "hx-boost")?.val === "true") {
|
||||
if (this.#shouldInitialize(elt)) {
|
||||
elt._htmx = {eventHandler: this.#createHtmxEventHandler(elt), requests: [], boosted: true}
|
||||
elt.setAttribute('data-htmx-powered', 'true');
|
||||
@@ -1077,7 +1087,8 @@ var htmx = (() => {
|
||||
|
||||
#makeFragment(text) {
|
||||
let response = text.replace(/<partial(\s+|>)/gi, '<template partial$1').replace(/<\/partial>/gi, '</template>');
|
||||
// TODO - store any head tag content on the fragment for head extension
|
||||
let title = '';
|
||||
response = response.replace(/<title[^>]*>([\s\S]*?)<\/title>/i, (m, t) => (title = t, ''));
|
||||
let responseWithNoHead = response.replace(/<head(\s[^>]*)?>[\s\S]*?<\/head>/i, '');
|
||||
let startTag = responseWithNoHead.match(/<([a-z][^\/>\x20\t\r\n\f]*)/i)?.[1]?.toLowerCase();
|
||||
|
||||
@@ -1092,10 +1103,11 @@ var htmx = (() => {
|
||||
doc = this.#parseHTML(`<template>${responseWithNoHead}</template>`);
|
||||
fragment = doc.querySelector('template').content;
|
||||
}
|
||||
this.#processScripts(fragment);
|
||||
|
||||
return {
|
||||
fragment,
|
||||
title: doc.title
|
||||
title
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1114,18 +1126,9 @@ var htmx = (() => {
|
||||
let swapSpec = this.#parseSwapSpec(oobValue);
|
||||
if (swapSpec.target) target = swapSpec.target;
|
||||
|
||||
let oobElementClone = elt.cloneNode(true);
|
||||
let fragment;
|
||||
if (swapSpec.strip === undefined && swapSpec.style !== 'outerHTML') {
|
||||
swapSpec.strip = true;
|
||||
}
|
||||
if (swapSpec.strip) {
|
||||
fragment = oobElementClone.content || oobElementClone;
|
||||
} else {
|
||||
fragment = document.createDocumentFragment();
|
||||
fragment.appendChild(oobElementClone);
|
||||
}
|
||||
elt.remove();
|
||||
swapSpec.strip ??= !swapSpec.style.startsWith('outer');
|
||||
let fragment = document.createDocumentFragment();
|
||||
fragment.append(elt);
|
||||
if (!target && !oobValue.includes('target:')) return;
|
||||
|
||||
tasks.push({
|
||||
@@ -1255,6 +1258,7 @@ var htmx = (() => {
|
||||
|
||||
async swap(ctx) {
|
||||
let {fragment, title} = this.#makeFragment(ctx.text);
|
||||
ctx.title = title;
|
||||
let tasks = [];
|
||||
|
||||
// Process OOB and partials
|
||||
@@ -1263,7 +1267,7 @@ var htmx = (() => {
|
||||
tasks.push(...oobTasks, ...partialTasks);
|
||||
|
||||
// Process main swap
|
||||
let mainSwap = this.#processMainSwap(ctx, fragment, partialTasks, title);
|
||||
let mainSwap = this.#processMainSwap(ctx, fragment, partialTasks);
|
||||
if (mainSwap) {
|
||||
tasks.push(mainSwap);
|
||||
}
|
||||
@@ -1299,7 +1303,7 @@ var htmx = (() => {
|
||||
}
|
||||
|
||||
this.#trigger(document, "htmx:after:swap", {ctx});
|
||||
if (mainSwap?.title) document.title = mainSwap.title;
|
||||
if (ctx.title && !mainSwap?.swapSpec?.ignoreTitle) document.title = ctx.title;
|
||||
await this.timeout(1);
|
||||
// invoke restore tasks
|
||||
for (let task of tasks) {
|
||||
@@ -1312,33 +1316,24 @@ var htmx = (() => {
|
||||
// if (ctx.hx?.triggerafterswap) this.#handleTriggerHeader(ctx.hx.triggerafterswap, ctx.sourceElement);
|
||||
}
|
||||
|
||||
#processMainSwap(ctx, fragment, partialTasks, title) {
|
||||
#processMainSwap(ctx, fragment, partialTasks) {
|
||||
// Create main task if needed
|
||||
let swapSpec = this.#parseSwapSpec(ctx.swap || this.config.defaultSwap);
|
||||
// skip creating main swap if extracting partials resulted in empty response except for delete style
|
||||
if (swapSpec.style === 'delete' || /\S/.test(fragment.innerHTML || '') || !partialTasks.length) {
|
||||
let resultFragment = document.createDocumentFragment();
|
||||
if (ctx.select) {
|
||||
let selected = fragment.querySelector(ctx.select);
|
||||
if (selected) {
|
||||
if (swapSpec.strip === false) {
|
||||
resultFragment.append(selected);
|
||||
} else {
|
||||
resultFragment.append(...selected.childNodes);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
resultFragment.append(...fragment.childNodes);
|
||||
let selected = fragment.querySelectorAll(ctx.select);
|
||||
fragment = document.createDocumentFragment();
|
||||
fragment.append(...selected);
|
||||
}
|
||||
|
||||
let mainSwap = {
|
||||
type: 'main',
|
||||
fragment: resultFragment,
|
||||
fragment,
|
||||
target: swapSpec.target || ctx.target,
|
||||
swapSpec,
|
||||
sourceElement: ctx.sourceElement,
|
||||
transition: (ctx.transition !== false) && (swapSpec.transition !== false),
|
||||
title
|
||||
transition: (ctx.transition !== false) && (swapSpec.transition !== false)
|
||||
};
|
||||
return mainSwap;
|
||||
}
|
||||
@@ -1350,8 +1345,13 @@ var htmx = (() => {
|
||||
target = document.querySelector(target);
|
||||
}
|
||||
if (!target) return;
|
||||
if (swapSpec.strip && fragment.firstElementChild) {
|
||||
let strip = document.createDocumentFragment();
|
||||
strip.append(...(fragment.firstElementChild.content || fragment.firstElementChild).childNodes);
|
||||
fragment = strip;
|
||||
}
|
||||
|
||||
let pantry = this.#handlePreservedElements(fragment);
|
||||
this.#processScripts(fragment);
|
||||
let parentNode = target.parentNode;
|
||||
let newContent = [...fragment.childNodes]
|
||||
if (swapSpec.style === 'innerHTML') {
|
||||
@@ -1407,6 +1407,8 @@ var htmx = (() => {
|
||||
if (this.config.logAll) {
|
||||
console.log(eventName, detail, on)
|
||||
}
|
||||
on = this.#normalizeElement(on)
|
||||
this.#triggerExtensions(on, this.#maybeAdjustMetaCharacter(eventName), detail);
|
||||
return this.trigger(on, eventName, detail, bubbles)
|
||||
}
|
||||
|
||||
@@ -1482,7 +1484,6 @@ var htmx = (() => {
|
||||
|
||||
trigger(on, eventName, detail = {}, bubbles = true) {
|
||||
on = this.#normalizeElement(on)
|
||||
this.#triggerExtensions(on, eventName, detail);
|
||||
let evt = new CustomEvent(eventName, {
|
||||
detail,
|
||||
cancelable: true,
|
||||
@@ -1597,8 +1598,9 @@ var htmx = (() => {
|
||||
|
||||
#handleHxOnAttributes(node) {
|
||||
for (let attr of node.getAttributeNames()) {
|
||||
if (attr.startsWith(this.#prefix("hx-on:"))) {
|
||||
let evtName = attr.substring(this.#prefix("hx-on:").length)
|
||||
var searchString = this.#maybeAdjustMetaCharacter(this.#prefix("hx-on:"));
|
||||
if (attr.startsWith(searchString)) {
|
||||
let evtName = attr.substring(searchString.length)
|
||||
let code = node.getAttribute(attr);
|
||||
node.addEventListener(evtName, async (evt) => {
|
||||
try {
|
||||
@@ -1674,7 +1676,7 @@ var htmx = (() => {
|
||||
formData.append(submitter.name, submitter.value)
|
||||
included.add(submitter);
|
||||
}
|
||||
let includeSelector = this.#attributeValue(elt, "hx-include");
|
||||
let {val: includeSelector} = this.#attributeValue(elt, "hx-include") || {};
|
||||
if (includeSelector) {
|
||||
let includeNodes = this.#findAllExt(elt, includeSelector);
|
||||
for (let node of includeNodes) {
|
||||
@@ -1715,7 +1717,7 @@ var htmx = (() => {
|
||||
}
|
||||
|
||||
#handleHxVals(elt, body) {
|
||||
let hxValsValue = this.#attributeValue(elt, "hx-vals");
|
||||
let {val: hxValsValue} = this.#attributeValue(elt, "hx-vals") || {};
|
||||
if (hxValsValue) {
|
||||
if (!hxValsValue.includes('{')) {
|
||||
hxValsValue = `{${hxValsValue}}`
|
||||
@@ -2065,7 +2067,7 @@ var htmx = (() => {
|
||||
}
|
||||
let str = status + ""
|
||||
for (let pattern of [str, str.slice(0, 2) + 'x', str[0] + 'xx']) {
|
||||
let swap = this.#attributeValue(ctx.sourceElement, "hx-status:" + pattern);
|
||||
let {val: swap} = this.#attributeValue(ctx.sourceElement, "hx-status:" + pattern) || {};
|
||||
if (swap) {
|
||||
ctx.swap = swap
|
||||
return
|
||||
@@ -2083,7 +2085,7 @@ var htmx = (() => {
|
||||
});
|
||||
}
|
||||
|
||||
async __processTransitionQueue() {
|
||||
async #processTransitionQueue() {
|
||||
if (this.#transitionQueue.length === 0 || this.#processingTransition) {
|
||||
return;
|
||||
}
|
||||
@@ -2133,6 +2135,14 @@ var htmx = (() => {
|
||||
return cssOrElement
|
||||
}
|
||||
}
|
||||
|
||||
#maybeAdjustMetaCharacter(string) {
|
||||
if (this.config.metaCharacter) {
|
||||
return string.replace(/:/g, this.config.metaCharacter);
|
||||
} else {
|
||||
return string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Htmx()
|
||||
|
||||
BIN
dist/htmx.esm.js.br
vendored
BIN
dist/htmx.esm.js.br
vendored
Binary file not shown.
2
dist/htmx.esm.min.js
vendored
2
dist/htmx.esm.min.js
vendored
File diff suppressed because one or more lines are too long
BIN
dist/htmx.esm.min.js.br
vendored
BIN
dist/htmx.esm.min.js.br
vendored
Binary file not shown.
2
dist/htmx.esm.min.js.map
vendored
2
dist/htmx.esm.min.js.map
vendored
File diff suppressed because one or more lines are too long
164
dist/htmx.js
vendored
164
dist/htmx.js
vendored
@@ -72,7 +72,7 @@ var htmx = (() => {
|
||||
this.#initHtmxConfig();
|
||||
this.#initRequestIndicatorCss();
|
||||
this.#actionSelector = `[${this.#prefix("hx-action")}],[${this.#prefix("hx-get")}],[${this.#prefix("hx-post")}],[${this.#prefix("hx-put")}],[${this.#prefix("hx-patch")}],[${this.#prefix("hx-delete")}]`;
|
||||
this.#hxOnQuery = new XPathEvaluator().createExpression(`.//*[@*[ starts-with(name(), "${this.#prefix("hx-on:")}")]]`);
|
||||
this.#hxOnQuery = new XPathEvaluator().createExpression(`.//*[@*[ starts-with(name(), "${this.#prefix("hx-on")}")]]`);
|
||||
this.#internalAPI = {
|
||||
attributeValue: this.#attributeValue.bind(this),
|
||||
parseTriggerSpecs: this.#parseTriggerSpecs.bind(this),
|
||||
@@ -110,6 +110,7 @@ var htmx = (() => {
|
||||
},
|
||||
morphIgnore: ["data-htmx-powered"],
|
||||
noSwap: [204],
|
||||
implicitInheritance: false
|
||||
}
|
||||
let metaConfig = document.querySelector('meta[name="htmx:config"]');
|
||||
if (metaConfig) {
|
||||
@@ -176,29 +177,41 @@ var htmx = (() => {
|
||||
|
||||
#attributeValue(elt, name, defaultVal) {
|
||||
name = this.#prefix(name);
|
||||
let appendName = name + ":append";
|
||||
let inheritName = name + ":inherited";
|
||||
let inheritAppendName = name + ":inherited:append";
|
||||
let appendName = name + this.#maybeAdjustMetaCharacter(":append");
|
||||
let inheritName = name + (this.config.implicitInheritance ? "" : this.#maybeAdjustMetaCharacter(":inherited"));
|
||||
let inheritAppendName = name + this.#maybeAdjustMetaCharacter(":inherited:append");
|
||||
|
||||
if (elt.hasAttribute(name) || elt.hasAttribute(inheritName)) {
|
||||
return elt.getAttribute(name) || elt.getAttribute(inheritName);
|
||||
if (elt.hasAttribute(name)) {
|
||||
return{ val: elt.getAttribute(name), src: elt };
|
||||
}
|
||||
|
||||
if (elt.hasAttribute(inheritName)) {
|
||||
return{ val: elt.getAttribute(inheritName), src: elt };
|
||||
}
|
||||
|
||||
if (elt.hasAttribute(appendName) || elt.hasAttribute(inheritAppendName)) {
|
||||
let appendValue = elt.getAttribute(appendName) || elt.getAttribute(inheritAppendName);
|
||||
let parent = elt.parentNode?.closest?.(`[${CSS.escape(inheritName)}],[${CSS.escape(inheritAppendName)}]`);
|
||||
if (parent) {
|
||||
let inheritedValue = this.#attributeValue(parent, name);
|
||||
return inheritedValue ? inheritedValue + "," + appendValue : appendValue;
|
||||
let inherited = this.#attributeValue(parent, name);
|
||||
return {
|
||||
val: inherited.val ? inherited.val + "," + appendValue : appendValue,
|
||||
src: elt
|
||||
};
|
||||
} else {
|
||||
return {val: appendValue, src: elt};
|
||||
}
|
||||
return appendValue;
|
||||
}
|
||||
|
||||
let parent = elt.parentNode?.closest?.(`[${CSS.escape(inheritName)}],[${CSS.escape(inheritAppendName)}]`);
|
||||
if (parent) {
|
||||
return this.#attributeValue(parent, name);
|
||||
let valAndSrc = this.#attributeValue(parent, name);
|
||||
if (valAndSrc && this.config.implicitInheritance) {
|
||||
this.#trigger(elt, "htmx:after:implicitInheritance", {elt, parent})
|
||||
}
|
||||
return valAndSrc;
|
||||
}
|
||||
return defaultVal;
|
||||
return{ val: defaultVal, src: elt };
|
||||
}
|
||||
|
||||
#tokenize(str) {
|
||||
@@ -263,13 +276,14 @@ var htmx = (() => {
|
||||
if (this.#isBoosted(elt)) {
|
||||
return this.#boostedMethodAndAction(elt, evt)
|
||||
} else {
|
||||
let method = this.#attributeValue(elt, "hx-method") || "get";
|
||||
let action = this.#attributeValue(elt, "hx-action");
|
||||
let valueAndElt = this.#attributeValue(elt, "hx-method")
|
||||
let method = valueAndElt.val || "GET"
|
||||
let {val: action} = this.#attributeValue(elt, "hx-action") || {};
|
||||
if (!action) {
|
||||
for (let verb of this.#verbs) {
|
||||
let verbAttribute = this.#attributeValue(elt, "hx-" + verb);
|
||||
if (verbAttribute) {
|
||||
action = verbAttribute;
|
||||
let {val: verbAction} = this.#attributeValue(elt, "hx-" + verb) || {};
|
||||
if (verbAction) {
|
||||
action = verbAction;
|
||||
method = verb;
|
||||
break;
|
||||
}
|
||||
@@ -319,15 +333,15 @@ var htmx = (() => {
|
||||
sourceElement,
|
||||
sourceEvent,
|
||||
status: "created",
|
||||
select: this.#attributeValue(sourceElement, "hx-select"),
|
||||
selectOOB: this.#attributeValue(sourceElement, "hx-select-oob"),
|
||||
target: this.#attributeValue(sourceElement, "hx-target"),
|
||||
swap: this.#attributeValue(sourceElement, "hx-swap", this.config.defaultSwap),
|
||||
push: this.#attributeValue(sourceElement, "hx-push-url"),
|
||||
replace: this.#attributeValue(sourceElement, "hx-replace-url"),
|
||||
select: this.#attributeValue(sourceElement, "hx-select")?.val,
|
||||
selectOOB: this.#attributeValue(sourceElement, "hx-select-oob")?.val,
|
||||
target: this.#attributeValue(sourceElement, "hx-target")?.val,
|
||||
swap: this.#attributeValue(sourceElement, "hx-swap", this.config.defaultSwap)?.val,
|
||||
push: this.#attributeValue(sourceElement, "hx-push-url")?.val,
|
||||
replace: this.#attributeValue(sourceElement, "hx-replace-url")?.val,
|
||||
transition: this.config.transitions,
|
||||
request: {
|
||||
validate: "true" === this.#attributeValue(sourceElement, "hx-validate", sourceElement.matches('form') ? "true" : "false"),
|
||||
validate: "true" === this.#attributeValue(sourceElement, "hx-validate", sourceElement.matches('form') ? "true" : "false")?.val,
|
||||
action,
|
||||
method,
|
||||
headers: this.#determineHeaders(sourceElement)
|
||||
@@ -335,7 +349,7 @@ var htmx = (() => {
|
||||
};
|
||||
|
||||
// Apply hx-config overrides
|
||||
let configAttr = this.#attributeValue(sourceElement, "hx-config");
|
||||
let {val: configAttr} = this.#attributeValue(sourceElement, "hx-config") || {};
|
||||
if (configAttr) {
|
||||
let configOverrides = JSON.parse(configAttr);
|
||||
let requestConfig = ctx.request;
|
||||
@@ -364,7 +378,7 @@ var htmx = (() => {
|
||||
if (this.#isBoosted(elt)) {
|
||||
headers["HX-Boosted"] = "true"
|
||||
}
|
||||
let headersAttribute = this.#attributeValue(elt, "hx-headers");
|
||||
let {val: headersAttribute} = this.#attributeValue(elt, "hx-headers") || {};
|
||||
if (headersAttribute) {
|
||||
Object.assign(headers, JSON.parse(headersAttribute));
|
||||
}
|
||||
@@ -375,11 +389,7 @@ var htmx = (() => {
|
||||
if (selector instanceof Element) {
|
||||
return selector;
|
||||
} else if (selector === 'this') {
|
||||
if (elt.hasAttribute(this.#prefix("hx-target"))) {
|
||||
return elt;
|
||||
} else {
|
||||
return elt.closest(`[${this.#prefix("hx-target")}\\:inherited='this']`)
|
||||
}
|
||||
return this.#attributeValue(elt, "hx-target").src
|
||||
} else if (selector != null) {
|
||||
return this.find(elt, selector);
|
||||
} else if (this.#isBoosted(elt)) {
|
||||
@@ -445,7 +455,7 @@ var htmx = (() => {
|
||||
let params = new URLSearchParams(ctx.request.body);
|
||||
if (params.size) ctx.request.action += (/\?/.test(ctx.request.action) ? "&" : "?") + params
|
||||
ctx.request.body = null
|
||||
} else if (this.#attributeValue(elt, "hx-encoding") !== "multipart/form-data") {
|
||||
} else if (this.#attributeValue(elt, "hx-encoding")?.val !== "multipart/form-data") {
|
||||
ctx.request.body = new URLSearchParams(ctx.request.body);
|
||||
}
|
||||
|
||||
@@ -462,14 +472,14 @@ var htmx = (() => {
|
||||
ctx.status = "issuing"
|
||||
this.#initTimeout(ctx);
|
||||
|
||||
let indicatorsSelector = this.#attributeValue(elt, "hx-indicator");
|
||||
let {val: indicatorsSelector} = this.#attributeValue(elt, "hx-indicator") || {};
|
||||
let indicators = this.#showIndicators(elt, indicatorsSelector);
|
||||
let disableSelector = this.#attributeValue(elt, "hx-disable");
|
||||
let {val: disableSelector} = this.#attributeValue(elt, "hx-disable") || {};
|
||||
let disableElements = this.#disableElements(elt, disableSelector);
|
||||
|
||||
try {
|
||||
// Confirm dialog
|
||||
let confirmVal = this.#attributeValue(elt, 'hx-confirm')
|
||||
let {val: confirmVal} = this.#attributeValue(elt, 'hx-confirm') || {};
|
||||
if (confirmVal) {
|
||||
let js = this.#extractJavascriptContent(confirmVal);
|
||||
if (js) {
|
||||
@@ -700,12 +710,12 @@ var htmx = (() => {
|
||||
}
|
||||
|
||||
#determineSyncStrategy(elt) {
|
||||
let syncValue = this.#attributeValue(elt, "hx-sync");
|
||||
let {val: syncValue} = this.#attributeValue(elt, "hx-sync") || {};
|
||||
return syncValue?.split(":")[1] || "queue first";
|
||||
}
|
||||
|
||||
#getRequestQueue(elt) {
|
||||
let syncValue = this.#attributeValue(elt, "hx-sync");
|
||||
let {val: syncValue} = this.#attributeValue(elt, "hx-sync") || {};
|
||||
let syncElt = elt
|
||||
if (syncValue && syncValue.includes(":")) {
|
||||
let strings = syncValue.split(":");
|
||||
@@ -742,7 +752,7 @@ var htmx = (() => {
|
||||
}
|
||||
|
||||
#initializeTriggers(elt, initialHandler = elt._htmx.eventHandler) {
|
||||
let specString = this.#attributeValue(elt, "hx-trigger");
|
||||
let {val: specString} = this.#attributeValue(elt, "hx-trigger") || {};
|
||||
if (!specString) {
|
||||
specString = elt.matches("form") ? "submit" :
|
||||
elt.matches("input:not([type=button]),select,textarea") ? "change" :
|
||||
@@ -892,7 +902,7 @@ var htmx = (() => {
|
||||
}
|
||||
|
||||
#initializeStreamConfig(elt) {
|
||||
let streamSpec = this.#attributeValue(elt, 'hx-stream');
|
||||
let {val: streamSpec} = this.#attributeValue(elt, 'hx-stream') || {};
|
||||
if (!streamSpec) return;
|
||||
|
||||
// Start with global defaults
|
||||
@@ -1002,7 +1012,7 @@ var htmx = (() => {
|
||||
}
|
||||
|
||||
#maybeBoost(elt) {
|
||||
if (this.#attributeValue(elt, "hx-boost") === "true") {
|
||||
if (this.#attributeValue(elt, "hx-boost")?.val === "true") {
|
||||
if (this.#shouldInitialize(elt)) {
|
||||
elt._htmx = {eventHandler: this.#createHtmxEventHandler(elt), requests: [], boosted: true}
|
||||
elt.setAttribute('data-htmx-powered', 'true');
|
||||
@@ -1077,7 +1087,8 @@ var htmx = (() => {
|
||||
|
||||
#makeFragment(text) {
|
||||
let response = text.replace(/<partial(\s+|>)/gi, '<template partial$1').replace(/<\/partial>/gi, '</template>');
|
||||
// TODO - store any head tag content on the fragment for head extension
|
||||
let title = '';
|
||||
response = response.replace(/<title[^>]*>([\s\S]*?)<\/title>/i, (m, t) => (title = t, ''));
|
||||
let responseWithNoHead = response.replace(/<head(\s[^>]*)?>[\s\S]*?<\/head>/i, '');
|
||||
let startTag = responseWithNoHead.match(/<([a-z][^\/>\x20\t\r\n\f]*)/i)?.[1]?.toLowerCase();
|
||||
|
||||
@@ -1092,10 +1103,11 @@ var htmx = (() => {
|
||||
doc = this.#parseHTML(`<template>${responseWithNoHead}</template>`);
|
||||
fragment = doc.querySelector('template').content;
|
||||
}
|
||||
this.#processScripts(fragment);
|
||||
|
||||
return {
|
||||
fragment,
|
||||
title: doc.title
|
||||
title
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1114,18 +1126,9 @@ var htmx = (() => {
|
||||
let swapSpec = this.#parseSwapSpec(oobValue);
|
||||
if (swapSpec.target) target = swapSpec.target;
|
||||
|
||||
let oobElementClone = elt.cloneNode(true);
|
||||
let fragment;
|
||||
if (swapSpec.strip === undefined && swapSpec.style !== 'outerHTML') {
|
||||
swapSpec.strip = true;
|
||||
}
|
||||
if (swapSpec.strip) {
|
||||
fragment = oobElementClone.content || oobElementClone;
|
||||
} else {
|
||||
fragment = document.createDocumentFragment();
|
||||
fragment.appendChild(oobElementClone);
|
||||
}
|
||||
elt.remove();
|
||||
swapSpec.strip ??= !swapSpec.style.startsWith('outer');
|
||||
let fragment = document.createDocumentFragment();
|
||||
fragment.append(elt);
|
||||
if (!target && !oobValue.includes('target:')) return;
|
||||
|
||||
tasks.push({
|
||||
@@ -1255,6 +1258,7 @@ var htmx = (() => {
|
||||
|
||||
async swap(ctx) {
|
||||
let {fragment, title} = this.#makeFragment(ctx.text);
|
||||
ctx.title = title;
|
||||
let tasks = [];
|
||||
|
||||
// Process OOB and partials
|
||||
@@ -1263,7 +1267,7 @@ var htmx = (() => {
|
||||
tasks.push(...oobTasks, ...partialTasks);
|
||||
|
||||
// Process main swap
|
||||
let mainSwap = this.#processMainSwap(ctx, fragment, partialTasks, title);
|
||||
let mainSwap = this.#processMainSwap(ctx, fragment, partialTasks);
|
||||
if (mainSwap) {
|
||||
tasks.push(mainSwap);
|
||||
}
|
||||
@@ -1299,7 +1303,7 @@ var htmx = (() => {
|
||||
}
|
||||
|
||||
this.#trigger(document, "htmx:after:swap", {ctx});
|
||||
if (mainSwap?.title) document.title = mainSwap.title;
|
||||
if (ctx.title && !mainSwap?.swapSpec?.ignoreTitle) document.title = ctx.title;
|
||||
await this.timeout(1);
|
||||
// invoke restore tasks
|
||||
for (let task of tasks) {
|
||||
@@ -1312,33 +1316,24 @@ var htmx = (() => {
|
||||
// if (ctx.hx?.triggerafterswap) this.#handleTriggerHeader(ctx.hx.triggerafterswap, ctx.sourceElement);
|
||||
}
|
||||
|
||||
#processMainSwap(ctx, fragment, partialTasks, title) {
|
||||
#processMainSwap(ctx, fragment, partialTasks) {
|
||||
// Create main task if needed
|
||||
let swapSpec = this.#parseSwapSpec(ctx.swap || this.config.defaultSwap);
|
||||
// skip creating main swap if extracting partials resulted in empty response except for delete style
|
||||
if (swapSpec.style === 'delete' || /\S/.test(fragment.innerHTML || '') || !partialTasks.length) {
|
||||
let resultFragment = document.createDocumentFragment();
|
||||
if (ctx.select) {
|
||||
let selected = fragment.querySelector(ctx.select);
|
||||
if (selected) {
|
||||
if (swapSpec.strip === false) {
|
||||
resultFragment.append(selected);
|
||||
} else {
|
||||
resultFragment.append(...selected.childNodes);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
resultFragment.append(...fragment.childNodes);
|
||||
let selected = fragment.querySelectorAll(ctx.select);
|
||||
fragment = document.createDocumentFragment();
|
||||
fragment.append(...selected);
|
||||
}
|
||||
|
||||
let mainSwap = {
|
||||
type: 'main',
|
||||
fragment: resultFragment,
|
||||
fragment,
|
||||
target: swapSpec.target || ctx.target,
|
||||
swapSpec,
|
||||
sourceElement: ctx.sourceElement,
|
||||
transition: (ctx.transition !== false) && (swapSpec.transition !== false),
|
||||
title
|
||||
transition: (ctx.transition !== false) && (swapSpec.transition !== false)
|
||||
};
|
||||
return mainSwap;
|
||||
}
|
||||
@@ -1350,8 +1345,13 @@ var htmx = (() => {
|
||||
target = document.querySelector(target);
|
||||
}
|
||||
if (!target) return;
|
||||
if (swapSpec.strip && fragment.firstElementChild) {
|
||||
let strip = document.createDocumentFragment();
|
||||
strip.append(...(fragment.firstElementChild.content || fragment.firstElementChild).childNodes);
|
||||
fragment = strip;
|
||||
}
|
||||
|
||||
let pantry = this.#handlePreservedElements(fragment);
|
||||
this.#processScripts(fragment);
|
||||
let parentNode = target.parentNode;
|
||||
let newContent = [...fragment.childNodes]
|
||||
if (swapSpec.style === 'innerHTML') {
|
||||
@@ -1407,6 +1407,8 @@ var htmx = (() => {
|
||||
if (this.config.logAll) {
|
||||
console.log(eventName, detail, on)
|
||||
}
|
||||
on = this.#normalizeElement(on)
|
||||
this.#triggerExtensions(on, this.#maybeAdjustMetaCharacter(eventName), detail);
|
||||
return this.trigger(on, eventName, detail, bubbles)
|
||||
}
|
||||
|
||||
@@ -1482,7 +1484,6 @@ var htmx = (() => {
|
||||
|
||||
trigger(on, eventName, detail = {}, bubbles = true) {
|
||||
on = this.#normalizeElement(on)
|
||||
this.#triggerExtensions(on, eventName, detail);
|
||||
let evt = new CustomEvent(eventName, {
|
||||
detail,
|
||||
cancelable: true,
|
||||
@@ -1597,8 +1598,9 @@ var htmx = (() => {
|
||||
|
||||
#handleHxOnAttributes(node) {
|
||||
for (let attr of node.getAttributeNames()) {
|
||||
if (attr.startsWith(this.#prefix("hx-on:"))) {
|
||||
let evtName = attr.substring(this.#prefix("hx-on:").length)
|
||||
var searchString = this.#maybeAdjustMetaCharacter(this.#prefix("hx-on:"));
|
||||
if (attr.startsWith(searchString)) {
|
||||
let evtName = attr.substring(searchString.length)
|
||||
let code = node.getAttribute(attr);
|
||||
node.addEventListener(evtName, async (evt) => {
|
||||
try {
|
||||
@@ -1674,7 +1676,7 @@ var htmx = (() => {
|
||||
formData.append(submitter.name, submitter.value)
|
||||
included.add(submitter);
|
||||
}
|
||||
let includeSelector = this.#attributeValue(elt, "hx-include");
|
||||
let {val: includeSelector} = this.#attributeValue(elt, "hx-include") || {};
|
||||
if (includeSelector) {
|
||||
let includeNodes = this.#findAllExt(elt, includeSelector);
|
||||
for (let node of includeNodes) {
|
||||
@@ -1715,7 +1717,7 @@ var htmx = (() => {
|
||||
}
|
||||
|
||||
#handleHxVals(elt, body) {
|
||||
let hxValsValue = this.#attributeValue(elt, "hx-vals");
|
||||
let {val: hxValsValue} = this.#attributeValue(elt, "hx-vals") || {};
|
||||
if (hxValsValue) {
|
||||
if (!hxValsValue.includes('{')) {
|
||||
hxValsValue = `{${hxValsValue}}`
|
||||
@@ -2065,7 +2067,7 @@ var htmx = (() => {
|
||||
}
|
||||
let str = status + ""
|
||||
for (let pattern of [str, str.slice(0, 2) + 'x', str[0] + 'xx']) {
|
||||
let swap = this.#attributeValue(ctx.sourceElement, "hx-status:" + pattern);
|
||||
let {val: swap} = this.#attributeValue(ctx.sourceElement, "hx-status:" + pattern) || {};
|
||||
if (swap) {
|
||||
ctx.swap = swap
|
||||
return
|
||||
@@ -2133,6 +2135,14 @@ var htmx = (() => {
|
||||
return cssOrElement
|
||||
}
|
||||
}
|
||||
|
||||
#maybeAdjustMetaCharacter(string) {
|
||||
if (this.config.metaCharacter) {
|
||||
return string.replace(/:/g, this.config.metaCharacter);
|
||||
} else {
|
||||
return string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Htmx()
|
||||
|
||||
BIN
dist/htmx.js.br
vendored
BIN
dist/htmx.js.br
vendored
Binary file not shown.
2
dist/htmx.min.js
vendored
2
dist/htmx.min.js
vendored
File diff suppressed because one or more lines are too long
BIN
dist/htmx.min.js.br
vendored
BIN
dist/htmx.min.js.br
vendored
Binary file not shown.
2
dist/htmx.min.js.map
vendored
2
dist/htmx.min.js.map
vendored
File diff suppressed because one or more lines are too long
@@ -91,7 +91,9 @@
|
||||
<!-- Test helpers -->
|
||||
<script src="./lib/helpers.js"></script>
|
||||
|
||||
<!-- Unit tests first -->
|
||||
<!-- ============================================ -->
|
||||
<!-- Unit Tests -->
|
||||
<!-- ============================================ -->
|
||||
<script src="./tests/unit/__attributeValue.js"></script>
|
||||
<script src="./tests/unit/__collectFormData.js"></script>
|
||||
<script src="./tests/unit/__disableEnableElements.js"></script>
|
||||
@@ -129,12 +131,9 @@
|
||||
<script src="./tests/unit/timeout.js"></script>
|
||||
<script src="./tests/unit/trigger.js"></script>
|
||||
|
||||
<!-- Direct/Fast tests -->
|
||||
<script src="./tests/direct/hx-get.js"></script>
|
||||
<script src="./tests/direct/hx-on-xxx.js"></script>
|
||||
<script src="./tests/direct/hx-optimistic.js"></script>
|
||||
|
||||
<!-- ============================================ -->
|
||||
<!-- Attribute Tests -->
|
||||
<!-- ============================================ -->
|
||||
<script src="./tests/attributes/hx-boost.js"></script>
|
||||
<script src="./tests/attributes/hx-config.js"></script>
|
||||
<script src="./tests/attributes/hx-get.js"></script>
|
||||
@@ -150,15 +149,15 @@
|
||||
<script src="./tests/attributes/hx-trigger.js"></script>
|
||||
<script src="./tests/attributes/hx-vals.js"></script>
|
||||
|
||||
<!-- E2E Tests -->
|
||||
<script src="./tests/e2e/cancel-behavior.js"></script>
|
||||
<script src="./tests/e2e/core.js"></script>
|
||||
<script src="./tests/e2e/oob.js"></script>
|
||||
<script src="./tests/e2e/sse.js"></script>
|
||||
<script src="./tests/e2e/strip.js"></script>
|
||||
|
||||
<!-- History Tests -->
|
||||
<script src="./tests/history/basic-history.js"></script>
|
||||
<!-- ============================================ -->
|
||||
<!-- End-to-End Tests -->
|
||||
<!-- ============================================ -->
|
||||
<script src="./tests/end2end/basic-history.js"></script>
|
||||
<script src="./tests/end2end/cancel-behavior.js"></script>
|
||||
<script src="./tests/end2end/core.js"></script>
|
||||
<script src="./tests/end2end/oob.js"></script>
|
||||
<script src="./tests/end2end/sse.js"></script>
|
||||
<script src="./tests/end2end/strip.js"></script>
|
||||
|
||||
<script>
|
||||
// Run tests on load
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
describe('direct hx-get attribute test', function() {
|
||||
|
||||
beforeEach(() => {
|
||||
setupTest(this.currentTest)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanupTest(this.currentTest)
|
||||
})
|
||||
|
||||
it('properly swaps content', async function () {
|
||||
mockResponse('GET', '/test', 'Clicked!')
|
||||
let btn = createProcessedHTML('<button hx-get="/test">Click Me!</button> Hey!');
|
||||
await directlyInvokeHandler(btn)
|
||||
playground().innerText.should.equal('Clicked! Hey!')
|
||||
})
|
||||
|
||||
it('properly swaps content with outer swap', async function () {
|
||||
mockResponse('GET', '/test', 'Clicked!')
|
||||
let btn = createProcessedHTML('<button hx-get="/test" hx-swap="outerHTML">Click Me!</button>');
|
||||
await directlyInvokeHandler(btn)
|
||||
playground().innerHTML.should.equal('Clicked!')
|
||||
})
|
||||
|
||||
})
|
||||
@@ -1,50 +0,0 @@
|
||||
describe('fast hx-on:xxx response retarting attribute test', function() {
|
||||
|
||||
beforeEach(() => {
|
||||
setupTest(this.currentTest)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanupTest(this.currentTest)
|
||||
})
|
||||
|
||||
it('retargets based on a full response code', async function () {
|
||||
mockResponse('GET', '/test', 'Clicked!', {status:400})
|
||||
let btn = createProcessedHTML('<button hx-status:400="innerHTML target:#d1" hx-get="/test">Click Me!</button><div id="d1"></div>');
|
||||
await directlyInvokeHandler(btn)
|
||||
htmx.find("#d1").innerText.should.equal('Clicked!')
|
||||
})
|
||||
|
||||
it('retargets based on a one char partial response code', async function () {
|
||||
mockResponse('GET', '/test', 'Clicked!', {status:400})
|
||||
let btn = createProcessedHTML('<button hx-status:40x="innerHTML target:#d1" hx-get="/test">Click Me!</button><div id="d1"></div>');
|
||||
await directlyInvokeHandler(btn)
|
||||
htmx.find("#d1").innerText.should.equal('Clicked!')
|
||||
})
|
||||
|
||||
it('retargets based on a two char partial response code', async function () {
|
||||
mockResponse('GET', '/test', 'Clicked!', {status:400})
|
||||
let btn = createProcessedHTML('<button hx-status:4xx="innerHTML target:#d1" hx-get="/test">Click Me!</button><div id="d1"></div>');
|
||||
await directlyInvokeHandler(btn)
|
||||
htmx.find("#d1").innerText.should.equal('Clicked!')
|
||||
})
|
||||
|
||||
it('more specific wins over less partial response code', async function () {
|
||||
mockResponse('GET', '/test', 'Clicked!', {status:400})
|
||||
let btn = createProcessedHTML('<button hx-status:400="innerHTML target:#d2" hx-status:40x="innerHTML target:#d1" hx-get="/test">Click Me!</button><div id="d1"></div><div id="d2"></div>');
|
||||
await directlyInvokeHandler(btn)
|
||||
htmx.find("#d1").innerText.should.equal('')
|
||||
htmx.find("#d2").innerText.should.equal('Clicked!')
|
||||
})
|
||||
|
||||
it('more specific inherited wins over less partial response code', async function () {
|
||||
mockResponse('GET', '/test', 'Clicked!', {status:400})
|
||||
let btn = createProcessedHTML('<div hx-status:400:inherited="innerHTML target:#d2"><button id="btn1" hx-status:40x="innerHTML target:#d1" hx-get="/test">Click Me!</button><div id="d1"></div><div id="d2"></div></div>div>');
|
||||
await directlyInvokeHandler(htmx.find("#btn1"))
|
||||
htmx.find("#d1").innerText.should.equal('')
|
||||
htmx.find("#d2").innerText.should.equal('Clicked!')
|
||||
})
|
||||
|
||||
|
||||
|
||||
})
|
||||
@@ -1,56 +0,0 @@
|
||||
describe('direct hx-optimistic attribute test', function() {
|
||||
|
||||
beforeEach(() => {
|
||||
setupTest(this.currentTest)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanupTest(this.currentTest)
|
||||
})
|
||||
|
||||
it('shows optimistic content then replaces with response w/innerHTML', async function () {
|
||||
mockResponse('GET', '/test', 'Server Response')
|
||||
let btn = createProcessedHTML('<button hx-get="/test" hx-optimistic="#optimistic-source">Initial</button><div id="optimistic-source">Optimistic</div>');
|
||||
let promise = directlyInvokeHandler(btn);
|
||||
btn.innerText.should.equal("InitialOptimistic") // text content is hidden
|
||||
await promise
|
||||
btn.innerText.should.equal("Server Response")
|
||||
})
|
||||
|
||||
it('shows optimistic content then reverts on request failure w/innerHTML', async function () {
|
||||
mockFailure('GET', '/test', 'Network error')
|
||||
let btn = createProcessedHTML('<button hx-get="/test" hx-optimistic="#optimistic-source">Initial</button><div id="optimistic-source">Optimistic</div>');
|
||||
let promise = directlyInvokeHandler(btn);
|
||||
btn.innerText.should.equal("InitialOptimistic")
|
||||
await promise.catch(() => {}) // swallow error
|
||||
btn.innerText.should.equal("Initial") // reverts to original
|
||||
})
|
||||
|
||||
it('shows optimistic content then replaces with response w/outerHTML', async function () {
|
||||
mockResponse('GET', '/test', 'Server Response')
|
||||
let btn = createProcessedHTML('<button hx-get="/test" hx-optimistic="#optimistic-source" hx-swap="outerHTML">Initial</button><div id="optimistic-source">Optimistic</div>');
|
||||
let promise = directlyInvokeHandler(btn);
|
||||
playground().innerText.trim().should.include("Optimistic")
|
||||
await promise
|
||||
playground().innerText.trim().should.include("Server Response")
|
||||
})
|
||||
|
||||
it('shows optimistic content then reverts on request failure w/outerHTML', async function () {
|
||||
mockFailure('GET', '/test', 'Network error')
|
||||
let btn = createProcessedHTML('<button hx-get="/test" hx-optimistic="#optimistic-source" hx-swap="outerHTML">Initial</button><div id="optimistic-source">Optimistic</div>');
|
||||
let promise = directlyInvokeHandler(btn);
|
||||
playground().innerText.should.equal("Optimistic\nOptimistic")
|
||||
await promise.catch(() => {}) // swallow error
|
||||
playground().innerText.should.equal("Initial\nOptimistic") // reverts to original
|
||||
})
|
||||
|
||||
it('works with new swap terminology: prepend', async function () {
|
||||
mockResponse('GET', '/test', 'Response')
|
||||
let btn = createProcessedHTML('<button hx-get="/test" hx-optimistic="#optimistic-source" hx-swap="prepend">Initial</button><div id="optimistic-source">Optimistic</div>');
|
||||
let promise = directlyInvokeHandler(btn);
|
||||
btn.innerText.should.equal("OptimisticInitial")
|
||||
await promise
|
||||
btn.innerText.should.equal("ResponseInitial")
|
||||
})
|
||||
|
||||
})
|
||||
@@ -1,31 +0,0 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>htmx scratch pad</title>
|
||||
<script src="../../lib/fetch-mock.js"></script>
|
||||
<script src="../../../src/htmx.js"></script>
|
||||
<script>
|
||||
// installFetchMock();
|
||||
fetchMock.mockResponse('GET', '/demo', new MockResponse('<div id="result">Success!</div>'));
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<form>
|
||||
<input required>
|
||||
<button hx-post="/demo" hx-validate="true">Submit</button>
|
||||
</form>
|
||||
|
||||
<button hx-get="demo.html">
|
||||
Get it...
|
||||
</button>
|
||||
|
||||
<button hx-action="js:alert('clicked!')">
|
||||
Javascript Action
|
||||
</button>
|
||||
|
||||
<div hx-trigger="every 1s" hx-get="demo.html">
|
||||
Test
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user