Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
237 changes: 129 additions & 108 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"chai": "^4.3.7",
"chai-dom": "^1.11.0",
"fs-extra": "^9.1.0",
"mocha": "^10.2.0",
"mocha": "^9.2.2",
"mocha-chrome": "^2.2.0",
"mocha-webdriver-runner": "^0.6.4",
"mock-socket": "^9.2.1",
Expand Down
92 changes: 48 additions & 44 deletions src/ext/loading-states.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,28 +25,28 @@
if (delayElt) {
const delayInMilliseconds =
delayElt.getAttribute('data-loading-delay') || 200
const timeout = setTimeout(() => {
const timeout = setTimeout(function () {
doCallback()

loadingStatesUndoQueue.push(() => {
mayProcessUndoCallback(targetElt, () => undoCallback())
loadingStatesUndoQueue.push(function () {
mayProcessUndoCallback(targetElt, undoCallback)
})
}, delayInMilliseconds)

loadingStatesUndoQueue.push(() => {
mayProcessUndoCallback(targetElt, () => clearTimeout(timeout))
loadingStatesUndoQueue.push(function () {
mayProcessUndoCallback(targetElt, function () { clearTimeout(timeout) })
})
} else {
doCallback()
loadingStatesUndoQueue.push(() => {
mayProcessUndoCallback(targetElt, () => undoCallback())
loadingStatesUndoQueue.push(function () {
mayProcessUndoCallback(targetElt, undoCallback)
})
}
}

function getLoadingStateElts(loadingScope, type, path) {
return Array.from(htmx.findAll(loadingScope, `[${type}]`)).filter(
(elt) => mayProcessLoadingStateByPath(elt, path)
return Array.from(htmx.findAll(loadingScope, "[" + type + "]")).filter(
function (elt) { return mayProcessLoadingStateByPath(elt, path) }
)
}

Expand Down Expand Up @@ -74,95 +74,99 @@

let loadingStateEltsByType = {}

loadingStateTypes.forEach((type) => {
loadingStateTypes.forEach(function (type) {
loadingStateEltsByType[type] = getLoadingStateElts(
container,
type,
evt.detail.pathInfo.requestPath
)
})

loadingStateEltsByType['data-loading'].forEach((sourceElt) => {
getLoadingTarget(sourceElt).forEach((targetElt) => {
loadingStateEltsByType['data-loading'].forEach(function (sourceElt) {
getLoadingTarget(sourceElt).forEach(function (targetElt) {
queueLoadingState(
sourceElt,
targetElt,
() =>
(targetElt.style.display =
function () {
targetElt.style.display =
sourceElt.getAttribute('data-loading') ||
'inline-block'),
() => (targetElt.style.display = 'none')
'inline-block' },
function () { targetElt.style.display = 'none' }
)
})
})

loadingStateEltsByType['data-loading-class'].forEach(
(sourceElt) => {
function (sourceElt) {
const classNames = sourceElt
.getAttribute('data-loading-class')
.split(' ')

getLoadingTarget(sourceElt).forEach((targetElt) => {
getLoadingTarget(sourceElt).forEach(function (targetElt) {
queueLoadingState(
sourceElt,
targetElt,
() =>
classNames.forEach((className) =>
targetElt.classList.add(className)
),
() =>
classNames.forEach((className) =>
targetElt.classList.remove(className)
)
function () {
classNames.forEach(function (className) {
targetElt.classList.add(className)
})
},
function() {
classNames.forEach(function (className) {
targetElt.classList.remove(className)
})
}
)
})
}
)

loadingStateEltsByType['data-loading-class-remove'].forEach(
(sourceElt) => {
function (sourceElt) {
const classNames = sourceElt
.getAttribute('data-loading-class-remove')
.split(' ')

getLoadingTarget(sourceElt).forEach((targetElt) => {
getLoadingTarget(sourceElt).forEach(function (targetElt) {
queueLoadingState(
sourceElt,
targetElt,
() =>
classNames.forEach((className) =>
targetElt.classList.remove(className)
),
() =>
classNames.forEach((className) =>
targetElt.classList.add(className)
)
function () {
classNames.forEach(function (className) {
targetElt.classList.remove(className)
})
},
function() {
classNames.forEach(function (className) {
targetElt.classList.add(className)
})
}
)
})
}
)

loadingStateEltsByType['data-loading-disable'].forEach(
(sourceElt) => {
getLoadingTarget(sourceElt).forEach((targetElt) => {
function (sourceElt) {
getLoadingTarget(sourceElt).forEach(function (targetElt) {
queueLoadingState(
sourceElt,
targetElt,
() => (targetElt.disabled = true),
() => (targetElt.disabled = false)
function() { targetElt.disabled = true },
function() { targetElt.disabled = false }
)
})
}
)

loadingStateEltsByType['data-loading-aria-busy'].forEach(
(sourceElt) => {
getLoadingTarget(sourceElt).forEach((targetElt) => {
function (sourceElt) {
getLoadingTarget(sourceElt).forEach(function (targetElt) {
queueLoadingState(
sourceElt,
targetElt,
() => (targetElt.setAttribute("aria-busy", "true")),
() => (targetElt.removeAttribute("aria-busy"))
function () { targetElt.setAttribute("aria-busy", "true") },
function () { targetElt.removeAttribute("aria-busy") }
)
})
}
Expand Down
3 changes: 2 additions & 1 deletion src/ext/morphdom-swap.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ htmx.defineExtension('morphdom-swap', {
handleSwap: function (swapStyle, target, fragment) {
if (swapStyle === 'morphdom') {
if (fragment.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
morphdom(target, fragment.firstElementChild);
// IE11 doesn't support DocumentFragment.firstElementChild
morphdom(target, fragment.firstElementChild || fragment.firstChild);
return [target];
} else {
morphdom(target, fragment.outerHTML);
Expand Down
64 changes: 50 additions & 14 deletions src/htmx.js
Original file line number Diff line number Diff line change
Expand Up @@ -573,9 +573,17 @@ return (function () {
}
}

function startsWith(str, prefix) {
return str.substring(0, prefix.length) === prefix
}

function endsWith(str, suffix) {
return str.substring(str.length - suffix.length) === suffix
}

function normalizeSelector(selector) {
var trimmedSelector = selector.trim();
if (trimmedSelector.startsWith("<") && trimmedSelector.endsWith("/>")) {
if (startsWith(trimmedSelector, "<") && endsWith(trimmedSelector, "/>")) {
return trimmedSelector.substring(1, trimmedSelector.length - 2);
} else {
return trimmedSelector;
Expand Down Expand Up @@ -1348,7 +1356,7 @@ return (function () {
var verb, path;
if (elt.tagName === "A") {
verb = "get";
path = elt.href; // DOM property gives the fully resolved href of a relative link
path = getRawAttribute(elt, 'href')
} else {
var rawAttribute = getRawAttribute(elt, "method");
verb = rawAttribute ? rawAttribute.toLowerCase() : "get";
Expand Down Expand Up @@ -1864,12 +1872,25 @@ return (function () {
}

function findHxOnWildcardElements(elt) {
if (!document.evaluate) return []
var node = null
var elements = []

if (document.evaluate) {
var iter = document.evaluate('//*[@*[ 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("*")
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])
}
}
}
}

let node = null
const elements = []
const iter = document.evaluate('//*[@*[ starts-with(name(), "hx-on:") or starts-with(name(), "data-hx-on:") ]]', elt)
while (node = iter.iterateNext()) elements.push(node)
return elements
}

Expand Down Expand Up @@ -1975,10 +1996,10 @@ return (function () {
for (var i = 0; i < elt.attributes.length; i++) {
var name = elt.attributes[i].name
var value = elt.attributes[i].value
if (name.startsWith("hx-on:") || name.startsWith("data-hx-on:")) {
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 (eventName.startsWith(":")) eventName = "htmx" + eventName
if (startsWith(eventName, ":")) eventName = "htmx" + eventName

addHxOnEventHandler(elt, eventName, value)
}
Expand Down Expand Up @@ -2207,7 +2228,13 @@ return (function () {
// so we can prevent privileged data entering the cache.
// The page will still be reachable as a history entry, but htmx will fetch it
// live from the server onpopstate rather than look in the localStorage cache
var disableHistoryCache = getDocument().querySelector('[hx-history="false" i],[data-hx-history="false" i]');
var disableHistoryCache
try {
disableHistoryCache = getDocument().querySelector('[hx-history="false" i],[data-hx-history="false" i]')
} catch (e) {
// IE11: insensitive modifier not supported so fallback to case sensitive selector
disableHistoryCache = getDocument().querySelector('[hx-history="false"],[data-hx-history="false"]')
}
if (!disableHistoryCache) {
triggerEvent(getDocument().body, "htmx:beforeHistorySave", {path: path, historyElt: elt});
saveToHistoryCache(path, cleanInnerHtmlForHistory(elt), getDocument().title, window.scrollY);
Expand All @@ -2220,7 +2247,7 @@ return (function () {
// remove the cache buster parameter, if any
if (htmx.config.getCacheBusterParam) {
path = path.replace(/org\.htmx\.cache-buster=[^&]*&?/, '')
if (path.endsWith('&') || path.endsWith("?")) {
if (endsWith(path, '&') || endsWith(path, "?")) {
path = path.slice(0, -1);
}
}
Expand Down Expand Up @@ -2853,9 +2880,18 @@ return (function () {
}

function verifyPath(elt, path, requestConfig) {
var url = new URL(path, document.location.href);
var origin = document.location.origin;
var sameHost = origin === url.origin;
var sameHost
var url
if (typeof URL === "function") {
url = new URL(path, document.location.href);
var origin = document.location.origin;
sameHost = origin === url.origin;
} else {
// IE11 doesn't support URL
url = path
sameHost = startsWith(path, document.location.origin)
}

if (htmx.config.selfRequestsOnly) {
if (!sameHost) {
return false;
Expand Down
24 changes: 16 additions & 8 deletions test/attributes/hx-disinherit.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,23 @@ describe("hx-disinherit attribute", function() {
var btn = byId("bx1");
btn.click();
this.server.respond();
btn.innerHTML.should.equal(response_inner);
btn.firstChild.id.should.equal("snowflake");
btn.innerText.should.equal("Hello world");
})


it('disinherit exclude single attribute', function () {
var response_inner = '<div id="snowflake" class="">Hello world</div>'
var response_inner = '<div id="snowflake">Hello world</div>'
var response = '<div id="unique">' + response_inner + '</div>'
this.server.respondWith("GET", "/test", response);

var div = make('<div hx-select="#snowflake" hx-target="#cta" hx-swap="beforebegin" hx-disinherit="hx-select"><button id="bx1" hx-get="/test"><span id="cta">Click Me!</span></button></div>')
var btn = byId("bx1");
btn.click();
this.server.respond();
btn.innerHTML.should.equal(response + '<span id="cta" class="">Click Me!</span>');
btn.firstChild.id.should.equal("unique")
btn.firstChild.firstChild.id.should.equal("snowflake")
btn.childNodes[1].innerText.should.equal("Click Me!")
});

it('disinherit exclude multiple attributes', function () {
Expand All @@ -47,7 +50,9 @@ describe("hx-disinherit attribute", function() {
this.server.respond();
console.log(btn.innerHTML);
console.log(response);
btn.innerHTML.should.equal('<span id="cta" class="">' + response + '</span>');
btn.firstChild.id.should.equal("cta")
btn.firstChild.firstChild.id.should.equal("unique")
btn.firstChild.firstChild.firstChild.id.should.equal("snowflake")
});

it('disinherit exclude all attributes', function () {
Expand All @@ -62,7 +67,8 @@ describe("hx-disinherit attribute", function() {
var btn = byId("bx1");
btn.click();
this.server.respond();
btn.innerHTML.should.equal(response);
btn.firstChild.id.should.equal("unique");
btn.firstChild.firstChild.id.should.equal("snowflake");
});

it('same-element inheritance disable', function () {
Expand All @@ -73,7 +79,8 @@ describe("hx-disinherit attribute", function() {
var btn = make('<button hx-select="#snowflake" hx-target="#container" hx-trigger="click" hx-get="/test" hx-swap="outerHTML" hx-disinherit="*"><div id="container"></div></button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal(response_inner);
btn.firstChild.id.should.equal("snowflake");
btn.firstChild.innerText.should.equal("Hello world");
});

it('same-element inheritance disable with child nodes', function () {
Expand All @@ -86,7 +93,8 @@ describe("hx-disinherit attribute", function() {
var btn = byId("bx1");
btn.click();
this.server.respond();
btn.innerHTML.should.equal('<div id="target" class="">unique-snowflake</div>');
btn.firstChild.id.should.equal('target');
btn.firstChild.innerText.should.equal('unique-snowflake');
var count = (div.parentElement.innerHTML.match(/snowflake/g) || []).length;
count.should.equal(2); // hx-select of parent div and newly loaded inner content
});
Expand All @@ -101,7 +109,7 @@ describe("hx-disinherit attribute", function() {
var link = byId("a1");
link.click();
// should match the fully resolved href of the boosted element
should.equal(request.detail.requestConfig.path, request.detail.elt.href);
should.equal(request.detail.requestConfig.path, '/test');
should.equal(request.detail.elt["htmx-internal-data"].boosted, true);
} finally {
htmx.off("htmx:beforeRequest", handler);
Expand Down
Loading