Merge remote-tracking branch 'origin/v2.0v2.0' into v2.0v2.0

This commit is contained in:
Carson Gross
2024-01-23 12:16:08 -07:00
9 changed files with 702 additions and 407 deletions

View File

@@ -27,8 +27,9 @@
"scripts": {
"dist": "./scripts/dist.sh",
"lint": "eslint src/htmx.js test/attributes/ test/core/ test/util/",
"lint-fix": "eslint src/htmx.js test/attributes/ test/core/ test/util/ --fix",
"format": "eslint --fix src/htmx.js test/attributes/ test/core/ test/util/",
"test": "mocha-chrome test/index.html",
"test": "npm run lint && mocha-chrome test/index.html",
"test-types": "tsc --project ./jsconfig.json",
"ws-tests": "cd ./test/ws-sse && node ./server.js",
"www": "bash ./scripts/www.sh"

16
src/htmx.d.ts vendored
View File

@@ -438,6 +438,22 @@ export interface HtmxConfig {
triggerSpecsCache?: {[trigger: string]: HtmxTriggerSpecification[]};
}
type HtmxSwapStyle = "innerHTML" | "outerHTML" | "beforebegin" | "afterbegin" | "beforeend" | "afterend" | "delete" | "none" | string
export interface HtmxSwapSpecification {
swapStyle: HtmxSwapStyle;
swapDelay?: number;
settleDelay?: number;
transition?: boolean;
ignoreTitle?: boolean;
head?: string;
scroll?: string;
scrollTarget?: string;
show?: string;
showTarget?: string;
focusScroll?: boolean;
}
/**
* https://htmx.org/extensions/#defining
*/

File diff suppressed because it is too large Load Diff

View File

@@ -139,26 +139,26 @@ describe('hx-push-url attribute', function() {
it('deals with malformed JSON in history cache when saving', function() {
localStorage.setItem(HTMX_HISTORY_CACHE_NAME, 'Invalid JSON')
htmx._('saveToHistoryCache')('url', make("<div>"))
htmx._('saveToHistoryCache')('url', make('<div>'))
var cache = JSON.parse(localStorage.getItem(HTMX_HISTORY_CACHE_NAME))
cache.length.should.equal(1)
})
it('does not blow out cache when saving a URL twice', function() {
htmx._('saveToHistoryCache')('url1', make("<div>"))
htmx._('saveToHistoryCache')('url2', make("<div>"))
htmx._('saveToHistoryCache')('url3', make("<div>"))
htmx._('saveToHistoryCache')('url2', make("<div>"))
htmx._('saveToHistoryCache')('url1', make('<div>'))
htmx._('saveToHistoryCache')('url2', make('<div>'))
htmx._('saveToHistoryCache')('url3', make('<div>'))
htmx._('saveToHistoryCache')('url2', make('<div>'))
var cache = JSON.parse(localStorage.getItem(HTMX_HISTORY_CACHE_NAME))
cache.length.should.equal(3)
})
it('history cache is LRU', function() {
htmx._('saveToHistoryCache')('url1', make("<div>"))
htmx._('saveToHistoryCache')('url2', make("<div>"))
htmx._('saveToHistoryCache')('url3', make("<div>"))
htmx._('saveToHistoryCache')('url2', make("<div>"))
htmx._('saveToHistoryCache')('url1', make("<div>"))
htmx._('saveToHistoryCache')('url1', make('<div>'))
htmx._('saveToHistoryCache')('url2', make('<div>'))
htmx._('saveToHistoryCache')('url3', make('<div>'))
htmx._('saveToHistoryCache')('url2', make('<div>'))
htmx._('saveToHistoryCache')('url1', make('<div>'))
var cache = JSON.parse(localStorage.getItem(HTMX_HISTORY_CACHE_NAME))
cache.length.should.equal(3)
cache[0].url.should.equal('url3')
@@ -210,7 +210,7 @@ describe('hx-push-url attribute', function() {
}
try {
localStorage.removeItem('htmx-history-cache')
htmx._('saveToHistoryCache')('/dummy', make("<div>" + bigContent + "</div>"), 'Foo', 0)
htmx._('saveToHistoryCache')('/dummy', make('<div>' + bigContent + '</div>'), 'Foo', 0)
should.equal(localStorage.getItem('htmx-history-cache'), null)
} finally {
// clear history cache afterwards

View File

@@ -892,10 +892,10 @@ describe('hx-trigger attribute', function() {
var div = make('<div hx-get="/test" hx-trigger="revealed, click" style="position: fixed; top: 1px; left: 1px; border: 3px solid red">foo</div>')
div.innerHTML.should.equal('foo')
this.server.respond()
div.innerHTML.should.equal('Requests: 1');
div.click();
div.innerHTML.should.equal('Requests: 1')
div.click()
this.server.respond()
div.innerHTML.should.equal('Requests: 2');
div.innerHTML.should.equal('Requests: 2')
})
it('revealed doesnt cause other events to trigger', function() {

View File

@@ -375,4 +375,32 @@ describe('Core htmx API test', function() {
htmx.trigger(div, 'myEvent')
myEventCalled.should.equal(true)
})
it('swaps content properly (basic)', function() {
var output = make('<output id="output"/>')
htmx.swap('#output', '<div>Swapped!</div>', { swapStyle: 'innerHTML' })
output.innerHTML.should.be.equal('<div>Swapped!</div>')
})
it('swaps content properly (with select)', function() {
var output = make('<output id="output"/>')
htmx.swap('#output', '<div><p id="select-me">Swapped!</p></div>', { swapStyle: 'innerHTML' }, { select: '#select-me' })
output.innerHTML.should.be.equal('<p id="select-me">Swapped!</p>')
})
it('swaps content properly (with oob)', function() {
var output = make('<output id="output"/>')
var oobDiv = make('<div id="oob"/>')
htmx.swap('#output', '<div id="oob" hx-swap-oob="innerHTML">OOB Swapped!</div><div>Swapped!</div>', { swapStyle: 'innerHTML' })
output.innerHTML.should.be.equal('<div>Swapped!</div>')
oobDiv.innerHTML.should.be.equal('OOB Swapped!')
})
it('swaps content properly (with select oob)', function() {
var output = make('<output id="output"/>')
var oobDiv = make('<div id="oob"/>')
htmx.swap('#output', '<div id="oob">OOB Swapped!</div><div>Swapped!</div>', { swapStyle: 'innerHTML' }, { selectOOB: '#oob:innerHTML' })
output.innerHTML.should.be.equal('<div>Swapped!</div>')
oobDiv.innerHTML.should.be.equal('OOB Swapped!')
})
})

View File

@@ -1,62 +1,59 @@
describe("default extensions behavior", function() {
var loadCalls, afterSwapCalls, afterSettleCalls;
describe('default extensions behavior', function() {
var loadCalls, afterSwapCalls, afterSettleCalls
beforeEach(function() {
loadCalls = [];
this.server = makeServer();
clearWorkArea();
loadCalls = []
this.server = makeServer()
clearWorkArea()
htmx.defineExtension("ext-testswap", {
htmx.defineExtension('ext-testswap', {
onEvent: function(name, evt) {
if (name === "htmx:load") {
loadCalls.push(evt.detail.elt);
if (name === 'htmx:load') {
loadCalls.push(evt.detail.elt)
}
},
handleSwap: function(swapStyle, target, fragment, settleInfo) {
// simple outerHTML replacement for tests
var parentEl = target.parentElement;
parentEl.removeChild(target);
let arr = [];
var parentEl = target.parentElement
parentEl.removeChild(target)
const arr = []
for (const child of fragment.childNodes) {
arr.push(parentEl.appendChild(child));
arr.push(parentEl.appendChild(child))
}
return arr; // return the newly added elements
return arr // return the newly added elements
}
});
});
})
})
afterEach(function() {
this.server.restore();
clearWorkArea();
htmx.removeExtension("ext-testswap");
});
this.server.restore()
clearWorkArea()
htmx.removeExtension('ext-testswap')
})
it('handleSwap: afterSwap and afterSettle triggered if extension defined on parent', function() {
this.server.respondWith("GET", "/test", '<button>Clicked!</button>');
var div = make('<div hx-ext="ext-testswap"><button hx-get="/test" hx-swap="testswap">Click Me!</button></div>');
var btn = div.firstChild;
this.server.respondWith('GET', '/test', '<button>Clicked!</button>')
var div = make('<div hx-ext="ext-testswap"><button hx-get="/test" hx-swap="testswap">Click Me!</button></div>')
var btn = div.firstChild
btn.click()
this.server.respond();
loadCalls.length.should.equal(1);
loadCalls[0].textContent.should.equal('Clicked!'); // the new button is loaded
});
this.server.respond()
loadCalls.length.should.equal(1)
loadCalls[0].textContent.should.equal('Clicked!') // the new button is loaded
})
it('handleSwap: new content is handled by htmx', function() {
this.server.respondWith("GET", "/test", '<button id="test-ext-testswap">Clicked!<span hx-get="/test-inner" hx-trigger="load"></span></button>');
this.server.respondWith("GET", "/test-inner", 'Loaded!');
make('<div hx-ext="ext-testswap"><button hx-get="/test" hx-swap="testswap">Click Me!</button></div>').querySelector('button').click();
this.server.respondWith('GET', '/test', '<button id="test-ext-testswap">Clicked!<span hx-get="/test-inner" hx-trigger="load"></span></button>')
this.server.respondWith('GET', '/test-inner', 'Loaded!')
make('<div hx-ext="ext-testswap"><button hx-get="/test" hx-swap="testswap">Click Me!</button></div>').querySelector('button').click()
this.server.respond(); // call /test via button trigger=click
var btn = byId('test-ext-testswap');
btn.textContent.should.equal('Clicked!');
loadCalls.length.should.equal(1);
loadCalls[0].textContent.should.equal('Clicked!'); // the new button is loaded
this.server.respond() // call /test via button trigger=click
var btn = byId('test-ext-testswap')
btn.textContent.should.equal('Clicked!')
loadCalls.length.should.equal(1)
loadCalls[0].textContent.should.equal('Clicked!') // the new button is loaded
this.server.respond(); // call /test-inner via span trigger=load
btn.textContent.should.equal("Clicked!Loaded!");
loadCalls.length.should.equal(1); // text should not trigger event
});
});
this.server.respond() // call /test-inner via span trigger=load
btn.textContent.should.equal('Clicked!Loaded!')
loadCalls.length.should.equal(1) // text should not trigger event
})
})

View File

@@ -448,6 +448,37 @@ Removes the given extension from htmx
htmx.removeExtension("my-extension");
```
### Method - `htmx.swap()` {#swap}
Performs swapping (and settling) of HTML content
##### Parameters
* `target` - the HTML element or string selector of swap target
* `content` - string representation of content to be swapped
* `swapSpec` - swapping specification, representing parameters from `hx-swap`
* `swapStyle` (required) - swapping style (`innerHTML`, `outerHTML`, `beforebegin` etc)
* `swapDelay`, `settleDelay` (number) - delays before swapping and settling respectively
* `transition` (bool) - whether to use HTML transitions for swap
* `ignoreTitle` (bool) - disables page title updates
* `head` (string) - specifies `head` tag handling strategy (`merge` or `append`). Leave empty to disable head handling
* `scroll`, `scrollTarget`, `show`, `showTarget`, `focusScroll` - specifies scroll handling after swap
* `swapOptions` - additional *optional* parameters for swapping
* `select` - selector for the content to be swapped (equivalent of `hx-select`)
* `selectOOB` - selector for the content to be swapped out-of-band (equivalent of `hx-select-oob`)
* `eventInfo` - an object to be attached to `htmx:afterSwap` and `htmx:afterSettle` elements
* `anchor` - an anchor element that triggered scroll, will be scrolled into view on settle. Provides simple alternative to full scroll handling
* `contextElement` - DOM element that serves as context to swapping operation. Currently used to find extensions enabled for specific element
* `afterSwapCallback`, `afterSettleCallback` - callback functions called after swap and settle respectively. Take no arguments
##### Example
```js
// swap #output element inner HTML with div element with "Swapped!" text
htmx.swap("#output", "<div>Swapped!</div>", {swapStyle: 'innerHTML'});
```
### Method - `htmx.takeClass()` {#takeClass}
Takes the given class from its siblings, so that among its siblings, only the given element will have the class.

View File

@@ -202,6 +202,7 @@ The table below lists all other attributes available in htmx.
| [`htmx.remove()`](@/api.md#remove) | Removes the given element
| [`htmx.removeClass()`](@/api.md#removeClass) | Removes a class from the given element
| [`htmx.removeExtension()`](@/api.md#removeExtension) | Removes an htmx [extension](@/extensions/_index.md)
| [`htmx.swap()`](@/api.md#swap) | Performs swapping (and settling) of HTML content
| [`htmx.takeClass()`](@/api.md#takeClass) | Takes a class from other elements for the given element
| [`htmx.toggleClass()`](@/api.md#toggleClass) | Toggles a class from the given element
| [`htmx.trigger()`](@/api.md#trigger) | Triggers an event on an element