Sync content from 1.8.6 release

- Update path for test generation script
This commit is contained in:
Daniel J. Summers
2023-03-08 11:34:42 -05:00
parent f36dde6eb4
commit 8564251136
203 changed files with 122693 additions and 22 deletions

View File

@@ -3,7 +3,7 @@ var fs = require('fs-extra');
console.log(config.version)
var testRoot = "www/test/";
var testRoot = "www/static/test/";
var currentReleaseRoot = testRoot + config.version;
fs.ensureDirSync(currentReleaseRoot);
fs.copySync("node_modules/mocha/mocha.js", currentReleaseRoot + "/node_modules/mocha/mocha.js");

View File

@@ -32,7 +32,7 @@ By removing these arbitrary constraints, htmx completes HTML as a [hypertext](ht
<h2>quick start</h2>
```html
<script src="https://unpkg.com/htmx.org@1.8.5"></script>
<script src="https://unpkg.com/htmx.org@1.8.6"></script>
<!-- have a button POST a click via AJAX -->
<button hx-post="/clicked" hx-swap="outerHTML">
Click Me

View File

@@ -113,7 +113,7 @@ The fastest way to get going with htmx is to load it via a CDN. You can simply a
and get going:
```html
<script src="https://unpkg.com/htmx.org@1.8.5" integrity="sha384-7aHh9lqPYGYZ7sTHvzP1t3BAfLhYSTy9ArHdP3Xsr9/3TlGurYgcPBoFmXX2TX/w" crossorigin="anonymous"></script>
<script src="https://unpkg.com/htmx.org@1.8.6" integrity="sha384-Bj8qm/6B+71E6FQSySofJOUjA/gq330vEqjFx9LakWybUySyI1IQHwPtbTU7bNwx" crossorigin="anonymous"></script>
```
While the CDN approach is extremely simple, you may want to consider [not using CDNs in production](https://blog.wesleyac.com/posts/why-not-javascript-cdn).

View File

@@ -0,0 +1,22 @@
+++
title = "htmx 1.8.6 has been released!"
date = 2023-03-02
[taxonomies]
tag = ["posts", "announcements"]
+++
## htmx 1.8.6 Release
I'm happy to announce the [1.8.6 release](https://unpkg.com/browse/htmx.org@1.8.6/) of htmx.
### New Features
* [ESM support](https://github.com/bigskysoftware/htmx/commit/a85ad4ac67c3a471dbb8472900ec1e583b571a67) (thank you @dkniffin!)
* Sass has been vanquished from the htmx.org website so python 2 (!!!) is no longer required for developing htmx (thank you @dz4k!)
### Improvements & Bug fixes
* Fixed a bug where the `changed` modifier on `keyup` did not work properly if an input was tabbed into
* Many other smaller bug fixes and doc fixes
Thank you to everyone who contributed, and enjoy!

View File

@@ -0,0 +1,429 @@
(function(chaiDom) {
if (typeof require === 'function' && typeof exports === 'object' && typeof module === 'object') {
module.exports = chaiDom
} else if (typeof define === 'function' && define.amd) {
define(function() {
return chaiDom
})
} else {
chai.use(chaiDom)
}
}(function(chai, utils) {
var flag = utils.flag,
elToString = function(el) {
var desc
if (isNodeList(el)) {
if (el.length === 0) {
return 'empty NodeList'
}
desc = Array.prototype.slice.call(el, 0, 5).map(elToString).join(', ')
return el.length > 5 ? desc + '... (+' + (el.length - 5) + ' more)' : desc
}
if (!isHTMLElement(el)) {
return String(el)
}
desc = el.tagName.toLowerCase()
if (el.id) {
desc += '#' + el.id
}
if (el.className) {
desc += '.' + String(el.className).replace(/\s+/g, '.')
}
Array.prototype.forEach.call(el.attributes, function(attr) {
if (attr.name !== 'class' && attr.name !== 'id') {
desc += '[' + attr.name + (attr.value ? '="' + attr.value + '"]' : ']')
}
})
return desc
},
attrAssert = function(name, val) {
var el = flag(this, 'object'), actual = el.getAttribute(name)
if (!flag(this, 'negate') || undefined === val) {
this.assert(
!!el.attributes[name]
, 'expected ' + elToString(el) + ' to have an attribute #{exp}'
, 'expected ' + elToString(el) + ' not to have an attribute #{exp}'
, name
)
}
if (undefined !== val) {
this.assert(
val === actual
, 'expected ' + elToString(el) + ' to have an attribute ' + utils.inspect(name) + ' with the value #{exp}, but the value was #{act}'
, 'expected ' + elToString(el) + ' not to have an attribute ' + utils.inspect(name) + ' with the value #{act}'
, val
, actual
)
}
flag(this, 'object', actual)
},
isHTMLElement = function(el) {
return el.nodeType === 1 // window.Node.ELEMENT_NODE
},
isNodeList = function(obj) {
return Object.prototype.toString.call(obj) === '[object NodeList]'
}
utils.elToString = elToString
chai.Assertion.addMethod('attr', attrAssert)
chai.Assertion.addMethod('attribute', attrAssert)
chai.Assertion.addMethod('class', function(className) {
var el = flag(this, 'object')
if (className instanceof RegExp) {
return this.assert(
Array.from(el.classList).some(function(cls) { return className.test(cls) })
, 'expected ' + elToString(el) + ' to have class matching #{exp}'
, 'expected ' + elToString(el) + ' not to have class matching #{exp}'
, className
)
}
this.assert(
el.classList.contains(className)
, 'expected ' + elToString(el) + ' to have class #{exp}'
, 'expected ' + elToString(el) + ' not to have class #{exp}'
, className
)
})
chai.Assertion.addMethod('id', function(id) {
var el = flag(this, 'object')
this.assert(
el.id == id
, 'expected ' + elToString(el) + ' to have id #{exp}'
, 'expected ' + elToString(el) + ' not to have id #{exp}'
, id
)
})
chai.Assertion.addMethod('html', function(html) {
var el = flag(this, 'object'), actual = flag(this, 'object').innerHTML
if (flag(this, 'contains')) {
this.assert(
actual.indexOf(html) >= 0
, 'expected #{act} to contain HTML #{exp}'
, 'expected #{act} not to contain HTML #{exp}'
, html
, actual
)
} else {
this.assert(
actual === html
, 'expected ' + elToString(el) + ' to have HTML #{exp}, but the HTML was #{act}'
, 'expected ' + elToString(el) + ' not to have HTML #{exp}'
, html
, actual
)
}
})
chai.Assertion.addChainableMethod('trimmed', null, function() {
flag(this, 'trim-text', true)
})
chai.Assertion.addProperty('rendered', function() {
flag(this, 'rendered-text', true)
})
chai.Assertion.addMethod('text', function(text) {
var obj = flag(this, 'object'), contains = flag(this, 'contains'),
trim = flag(this, 'trim-text'), actual, result
var property = flag(this, 'rendered-text') ? 'innerText' : 'textContent'
if (isNodeList(obj)) {
actual = Array.prototype.map.call(obj, function(el) { return trim ? el[property].trim() : el[property] })
if (Array.isArray(text)) {
result = contains ?
text[flag(this, 'negate') ? 'some' : 'every'](function(t) {
return Array.prototype.some.call(obj, function(el) {
return (trim ? el[property].trim() : el[property]) === t
})
})
:
utils.eql(actual, text)
actual = actual.join()
text = text.join()
} else {
actual = actual.join('')
result = contains ? actual.indexOf(text) >= 0 : actual === text
}
} else {
actual = trim ? obj[property].trim() : obj[property]
result = contains ? actual.indexOf(text) >= 0 : actual === text
}
var objDesc = elToString(obj)
var textMsg = ''
if (trim) {
textMsg += 'trimmed '
}
if (flag(this, 'rendered-text')) {
textMsg += 'rendered '
}
textMsg += 'text'
if (contains) {
this.assert(
result
, 'expected ' + objDesc + ' to contain #{exp}, but the ' + textMsg + ' was #{act}'
, 'expected ' + objDesc + ' not to contain #{exp}, but the ' + textMsg + ' was #{act}'
, text
, actual
)
} else {
this.assert(
result
, 'expected ' + objDesc + ' to have ' + textMsg + ' #{exp}, but the ' + textMsg + ' was #{act}'
, 'expected ' + objDesc + ' not to have ' + textMsg + ' #{exp}'
, text
, actual
)
}
})
chai.Assertion.addMethod('value', function(value) {
var el = flag(this, 'object'), actual = flag(this, 'object').value
this.assert(
flag(this, 'object').value === value
, 'expected ' + elToString(el) + ' to have value #{exp}, but the value was #{act}'
, 'expected ' + elToString(el) + ' not to have value #{exp}'
, value
, actual
)
})
chai.Assertion.overwriteProperty('exist', function(_super) {
return function() {
var obj = flag(this, 'object')
if (isNodeList(obj)) {
this.assert(
obj.length > 0
, 'expected an empty NodeList to have nodes'
, 'expected ' + elToString(obj) + ' to not exist')
} else {
_super.apply(this, arguments)
}
}
})
chai.Assertion.overwriteProperty('empty', function(_super) {
return function() {
var obj = flag(this, 'object')
if (isHTMLElement(obj)) {
this.assert(
obj.children.length === 0
, 'expected ' + elToString(obj) + ' to be empty'
, 'expected ' + elToString(obj) + ' to not be empty')
} else if (isNodeList(obj)) {
this.assert(
obj.length === 0
, 'expected ' + elToString(obj) + ' to be empty'
, 'expected ' + elToString(obj) + ' to not be empty')
} else {
_super.apply(this, arguments)
}
}
})
chai.Assertion.overwriteChainableMethod('length',
function(_super) {
return function(length) {
var obj = flag(this, 'object')
if (isNodeList(obj) || isHTMLElement(obj)) {
var actualLength = obj.children ? obj.children.length : obj.length
this.assert(
actualLength === length
, 'expected ' + elToString(obj) + ' to have #{exp} children but it had #{act} children'
, 'expected ' + elToString(obj) + ' to not have #{exp} children'
, length
, actualLength
)
} else {
_super.apply(this, arguments)
}
}
},
function(_super) {
return function() {
_super.call(this)
}
}
)
chai.Assertion.overwriteMethod('match', function(_super) {
return function(selector) {
var obj = flag(this, 'object')
if (isHTMLElement(obj)) {
this.assert(
obj.matches(selector)
, 'expected ' + elToString(obj) + ' to match #{exp}'
, 'expected ' + elToString(obj) + ' to not match #{exp}'
, selector
)
} else if (isNodeList(obj)) {
this.assert(
(!!obj.length && Array.prototype.every.call(obj, function(el) { return el.matches(selector) }))
, 'expected ' + elToString(obj) + ' to match #{exp}'
, 'expected ' + elToString(obj) + ' to not match #{exp}'
, selector
)
} else {
_super.apply(this, arguments)
}
}
})
chai.Assertion.overwriteChainableMethod('contain',
function(_super) {
return function(subitem) {
var obj = flag(this, 'object')
if (isHTMLElement(obj)) {
if (typeof subitem === 'string') {
this.assert(
!!obj.querySelector(subitem)
, 'expected ' + elToString(obj) + ' to contain #{exp}'
, 'expected ' + elToString(obj) + ' to not contain #{exp}'
, subitem)
} else {
this.assert(
obj.contains(subitem)
, 'expected ' + elToString(obj) + ' to contain ' + elToString(subitem)
, 'expected ' + elToString(obj) + ' to not contain ' + elToString(subitem))
}
} else {
_super.apply(this, arguments)
}
}
},
function(_super) {
return function() {
_super.call(this)
}
}
)
chai.Assertion.addMethod('descendant', function(subitem) {
var obj = flag(this, 'object'), actual = subitem
if (typeof subitem === 'string') {
actual = obj.querySelector(subitem)
this.assert(
!!actual
, 'expected ' + elToString(obj) + ' to have descendant #{exp}'
, 'expected ' + elToString(obj) + ' to not have descendant #{exp}'
, subitem)
} else {
this.assert(
obj.contains(subitem)
, 'expected ' + elToString(obj) + ' to contain ' + elToString(subitem)
, 'expected ' + elToString(obj) + ' to not contain ' + elToString(subitem))
}
flag(this, 'object', actual)
})
chai.Assertion.addMethod('descendants', function(selector) {
var obj = flag(this, 'object'),
actual = obj.querySelectorAll(selector)
this.assert(
!!actual.length
, 'expected ' + elToString(obj) + ' to have descendants #{exp}'
, 'expected ' + elToString(obj) + ' to not have descendants #{exp}'
, selector)
flag(this, 'object', actual)
})
chai.Assertion.addProperty('displayed', function() {
var el = flag(this, 'object'),
actual = document.body.contains(el) ? window.getComputedStyle(el).display : el.style.display
this.assert(
actual !== 'none'
, 'expected ' + elToString(el) + ' to be displayed, but it was not'
, 'expected ' + elToString(el) + ' to not be displayed, but it was as ' + actual
, actual
)
})
chai.Assertion.addProperty('visible', function() {
var el = flag(this, 'object'),
actual = document.body.contains(el) ? window.getComputedStyle(el).visibility : el.style.visibility
this.assert(
actual !== 'hidden' && actual !== 'collapse'
, 'expected ' + elToString(el) + ' to be visible, but it was ' + (actual === 'hidden' ? 'hidden' : 'collapsed')
, 'expected ' + elToString(el) + ' to not be visible, but it was'
, actual
)
})
chai.Assertion.addMethod('tagName', function(tagName) {
var el = flag(this, 'object'),
actual = el.tagName;
this.assert(
actual.toUpperCase() === tagName.toUpperCase()
, 'expected ' + elToString(el) + ' to have tagName ' + tagName + ', but it was ' + actual
, 'expected ' + elToString(el) + ' to not have tagName ' + tagName + ', but it was ' + actual
, actual
)
})
chai.Assertion.addMethod('style', function (styleProp, styleValue) {
var el = flag(this, 'object'),
style = window.getComputedStyle(el),
actual = style.getPropertyValue(styleProp).trim();
this.assert(
actual === styleValue
, 'expected ' + elToString(el) + ' to have style property ' + styleProp + ' equal to ' + styleValue + ', but it was equal to ' + actual
, 'expected ' + elToString(el) + ' to not have style property ' + styleProp + ' equal to ' + styleValue + ', but it was equal to ' + actual
, actual
)
})
chai.Assertion.overwriteProperty('focus', function() {
return function () {
var el = flag(this, 'object'), actual = el.ownerDocument.activeElement
this.assert(
el === el.ownerDocument.activeElement
, 'expected #{this} to have focus'
, 'expected #{this} not to have focus'
, el
, actual
)
}
})
chai.Assertion.overwriteProperty('checked', function() {
return function () {
var el = flag(this, 'object')
if(!(el instanceof HTMLInputElement && (el.type === 'checkbox' || el.type === 'radio'))) {
throw new TypeError(elToString(el) + ' is not a checkbox or radio input');
}
this.assert(
el.checked
, 'expected ' + elToString(el) + ' to be checked'
, 'expected ' + elToString(el) + ' to not be checked')
}
})
}));

11464
www-zola/static/test/1.8.6/node_modules/chai/chai.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

392
www-zola/static/test/1.8.6/node_modules/mocha/mocha.css generated vendored Normal file
View File

@@ -0,0 +1,392 @@
@charset "utf-8";
:root {
--mocha-color: #000;
--mocha-bg-color: #fff;
--mocha-pass-icon-color: #00d6b2;
--mocha-pass-color: #fff;
--mocha-pass-shadow-color: rgba(0,0,0,.2);
--mocha-pass-mediump-color: #c09853;
--mocha-pass-slow-color: #b94a48;
--mocha-test-pending-color: #0b97c4;
--mocha-test-pending-icon-color: #0b97c4;
--mocha-test-fail-color: #c00;
--mocha-test-fail-icon-color: #c00;
--mocha-test-fail-pre-color: #000;
--mocha-test-fail-pre-error-color: #c00;
--mocha-test-html-error-color: #000;
--mocha-box-shadow-color: #eee;
--mocha-box-bottom-color: #ddd;
--mocha-test-replay-color: #000;
--mocha-test-replay-bg-color: #eee;
--mocha-stats-color: #888;
--mocha-stats-em-color: #000;
--mocha-stats-hover-color: #eee;
--mocha-error-color: #c00;
--mocha-code-comment: #ddd;
--mocha-code-init: #2f6fad;
--mocha-code-string: #5890ad;
--mocha-code-keyword: #8a6343;
--mocha-code-number: #2f6fad;
}
@media (prefers-color-scheme: dark) {
:root {
--mocha-color: #fff;
--mocha-bg-color: #222;
--mocha-pass-icon-color: #00d6b2;
--mocha-pass-color: #222;
--mocha-pass-shadow-color: rgba(255,255,255,.2);
--mocha-pass-mediump-color: #f1be67;
--mocha-pass-slow-color: #f49896;
--mocha-test-pending-color: #0b97c4;
--mocha-test-pending-icon-color: #0b97c4;
--mocha-test-fail-color: #f44;
--mocha-test-fail-icon-color: #f44;
--mocha-test-fail-pre-color: #fff;
--mocha-test-fail-pre-error-color: #f44;
--mocha-test-html-error-color: #fff;
--mocha-box-shadow-color: #444;
--mocha-box-bottom-color: #555;
--mocha-test-replay-color: #fff;
--mocha-test-replay-bg-color: #444;
--mocha-stats-color: #aaa;
--mocha-stats-em-color: #fff;
--mocha-stats-hover-color: #444;
--mocha-error-color: #f44;
--mocha-code-comment: #ddd;
--mocha-code-init: #9cc7f1;
--mocha-code-string: #80d4ff;
--mocha-code-keyword: #e3a470;
--mocha-code-number: #4ca7ff;
}
}
body {
margin:0;
background-color: var(--mocha-bg-color);
color: var(--mocha-color);
}
#mocha {
font: 20px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif;
margin: 60px 50px;
}
#mocha ul,
#mocha li {
margin: 0;
padding: 0;
}
#mocha ul {
list-style: none;
}
#mocha h1,
#mocha h2 {
margin: 0;
}
#mocha h1 {
margin-top: 15px;
font-size: 1em;
font-weight: 200;
}
#mocha h1 a {
text-decoration: none;
color: inherit;
}
#mocha h1 a:hover {
text-decoration: underline;
}
#mocha .suite .suite h1 {
margin-top: 0;
font-size: .8em;
}
#mocha .hidden {
display: none;
}
#mocha h2 {
font-size: 12px;
font-weight: normal;
cursor: pointer;
}
#mocha .suite {
margin-left: 15px;
}
#mocha .test {
margin-left: 15px;
overflow: hidden;
}
#mocha .test.pending:hover h2::after {
content: '(pending)';
font-family: arial, sans-serif;
}
#mocha .test.pass.medium .duration {
background: var(--mocha-pass-mediump-color);
}
#mocha .test.pass.slow .duration {
background: var(--mocha-pass-slow-color);
}
#mocha .test.pass::before {
content: '✓';
font-size: 12px;
display: block;
float: left;
margin-right: 5px;
color: var(--mocha-pass-icon-color);
}
#mocha .test.pass .duration {
font-size: 9px;
margin-left: 5px;
padding: 2px 5px;
color: var(--mocha-pass-color);
-webkit-box-shadow: inset 0 1px 1px var(--mocha-pass-shadow-color);
-moz-box-shadow: inset 0 1px 1px var(--mocha-pass-shadow-color);
box-shadow: inset 0 1px 1px var(--mocha-pass-shadow-color);
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
-ms-border-radius: 5px;
-o-border-radius: 5px;
border-radius: 5px;
}
#mocha .test.pass.fast .duration {
display: none;
}
#mocha .test.pending {
color: var(--mocha-test-pending-color);
}
#mocha .test.pending::before {
content: '◦';
color: var(--mocha-test-pending-icon-color);
}
#mocha .test.fail {
color: var(--mocha-test-fail-color);
}
#mocha .test.fail pre {
color: var(--mocha-test-fail-pre-color);
}
#mocha .test.fail::before {
content: '✖';
font-size: 12px;
display: block;
float: left;
margin-right: 5px;
color: var(--mocha-test-fail-icon-color);
}
#mocha .test pre.error {
color: var(--mocha-test-fail-pre-error-color);
max-height: 300px;
overflow: auto;
}
#mocha .test .html-error {
overflow: auto;
color: var(--mocha-test-html-error-color);
display: block;
float: left;
clear: left;
font: 12px/1.5 monaco, monospace;
margin: 5px;
padding: 15px;
border: 1px solid var(--mocha-box-shadow-color);
max-width: 85%; /*(1)*/
max-width: -webkit-calc(100% - 42px);
max-width: -moz-calc(100% - 42px);
max-width: calc(100% - 42px); /*(2)*/
max-height: 300px;
word-wrap: break-word;
border-bottom-color: var(--mocha-box-bottom-color);
-webkit-box-shadow: 0 1px 3px var(--mocha-box-shadow-color);
-moz-box-shadow: 0 1px 3px var(--mocha-box-shadow-color);
box-shadow: 0 1px 3px var(--mocha-box-shadow-color);
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
}
#mocha .test .html-error pre.error {
border: none;
-webkit-border-radius: 0;
-moz-border-radius: 0;
border-radius: 0;
-webkit-box-shadow: 0;
-moz-box-shadow: 0;
box-shadow: 0;
padding: 0;
margin: 0;
margin-top: 18px;
max-height: none;
}
/**
* (1): approximate for browsers not supporting calc
* (2): 42 = 2*15 + 2*10 + 2*1 (padding + margin + border)
* ^^ seriously
*/
#mocha .test pre {
display: block;
float: left;
clear: left;
font: 12px/1.5 monaco, monospace;
margin: 5px;
padding: 15px;
border: 1px solid var(--mocha-box-shadow-color);
max-width: 85%; /*(1)*/
max-width: -webkit-calc(100% - 42px);
max-width: -moz-calc(100% - 42px);
max-width: calc(100% - 42px); /*(2)*/
word-wrap: break-word;
border-bottom-color: var(--mocha-box-bottom-color);
-webkit-box-shadow: 0 1px 3px var(--mocha-box-shadow-color);
-moz-box-shadow: 0 1px 3px var(--mocha-box-shadow-color);
box-shadow: 0 1px 3px var(--mocha-box-shadow-color);
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
}
#mocha .test h2 {
position: relative;
}
#mocha .test a.replay {
position: absolute;
top: 3px;
right: 0;
text-decoration: none;
vertical-align: middle;
display: block;
width: 15px;
height: 15px;
line-height: 15px;
text-align: center;
background: var(--mocha-test-replay-bg-color);
font-size: 15px;
-webkit-border-radius: 15px;
-moz-border-radius: 15px;
border-radius: 15px;
-webkit-transition:opacity 200ms;
-moz-transition:opacity 200ms;
-o-transition:opacity 200ms;
transition: opacity 200ms;
opacity: 0.7;
color: var(--mocha-test-replay-color);
}
#mocha .test:hover a.replay {
box-shadow: 0 0 1px inset var(--mocha-test-replay-color);
opacity: 1;
}
#mocha-report.pass .test.fail {
display: none;
}
#mocha-report.fail .test.pass {
display: none;
}
#mocha-report.pending .test.pass,
#mocha-report.pending .test.fail {
display: none;
}
#mocha-report.pending .test.pass.pending {
display: block;
}
#mocha-error {
color: var(--mocha-error-color);
font-size: 1.5em;
font-weight: 100;
letter-spacing: 1px;
}
#mocha-stats {
position: fixed;
top: 15px;
right: 10px;
font-size: 12px;
margin: 0;
color: var(--mocha-stats-color);
z-index: 1;
}
#mocha-stats .progress {
float: right;
padding-top: 0;
/**
* Set safe initial values, so mochas .progress does not inherit these
* properties from Bootstrap .progress (which causes .progress height to
* equal line height set in Bootstrap).
*/
height: auto;
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
background-color: initial;
}
#mocha-stats em {
color: var(--mocha-stats-em-color);
}
#mocha-stats a {
text-decoration: none;
color: inherit;
}
#mocha-stats a:hover {
border-bottom: 1px solid var(--mocha-stats-hover-color);
}
#mocha-stats li {
display: inline-block;
margin: 0 5px;
list-style: none;
padding-top: 11px;
}
#mocha-stats canvas {
width: 40px;
height: 40px;
}
#mocha code .comment { color: var(--mocha-code-comment); }
#mocha code .init { color: var(--mocha-code-init); }
#mocha code .string { color: var(--mocha-code-string); }
#mocha code .keyword { color: var(--mocha-code-keyword); }
#mocha code .number { color: var(--mocha-code-number); }
@media screen and (max-device-width: 480px) {
#mocha {
margin: 60px 0px;
}
#mocha #stats {
position: absolute;
}
}

20635
www-zola/static/test/1.8.6/node_modules/mocha/mocha.js generated vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

16184
www-zola/static/test/1.8.6/node_modules/sinon/pkg/sinon.js generated vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,7 @@
htmx.defineExtension('ajax-header', {
onEvent: function (name, evt) {
if (name === "htmx:configRequest") {
evt.detail.headers['X-Requested-With'] = 'XMLHttpRequest';
}
}
});

View File

@@ -0,0 +1,16 @@
htmx.defineExtension('alpine-morph', {
isInlineSwap: function (swapStyle) {
return swapStyle === 'morph';
},
handleSwap: function (swapStyle, target, fragment) {
if (swapStyle === 'morph') {
if (fragment.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
Alpine.morph(target, fragment.firstElementChild);
return [target];
} else {
Alpine.morph(target, fragment.outerHTML);
return [target];
}
}
}
});

View File

@@ -0,0 +1,92 @@
(function () {
function splitOnWhitespace(trigger) {
return trigger.split(/\s+/);
}
function parseClassOperation(trimmedValue) {
var split = splitOnWhitespace(trimmedValue);
if (split.length > 1) {
var operation = split[0];
var classDef = split[1].trim();
var cssClass;
var delay;
if (classDef.indexOf(":") > 0) {
var splitCssClass = classDef.split(':');
cssClass = splitCssClass[0];
delay = htmx.parseInterval(splitCssClass[1]);
} else {
cssClass = classDef;
delay = 100;
}
return {
operation: operation,
cssClass: cssClass,
delay: delay
}
} else {
return null;
}
}
function performOperation(elt, classOperation, classList, currentRunTime) {
setTimeout(function () {
elt.classList[classOperation.operation].call(elt.classList, classOperation.cssClass);
}, currentRunTime)
}
function toggleOperation(elt, classOperation, classList, currentRunTime) {
setTimeout(function () {
setInterval(function () {
elt.classList[classOperation.operation].call(elt.classList, classOperation.cssClass);
}, classOperation.delay);
}, currentRunTime)
}
function processClassList(elt, classList) {
var runs = classList.split("&");
for (var i = 0; i < runs.length; i++) {
var run = runs[i];
var currentRunTime = 0;
var classOperations = run.split(",");
for (var j = 0; j < classOperations.length; j++) {
var value = classOperations[j];
var trimmedValue = value.trim();
var classOperation = parseClassOperation(trimmedValue);
if (classOperation) {
if (classOperation.operation === "toggle") {
toggleOperation(elt, classOperation, classList, currentRunTime);
currentRunTime = currentRunTime + classOperation.delay;
} else {
currentRunTime = currentRunTime + classOperation.delay;
performOperation(elt, classOperation, classList, currentRunTime);
}
}
}
}
}
function maybeProcessClasses(elt) {
if (elt.getAttribute) {
var classList = elt.getAttribute("classes") || elt.getAttribute("data-classes");
if (classList) {
processClassList(elt, classList);
}
}
}
htmx.defineExtension('class-tools', {
onEvent: function (name, evt) {
if (name === "htmx:afterProcessNode") {
var elt = evt.detail.elt;
maybeProcessClasses(elt);
if (elt.querySelectorAll) {
var children = elt.querySelectorAll("[classes], [data-classes]");
for (var i = 0; i < children.length; i++) {
maybeProcessClasses(children[i]);
}
}
}
}
});
})();

View File

@@ -0,0 +1,37 @@
htmx.defineExtension('client-side-templates', {
transformResponse : function(text, xhr, elt) {
var mustacheTemplate = htmx.closest(elt, "[mustache-template]");
if (mustacheTemplate) {
var data = JSON.parse(text);
var templateId = mustacheTemplate.getAttribute('mustache-template');
var template = htmx.find("#" + templateId);
if (template) {
return Mustache.render(template.innerHTML, data);
} else {
throw "Unknown mustache template: " + templateId;
}
}
var handlebarsTemplate = htmx.closest(elt, "[handlebars-template]");
if (handlebarsTemplate) {
var data = JSON.parse(text);
var templateName = handlebarsTemplate.getAttribute('handlebars-template');
return Handlebars.partials[templateName](data);
}
var nunjucksTemplate = htmx.closest(elt, "[nunjucks-template]");
if (nunjucksTemplate) {
var data = JSON.parse(text);
var templateName = nunjucksTemplate.getAttribute('nunjucks-template');
var template = htmx.find('#' + templateName);
if (template) {
return nunjucks.renderString(template.innerHTML, data);
} else {
return nunjucks.render(templateName, data);
}
}
return text;
}
});

View File

@@ -0,0 +1,11 @@
htmx.defineExtension('debug', {
onEvent: function (name, evt) {
if (console.debug) {
console.debug(name, evt);
} else if (console) {
console.log("DEBUG:", name, evt);
} else {
throw "NO CONSOLE SUPPORTED"
}
}
});

View File

@@ -0,0 +1,16 @@
"use strict";
// Disable Submit Button
htmx.defineExtension('disable-element', {
onEvent: function (name, evt) {
let elt = evt.detail.elt;
let target = elt.getAttribute("hx-disable-element");
let targetElement = (target == "self") ? elt : document.querySelector(target);
if (name === "htmx:beforeRequest" && targetElement) {
targetElement.disabled = true;
} else if (name == "htmx:afterRequest" && targetElement) {
targetElement.disabled = false;
}
}
});

View File

@@ -0,0 +1,37 @@
(function(){
function stringifyEvent(event) {
var obj = {};
for (var key in event) {
obj[key] = event[key];
}
return JSON.stringify(obj, function(key, value){
if(value instanceof Node){
var nodeRep = value.tagName;
if (nodeRep) {
nodeRep = nodeRep.toLowerCase();
if(value.id){
nodeRep += "#" + value.id;
}
if(value.classList && value.classList.length){
nodeRep += "." + value.classList.toString().replace(" ", ".")
}
return nodeRep;
} else {
return "Node"
}
}
if (value instanceof Window) return 'Window';
return value;
});
}
htmx.defineExtension('event-header', {
onEvent: function (name, evt) {
if (name === "htmx:configRequest") {
if (evt.detail.triggeringEvent) {
evt.detail.headers['Triggering-Event'] = stringifyEvent(evt.detail.triggeringEvent);
}
}
}
});
})();

View File

@@ -0,0 +1,141 @@
//==========================================================
// head-support.js
//
// An extension to htmx 1.0 to add head tag merging.
//==========================================================
(function(){
var api = null;
function log() {
//console.log(arguments);
}
function mergeHead(newContent, defaultMergeStrategy) {
if (newContent && newContent.indexOf('<head') > -1) {
const htmlDoc = document.createElement("html");
// remove svgs to avoid conflicts
var contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, '');
// extract head tag
var headTag = contentWithSvgsRemoved.match(/(<head(\s[^>]*>|>)([\s\S]*?)<\/head>)/im);
// if the head tag exists...
if (headTag) {
var added = []
var removed = []
var preserved = []
var nodesToAppend = []
htmlDoc.innerHTML = headTag;
var newHeadTag = htmlDoc.querySelector("head");
var currentHead = document.head;
if (newHeadTag == null) {
return;
} else {
// put all new head elements into a Map, by their outerHTML
var srcToNewHeadNodes = new Map();
for (const newHeadChild of newHeadTag.children) {
srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild);
}
}
// determine merge strategy
var mergeStrategy = api.getAttributeValue(newHeadTag, "hx-head") || defaultMergeStrategy;
// get the current head
for (const currentHeadElt of currentHead.children) {
// If the current head element is in the map
var inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML);
var isReAppended = currentHeadElt.getAttribute("hx-head") === "re-eval";
var isPreserved = api.getAttributeValue(currentHeadElt, "hx-preserve") === "true";
if (inNewContent || isPreserved) {
if (isReAppended) {
// remove the current version and let the new version replace it and re-execute
removed.push(currentHeadElt);
} else {
// this element already exists and should not be re-appended, so remove it from
// the new content map, preserving it in the DOM
srcToNewHeadNodes.delete(currentHeadElt.outerHTML);
preserved.push(currentHeadElt);
}
} else {
if (mergeStrategy === "append") {
// we are appending and this existing element is not new content
// so if and only if it is marked for re-append do we do anything
if (isReAppended) {
removed.push(currentHeadElt);
nodesToAppend.push(currentHeadElt);
}
} else {
// if this is a merge, we remove this content since it is not in the new head
if (api.triggerEvent(document.body, "htmx:removingHeadElement", {headElement: currentHeadElt}) !== false) {
removed.push(currentHeadElt);
}
}
}
}
// Push the tremaining new head elements in the Map into the
// nodes to append to the head tag
nodesToAppend.push(...srcToNewHeadNodes.values());
log("to append: ", nodesToAppend);
for (const newNode of nodesToAppend) {
log("adding: ", newNode);
var newElt = document.createRange().createContextualFragment(newNode.outerHTML);
log(newElt);
if (api.triggerEvent(document.body, "htmx:addingHeadElement", {headElement: newElt}) !== false) {
currentHead.appendChild(newElt);
added.push(newElt);
}
}
// remove all removed elements, after we have appended the new elements to avoid
// additional network requests for things like style sheets
for (const removedElement of removed) {
if (api.triggerEvent(document.body, "htmx:removingHeadElement", {headElement: removedElement}) !== false) {
currentHead.removeChild(removedElement);
}
}
api.triggerEvent(document.body, "htmx:afterHeadMerge", {added: added, kept: preserved, removed: removed});
}
}
}
htmx.defineExtension("head-support", {
init: function(apiRef) {
// store a reference to the internal API.
api = apiRef;
htmx.on('htmx:afterSwap', function(evt){
var serverResponse = evt.detail.xhr.response;
if (api.triggerEvent(document.body, "htmx:beforeHeadMerge", evt.detail)) {
mergeHead(serverResponse, evt.detail.boosted ? "merge" : "append");
}
})
htmx.on('htmx:historyRestore', function(evt){
if (api.triggerEvent(document.body, "htmx:beforeHeadMerge", evt.detail)) {
if (evt.detail.cacheMiss) {
mergeHead(evt.detail.serverResponse, "merge");
} else {
mergeHead(evt.detail.item.head, "merge");
}
}
})
htmx.on('htmx:historyItemCreated', function(evt){
var historyItem = evt.detail.item;
historyItem.head = document.head.outerHTML;
})
}
});
})()

View File

@@ -0,0 +1,24 @@
(function(){
function mergeObjects(obj1, obj2) {
for (var key in obj2) {
if (obj2.hasOwnProperty(key)) {
obj1[key] = obj2[key];
}
}
return obj1;
}
htmx.defineExtension('include-vals', {
onEvent: function (name, evt) {
if (name === "htmx:configRequest") {
var includeValsElt = htmx.closest(evt.detail.elt, "[include-vals],[data-include-vals]");
if (includeValsElt) {
var includeVals = includeValsElt.getAttribute("include-vals") || includeValsElt.getAttribute("data-include-vals");
var valuesToInclude = eval("({" + includeVals + "})");
mergeObjects(evt.detail.parameters, valuesToInclude);
}
}
}
});
})();

View File

@@ -0,0 +1,12 @@
htmx.defineExtension('json-enc', {
onEvent: function (name, evt) {
if (name === "htmx:configRequest") {
evt.detail.headers['Content-Type'] = "application/json";
}
},
encodeParameters : function(xhr, parameters, elt) {
xhr.overrideMimeType('text/json');
return (JSON.stringify(parameters));
}
});

View File

@@ -0,0 +1,179 @@
;(function () {
let loadingStatesUndoQueue = []
function loadingStateContainer(target) {
return htmx.closest(target, '[data-loading-states]') || document.body
}
function mayProcessUndoCallback(target, callback) {
if (document.body.contains(target)) {
callback()
}
}
function mayProcessLoadingStateByPath(elt, requestPath) {
const pathElt = htmx.closest(elt, '[data-loading-path]')
if (!pathElt) {
return true
}
return pathElt.getAttribute('data-loading-path') === requestPath
}
function queueLoadingState(sourceElt, targetElt, doCallback, undoCallback) {
const delayElt = htmx.closest(sourceElt, '[data-loading-delay]')
if (delayElt) {
const delayInMilliseconds =
delayElt.getAttribute('data-loading-delay') || 200
const timeout = setTimeout(() => {
doCallback()
loadingStatesUndoQueue.push(() => {
mayProcessUndoCallback(targetElt, () => undoCallback())
})
}, delayInMilliseconds)
loadingStatesUndoQueue.push(() => {
mayProcessUndoCallback(targetElt, () => clearTimeout(timeout))
})
} else {
doCallback()
loadingStatesUndoQueue.push(() => {
mayProcessUndoCallback(targetElt, () => undoCallback())
})
}
}
function getLoadingStateElts(loadingScope, type, path) {
return Array.from(htmx.findAll(loadingScope, `[${type}]`)).filter(
(elt) => mayProcessLoadingStateByPath(elt, path)
)
}
function getLoadingTarget(elt) {
if (elt.getAttribute('data-loading-target')) {
return Array.from(
htmx.findAll(elt.getAttribute('data-loading-target'))
)
}
return [elt]
}
htmx.defineExtension('loading-states', {
onEvent: function (name, evt) {
if (name === 'htmx:beforeRequest') {
const container = loadingStateContainer(evt.target)
const loadingStateTypes = [
'data-loading',
'data-loading-class',
'data-loading-class-remove',
'data-loading-disable',
'data-loading-aria-busy',
]
let loadingStateEltsByType = {}
loadingStateTypes.forEach((type) => {
loadingStateEltsByType[type] = getLoadingStateElts(
container,
type,
evt.detail.pathInfo.requestPath
)
})
loadingStateEltsByType['data-loading'].forEach((sourceElt) => {
getLoadingTarget(sourceElt).forEach((targetElt) => {
queueLoadingState(
sourceElt,
targetElt,
() =>
(targetElt.style.display =
sourceElt.getAttribute('data-loading') ||
'inline-block'),
() => (targetElt.style.display = 'none')
)
})
})
loadingStateEltsByType['data-loading-class'].forEach(
(sourceElt) => {
const classNames = sourceElt
.getAttribute('data-loading-class')
.split(' ')
getLoadingTarget(sourceElt).forEach((targetElt) => {
queueLoadingState(
sourceElt,
targetElt,
() =>
classNames.forEach((className) =>
targetElt.classList.add(className)
),
() =>
classNames.forEach((className) =>
targetElt.classList.remove(className)
)
)
})
}
)
loadingStateEltsByType['data-loading-class-remove'].forEach(
(sourceElt) => {
const classNames = sourceElt
.getAttribute('data-loading-class-remove')
.split(' ')
getLoadingTarget(sourceElt).forEach((targetElt) => {
queueLoadingState(
sourceElt,
targetElt,
() =>
classNames.forEach((className) =>
targetElt.classList.remove(className)
),
() =>
classNames.forEach((className) =>
targetElt.classList.add(className)
)
)
})
}
)
loadingStateEltsByType['data-loading-disable'].forEach(
(sourceElt) => {
getLoadingTarget(sourceElt).forEach((targetElt) => {
queueLoadingState(
sourceElt,
targetElt,
() => (targetElt.disabled = true),
() => (targetElt.disabled = false)
)
})
}
)
loadingStateEltsByType['data-loading-aria-busy'].forEach(
(sourceElt) => {
getLoadingTarget(sourceElt).forEach((targetElt) => {
queueLoadingState(
sourceElt,
targetElt,
() => (targetElt.setAttribute("aria-busy", "true")),
() => (targetElt.removeAttribute("aria-busy"))
)
})
}
)
}
if (name === 'htmx:beforeOnLoad') {
while (loadingStatesUndoQueue.length > 0) {
loadingStatesUndoQueue.shift()()
}
}
},
})
})()

View File

@@ -0,0 +1,11 @@
htmx.defineExtension('method-override', {
onEvent: function (name, evt) {
if (name === "htmx:configRequest") {
var method = evt.detail.verb;
if (method !== "get" || method !== "post") {
evt.detail.headers['X-HTTP-Method-Override'] = method.toUpperCase();
evt.detail.verb = "post";
}
}
}
});

View File

@@ -0,0 +1,16 @@
htmx.defineExtension('morphdom-swap', {
isInlineSwap: function(swapStyle) {
return swapStyle === 'morphdom';
},
handleSwap: function (swapStyle, target, fragment) {
if (swapStyle === 'morphdom') {
if (fragment.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
morphdom(target, fragment.firstElementChild);
return [target];
} else {
morphdom(target, fragment.outerHTML);
return [target];
}
}
}
});

View File

@@ -0,0 +1,45 @@
(function () {
/** @type {import("../htmx").HtmxInternalApi} */
var api;
htmx.defineExtension('multi-swap', {
init: function (apiRef) {
api = apiRef;
},
isInlineSwap: function (swapStyle) {
return swapStyle.indexOf('multi:') === 0;
},
handleSwap: function (swapStyle, target, fragment, settleInfo) {
if (swapStyle.indexOf('multi:') === 0) {
var selectorToSwapStyle = {};
var elements = swapStyle.replace(/^multi\s*:\s*/, '').split(/\s*,\s*/);
elements.map(function (element) {
var split = element.split(/\s*:\s*/);
var elementSelector = split[0];
var elementSwapStyle = typeof (split[1]) !== "undefined" ? split[1] : "innerHTML";
if (elementSelector.charAt(0) !== '#') {
console.error("HTMX multi-swap: unsupported selector '" + elementSelector + "'. Only ID selectors starting with '#' are supported.");
return;
}
selectorToSwapStyle[elementSelector] = elementSwapStyle;
});
for (var selector in selectorToSwapStyle) {
var swapStyle = selectorToSwapStyle[selector];
var elementToSwap = fragment.querySelector(selector);
if (elementToSwap) {
api.oobSwap(swapStyle, elementToSwap, settleInfo);
} else {
console.warn("HTMX multi-swap: selector '" + selector + "' not found in source content.");
}
}
return true;
}
}
});
})();

View File

@@ -0,0 +1,60 @@
(function(undefined){
'use strict';
// Save a reference to the global object (window in the browser)
var _root = this;
function dependsOn(pathSpec, url) {
if (pathSpec === "ignore") {
return false;
}
var dependencyPath = pathSpec.split("/");
var urlPath = url.split("/");
for (var i = 0; i < urlPath.length; i++) {
var dependencyElement = dependencyPath.shift();
var pathElement = urlPath[i];
if (dependencyElement !== pathElement && dependencyElement !== "*") {
return false;
}
if (dependencyPath.length === 0 || (dependencyPath.length === 1 && dependencyPath[0] === "")) {
return true;
}
}
return false;
}
function refreshPath(path) {
var eltsWithDeps = htmx.findAll("[path-deps]");
for (var i = 0; i < eltsWithDeps.length; i++) {
var elt = eltsWithDeps[i];
if (dependsOn(elt.getAttribute('path-deps'), path)) {
htmx.trigger(elt, "path-deps");
}
}
}
htmx.defineExtension('path-deps', {
onEvent: function (name, evt) {
if (name === "htmx:beforeOnLoad") {
var config = evt.detail.requestConfig;
// mutating call
if (config.verb !== "get" && evt.target.getAttribute('path-deps') !== 'ignore') {
refreshPath(config.path);
}
}
}
});
/**
* ********************
* Expose functionality
* ********************
*/
_root.PathDeps = {
refresh: function(path) {
refreshPath(path);
}
};
}).call(this);

View File

@@ -0,0 +1,144 @@
// This adds the "preload" extension to htmx. By default, this will
// preload the targets of any tags with `href` or `hx-get` attributes
// if they also have a `preload` attribute as well. See documentation
// for more details
htmx.defineExtension("preload", {
onEvent: function(name, event) {
// Only take actions on "htmx:afterProcessNode"
if (name !== "htmx:afterProcessNode") {
return;
}
// SOME HELPER FUNCTIONS WE'LL NEED ALONG THE WAY
// attr gets the closest non-empty value from the attribute.
var attr = function(node, property) {
if (node == undefined) {return undefined;}
return node.getAttribute(property) || node.getAttribute("data-" + property) || attr(node.parentElement, property)
}
// load handles the actual HTTP fetch, and uses htmx.ajax in cases where we're
// preloading an htmx resource (this sends the same HTTP headers as a regular htmx request)
var load = function(node) {
// Called after a successful AJAX request, to mark the
// content as loaded (and prevent additional AJAX calls.)
var done = function(html) {
if (!node.preloadAlways) {
node.preloadState = "DONE"
}
if (attr(node, "preload-images") == "true") {
document.createElement("div").innerHTML = html // create and populate a node to load linked resources, too.
}
}
return function() {
// If this value has already been loaded, then do not try again.
if (node.preloadState !== "READY") {
return;
}
// Special handling for HX-GET - use built-in htmx.ajax function
// so that headers match other htmx requests, then set
// node.preloadState = TRUE so that requests are not duplicated
// in the future
var hxGet = node.getAttribute("hx-get") || node.getAttribute("data-hx-get")
if (hxGet) {
htmx.ajax("GET", hxGet, {handler:function(elt, info) {
done(info.xhr.responseText);
}});
return;
}
// Otherwise, perform a standard xhr request, then set
// node.preloadState = TRUE so that requests are not duplicated
// in the future.
if (node.getAttribute("href")) {
var r = new XMLHttpRequest();
r.open("GET", node.getAttribute("href"));
r.onload = function() {done(r.responseText);};
r.send();
return;
}
}
}
// This function processes a specific node and sets up event handlers.
// We'll search for nodes and use it below.
var init = function(node) {
// If this node DOES NOT include a "GET" transaction, then there's nothing to do here.
if (node.getAttribute("href") + node.getAttribute("hx-get") + node.getAttribute("data-hx-get") == "") {
return;
}
// Guarantee that we only initialize each node once.
if (node.preloadState !== undefined) {
return;
}
// Get event name from config.
var on = attr(node, "preload") || "mousedown"
const always = on.indexOf("always") !== -1
if (always) {
on = on.replace('always', '').trim()
}
// FALL THROUGH to here means we need to add an EventListener
// Apply the listener to the node
node.addEventListener(on, function(evt) {
if (node.preloadState === "PAUSE") { // Only add one event listener
node.preloadState = "READY"; // Requred for the `load` function to trigger
// Special handling for "mouseover" events. Wait 100ms before triggering load.
if (on === "mouseover") {
window.setTimeout(load(node), 100);
} else {
load(node)() // all other events trigger immediately.
}
}
})
// Special handling for certain built-in event handlers
switch (on) {
case "mouseover":
// Mirror `touchstart` events (fires immediately)
node.addEventListener("touchstart", load(node));
// WHhen the mouse leaves, immediately disable the preload
node.addEventListener("mouseout", function(evt) {
if ((evt.target === node) && (node.preloadState === "READY")) {
node.preloadState = "PAUSE";
}
})
break;
case "mousedown":
// Mirror `touchstart` events (fires immediately)
node.addEventListener("touchstart", load(node));
break;
}
// Mark the node as ready to run.
node.preloadState = "PAUSE";
node.preloadAlways = always;
htmx.trigger(node, "preload:init") // This event can be used to load content immediately.
}
// Search for all child nodes that have a "preload" attribute
event.target.querySelectorAll("[preload]").forEach(function(node) {
// Initialize the node with the "preload" attribute
init(node)
// Initialize all child elements that are anchors or have `hx-get` (use with care)
node.querySelectorAll("a,[hx-get],[data-hx-get]").forEach(init)
})
}
})

View File

@@ -0,0 +1,27 @@
(function(){
function maybeRemoveMe(elt) {
var timing = elt.getAttribute("remove-me") || elt.getAttribute("data-remove-me");
if (timing) {
setTimeout(function () {
elt.parentElement.removeChild(elt);
}, htmx.parseInterval(timing));
}
}
htmx.defineExtension('remove-me', {
onEvent: function (name, evt) {
if (name === "htmx:afterProcessNode") {
var elt = evt.detail.elt;
if (elt.getAttribute) {
maybeRemoveMe(elt);
if (elt.querySelectorAll) {
var children = elt.querySelectorAll("[remove-me], [data-remove-me]");
for (var i = 0; i < children.length; i++) {
maybeRemoveMe(children[i]);
}
}
}
}
}
});
})();

View File

@@ -0,0 +1,15 @@
htmx.defineExtension('restored', {
onEvent : function(name, evt) {
if (name === 'htmx:restored'){
var restoredElts = evt.detail.document.querySelectorAll(
"[hx-trigger='restored'],[data-hx-trigger='restored']"
);
// need a better way to do this, would prefer to just trigger from evt.detail.elt
var foundElt = Array.from(restoredElts).find(
(x) => (x.outerHTML === evt.detail.elt.outerHTML)
);
var restoredEvent = evt.detail.triggerEvent(foundElt, 'restored');
}
return;
}
})

View File

@@ -0,0 +1,318 @@
/*
Server Sent Events Extension
============================
This extension adds support for Server Sent Events to htmx. See /www/extensions/sse.md for usage instructions.
*/
(function(){
/** @type {import("../htmx").HtmxInternalApi} */
var api;
htmx.defineExtension("sse", {
/**
* Init saves the provided reference to the internal HTMX API.
*
* @param {import("../htmx").HtmxInternalApi} api
* @returns void
*/
init: function(apiRef) {
// store a reference to the internal API.
api = apiRef;
// set a function in the public API for creating new EventSource objects
if (htmx.createEventSource == undefined) {
htmx.createEventSource = createEventSource;
}
},
/**
* onEvent handles all events passed to this extension.
*
* @param {string} name
* @param {Event} evt
* @returns void
*/
onEvent: function(name, evt) {
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;
// Try to create EventSources when elements are processed
case "htmx:afterProcessNode":
createEventSourceOnElement(evt.target);
}
}
});
///////////////////////////////////////////////
// HELPER FUNCTIONS
///////////////////////////////////////////////
/**
* createEventSource is the default method for creating new EventSource objects.
* it is hoisted into htmx.config.createEventSource to be overridden by the user, if needed.
*
* @param {string} url
* @returns EventSource
*/
function createEventSource(url) {
return new EventSource(url, {withCredentials:true});
}
function splitOnWhitespace(trigger) {
return trigger.trim().split(/\s+/);
}
function getLegacySSEURL(elt) {
var legacySSEValue = api.getAttributeValue(elt, "hx-sse");
if (legacySSEValue) {
var values = splitOnWhitespace(legacySSEValue);
for (var i = 0; i < values.length; i++) {
var value = values[i].split(/:(.+)/);
if (value[0] === "connect") {
return value[1];
}
}
}
}
function getLegacySSESwaps(elt) {
var legacySSEValue = api.getAttributeValue(elt, "hx-sse");
var returnArr = [];
if (legacySSEValue) {
var values = splitOnWhitespace(legacySSEValue);
for (var i = 0; i < values.length; i++) {
var value = values[i].split(/:(.+)/);
if (value[0] === "swap") {
returnArr.push(value[1]);
}
}
}
return returnArr;
}
/**
* 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.
* @param {HTMLElement} elt
* @param {number} retryCount
* @returns {EventSource | null}
*/
function createEventSourceOnElement(elt, retryCount) {
if (elt == null) {
return null;
}
var internalData = api.getInternalData(elt);
// 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);
}
};
// Add message handlers for every `sse-swap` attribute
queryAttributeOnThisOrChildren(elt, "sse-swap").forEach(function(child) {
var sseSwapAttr = api.getAttributeValue(child, "sse-swap");
if (sseSwapAttr) {
var sseEventNames = sseSwapAttr.split(",");
} else {
var sseEventNames = getLegacySSESwaps(child);
}
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);
return;
}
// 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;
source.addEventListener(sseEventName, listener);
}
});
// Add message handlers for every `hx-trigger="sse:*"` attribute
queryAttributeOnThisOrChildren(elt, "hx-trigger").forEach(function(child) {
var sseEventName = api.getAttributeValue(child, "hx-trigger");
if (sseEventName == null) {
return;
}
// Only process hx-triggers for events with the "sse:" prefix
if (sseEventName.slice(0, 4) != "sse:") {
return;
}
var listener = function(event) {
// If parent is missing, then close SSE and remove listener
if (maybeCloseSSESource(elt)) {
source.removeEventListener(sseEventName, listener);
return;
}
// Trigger events to be handled by the rest of htmx
htmx.trigger(child, sseEventName, event);
htmx.trigger(child, "htmx:sseMessage", event);
}
// Register the new listener
api.getInternalData(elt).sseEventListener = listener;
source.addEventListener(sseEventName.slice(4), listener);
});
}
/**
* maybeCloseSSESource confirms that the parent element still exists.
* If not, then any associated SSE source is closed and the function returns true.
*
* @param {HTMLElement} elt
* @returns boolean
*/
function maybeCloseSSESource(elt) {
if (!api.bodyContains(elt)) {
var source = api.getInternalData(elt).sseEventSource;
if (source != undefined) {
source.close();
// source = null
return true;
}
}
return false;
}
/**
* queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT.
*
* @param {HTMLElement} elt
* @param {string} attributeName
*/
function queryAttributeOnThisOrChildren(elt, attributeName) {
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")) {
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) {
result.push(node);
});
return result;
}
/**
* @param {HTMLElement} elt
* @param {string} content
*/
function swap(elt, content) {
api.withExtensions(elt, function(extension) {
content = extension.transformResponse(content, null, elt);
});
var swapSpec = api.getSwapSpecification(elt);
var target = api.getTarget(elt);
var settleInfo = api.makeSettleInfo(elt);
api.selectAndSwap(swapSpec.swapStyle, target, elt, content, settleInfo);
settleInfo.elts.forEach(function (elt) {
if (elt.classList) {
elt.classList.add(htmx.config.settlingClass);
}
api.triggerEvent(elt, 'htmx:beforeSettle');
});
// Handle settle tasks (with delay if requested)
if (swapSpec.settleDelay > 0) {
setTimeout(doSettle(settleInfo), swapSpec.settleDelay);
} else {
doSettle(settleInfo)();
}
}
/**
* doSettle mirrors much of the functionality in htmx that
* settles elements after their content has been swapped.
* TODO: this should be published by htmx, and not duplicated here
* @param {import("../htmx").HtmxSettleInfo} settleInfo
* @returns () => void
*/
function doSettle(settleInfo) {
return function() {
settleInfo.tasks.forEach(function (task) {
task.call();
});
settleInfo.elts.forEach(function (elt) {
if (elt.classList) {
elt.classList.remove(htmx.config.settlingClass);
}
api.triggerEvent(elt, 'htmx:afterSettle');
});
}
}
})();

View File

@@ -0,0 +1,477 @@
/*
WebSockets Extension
============================
This extension adds support for WebSockets to htmx. See /www/extensions/ws.md for usage instructions.
*/
(function () {
/** @type {import("../htmx").HtmxInternalApi} */
var api;
htmx.defineExtension("ws", {
/**
* init is called once, when this extension is first registered.
* @param {import("../htmx").HtmxInternalApi} apiRef
*/
init: function (apiRef) {
// Store reference to internal API
api = apiRef;
// Default function for creating new EventSource objects
if (!htmx.createWebSocket) {
htmx.createWebSocket = createWebSocket;
}
// Default setting for reconnect delay
if (!htmx.config.wsReconnectDelay) {
htmx.config.wsReconnectDelay = "full-jitter";
}
},
/**
* onEvent handles all events passed to this extension.
*
* @param {string} name
* @param {Event} evt
*/
onEvent: function (name, evt) {
switch (name) {
// Try to close the socket when elements are removed
case "htmx:beforeCleanupElement":
var internalData = api.getInternalData(evt.target)
if (internalData.webSocket) {
internalData.webSocket.close();
}
return;
// Try to create websockets when elements are processed
case "htmx:afterProcessNode":
var parent = evt.target;
forEach(queryAttributeOnThisOrChildren(parent, "ws-connect"), function (child) {
ensureWebSocket(child)
});
forEach(queryAttributeOnThisOrChildren(parent, "ws-send"), function (child) {
ensureWebSocketSend(child)
});
}
}
});
function splitOnWhitespace(trigger) {
return trigger.trim().split(/\s+/);
}
function getLegacyWebsocketURL(elt) {
var legacySSEValue = api.getAttributeValue(elt, "hx-ws");
if (legacySSEValue) {
var values = splitOnWhitespace(legacySSEValue);
for (var i = 0; i < values.length; i++) {
var value = values[i].split(/:(.+)/);
if (value[0] === "connect") {
return value[1];
}
}
}
}
/**
* ensureWebSocket creates a new WebSocket on the designated element, using
* the element's "ws-connect" attribute.
* @param {HTMLElement} socketElt
* @returns
*/
function ensureWebSocket(socketElt) {
// If the element containing the WebSocket connection no longer exists, then
// do not connect/reconnect the WebSocket.
if (!api.bodyContains(socketElt)) {
return;
}
// Get the source straight from the element's value
var wssSource = api.getAttributeValue(socketElt, "ws-connect")
if (wssSource == null || wssSource === "") {
var legacySource = getLegacyWebsocketURL(socketElt);
if (legacySource == null) {
return;
} else {
wssSource = legacySource;
}
}
// Guarantee that the wssSource value is a fully qualified URL
if (wssSource.indexOf("/") === 0) {
var base_part = location.hostname + (location.port ? ':' + location.port : '');
if (location.protocol === 'https:') {
wssSource = "wss://" + base_part + wssSource;
} else if (location.protocol === 'http:') {
wssSource = "ws://" + base_part + wssSource;
}
}
var socketWrapper = createWebsocketWrapper(socketElt, function () {
return htmx.createWebSocket(wssSource)
});
socketWrapper.addEventListener('message', function (event) {
if (maybeCloseWebSocketSource(socketElt)) {
return;
}
var response = event.data;
if (!api.triggerEvent(socketElt, "htmx:wsBeforeMessage", {
message: response,
socketWrapper: socketWrapper.publicInterface
})) {
return;
}
api.withExtensions(socketElt, function (extension) {
response = extension.transformResponse(response, null, socketElt);
});
var settleInfo = api.makeSettleInfo(socketElt);
var fragment = api.makeFragment(response);
if (fragment.children.length) {
var children = Array.from(fragment.children);
for (var i = 0; i < children.length; i++) {
api.oobSwap(api.getAttributeValue(children[i], "hx-swap-oob") || "true", children[i], settleInfo);
}
}
api.settleImmediately(settleInfo.tasks);
api.triggerEvent(socketElt, "htmx:wsAfterMessage", { message: response, socketWrapper: socketWrapper.publicInterface })
});
// Put the WebSocket into the HTML Element's custom data.
api.getInternalData(socketElt).webSocket = socketWrapper;
}
/**
* @typedef {Object} WebSocketWrapper
* @property {WebSocket} socket
* @property {Array<{message: string, sendElt: Element}>} messageQueue
* @property {number} retryCount
* @property {(message: string, sendElt: Element) => void} sendImmediately sendImmediately sends message regardless of websocket connection state
* @property {(message: string, sendElt: Element) => void} send
* @property {(event: string, handler: Function) => void} addEventListener
* @property {() => void} handleQueuedMessages
* @property {() => void} init
* @property {() => void} close
*/
/**
*
* @param socketElt
* @param socketFunc
* @returns {WebSocketWrapper}
*/
function createWebsocketWrapper(socketElt, socketFunc) {
var wrapper = {
socket: null,
messageQueue: [],
retryCount: 0,
/** @type {Object<string, Function[]>} */
events: {},
addEventListener: function (event, handler) {
if (this.socket) {
this.socket.addEventListener(event, handler);
}
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(handler);
},
sendImmediately: function (message, sendElt) {
if (!this.socket) {
api.triggerErrorEvent()
}
if (sendElt && api.triggerEvent(sendElt, 'htmx:wsBeforeSend', {
message: message,
socketWrapper: this.publicInterface
})) {
this.socket.send(message);
sendElt && api.triggerEvent(sendElt, 'htmx:wsAfterSend', {
message: message,
socketWrapper: this.publicInterface
})
}
},
send: function (message, sendElt) {
if (this.socket.readyState !== this.socket.OPEN) {
this.messageQueue.push({ message: message, sendElt: sendElt });
} else {
this.sendImmediately(message, sendElt);
}
},
handleQueuedMessages: function () {
while (this.messageQueue.length > 0) {
var queuedItem = this.messageQueue[0]
if (this.socket.readyState === this.socket.OPEN) {
this.sendImmediately(queuedItem.message, queuedItem.sendElt);
this.messageQueue.shift();
} else {
break;
}
}
},
init: function () {
if (this.socket && this.socket.readyState === this.socket.OPEN) {
// Close discarded socket
this.socket.close()
}
// Create a new WebSocket and event handlers
/** @type {WebSocket} */
var socket = socketFunc();
// The event.type detail is added for interface conformance with the
// other two lifecycle events (open and close) so a single handler method
// can handle them polymorphically, if required.
api.triggerEvent(socketElt, "htmx:wsConnecting", { event: { type: 'connecting' } });
this.socket = socket;
socket.onopen = function (e) {
wrapper.retryCount = 0;
api.triggerEvent(socketElt, "htmx:wsOpen", { event: e, socketWrapper: wrapper.publicInterface });
wrapper.handleQueuedMessages();
}
socket.onclose = function (e) {
// If socket should not be connected, stop further attempts to establish connection
// If Abnormal Closure/Service Restart/Try Again Later, then set a timer to reconnect after a pause.
if (!maybeCloseWebSocketSource(socketElt) && [1006, 1012, 1013].indexOf(e.code) >= 0) {
var delay = getWebSocketReconnectDelay(wrapper.retryCount);
setTimeout(function () {
wrapper.retryCount += 1;
wrapper.init();
}, delay);
}
// Notify client code that connection has been closed. Client code can inspect `event` field
// to determine whether closure has been valid or abnormal
api.triggerEvent(socketElt, "htmx:wsClose", { event: e, socketWrapper: wrapper.publicInterface })
};
socket.onerror = function (e) {
api.triggerErrorEvent(socketElt, "htmx:wsError", { error: e, socketWrapper: wrapper });
maybeCloseWebSocketSource(socketElt);
};
var events = this.events;
Object.keys(events).forEach(function (k) {
events[k].forEach(function (e) {
socket.addEventListener(k, e);
})
});
},
close: function () {
this.socket.close()
}
}
wrapper.init();
wrapper.publicInterface = {
send: wrapper.send.bind(wrapper),
sendImmediately: wrapper.sendImmediately.bind(wrapper),
queue: wrapper.messageQueue
};
return wrapper;
}
/**
* ensureWebSocketSend attaches trigger handles to elements with
* "ws-send" attribute
* @param {HTMLElement} elt
*/
function ensureWebSocketSend(elt) {
var legacyAttribute = api.getAttributeValue(elt, "hx-ws");
if (legacyAttribute && legacyAttribute !== 'send') {
return;
}
var webSocketParent = api.getClosestMatch(elt, hasWebSocket)
processWebSocketSend(webSocketParent, elt);
}
/**
* hasWebSocket function checks if a node has webSocket instance attached
* @param {HTMLElement} node
* @returns {boolean}
*/
function hasWebSocket(node) {
return api.getInternalData(node).webSocket != null;
}
/**
* processWebSocketSend adds event listeners to the <form> element so that
* messages can be sent to the WebSocket server when the form is submitted.
* @param {HTMLElement} socketElt
* @param {HTMLElement} sendElt
*/
function processWebSocketSend(socketElt, sendElt) {
var nodeData = api.getInternalData(sendElt);
var triggerSpecs = api.getTriggerSpecs(sendElt);
triggerSpecs.forEach(function (ts) {
api.addTriggerHandler(sendElt, ts, nodeData, function (elt, evt) {
if (maybeCloseWebSocketSource(socketElt)) {
return;
}
/** @type {WebSocketWrapper} */
var socketWrapper = api.getInternalData(socketElt).webSocket;
var headers = api.getHeaders(sendElt, socketElt);
var results = api.getInputValues(sendElt, 'post');
var errors = results.errors;
var rawParameters = results.values;
var expressionVars = api.getExpressionVars(sendElt);
var allParameters = api.mergeObjects(rawParameters, expressionVars);
var filteredParameters = api.filterValues(allParameters, sendElt);
var sendConfig = {
parameters: filteredParameters,
unfilteredParameters: allParameters,
headers: headers,
errors: errors,
triggeringEvent: evt,
messageBody: undefined,
socketWrapper: socketWrapper.publicInterface
};
if (!api.triggerEvent(elt, 'htmx:wsConfigSend', sendConfig)) {
return;
}
if (errors && errors.length > 0) {
api.triggerEvent(elt, 'htmx:validation:halted', errors);
return;
}
var body = sendConfig.messageBody;
if (body === undefined) {
var toSend = Object.assign({}, sendConfig.parameters);
if (sendConfig.headers)
toSend['HEADERS'] = headers;
body = JSON.stringify(toSend);
}
socketWrapper.send(body, elt);
if (api.shouldCancel(evt, elt)) {
evt.preventDefault();
}
});
});
}
/**
* getWebSocketReconnectDelay is the default easing function for WebSocket reconnects.
* @param {number} retryCount // The number of retries that have already taken place
* @returns {number}
*/
function getWebSocketReconnectDelay(retryCount) {
/** @type {"full-jitter" | ((retryCount:number) => number)} */
var delay = htmx.config.wsReconnectDelay;
if (typeof delay === 'function') {
return delay(retryCount);
}
if (delay === 'full-jitter') {
var exp = Math.min(retryCount, 6);
var maxDelay = 1000 * Math.pow(2, exp);
return maxDelay * Math.random();
}
logError('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"');
}
/**
* maybeCloseWebSocketSource checks to the if the element that created the WebSocket
* still exists in the DOM. If NOT, then the WebSocket is closed and this function
* returns TRUE. If the element DOES EXIST, then no action is taken, and this function
* returns FALSE.
*
* @param {*} elt
* @returns
*/
function maybeCloseWebSocketSource(elt) {
if (!api.bodyContains(elt)) {
api.getInternalData(elt).webSocket.close();
return true;
}
return false;
}
/**
* createWebSocket is the default method for creating new WebSocket objects.
* it is hoisted into htmx.createWebSocket to be overridden by the user, if needed.
*
* @param {string} url
* @returns WebSocket
*/
function createWebSocket(url) {
var sock = new WebSocket(url, []);
sock.binaryType = htmx.config.wsBinaryType;
return sock;
}
/**
* queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT.
*
* @param {HTMLElement} elt
* @param {string} attributeName
*/
function queryAttributeOnThisOrChildren(elt, attributeName) {
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-ws")) {
result.push(elt);
}
// Search all child nodes that match the requested attribute
elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "], [data-hx-ws], [hx-ws]").forEach(function (node) {
result.push(node)
})
return result
}
/**
* @template T
* @param {T[]} arr
* @param {(T) => void} func
*/
function forEach(arr, func) {
if (arr) {
for (var i = 0; i < arr.length; i++) {
func(arr[i]);
}
}
}
})();

339
www-zola/static/test/1.8.6/src/htmx.d.ts vendored Normal file
View File

@@ -0,0 +1,339 @@
// https://htmx.org/reference/#api
/**
* This method adds a class to the given element.
*
* https://htmx.org/api/#addClass
*
* @param elt the element to add the class to
* @param clazz the class to add
* @param delay the delay (in milliseconds before class is added)
*/
export function addClass(elt: Element, clazz: string, delay?: number): void;
/**
* Issues an htmx-style AJAX request
*
* https://htmx.org/api/#ajax
*
* @param verb 'GET', 'POST', etc.
* @param path the URL path to make the AJAX
* @param element the element to target (defaults to the **body**)
*/
export function ajax(verb: string, path: string, element: Element): void;
/**
* Issues an htmx-style AJAX request
*
* https://htmx.org/api/#ajax
*
* @param verb 'GET', 'POST', etc.
* @param path the URL path to make the AJAX
* @param selector a selector for the target
*/
export function ajax(verb: string, path: string, selector: string): void;
/**
* Issues an htmx-style AJAX request
*
* https://htmx.org/api/#ajax
*
* @param verb 'GET', 'POST', etc.
* @param path the URL path to make the AJAX
* @param context a context object that contains any of the following
*/
export function ajax(
verb: string,
path: string,
context: Partial<{ source: any; event: any; handler: any; target: any; values: any; headers: any }>
): void;
/**
* Finds the closest matching element in the given elements parentage, inclusive of the element
*
* https://htmx.org/api/#closest
*
* @param elt the element to find the selector from
* @param selector the selector to find
*/
export function closest(elt: Element, selector: string): Element | null;
/**
* A property holding the configuration htmx uses at runtime.
*
* Note that using a [meta tag](https://htmx.org/docs/#config) is the preferred mechanism for setting these properties.
*
* https://htmx.org/api/#config
*/
export var config: HtmxConfig;
/**
* A property used to create new [Server Sent Event](https://htmx.org/docs/#sse) sources. This can be updated to provide custom SSE setup.
*
* https://htmx.org/api/#createEventSource
*/
export var createEventSource: (url: string) => EventSource;
/**
* A property used to create new [WebSocket](https://htmx.org/docs/#websockets). This can be updated to provide custom WebSocket setup.
*
* https://htmx.org/api/#createWebSocket
*/
export var createWebSocket: (url: string) => WebSocket;
/**
* Defines a new htmx [extension](https://htmx.org/extensions).
*
* https://htmx.org/api/#defineExtension
*
* @param name the extension name
* @param ext the extension definition
*/
export function defineExtension(name: string, ext: HtmxExtension): void;
/**
* Finds an element matching the selector
*
* https://htmx.org/api/#find
*
* @param selector the selector to match
*/
export function find(selector: string): Element | null;
/**
* Finds an element matching the selector
*
* https://htmx.org/api/#find
*
* @param elt the root element to find the matching element in, inclusive
* @param selector the selector to match
*/
export function find(elt: Element, selector: string): Element | null;
/**
* Finds all elements matching the selector
*
* https://htmx.org/api/#findAll
*
* @param selector the selector to match
*/
export function findAll(selector: string): NodeListOf<Element>;
/**
* Finds all elements matching the selector
*
* https://htmx.org/api/#findAll
*
* @param elt the root element to find the matching elements in, inclusive
* @param selector the selector to match
*/
export function findAll(elt: Element, selector: string): NodeListOf<Element>;
/**
* Log all htmx events, useful for debugging.
*
* https://htmx.org/api/#logAll
*/
export function logAll(): void;
/**
* The logger htmx uses to log with
*
* https://htmx.org/api/#logger
*/
export var logger: (elt: Element, eventName: string, detail: any) => void | null;
/**
* Removes an event listener from an element
*
* https://htmx.org/api/#off
*
* @param eventName the event name to remove the listener from
* @param listener the listener to remove
*/
export function off(eventName: string, listener: (evt: Event) => void): (evt: Event) => void;
/**
* Removes an event listener from an element
*
* https://htmx.org/api/#off
*
* @param target the element to remove the listener from
* @param eventName the event name to remove the listener from
* @param listener the listener to remove
*/
export function off(target: string, eventName: string, listener: (evt: Event) => void): (evt: Event) => void;
/**
* Adds an event listener to an element
*
* https://htmx.org/api/#on
*
* @param eventName the event name to add the listener for
* @param listener the listener to add
*/
export function on(eventName: string, listener: (evt: Event) => void): (evt: Event) => void;
/**
* Adds an event listener to an element
*
* https://htmx.org/api/#on
*
* @param target the element to add the listener to
* @param eventName the event name to add the listener for
* @param listener the listener to add
*/
export function on(target: string, eventName: string, listener: (evt: Event) => void): (evt: Event) => void;
/**
* Adds a callback for the **htmx:load** event. This can be used to process new content, for example initializing the content with a javascript library
*
* https://htmx.org/api/#onLoad
*
* @param callback the callback to call on newly loaded content
*/
export function onLoad(callback: (element: Element) => void): void;
/**
* Parses an interval string consistent with the way htmx does. Useful for plugins that have timing-related attributes.
*
* Caution: Accepts an int followed by either **s** or **ms**. All other values use **parseFloat**
*
* https://htmx.org/api/#parseInterval
*
* @param str timing string
*/
export function parseInterval(str: string): number;
/**
* Processes new content, enabling htmx behavior. This can be useful if you have content that is added to the DOM outside of the normal htmx request cycle but still want htmx attributes to work.
*
* https://htmx.org/api/#process
*
* @param element element to process
*/
export function process(element: Element): void;
/**
* Removes an element from the DOM
*
* https://htmx.org/api/#remove
*
* @param elt element to remove
* @param delay the delay (in milliseconds before element is removed)
*/
export function remove(elt: Element, delay?: number): void;
/**
* Removes a class from the given element
*
* https://htmx.org/api/#removeClass
*
* @param elt element to remove the class from
* @param clazz the class to remove
* @param delay the delay (in milliseconds before class is removed)
*/
export function removeClass(elt: Element, clazz: string, delay?: number): void;
/**
* Removes the given extension from htmx
*
* https://htmx.org/api/#removeExtension
*
* @param name the name of the extension to remove
*/
export function removeExtension(name: string): void;
/**
* Takes the given class from its siblings, so that among its siblings, only the given element will have the class.
*
* https://htmx.org/api/#takeClass
*
* @param elt the element that will take the class
* @param clazz the class to take
*/
export function takeClass(elt: Element, clazz: string): void;
/**
* Toggles the given class on an element
*
* https://htmx.org/api/#toggleClass
*
* @param elt the element to toggle the class on
* @param clazz the class to toggle
*/
export function toggleClass(elt: Element, clazz: string): void;
/**
* Triggers a given event on an element
*
* https://htmx.org/api/#trigger
*
* @param elt the element to trigger the event on
* @param name the name of the event to trigger
* @param detail details for the event
*/
export function trigger(elt: Element, name: string, detail: any): void;
/**
* Returns the input values that would resolve for a given element via the htmx value resolution mechanism
*
* https://htmx.org/api/#values
*
* @param elt the element to resolve values on
* @param requestType the request type (e.g. **get** or **post**) non-GET's will include the enclosing form of the element. Defaults to **post**
*/
export function values(elt: Element, requestType?: string): any;
export const version: string;
export interface HtmxConfig {
/** array of strings: the attributes to settle during the settling phase */
attributesToSettle?: ["class", "style", "width", "height"] | string[];
/** the default delay between completing the content swap and settling attributes */
defaultSettleDelay?: number;
/** the default delay between receiving a response from the server and doing the swap */
defaultSwapDelay?: number;
/** the default swap style to use if **[hx-swap](https://htmx.org/attributes/hx-swap)** is omitted */
defaultSwapStyle?: "innerHTML" | string;
/** the number of pages to keep in **localStorage** for history support */
historyCacheSize?: number;
/** whether or not to use history */
historyEnabled?: boolean;
/** if true, htmx will inject a small amount of CSS into the page to make indicators invisible unless the **htmx-indicator** class is present */
includeIndicatorStyles?: boolean;
/** the class to place on indicators when a request is in flight */
indicatorClass?: "htmx-indicator" | string;
/** the class to place on triggering elements when a request is in flight */
requestClass?: "htmx-request" | string;
/** the class to temporarily place on elements that htmx has added to the DOM */
addedClass?: "htmx-added" | string;
/** the class to place on target elements when htmx is in the settling phase */
settlingClass?: "htmx-settling" | string;
/** the class to place on target elements when htmx is in the swapping phase */
swappingClass?: "htmx-swapping" | string;
/** allows the use of eval-like functionality in htmx, to enable **hx-vars**, trigger conditions & script tag evaluation. Can be set to **false** for CSP compatibility */
allowEval?: boolean;
/** use HTML template tags for parsing content from the server. This allows you to use Out of Band content when returning things like table rows, but it is *not* IE11 compatible. */
useTemplateFragments?: boolean;
/** allow cross-site Access-Control requests using credentials such as cookies, authorization headers or TLS client certificates */
withCredentials?: boolean;
/** the default implementation of **getWebSocketReconnectDelay** for reconnecting after unexpected connection loss by the event code **Abnormal Closure**, **Service Restart** or **Try Again Later** */
wsReconnectDelay?: "full-jitter" | string | ((retryCount: number) => number);
// following don't appear in the docs
refreshOnHistoryMiss?: boolean;
timeout?: number;
disableSelector?: "[hx-disable], [data-hx-disable]" | string;
scrollBehavior?: "smooth";
}
/**
* https://htmx.org/extensions/#defining
*/
export interface HtmxExtension {
onEvent?: (name: string, evt: CustomEvent) => any;
transformResponse?: (text: any, xhr: XMLHttpRequest, elt: any) => any;
isInlineSwap?: (swapStyle: any) => any;
handleSwap?: (swapStyle: any, target: any, fragment: any, settleInfo: any) => any;
encodeParameters?: (xhr: XMLHttpRequest, parameters: any, elt: any) => any;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,96 @@
import htmx from "./htmx";
// add the class 'myClass' to the element with the id 'demo'
htmx.addClass(htmx.find("#demo"), "myClass");
// issue a GET to /example and put the response HTML into #myDiv
htmx.ajax("GET", "/example", "#myDiv");
// find the closest enclosing div of the element with the id 'demo'
htmx.closest(htmx.find("#demo"), "div");
// update the history cache size to 30
htmx.config.historyCacheSize = 30;
// override SSE event sources to not use credentials
htmx.createEventSource = function (url) {
return new EventSource(url, { withCredentials: false });
};
// override WebSocket to use a specific protocol
htmx.createWebSocket = function (url) {
return new WebSocket(url, ["wss"]);
};
// defines a silly extension that just logs the name of all events triggered
htmx.defineExtension("silly", {
onEvent: function (name, evt) {
console.log("Event " + name + " was triggered!");
}
});
// find div with id my-div
var div = htmx.find("#my-div");
// find div with id another-div within that div
var anotherDiv = htmx.find(div, "#another-div");
// find all divs
var allDivs = htmx.findAll("div");
// find all paragraphs within a given div
var allParagraphsInMyDiv = htmx.findAll(htmx.find("#my-div"), "p");
htmx.logAll();
// remove this click listener from the body
htmx.off("click", myEventListener);
// remove this click listener from the given div
htmx.off("#my-div", "click", myEventListener);
// add a click listener to the body
var myEventListener = htmx.on("click", function (evt) {
console.log(evt);
});
// add a click listener to the given div
var myEventListener = htmx.on("#my-div", "click", function (evt) {
console.log(evt);
});
const MyLibrary: any = null;
htmx.onLoad(function (elt) {
MyLibrary.init(elt);
});
// returns 3000
var milliseconds = htmx.parseInterval("3s");
// returns 3 - Caution
var milliseconds = htmx.parseInterval("3m");
document.body.innerHTML = "<div hx-get='/example'>Get it!</div>";
// process the newly added content
htmx.process(document.body);
// removes my-div from the DOM
htmx.remove(htmx.find("#my-div"));
// removes .myClass from my-div
htmx.removeClass(htmx.find("#my-div"), "myClass");
htmx.removeExtension("my-extension");
// takes the selected class from tab2"s siblings
htmx.takeClass(htmx.find("#tab2"), "selected");
// toggles the selected class on tab2
htmx.toggleClass(htmx.find("#tab2"), "selected");
// triggers the myEvent event on #tab2 with the answer 42
htmx.trigger(htmx.find("#tab2"), "myEvent", { answer: 42 });
// gets the values associated with this form
var values = htmx.values(htmx.find("#myForm"));

View File

@@ -0,0 +1,93 @@
describe("hx-boost attribute", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('handles basic anchor properly', function () {
this.server.respondWith("GET", "/test", "Boosted");
var div = make('<div hx-target="this" hx-boost="true"><a id="a1" href="/test">Foo</a></div>');
var a = byId('a1');
a.click();
this.server.respond();
div.innerHTML.should.equal("Boosted");
})
it('handles basic form post properly', function () {
this.server.respondWith("POST", "/test", "Boosted");
this.server.respondWith("POST", "/test", "Boosted");
var div = make('<div hx-target="this" hx-boost="true"><form id="f1" action="/test" method="post"><button id="b1">Submit</button></form></div>');
var btn = byId('b1');
btn.click();
this.server.respond();
div.innerHTML.should.equal("Boosted");
})
it('handles basic form get properly', function () {
this.server.respondWith("GET", "/test", "Boosted");
var div = make('<div hx-target="this" hx-boost="true"><form id="f1" action="/test" method="get"><button id="b1">Submit</button></form></div>');
var btn = byId('b1');
btn.click();
this.server.respond();
div.innerHTML.should.equal("Boosted");
})
it('handles basic form with no explicit method property', function () {
this.server.respondWith("GET", "/test", "Boosted");
var div = make('<div hx-target="this" hx-boost="true"><form id="f1" action="/test"><button id="b1">Submit</button></form></div>');
var btn = byId('b1');
btn.click();
this.server.respond();
div.innerHTML.should.equal("Boosted");
})
it('handles basic anchor properly w/ data-* prefix', function () {
this.server.respondWith("GET", "/test", "Boosted");
var div = make('<div data-hx-target="this" data-hx-boost="true"><a id="a1" href="/test">Foo</a></div>');
var a = byId('a1');
a.click();
this.server.respond();
div.innerHTML.should.equal("Boosted");
})
it('overriding default swap style does not effect boosting', function () {
htmx.config.defaultSwapStyle = "afterend";
try {
this.server.respondWith("GET", "/test", "Boosted");
var a = make('<a hx-target="this" hx-boost="true" id="a1" href="/test">Foo</a>');
a.click();
this.server.respond();
a.innerHTML.should.equal("Boosted");
} finally {
htmx.config.defaultSwapStyle = "innerHTML";
}
})
it('anchors w/ explicit targets are not boosted', function () {
var a = make('<a hx-target="this" hx-boost="true" id="a1" href="/test" target="_blank">Foo</a>');
var internalData = htmx._("getInternalData")(a);
should.equal(undefined, internalData.boosted);
})
it('includes an HX-Boosted Header', function()
{
this.server.respondWith("GET", "/test", function(xhr){
should.equal(xhr.requestHeaders['HX-Boosted'], "true");
xhr.respond(200, {}, "Boosted!");
});
var btn = make('<a hx-boost="true" hx-target="this" href="/test">Click Me!</a>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Boosted!");
});
});

View File

@@ -0,0 +1,34 @@
describe("hx-delete attribute", function(){
beforeEach(function() {
this.server = makeServer();
clearWorkArea();
});
afterEach(function() {
this.server.restore();
clearWorkArea();
});
it('issues a DELETE request', function()
{
this.server.respondWith("DELETE", "/test", function(xhr){
xhr.respond(200, {}, "Deleted!");
});
var btn = make('<button hx-delete="/test">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Deleted!");
});
it('issues a DELETE request w/ data-* prefix', function()
{
this.server.respondWith("DELETE", "/test", function(xhr){
xhr.respond(200, {}, "Deleted!");
});
var btn = make('<button data-hx-delete="/test">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Deleted!");
});
})

View File

@@ -0,0 +1,142 @@
describe("hx-disinherit attribute", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('basic inheritance sanity-check', function () {
var response_inner = '<div id="snowflake" class="">Hello world</div>'
var response = '<div id="unique" class="">' + response_inner + '</div>'
this.server.respondWith("GET", "/test", response);
var div = make('<div hx-select="#snowflake" hx-target="#cta" hx-swap="outerHTML"><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_inner);
})
it('disinherit exclude single attribute', function () {
var response_inner = '<div id="snowflake" class="">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>');
});
it('disinherit exclude multiple attributes', function () {
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 hx-swap">' +
' <button id="bx1" hx-get="/test"><span id="cta">Click Me!</span></button>' +
'</div>')
var btn = byId("bx1");
btn.click();
this.server.respond();
console.log(btn.innerHTML);
console.log(response);
btn.innerHTML.should.equal('<span id="cta" class="">' + response + '</span>');
});
it('disinherit exclude all attributes', function () {
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="*">' +
' <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);
});
it('same-element inheritance disable', function () {
var response_inner = '<div id="snowflake" class="">Hello world</div>'
var response = '<div id="unique">' + response_inner + '</div>'
this.server.respondWith("GET", "/test", response);
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);
});
it('same-element inheritance disable with child nodes', function () {
var response_inner = '<div id="snowflake" class="">Hello world</div>'
var response = '<div id="unique">' + response_inner + '</div>'
this.server.respondWith("GET", "/test", response);
this.server.respondWith("GET", "/test2", 'unique-snowflake');
var div = make('<div hx-select="#snowflake" hx-target="#container" hx-get="/test" hx-swap="outerHTML" hx-trigger="keyup" hx-disinherit="*"><div id="container"><button id="bx1" hx-get="/test2" hx-trigger="click" hx-target="#target"><div id="target"></div></button></div></div>')
var btn = byId("bx1");
btn.click();
this.server.respond();
btn.innerHTML.should.equal('<div id="target" class="">unique-snowflake</div>');
var count = (div.parentElement.innerHTML.match(/snowflake/g) || []).length;
count.should.equal(2); // hx-select of parent div and newly loaded inner content
});
it('boosted element hx-disinherit sanity check', function () {
try {
var request;
var handler = htmx.on("htmx:beforeRequest", function (evt) {
request = evt;
});
var div = make('<div hx-boost="true" hx-disinherit="false"><a id="a1" href="/test">Click me</a></div>');
var link = byId("a1");
link.click();
should.equal(request.detail.requestConfig.path, '/test');
should.equal(request.detail.elt["htmx-internal-data"].boosted, true);
} finally {
htmx.off("htmx:beforeRequest", handler);
}
});
it('boosted element inheritance manual unset', function () {
try {
var request;
var handler = htmx.on("htmx:beforeRequest", function (evt) {
request = evt;
});
var div = make('<div hx-boost="true" hx-get="/test"><div hx-boost="unset"><a id="a1" href="/test">Click me</a></div></div>');
var link = byId("a1");
should.equal(link["htmx-internal-data"].boosted, undefined);
} finally {
htmx.off("htmx:beforeRequest", handler);
}
});
it('nested htmx-node with boosting parent', function () {
try {
var request;
var handler = htmx.on("htmx:beforeRequest", function (evt) {
request = evt;
});
var div = make('<div hx-boost="true" hx-target="#test" hx-disinherit="*"><div id="test"></div><a id="a1" href="/test" hx-get="/test2">Click me</a></div>');
var link = byId("a1");
link.click();
should.equal(request.detail.requestConfig.path, '/test2');
should.equal(request.detail.elt["htmx-internal-data"].boosted, undefined);
should.equal(request.detail.target.id, "a1");
} finally {
htmx.off("htmx:beforeRequest", handler);
}
});
});

View File

@@ -0,0 +1,138 @@
describe("hx-ext attribute", function() {
var ext1Calls, ext2Calls, ext3Calls, ext4Calls;
beforeEach(function () {
ext1Calls = ext2Calls = ext3Calls = ext4Calls = 0;
this.server = makeServer();
clearWorkArea();
htmx.defineExtension("ext-1", {
onEvent : function(name, evt) {
if(name === "htmx:afterRequest"){
ext1Calls++;
}
}
});
htmx.defineExtension("ext-2", {
onEvent : function(name, evt) {
if(name === "htmx:afterRequest"){
ext2Calls++;
}
}
});
htmx.defineExtension("ext-3", {
onEvent : function(name, evt) {
if(name === "htmx:afterRequest"){
ext3Calls++;
}
}
});
htmx.defineExtension("ext-4", {
onEvent : function(name, evt) {
if(name === "namespace:example"){
ext4Calls++;
}
}
});
});
afterEach(function () {
this.server.restore();
clearWorkArea();
htmx.removeExtension("ext-1");
htmx.removeExtension("ext-2");
htmx.removeExtension("ext-3");
});
it('A simple extension is invoked properly', function () {
this.server.respondWith("GET", "/test", "Clicked!");
var btn = make('<button hx-get="/test" hx-ext="ext-1">Click Me!</button>')
btn.click();
this.server.respond();
ext1Calls.should.equal(1);
ext2Calls.should.equal(0);
ext3Calls.should.equal(0);
});
it('Extensions are merged properly', function () {
this.server.respondWith("GET", "/test", "Clicked!");
make('<div hx-ext="ext-1"><button id="btn-1" hx-get="/test" hx-ext="ext-2">Click Me!</button>' +
'<button id="btn-2" hx-get="/test" hx-ext="ext-3">Click Me!</button></div>')
var btn1 = byId("btn-1");
var btn2 = byId("btn-2");
btn1.click();
this.server.respond();
ext1Calls.should.equal(1);
ext2Calls.should.equal(1);
ext3Calls.should.equal(0);
btn2.click();
this.server.respond();
ext1Calls.should.equal(2);
ext2Calls.should.equal(1);
ext3Calls.should.equal(1);
});
it('supports comma separated lists', function () {
this.server.respondWith("GET", "/test", "Clicked!");
make('<div hx-ext="ext-1"><button id="btn-1" hx-get="/test" hx-ext="ext-2, ext-3 ">Click Me!</button></div>')
var btn1 = byId("btn-1");
var btn2 = byId("btn-2");
btn1.click();
this.server.respond();
ext1Calls.should.equal(1);
ext2Calls.should.equal(1);
ext3Calls.should.equal(1);
});
it('A simple extension is invoked properly w/ data-* prefix', function () {
this.server.respondWith("GET", "/test", "Clicked!");
var btn = make('<button data-hx-get="/test" data-hx-ext="ext-1">Click Me!</button>')
btn.click();
this.server.respond();
ext1Calls.should.equal(1);
ext2Calls.should.equal(0);
ext3Calls.should.equal(0);
});
it('A simple extension is invoked properly when an HX-Trigger event w/ a namespace fires', function () {
this.server.respondWith("GET", "/test", [200, {"HX-Trigger":"namespace:example"}, ""]);
var btn = make('<button data-hx-get="/test" data-hx-ext="ext-4">Click Me!</button>')
btn.click();
this.server.respond();
ext1Calls.should.equal(0);
ext2Calls.should.equal(0);
ext3Calls.should.equal(0);
ext4Calls.should.equal(1);
});
it('Extensions are ignored properly', function () {
this.server.respondWith("GET", "/test", "Clicked!");
make('<div id="div-AA" hx-ext="ext-1, ext-2"><button id="btn-AA" hx-get="/test">Click Me!</button>' +
'<div id="div-BB" hx-ext="ignore:ext-1"><button id="btn-BB" hx-get="/test"></div></div>')
var btn1 = byId("btn-AA");
var btn2 = byId("btn-BB");
btn1.click();
this.server.respond();
ext1Calls.should.equal(1);
ext2Calls.should.equal(1);
ext3Calls.should.equal(0);
btn2.click();
this.server.respond();
ext1Calls.should.equal(1);
ext2Calls.should.equal(2);
ext3Calls.should.equal(0);
})
});

View File

@@ -0,0 +1,129 @@
describe("hx-get attribute", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('issues a GET request on click and swaps content', function () {
this.server.respondWith("GET", "/test", "Clicked!");
var btn = make('<button hx-get="/test">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Clicked!");
});
it('GET does not include surrounding data by default', function () {
this.server.respondWith("GET", "/test", function (xhr) {
should.equal(getParameters(xhr)["i1"], undefined);
xhr.respond(200, {}, "Clicked!");
});
make('<form><input name="i1" value="value"/><button id="b1" hx-get="/test">Click Me!</button></form>')
var btn = byId("b1");
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Clicked!");
});
it('GET on form includes its own data by default', function () {
this.server.respondWith("GET", /\/test.*/, function (xhr) {
getParameters(xhr)["i1"].should.equal("value");
xhr.respond(200, {}, "Clicked!");
});
var form = make('<form hx-trigger="click" hx-get="/test"><input name="i1" value="value"/><button id="b1">Click Me!</button></form>');
form.click();
this.server.respond();
form.innerHTML.should.equal("Clicked!");
});
it('GET on form with existing parameters works properly', function () {
this.server.respondWith("GET", /\/test.*/, function (xhr) {
getParameters(xhr)["foo"].should.equal("bar");
getParameters(xhr)["i1"].should.equal("value");
xhr.respond(200, {}, "Clicked!");
});
var form = make('<form hx-trigger="click" hx-get="/test?foo=bar"><input name="i1" value="value"/><button id="b1">Click Me!</button></form>');
form.click();
this.server.respond();
form.innerHTML.should.equal("Clicked!");
});
it('GET on form with anchor works properly', function () {
this.server.respondWith("GET", /\/test.*/, function (xhr) {
getParameters(xhr)["foo"].should.equal("bar");
getParameters(xhr)["i1"].should.equal("value");
xhr.respond(200, {}, "Clicked!");
});
var form = make('<form hx-trigger="click" hx-get="/test?foo=bar#foo"><input name="i1" value="value"/><button id="b1">Click Me!</button></form>');
form.click();
this.server.respond();
form.innerHTML.should.equal("Clicked!");
});
it('issues a GET request on click and swaps content w/ data-* prefix', function () {
this.server.respondWith("GET", "/test", "Clicked!");
var btn = make('<button data-hx-get="/test">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Clicked!");
});
it('does not include a cache-busting parameter when not enabled', function () {
this.server.respondWith("GET", /\/test.*/, function (xhr) {
should.not.exist(getParameters(xhr)["org.htmx.cache-buster"]);
xhr.respond(200, {}, "Clicked!");
});
try {
htmx.config.getCacheBusterParam = false;
var btn = make('<button hx-get="/test">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Clicked!");
} finally {
htmx.config.getCacheBusterParam = false;
}
});
it('includes a cache-busting parameter when enabled w/ value "true" if no id on target', function () {
this.server.respondWith("GET", /\/test.*/, function (xhr) {
getParameters(xhr)["org.htmx.cache-buster"].should.equal("true");
xhr.respond(200, {}, "Clicked!");
});
try {
htmx.config.getCacheBusterParam = true;
var btn = make('<button hx-get="/test">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Clicked!");
} finally {
htmx.config.getCacheBusterParam = false;
}
});
it('includes a cache-busting parameter when enabled w/ the id of the target if there is one', function () {
this.server.respondWith("GET", /\/test.*/, function (xhr) {
getParameters(xhr)["org.htmx.cache-buster"].should.equal("foo");
xhr.respond(200, {}, "Clicked!");
});
try {
htmx.config.getCacheBusterParam = true;
var btn = make('<button hx-get="/test" id="foo">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Clicked!");
} finally {
htmx.config.getCacheBusterParam = false;
}
});
});

View File

@@ -0,0 +1,164 @@
describe("hx-headers attribute", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('basic hx-headers works', function () {
this.server.respondWith("POST", "/vars", function (xhr) {
xhr.requestHeaders['i1'].should.equal("test");
xhr.respond(200, {}, "Clicked!")
});
var div = make("<div hx-post='/vars' hx-headers='\"i1\":\"test\"'></div>")
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('basic hx-headers works with braces', function () {
this.server.respondWith("POST", "/vars", function (xhr) {
xhr.requestHeaders['i1'].should.equal("test");
xhr.respond(200, {}, "Clicked!")
});
var div = make("<div hx-post='/vars' hx-headers='{\"i1\":\"test\"}'></div>")
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('multiple hx-headers works', function () {
this.server.respondWith("POST", "/vars", function (xhr) {
xhr.requestHeaders['v1'].should.equal("test");
xhr.requestHeaders['v2'].should.equal("42");
xhr.respond(200, {}, "Clicked!")
});
var div = make("<div hx-post='/vars' hx-headers='\"v1\":\"test\", \"v2\":42'></div>")
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('hx-headers can be on parents', function () {
this.server.respondWith("POST", "/vars", function (xhr) {
xhr.requestHeaders['i1'].should.equal("test");
xhr.respond(200, {}, "Clicked!")
});
make("<div hx-headers='\"i1\":\"test\"'><div id='d1' hx-post='/vars'></div></div>");
var div = byId("d1");
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('hx-headers can override parents', function () {
this.server.respondWith("POST", "/vars", function (xhr) {
xhr.requestHeaders['i1'].should.equal("best");
xhr.respond(200, {}, "Clicked!")
});
make("<div hx-headers='\"i1\":\"test\"'><div id='d1' hx-headers='\"i1\":\"best\"' hx-post='/vars'></div></div>");
var div = byId("d1");
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('hx-headers overrides inputs', function () {
this.server.respondWith("POST", "/include", function (xhr) {
xhr.requestHeaders['i1'].should.equal("best");
xhr.respond(200, {}, "Clicked!")
});
var div = make("<div hx-target='this'><input hx-post='/include' hx-headers='\"i1\":\"best\"' hx-trigger='click' id='i1' name='i1' value='test'/></div>")
var input = byId("i1")
input.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('basic hx-headers javascript: works', function () {
this.server.respondWith("POST", "/vars", function (xhr) {
xhr.requestHeaders['i1'].should.equal("test");
xhr.respond(200, {}, "Clicked!")
});
var div = make('<div hx-post="/vars" hx-headers="javascript:i1:\'test\'"></div>')
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('hx-headers works with braces', function () {
this.server.respondWith("POST", "/vars", function (xhr) {
xhr.requestHeaders['i1'].should.equal("test");
xhr.respond(200, {}, "Clicked!")
});
var div = make('<div hx-post="/vars" hx-headers="javascript:{i1:\'test\'}"></div>')
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('multiple hx-headers works', function () {
this.server.respondWith("POST", "/vars", function (xhr) {
xhr.requestHeaders['v1'].should.equal("test");
xhr.requestHeaders['v2'].should.equal("42");
xhr.respond(200, {}, "Clicked!")
});
var div = make('<div hx-post="/vars" hx-headers="javascript:v1:\'test\', v2:42"></div>')
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('hx-headers can be on parents', function () {
this.server.respondWith("POST", "/vars", function (xhr) {
xhr.requestHeaders['i1'].should.equal("test");
xhr.respond(200, {}, "Clicked!")
});
make('<div hx-headers="javascript:i1:\'test\'"><div id="d1" hx-post="/vars"></div></div>')
var div = byId("d1");
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('hx-headers can override parents', function () {
this.server.respondWith("POST", "/vars", function (xhr) {
xhr.requestHeaders['i1'].should.equal("best");
xhr.respond(200, {}, "Clicked!")
});
make('<div hx-headers="javascript:i1:\'test\'"><div id="d1" hx-headers="javascript:i1:\'best\'" hx-post="/vars"></div></div>')
var div = byId("d1");
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('hx-headers overrides inputs', function () {
this.server.respondWith("POST", "/include", function (xhr) {
xhr.requestHeaders['i1'].should.equal("best");
xhr.respond(200, {}, "Clicked!")
});
var div = make('<div hx-target="this"><input hx-post="/include" hx-headers="javascript:i1:\'best\'" hx-trigger="click" id="i1" name="i1" value="test"/></div>')
var input = byId("i1")
input.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
});

View File

@@ -0,0 +1,46 @@
describe("hx-history attribute", function() {
var HTMX_HISTORY_CACHE_NAME = "htmx-history-cache";
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
localStorage.removeItem(HTMX_HISTORY_CACHE_NAME);
});
afterEach(function () {
this.server.restore();
clearWorkArea();
localStorage.removeItem(HTMX_HISTORY_CACHE_NAME);
});
it("history cache should not contain embargoed content", function () {
this.server.respondWith("GET", "/test1", '<div id="d2" hx-push-url="true" hx-get="/test2" hx-swap="outerHTML settle:0" hx-history="true">test1</div>');
this.server.respondWith("GET", "/test2", '<div id="d3" hx-push-url="true" hx-get="/test3" hx-swap="outerHTML settle:0" hx-history="false">test2</div>');
this.server.respondWith("GET", "/test3", '<div id="d4" hx-push-url="true" hx-get="/test3" hx-swap="outerHTML settle:0" hx-history="true">test3</div>');
make('<div id="d1" hx-push-url="true" hx-get="/test1" hx-swap="outerHTML settle:0">init</div>');
byId("d1").click();
this.server.respond();
var workArea = getWorkArea();
workArea.textContent.should.equal("test1");
byId("d2").click();
this.server.respond();
workArea.textContent.should.equal("test2");
byId("d3").click();
this.server.respond();
workArea.textContent.should.equal("test3");
// embargoed content should NOT be in the localStorage cache
var cache = JSON.parse(localStorage.getItem(HTMX_HISTORY_CACHE_NAME));
cache.length.should.equal(2);
// on history navigation, embargoed content is retrieved from server
htmx._('restoreHistory')("/test2");
this.server.respond();
getWorkArea().textContent.should.equal("test2");
});
});

View File

@@ -0,0 +1,228 @@
describe("hx-include attribute", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('By default an input includes itself', function () {
this.server.respondWith("POST", "/include", function (xhr) {
var params = getParameters(xhr);
params['i1'].should.equal("test");
xhr.respond(200, {}, "Clicked!")
});
var div = make('<div hx-target="this"><input hx-post="/include" hx-trigger="click" id="i1" name="i1" value="test"/></div>')
var input = byId("i1")
input.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('non-GET includes closest form', function () {
this.server.respondWith("POST", "/include", function (xhr) {
var params = getParameters(xhr);
params['i1'].should.equal("test");
xhr.respond(200, {}, "Clicked!")
});
var div = make('<form hx-target="this"><div id="d1" hx-post="/include"></div><input name="i1" value="test"/></form>')
var input = byId("d1")
input.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('non-GET includes closest form and overrides values included that exist outside the form', function () {
this.server.respondWith("POST", "/include", function (xhr) {
var params = getParameters(xhr);
params['i1'].should.equal("test");
xhr.respond(200, {}, "Clicked!")
});
var div = make('<div hx-include="*" hx-target="this">' +
'<input name="i1" value="before"/>' +
'<form><div id="d1" hx-post="/include"></div><input name="i1" value="test"/></form>' +
'<input name="i1" value="after"/>')
var input = byId("d1")
input.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('GET does not include closest form by default', function () {
this.server.respondWith("GET", "/include", function (xhr) {
var params = getParameters(xhr);
should.equal(params['i1'], undefined);
xhr.respond(200, {}, "Clicked!")
});
var div = make('<form hx-target="this"><div id="d1" hx-get="/include"></div><input name="i1" value="test"/></form>')
var input = byId("d1")
input.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('Single input not included twice when in form', function () {
this.server.respondWith("POST", "/include", function (xhr) {
var params = getParameters(xhr);
params['i1'].should.equal("test");
xhr.respond(200, {}, "Clicked!")
});
var div = make('<form hx-target="this"><input hx-post="/include" hx-trigger="click" id="i1" name="i1" value="test"/></form>')
var input = byId("i1")
input.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('Two inputs are included twice when they have the same name', function () {
this.server.respondWith("POST", "/include", function (xhr) {
var params = getParameters(xhr);
params['i1'].should.deep.equal(["test", "test2"]);
xhr.respond(200, {}, "Clicked!")
});
var div = make('<div hx-include="*" hx-target="this">' +
'<input hx-post="/include" hx-trigger="click" id="i1" name="i1" value="test"/>' +
'<input name="i1" value="test2"/>' +
'</div>')
var input = byId("i1")
input.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('Two inputs are included twice when in form when they have the same name', function () {
this.server.respondWith("POST", "/include", function (xhr) {
var params = getParameters(xhr);
params['i1'].should.deep.equal(["test", "test2"]);
xhr.respond(200, {}, "Clicked!")
});
var div = make('<form hx-target="this">' +
'<input hx-post="/include" hx-trigger="click" id="i1" name="i1" value="test"/>' +
'<input name="i1" value="test2"/>' +
'</form>')
var input = byId("i1")
input.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('Input not included twice when it explicitly refers to parent form', function () {
this.server.respondWith("POST", "/include", function (xhr) {
var params = getParameters(xhr);
params['i1'].should.equal("test");
xhr.respond(200, {}, "Clicked!")
});
var div = make('<form id="f1" hx-target="this">' +
'<input hx-include="#f1" hx-post="/include" hx-trigger="click" id="i1" name="i1" value="test"/>' +
'</form>')
var input = byId("i1")
input.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('Input can be referred to externally', function () {
this.server.respondWith("POST", "/include", function (xhr) {
var params = getParameters(xhr);
params['i1'].should.equal("test");
xhr.respond(200, {}, "Clicked!")
});
make('<input id="i1" name="i1" value="test"/>');
var div = make('<div hx-post="/include" hx-include="#i1"></div>')
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('Two inputs can be referred to externally', function () {
this.server.respondWith("POST", "/include", function (xhr) {
var params = getParameters(xhr);
params['i1'].should.equal("test");
params['i2'].should.equal("test");
xhr.respond(200, {}, "Clicked!")
});
make('<input id="i1" name="i1" value="test"/>');
make('<input id="i2" name="i2" value="test"/>');
var div = make('<div hx-post="/include" hx-include="#i1, #i2"></div>')
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('A form can be referred to externally', function () {
this.server.respondWith("POST", "/include", function (xhr) {
var params = getParameters(xhr);
params['i1'].should.equal("test");
params['i2'].should.equal("test");
xhr.respond(200, {}, "Clicked!")
});
make('<form id="f1">' +
'<input name="i1" value="test"/>' +
'<input name="i2" value="test"/>' +
'</form> ');
var div = make('<div hx-post="/include" hx-include="#f1"></div>')
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('By default an input includes itself w/ data-* prefix', function () {
this.server.respondWith("POST", "/include", function (xhr) {
var params = getParameters(xhr);
params['i1'].should.equal("test");
xhr.respond(200, {}, "Clicked!")
});
var div = make('<div data-hx-target="this"><input data-hx-post="/include" data-hx-trigger="click" id="i1" name="i1" value="test"/></div>')
var input = byId("i1")
input.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('If the element is not includeable, its descendant inputs are included', function () {
this.server.respondWith("POST", "/include", function (xhr) {
var params = getParameters(xhr);
params['i1'].should.equal("test");
params['i2'].should.equal("test");
xhr.respond(200, {}, "Clicked!")
});
make('<div id="i"><input name="i1" value="test"/><input name="i2" value="test"/></div>');
var div = make('<div hx-post="/include" hx-include="#i"></div>')
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
})
it('The `closest` modifier can be used in the hx-include selector', function () {
this.server.respondWith("POST", "/include", function (xhr) {
var params = getParameters(xhr);
params['i1'].should.equal("test");
params['i2'].should.equal("test");
xhr.respond(200, {}, "Clicked!")
});
make('<div id="i"><input name="i1" value="test"/><input name="i2" value="test"/>'+
'<button id="btn" hx-post="/include" hx-include="closest div"></button></div>');
var btn = byId('btn')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Clicked!");
})
it('The `this` modifier can be used in the hx-include selector', function () {
this.server.respondWith("POST", "/include", function (xhr) {
var params = getParameters(xhr);
params['i1'].should.equal("test");
params['i2'].should.equal("test");
xhr.respond(200, {}, "Clicked!")
});
make('<div id="i" hx-include="this"><input name="i1" value="test"/><input name="i2" value="test"/>'+
'<button id="btn" hx-post="/include"></button></div>');
var btn = byId('btn')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Clicked!");
})
});

View File

@@ -0,0 +1,124 @@
describe("hx-indicator attribute", function(){
beforeEach(function() {
this.server = sinon.fakeServer.create();
clearWorkArea();
});
afterEach(function() {
this.server.restore();
clearWorkArea();
});
it('Indicator classes are properly put on element with no explicit indicator', function()
{
this.server.respondWith("GET", "/test", "Clicked!");
var btn = make('<button hx-get="/test">Click Me!</button>')
btn.click();
btn.classList.contains("htmx-request").should.equal(true);
this.server.respond();
btn.classList.contains("htmx-request").should.equal(false);
});
it('Indicator classes are properly put on element with explicit indicator', function()
{
this.server.respondWith("GET", "/test", "Clicked!");
var btn = make('<button hx-get="/test" hx-indicator="#a1, #a2">Click Me!</button>')
var a1 = make('<a id="a1"></a>')
var a2 = make('<a id="a2"></a>')
btn.click();
btn.classList.contains("htmx-request").should.equal(false);
a1.classList.contains("htmx-request").should.equal(true);
a2.classList.contains("htmx-request").should.equal(true);
this.server.respond();
btn.classList.contains("htmx-request").should.equal(false);
a1.classList.contains("htmx-request").should.equal(false);
a2.classList.contains("htmx-request").should.equal(false);
});
it('Indicator classes are properly put on element with explicit indicator w/ data-* prefix', function()
{
this.server.respondWith("GET", "/test", "Clicked!");
var btn = make('<button hx-get="/test" data-hx-indicator="#a1, #a2">Click Me!</button>')
var a1 = make('<a id="a1"></a>')
var a2 = make('<a id="a2"></a>')
btn.click();
btn.classList.contains("htmx-request").should.equal(false);
a1.classList.contains("htmx-request").should.equal(true);
a2.classList.contains("htmx-request").should.equal(true);
this.server.respond();
btn.classList.contains("htmx-request").should.equal(false);
a1.classList.contains("htmx-request").should.equal(false);
a2.classList.contains("htmx-request").should.equal(false);
});
it('allows closest syntax in hx-indicator', function()
{
this.server.respondWith("GET", "/test", "Clicked!");
var div = make('<div id="d1"><button id="b1" hx-get="/test" hx-indicator="closest div">Click Me!</button></div>')
var btn = byId("b1");
btn.click();
btn.classList.contains("htmx-request").should.equal(false);
div.classList.contains("htmx-request").should.equal(true);
this.server.respond();
btn.classList.contains("htmx-request").should.equal(false);
div.classList.contains("htmx-request").should.equal(false);
});
it('is removed when initiating element is removed from the DOM', function()
{
this.server.respondWith("GET", "/test", "Clicked!");
var indicator = make('<div id="ind1">Indicator</div>')
var div = make('<div id="d1" hx-target="this" hx-indicator="#ind1"><button id="b1" hx-get="/test">Click Me!</button></div>')
var btn = byId("b1");
btn.click();
indicator.classList.contains("htmx-request").should.equal(true);
this.server.respond();
indicator.classList.contains("htmx-request").should.equal(false);
});
it('allows this syntax in hx-indicator', function()
{
this.server.respondWith("GET", "/test", "Clicked!");
var div = make('<div id="d1" hx-indicator="this"><button id="b1" hx-get="/test">Click Me!</button></div>')
var btn = byId("b1");
btn.click();
btn.classList.contains("htmx-request").should.equal(false);
div.classList.contains("htmx-request").should.equal(true);
this.server.respond();
btn.classList.contains("htmx-request").should.equal(false);
div.classList.contains("htmx-request").should.equal(false);
});
it('multiple requests with same indicator are handled properly', function()
{
this.server.respondWith("GET", "/test", "Clicked!");
var b1 = make('<button hx-get="/test" hx-indicator=".a1">Click Me!</button>')
var b2 = make('<button hx-get="/test" hx-indicator=".a1">Click Me!</button>')
var a1 = make('<a class="a1"></a>')
b1.click();
b1.classList.contains("htmx-request").should.equal(false);
b2.classList.contains("htmx-request").should.equal(false);
a1.classList.contains("htmx-request").should.equal(true);
b2.click();
b1.classList.contains("htmx-request").should.equal(false);
b2.classList.contains("htmx-request").should.equal(false);
a1.classList.contains("htmx-request").should.equal(true);
// hack to make sinon process only one response
this.server.processRequest(this.server.queue.shift());
b1.classList.contains("htmx-request").should.equal(false);
b2.classList.contains("htmx-request").should.equal(false);
a1.classList.contains("htmx-request").should.equal(true);
this.server.respond();
b1.classList.contains("htmx-request").should.equal(false);
b2.classList.contains("htmx-request").should.equal(false);
a1.classList.contains("htmx-request").should.equal(false);
});
})

View File

@@ -0,0 +1,101 @@
describe("hx-params attribute", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('none excludes all params', function () {
this.server.respondWith("POST", "/params", function (xhr) {
var params = getParameters(xhr);
should.equal(params['i1'], undefined);
should.equal(params['i2'], undefined);
should.equal(params['i3'], undefined);
xhr.respond(200, {}, "Clicked!")
});
var form = make('<form hx-trigger="click" hx-post="/params" hx-params="none">' +
'<input name="i1" value="test"/>' +
'<input name="i2" value="test"/>' +
'<input name="i3" value="test"/>' +
'</form> ');
form.click();
this.server.respond();
form.innerHTML.should.equal("Clicked!");
});
it('"*" includes all params', function () {
this.server.respondWith("POST", "/params", function (xhr) {
var params = getParameters(xhr);
should.equal(params['i1'], "test");
should.equal(params['i2'], "test");
should.equal(params['i3'], "test");
xhr.respond(200, {}, "Clicked!")
});
var form = make('<form hx-trigger="click" hx-post="/params" hx-params="*">' +
'<input name="i1" value="test"/>' +
'<input name="i2" value="test"/>' +
'<input name="i3" value="test"/>' +
'</form> ');
form.click();
this.server.respond();
form.innerHTML.should.equal("Clicked!");
});
it('named includes works', function () {
this.server.respondWith("POST", "/params", function (xhr) {
var params = getParameters(xhr);
should.equal(params['i1'], "test");
should.equal(params['i2'], undefined);
should.equal(params['i3'], "test");
xhr.respond(200, {}, "Clicked!")
});
var form = make('<form hx-trigger="click" hx-post="/params" hx-params="i1, i3">' +
'<input name="i1" value="test"/>' +
'<input name="i2" value="test"/>' +
'<input name="i3" value="test"/>' +
'</form> ');
form.click();
this.server.respond();
form.innerHTML.should.equal("Clicked!");
});
it('named exclude works', function () {
this.server.respondWith("POST", "/params", function (xhr) {
var params = getParameters(xhr);
should.equal(params['i1'], undefined);
should.equal(params['i2'], "test");
should.equal(params['i3'], undefined);
xhr.respond(200, {}, "Clicked!")
});
var form = make('<form hx-trigger="click" hx-post="/params" hx-params="not i1, i3">' +
'<input name="i1" value="test"/>' +
'<input name="i2" value="test"/>' +
'<input name="i3" value="test"/>' +
'</form> ');
form.click();
this.server.respond();
form.innerHTML.should.equal("Clicked!");
});
it('named exclude works w/ data-* prefix', function () {
this.server.respondWith("POST", "/params", function (xhr) {
var params = getParameters(xhr);
should.equal(params['i1'], undefined);
should.equal(params['i2'], "test");
should.equal(params['i3'], undefined);
xhr.respond(200, {}, "Clicked!")
});
var form = make('<form data-hx-trigger="click" data-hx-post="/params" data-hx-params="not i1, i3">' +
'<input name="i1" value="test"/>' +
'<input name="i2" value="test"/>' +
'<input name="i3" value="test"/>' +
'</form> ');
form.click();
this.server.respond();
form.innerHTML.should.equal("Clicked!");
});
});

View File

@@ -0,0 +1,34 @@
describe("hx-patch attribute", function(){
beforeEach(function() {
this.server = makeServer();
clearWorkArea();
});
afterEach(function() {
this.server.restore();
clearWorkArea();
});
it('issues a PATCH request', function()
{
this.server.respondWith("PATCH", "/test", function(xhr){
xhr.respond(200, {}, "Patched!");
});
var btn = make('<button hx-patch="/test">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Patched!");
});
it('issues a PATCH request w/ data-* prefix', function()
{
this.server.respondWith("PATCH", "/test", function(xhr){
xhr.respond(200, {}, "Patched!");
});
var btn = make('<button data-hx-patch="/test">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Patched!");
});
})

View File

@@ -0,0 +1,36 @@
describe("hx-post attribute", function(){
beforeEach(function() {
this.server = makeServer();
clearWorkArea();
});
afterEach(function() {
this.server.restore();
clearWorkArea();
});
it('issues a POST request with proper headers', function()
{
this.server.respondWith("POST", "/test", function(xhr){
should.equal(xhr.requestHeaders['X-HTTP-Method-Override'], undefined);
xhr.respond(200, {}, "Posted!");
});
var btn = make('<button hx-post="/test">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Posted!");
});
it('issues a POST request with proper headers w/ data-* prefix', function()
{
this.server.respondWith("POST", "/test", function(xhr){
should.equal(xhr.requestHeaders['X-HTTP-Method-Override'], undefined);
xhr.respond(200, {}, "Posted!");
});
var btn = make('<button data-hx-post="/test">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Posted!");
});
})

View File

@@ -0,0 +1,39 @@
describe("hx-preserve attribute", function () {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('handles basic response properly', function () {
this.server.respondWith("GET", "/test", "<div id='d1' hx-preserve>New Content</div><div id='d2'>New Content</div>");
var div = make("<div hx-get='/test'><div id='d1' hx-preserve>Old Content</div><div id='d2'>Old Content</div></div>");
div.click();
this.server.respond();
byId("d1").innerHTML.should.equal("Old Content");
byId("d2").innerHTML.should.equal("New Content");
})
it('handles preserved element that might not be existing', function () {
this.server.respondWith("GET", "/test", "<div id='d1' hx-preserve>New Content</div><div id='d2'>New Content</div>");
var div = make("<div hx-get='/test'><div id='d2'>Old Content</div></div>");
div.click();
this.server.respond();
byId("d1").innerHTML.should.equal("New Content");
byId("d2").innerHTML.should.equal("New Content");
})
it('preserved element should not be swapped if it lies outside of hx-select', function () {
this.server.respondWith("GET", "/test", "<div id='d1' hx-preserve>New Content</div><div id='d2'>New Content</div>");
var div = make("<div hx-get='/test' hx-target='#d2' hx-select='#d2' hx-swap='outerHTML'><div id='d1' hx-preserve>Old Content</div><div id='d2'>Old Content</div></div>");
div.click();
this.server.respond();
byId("d1").innerHTML.should.equal("Old Content");
byId("d2").innerHTML.should.equal("New Content");
})
});

View File

@@ -0,0 +1,223 @@
describe("hx-push-url attribute", function() {
var HTMX_HISTORY_CACHE_NAME = "htmx-history-cache";
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
localStorage.removeItem(HTMX_HISTORY_CACHE_NAME);
});
afterEach(function () {
this.server.restore();
clearWorkArea();
localStorage.removeItem(HTMX_HISTORY_CACHE_NAME);
});
it("navigation should push an element into the cache when true", function () {
this.server.respondWith("GET", "/test", "second");
getWorkArea().innerHTML.should.be.equal("");
var div = make('<div hx-push-url="true" hx-get="/test">first</div>');
div.click();
this.server.respond();
div.click();
this.server.respond();
getWorkArea().textContent.should.equal("second")
var cache = JSON.parse(localStorage.getItem(HTMX_HISTORY_CACHE_NAME));
console.log(cache);
cache[cache.length - 1].url.should.equal("/test");
});
it("navigation should push an element into the cache when string", function () {
this.server.respondWith("GET", "/test", "second");
getWorkArea().innerHTML.should.be.equal("");
var div = make('<div hx-push-url="/abc123" hx-get="/test">first</div>');
div.click();
this.server.respond();
div.click();
this.server.respond();
getWorkArea().textContent.should.equal("second")
var cache = JSON.parse(localStorage.getItem(HTMX_HISTORY_CACHE_NAME));
cache.length.should.equal(2);
cache[1].url.should.equal("/abc123");
});
it("restore should return old value", function () {
this.server.respondWith("GET", "/test1", '<div id="d2" hx-push-url="true" hx-get="/test2" hx-swap="outerHTML settle:0">test1</div>');
this.server.respondWith("GET", "/test2", '<div id="d3" hx-push-url="true" hx-get="/test3" hx-swap="outerHTML settle:0">test2</div>');
make('<div id="d1" hx-push-url="true" hx-get="/test1" hx-swap="outerHTML settle:0">init</div>');
byId("d1").click();
this.server.respond();
var workArea = getWorkArea();
workArea.textContent.should.equal("test1")
byId("d2").click();
this.server.respond();
workArea.textContent.should.equal("test2")
var cache = JSON.parse(localStorage.getItem(HTMX_HISTORY_CACHE_NAME));
cache.length.should.equal(2);
htmx._('restoreHistory')("/test1")
getWorkArea().textContent.should.equal("test1")
});
it("history restore should not have htmx support classes in content", function () {
this.server.respondWith("GET", "/test1", '<div id="d2" hx-push-url="true" hx-get="/test2" hx-swap="outerHTML settle:0">test1</div>');
this.server.respondWith("GET", "/test2", '<div id="d3" hx-push-url="true" hx-get="/test3" hx-swap="outerHTML settle:0">test2</div>');
make('<div id="d1" hx-push-url="true" hx-get="/test1" hx-swap="outerHTML settle:0">init</div>');
byId("d1").click();
this.server.respond();
var workArea = getWorkArea();
workArea.textContent.should.equal("test1")
byId("d2").click();
this.server.respond();
workArea.textContent.should.equal("test2")
htmx._('restoreHistory')("/test1")
getWorkArea().getElementsByClassName("htmx-request").length.should.equal(0);
});
it("cache should only store 10 entries", function () {
var x = 0;
this.server.respondWith("GET", /test.*/, function(xhr){
x++;
xhr.respond(200, {}, '<div id="d1" hx-push-url="true" hx-get="/test' + x + '" hx-swap="outerHTML settle:0"></div>')
});
getWorkArea().innerHTML.should.be.equal("");
make('<div id="d1" hx-push-url="true" hx-get="/test" hx-swap="outerHTML settle:0"></div>');
for (var i = 0; i < 20; i++) { // issue 20 requests
byId("d1").click();
this.server.respond();
}
var cache = JSON.parse(localStorage.getItem(HTMX_HISTORY_CACHE_NAME));
cache.length.should.equal(10); // should only be 10 elements
});
it("cache miss should issue another GET", function () {
this.server.respondWith("GET", "/test1", '<div id="d2" hx-push-url="true" hx-get="/test2" hx-swap="outerHTML settle:0">test1</div>');
this.server.respondWith("GET", "/test2", '<div id="d3" hx-push-url="true" hx-get="/test3" hx-swap="outerHTML settle:0">test2</div>');
make('<div id="d1" hx-push-url="true" hx-get="/test1" hx-swap="outerHTML settle:0">init</div>');
byId("d1").click();
this.server.respond();
var workArea = getWorkArea();
workArea.textContent.should.equal("test1")
byId("d2").click();
this.server.respond();
workArea.textContent.should.equal("test2")
var cache = JSON.parse(localStorage.getItem(HTMX_HISTORY_CACHE_NAME));
cache.length.should.equal(2);
localStorage.removeItem(HTMX_HISTORY_CACHE_NAME); // clear cache
htmx._('restoreHistory')("/test1")
this.server.respond();
getWorkArea().textContent.should.equal("test1")
});
it("navigation should push an element into the cache w/ data-* prefix", function () {
this.server.respondWith("GET", "/test", "second");
getWorkArea().innerHTML.should.be.equal("");
var div = make('<div data-hx-push-url="true" data-hx-get="/test">first</div>');
div.click();
this.server.respond();
getWorkArea().textContent.should.equal("second")
var cache = JSON.parse(localStorage.getItem(HTMX_HISTORY_CACHE_NAME));
cache.length.should.equal(1);
});
it("deals with malformed JSON in history cache when getting", function () {
localStorage.setItem(HTMX_HISTORY_CACHE_NAME, "Invalid JSON");
var history = htmx._('getCachedHistory')('url');
should.equal(history, null);
});
it("deals with malformed JSON in history cache when saving", function () {
localStorage.setItem(HTMX_HISTORY_CACHE_NAME, "Invalid JSON");
htmx._('saveToHistoryCache')('url', 'content', 'title', 'scroll');
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', 'content', 'title', 'scroll');
htmx._('saveToHistoryCache')('url2', 'content', 'title', 'scroll');
htmx._('saveToHistoryCache')('url3', 'content', 'title', 'scroll');
htmx._('saveToHistoryCache')('url2', 'content', 'title', 'scroll');
var cache = JSON.parse(localStorage.getItem(HTMX_HISTORY_CACHE_NAME));
cache.length.should.equal(3);
});
it("history cache is LRU", function () {
htmx._('saveToHistoryCache')('url1', 'content', 'title', 'scroll');
htmx._('saveToHistoryCache')('url2', 'content', 'title', 'scroll');
htmx._('saveToHistoryCache')('url3', 'content', 'title', 'scroll');
htmx._('saveToHistoryCache')('url2', 'content', 'title', 'scroll');
htmx._('saveToHistoryCache')('url1', 'content', 'title', 'scroll');
var cache = JSON.parse(localStorage.getItem(HTMX_HISTORY_CACHE_NAME));
cache.length.should.equal(3);
cache[0].url.should.equal("url3");
cache[1].url.should.equal("url2");
cache[2].url.should.equal("url1");
});
it("htmx:afterSettle is called when replacing outerHTML", function () {
var called = false;
var handler = htmx.on("htmx:afterSettle", function (evt) {
called = true;
});
try {
this.server.respondWith("POST", "/test", function (xhr) {
xhr.respond(200, {}, "<button>Bar</button>");
});
var div = make("<button hx-post='/test' hx-swap='outerHTML'>Foo</button>");
div.click();
this.server.respond();
should.equal(called, true);
} finally {
htmx.off("htmx:afterSettle", handler);
}
});
it("should include parameters on a get", function () {
var path = "";
var handler = htmx.on("htmx:pushedIntoHistory", function (evt) {
path = evt.detail.path;
});
try {
this.server.respondWith("GET", /test.*/, function (xhr) {
xhr.respond(200, {}, "second")
});
var form = make('<form hx-trigger="click" hx-push-url="true" hx-get="/test"><input type="hidden" name="foo" value="bar"/>first</form>');
form.click();
this.server.respond();
form.textContent.should.equal("second")
path.should.equal("/test?foo=bar")
} finally {
htmx.off("htmx:pushedIntoHistory", handler);
}
});
it("saveToHistoryCache should not throw", function () {
var bigContent = "Dummy";
for (var i = 0; i < 20; i++) {
bigContent += bigContent;
}
try {
localStorage.removeItem("htmx-history-cache");
htmx._("saveToHistoryCache")("/dummy", bigContent, "Foo", 0);
should.equal(localStorage.getItem("htmx-history-cache"), null);
} finally {
// clear history cache afterwards
localStorage.removeItem("htmx-history-cache");
}
});
});

View File

@@ -0,0 +1,34 @@
describe("hx-put attribute", function(){
beforeEach(function() {
this.server = makeServer();
clearWorkArea();
});
afterEach(function() {
this.server.restore();
clearWorkArea();
});
it('issues a PUT request', function()
{
this.server.respondWith("PUT", "/test", function(xhr){
xhr.respond(200, {}, "Putted!");
});
var btn = make('<button hx-put="/test">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Putted!");
});
it('issues a PUT request w/ data-* prefix', function()
{
this.server.respondWith("PUT", "/test", function(xhr){
xhr.respond(200, {}, "Putted!");
});
var btn = make('<button data-hx-put="/test">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Putted!");
});
})

View File

@@ -0,0 +1,39 @@
describe("hx-request attribute", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('basic hx-request timeout works', function (done) {
var timedOut = false;
this.server.respondWith("GET", "/test", "Clicked!");
var div = make("<div hx-post='/vars' hx-request='\"timeout\":1'></div>")
htmx.on(div, 'htmx:timeout', function(){
timedOut = true;
})
div.click();
setTimeout(function(){
div.innerHTML.should.equal("");
// unfortunately it looks like sinon.js doesn't implement the timeout functionality
// timedOut.should.equal(true);
done();
}, 400)
});
it('hx-request header works', function () {
this.server.respondWith("POST", "/vars", function (xhr) {
should.equal(xhr.requestHeaders['HX-Request'], undefined);
xhr.respond(200, {}, "Clicked!")
});
var div = make("<div hx-post='/vars' hx-request='{\"noHeaders\":true}'></div>")
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
});

View File

@@ -0,0 +1,37 @@
describe("hx-select-oob attribute", function () {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('basic hx-select-oob works', function()
{
this.server.respondWith("GET", "/test", "<div id='d1'>foo</div><div id='d2'>bar</div>");
var div = make('<div hx-get="/test" hx-select="#d1" hx-select-oob="#d2"></div>');
make('<div id="d2"></div>');
div.click();
this.server.respond();
div.innerHTML.should.equal("<div id=\"d1\">foo</div>");
var div2 = byId('d2');
div2.innerHTML.should.equal("bar");
});
it('basic hx-select-oob ignores bad selector', function()
{
this.server.respondWith("GET", "/test", "<div id='d1'>foo</div><div id='d2'>bar</div>");
var div = make('<div hx-get="/test" hx-select="#d1" hx-select-oob="#bad"></div>');
make('<div id="d2"></div>');
div.click();
this.server.respond();
div.innerHTML.should.equal("<div id=\"d1\">foo</div>");
var div2 = byId('d2');
div2.innerHTML.should.equal("");
});
});

View File

@@ -0,0 +1,40 @@
describe("BOOTSTRAP - htmx AJAX Tests", function(){
beforeEach(function() {
this.server = makeServer();
clearWorkArea();
});
afterEach(function() {
this.server.restore();
clearWorkArea();
});
it('properly handles a partial of HTML', function()
{
var i = 1;
this.server.respondWith("GET", "/test", "<div id='d1'>foo</div><div id='d2'>bar</div>");
var div = make('<div hx-get="/test" hx-select="#d1"></div>');
div.click();
this.server.respond();
div.innerHTML.should.equal("<div id=\"d1\">foo</div>");
});
it('properly handles a full HTML document', function()
{
var i = 1;
this.server.respondWith("GET", "/test", "<html><body><div id='d1'>foo</div><div id='d2'>bar</div></body></html>");
var div = make('<div hx-get="/test" hx-select="#d1"></div>');
div.click();
this.server.respond();
div.innerHTML.should.equal("<div id=\"d1\">foo</div>");
});
it('properly handles a full HTML document w/ data-* prefix', function()
{
var i = 1;
this.server.respondWith("GET", "/test", "<html><body><div id='d1'>foo</div><div id='d2'>bar</div></body></html>");
var div = make('<div hx-get="/test" data-hx-select="#d1"></div>');
div.click();
this.server.respond();
div.innerHTML.should.equal("<div id=\"d1\">foo</div>");
});
})

View File

@@ -0,0 +1,140 @@
describe("hx-sse attribute", function() {
function mockEventSource() {
var listeners = {};
var wasClosed = false;
var mockEventSource = {
removeEventListener: function(name) {
delete listeners[name];
},
addEventListener: function (message, l) {
listeners[message] = l;
},
sendEvent: function (eventName, data) {
var listener = listeners[eventName];
if (listener) {
var event = htmx._("makeEvent")(eventName);
event.data = data;
listener(event);
}
},
close: function () {
wasClosed = true;
},
wasClosed: function () {
return wasClosed;
}
};
return mockEventSource;
}
beforeEach(function () {
this.server = makeServer();
var eventSource = mockEventSource();
this.eventSource = eventSource;
clearWorkArea();
htmx.createEventSource = function(){ 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-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-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 decendents', function () {
this.server.respondWith("GET", "/d1", "div1 updated");
var div = make('<div hx-sse="connect:/foo"></div>' +
'<div id="d1" hx-trigger="sse:e1" hx-get="/d1">div1</div>');
this.eventSource.sendEvent("foo");
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', 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>' +
'</div>');
div.click();
this.server.respond();
this.eventSource.wasClosed().should.equal(true)
})
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>');
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-sse="connect:/event_stream">\n' +
' <div id="d1" hx-sse="swap:e1"></div>\n' +
' <div id="d2" hx-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")
})
});

View File

@@ -0,0 +1,132 @@
describe("hx-swap-oob attribute", function () {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('handles basic response properly', function () {
this.server.respondWith("GET", "/test", "Clicked<div id='d1' hx-swap-oob='true'>Swapped0</div>");
var div = make('<div hx-get="/test">click me</div>');
make('<div id="d1"></div>');
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked");
byId("d1").innerHTML.should.equal("Swapped0");
})
it('handles more than one oob swap properly', function () {
this.server.respondWith("GET", "/test", "Clicked<div id='d1' hx-swap-oob='true'>Swapped1</div><div id='d2' hx-swap-oob='true'>Swapped2</div>");
var div = make('<div hx-get="/test">click me</div>');
make('<div id="d1"></div>');
make('<div id="d2"></div>');
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked");
byId("d1").innerHTML.should.equal("Swapped1");
byId("d2").innerHTML.should.equal("Swapped2");
})
it('handles no id match properly', function () {
this.server.respondWith("GET", "/test", "Clicked<div id='d1' hx-swap-oob='true'>Swapped2</div>");
var div = make('<div hx-get="/test">click me</div>');
div.click();
this.server.respond();
div.innerText.should.equal("Clicked");
})
it('handles basic response properly w/ data-* prefix', function () {
this.server.respondWith("GET", "/test", "Clicked<div id='d1' data-hx-swap-oob='true'>Swapped3</div>");
var div = make('<div data-hx-get="/test">click me</div>');
make('<div id="d1"></div>');
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked");
byId("d1").innerHTML.should.equal("Swapped3");
})
it('handles outerHTML response properly', function () {
this.server.respondWith("GET", "/test", "Clicked<div id='d1' foo='bar' hx-swap-oob='outerHTML'>Swapped4</div>");
var div = make('<div hx-get="/test">click me</div>');
make('<div id="d1"></div>');
div.click();
this.server.respond();
byId("d1").getAttribute("foo").should.equal("bar");
div.innerHTML.should.equal("Clicked");
byId("d1").innerHTML.should.equal("Swapped4");
})
it('handles innerHTML response properly', function () {
this.server.respondWith("GET", "/test", "Clicked<div id='d1' foo='bar' hx-swap-oob='innerHTML'>Swapped5</div>");
var div = make('<div hx-get="/test">click me</div>');
make('<div id="d1"></div>');
div.click();
this.server.respond();
should.equal(byId("d1").getAttribute("foo"), null);
div.innerHTML.should.equal("Clicked");
byId("d1").innerHTML.should.equal("Swapped5");
})
it('oob swaps can be nested in content', function () {
this.server.respondWith("GET", "/test", "<div>Clicked<div id='d1' foo='bar' hx-swap-oob='innerHTML'>Swapped6</div></div>");
var div = make('<div hx-get="/test">click me</div>');
make('<div id="d1"></div>');
div.click();
this.server.respond();
should.equal(byId("d1").getAttribute("foo"), null);
div.innerHTML.should.equal("<div>Clicked</div>");
byId("d1").innerHTML.should.equal("Swapped6");
})
it('oob swaps can use selectors to match up', function () {
this.server.respondWith("GET", "/test", "<div>Clicked<div hx-swap-oob='innerHTML:[oob-foo]'>Swapped7</div></div>");
var div = make('<div hx-get="/test">click me</div>');
make('<div id="d1" oob-foo="bar"></div>');
div.click();
this.server.respond();
should.equal(byId("d1").getAttribute("oob-foo"), "bar");
div.innerHTML.should.equal("<div>Clicked</div>");
byId("d1").innerHTML.should.equal("Swapped7");
})
it('swaps into all targets that match the selector (innerHTML)', function () {
this.server.respondWith("GET", "/test", "<div>Clicked</div><div class='target' hx-swap-oob='innerHTML:.target'>Swapped8</div>");
var div = make('<div hx-get="/test">click me</div>');
make('<div id="d1">No swap</div>');
make('<div id="d2" class="target">Not swapped</div>');
make('<div id="d3" class="target">Not swapped</div>');
div.click();
this.server.respond();
byId("d1").innerHTML.should.equal("No swap");
byId("d2").innerHTML.should.equal("Swapped8");
byId("d3").innerHTML.should.equal("Swapped8");
})
it('swaps into all targets that match the selector (outerHTML)', function () {
var oobSwapContent = '<div class="new-target" hx-swap-oob="outerHTML:.target">Swapped9</div>';
this.server.respondWith("GET", "/test", "<div>Clicked</div>" + oobSwapContent);
var div = make('<div hx-get="/test">click me</div>');
make('<div id="d1"><div>No swap</div></div>');
make('<div id="d2"><div class="target">Not swapped</div></div>');
make('<div id="d3"><div class="target">Not swapped</div></div>');
div.click();
this.server.respond();
byId("d1").innerHTML.should.equal("<div>No swap</div>");
byId("d2").innerHTML.should.equal(oobSwapContent);
byId("d3").innerHTML.should.equal(oobSwapContent);
})
it('oob swap delete works properly', function()
{
this.server.respondWith("GET", "/test", '<div hx-swap-oob="delete" id="d1"></div>');
var div = make('<div id="d1" hx-get="/test">Foo</div>')
div.click();
this.server.respond();
should.equal(byId("d1"), null);
});
});

View File

@@ -0,0 +1,301 @@
describe("hx-swap attribute", function(){
beforeEach(function() {
this.server = makeServer();
clearWorkArea();
});
afterEach(function() {
this.server.restore();
clearWorkArea();
});
it('swap innerHTML properly', function()
{
this.server.respondWith("GET", "/test", '<a hx-get="/test2">Click Me</a>');
this.server.respondWith("GET", "/test2", "Clicked!");
var div = make('<div hx-get="/test"></div>')
div.click();
this.server.respond();
div.innerHTML.should.equal('<a hx-get="/test2">Click Me</a>');
var a = div.querySelector('a');
a.click();
this.server.respond();
a.innerHTML.should.equal('Clicked!');
});
it('swap outerHTML properly', function()
{
this.server.respondWith("GET", "/test", '<a id="a1" hx-get="/test2">Click Me</a>');
this.server.respondWith("GET", "/test2", "Clicked!");
var div = make('<div id="d1" hx-get="/test" hx-swap="outerHTML"></div>')
div.click();
should.equal(byId("d1"), div);
this.server.respond();
should.equal(byId("d1"), null);
byId("a1").click();
this.server.respond();
byId("a1").innerHTML.should.equal('Clicked!');
});
it('swap beforebegin properly', function()
{
var i = 0;
this.server.respondWith("GET", "/test", function(xhr){
i++;
xhr.respond(200, {}, '<a id="a' + i + '" hx-get="/test2" hx-swap="innerHTML">' + i + '</a>');
});
this.server.respondWith("GET", "/test2", "*");
var div = make('<div hx-get="/test" hx-swap="beforebegin">*</div>')
var parent = div.parentElement;
div.click();
this.server.respond();
div.innerText.should.equal("*");
removeWhiteSpace(parent.innerText).should.equal("1*");
byId("a1").click();
this.server.respond();
removeWhiteSpace(parent.innerText).should.equal("**");
div.click();
this.server.respond();
div.innerText.should.equal("*");
removeWhiteSpace(parent.innerText).should.equal("*2*");
byId("a2").click();
this.server.respond();
removeWhiteSpace(parent.innerText).should.equal("***");
});
it('swap afterbegin properly', function()
{
var i = 0;
this.server.respondWith("GET", "/test", function(xhr){
i++;
xhr.respond(200, {}, "" + i);
});
var div = make('<div hx-get="/test" hx-swap="afterbegin">*</div>')
div.click();
this.server.respond();
div.innerText.should.equal("1*");
div.click();
this.server.respond();
div.innerText.should.equal("21*");
div.click();
this.server.respond();
div.innerText.should.equal("321*");
});
it('swap afterbegin properly with no initial content', function()
{
var i = 0;
this.server.respondWith("GET", "/test", function(xhr){
i++;
xhr.respond(200, {}, "" + i);
});
var div = make('<div hx-get="/test" hx-swap="afterbegin"></div>')
div.click();
this.server.respond();
div.innerText.should.equal("1");
div.click();
this.server.respond();
div.innerText.should.equal("21");
div.click();
this.server.respond();
div.innerText.should.equal("321");
});
it('swap afterend properly', function()
{
var i = 0;
this.server.respondWith("GET", "/test", function(xhr){
i++;
xhr.respond(200, {}, '<a id="a' + i + '" hx-get="/test2" hx-swap="innerHTML">' + i + '</a>');
});
this.server.respondWith("GET", "/test2", "*");
var div = make('<div hx-get="/test" hx-swap="afterend">*</div>')
var parent = div.parentElement;
div.click();
this.server.respond();
div.innerText.should.equal("*");
removeWhiteSpace(parent.innerText).should.equal("*1");
byId("a1").click();
this.server.respond();
removeWhiteSpace(parent.innerText).should.equal("**");
div.click();
this.server.respond();
div.innerText.should.equal("*");
removeWhiteSpace(parent.innerText).should.equal("*2*");
byId("a2").click();
this.server.respond();
removeWhiteSpace(parent.innerText).should.equal("***");
});
it('handles beforeend properly', function()
{
var i = 0;
this.server.respondWith("GET", "/test", function(xhr){
i++;
xhr.respond(200, {}, "" + i);
});
var div = make('<div hx-get="/test" hx-swap="beforeend">*</div>')
div.click();
this.server.respond();
div.innerText.should.equal("*1");
div.click();
this.server.respond();
div.innerText.should.equal("*12");
div.click();
this.server.respond();
div.innerText.should.equal("*123");
});
it('handles beforeend properly with no initial content', function()
{
var i = 0;
this.server.respondWith("GET", "/test", function(xhr){
i++;
xhr.respond(200, {}, "" + i);
});
var div = make('<div hx-get="/test" hx-swap="beforeend"></div>')
div.click();
this.server.respond();
div.innerText.should.equal("1");
div.click();
this.server.respond();
div.innerText.should.equal("12");
div.click();
this.server.respond();
div.innerText.should.equal("123");
});
it('properly parses various swap specifications', function(){
var swapSpec = htmx._("getSwapSpecification"); // internal function for swap spec
swapSpec(make("<div/>")).swapStyle.should.equal("innerHTML")
swapSpec(make("<div hx-swap='innerHTML'/>")).swapStyle.should.equal("innerHTML")
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 settle:10'/>")).settleDelay.should.equal(10)
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 nonsense settle:11 swap:10'/>")).settleDelay.should.equal(11)
swapSpec(make("<div hx-swap='innerHTML nonsense settle:11 swap:10 '/>")).settleDelay.should.equal(11)
})
it('works with a swap delay', function(done) {
this.server.respondWith("GET", "/test", "Clicked!");
var div = make("<div hx-get='/test' hx-swap='innerHTML swap:10ms'></div>");
div.click();
this.server.respond();
div.innerText.should.equal("");
setTimeout(function () {
div.innerText.should.equal("Clicked!");
done();
}, 30);
});
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>");
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>');
this.server.respondWith("GET", "/test2", "Clicked!");
var div = make('<div id="d1" data-hx-get="/test" data-hx-swap="outerHTML"></div>')
div.click();
should.equal(byId("d1"), div);
this.server.respond();
should.equal(byId("d1"), null);
byId("a1").click();
this.server.respond();
byId("a1").innerHTML.should.equal('Clicked!');
});
it('swap none works properly', function()
{
this.server.respondWith("GET", "/test", 'Ooops, swapped');
var div = make('<div hx-swap="none" hx-get="/test">Foo</div>')
div.click();
this.server.respond();
div.innerHTML.should.equal('Foo');
});
it('swap outerHTML does not trigger htmx:afterSwap on original element', function()
{
this.server.respondWith("GET", "/test", 'Clicked!');
var div = make('<div id="d1" hx-get="/test" hx-swap="outerHTML"></div>')
div.addEventListener("htmx:afterSwap", function(){
count++;
})
div.click();
var count = 0;
should.equal(byId("d1"), div);
this.server.respond();
should.equal(byId("d1"), null);
count.should.equal(0);
});
it('swap delete works properly', function()
{
this.server.respondWith("GET", "/test", 'Oops, deleted!');
var div = make('<div id="d1" hx-swap="delete" hx-get="/test">Foo</div>')
div.click();
this.server.respond();
should.equal(byId("d1"), null);
});
it('in presence of bad swap spec, it uses the default swap strategy', function()
{
var initialSwapStyle = htmx.config.defaultSwapStyle;
htmx.config.defaultSwapStyle = "outerHTML";
try {
this.server.respondWith("GET", "/test", "Clicked!");
var div = make('<div><button id="b1" hx-swap="foo" hx-get="/test">Initial</button></div>')
var b1 = byId("b1");
b1.click();
this.server.respond();
div.innerHTML.should.equal('Clicked!');
} finally {
htmx.config.defaultSwapStyle = initialSwapStyle;
}
});
})

View File

@@ -0,0 +1,224 @@
describe("hx-sync attribute", function(){
beforeEach(function() {
this.server = makeServer();
clearWorkArea();
});
afterEach(function() {
this.server.restore();
clearWorkArea();
});
it('can use drop strategy', function()
{
var count = 0;
this.server.respondWith("GET", "/test", function(xhr){
xhr.respond(200, {}, "Click " + count++);
});
make('<div hx-sync="this:drop"><button id="b1" hx-get="/test">Initial</button>' +
' <button id="b2" hx-get="/test">Initial</button></div>')
var b1 = byId("b1");
var b2 = byId("b2");
b1.click();
b2.click();
this.server.respond();
this.server.respond();
b1.innerHTML.should.equal('Click 0');
b2.innerHTML.should.equal('Initial');
});
it('defaults to the drop strategy', function()
{
var count = 0;
this.server.respondWith("GET", "/test", function(xhr){
xhr.respond(200, {}, "Click " + count++);
});
make('<div hx-sync="this"><button id="b1" hx-get="/test">Initial</button>' +
' <button id="b2" hx-get="/test">Initial</button></div>')
var b1 = byId("b1");
var b2 = byId("b2");
b1.click();
b2.click();
this.server.respond();
this.server.respond();
b1.innerHTML.should.equal('Click 0');
b2.innerHTML.should.equal('Initial');
});
it('can use replace strategy', function()
{
var count = 0;
this.server.respondWith("GET", "/test", function(xhr){
xhr.respond(200, {}, "Click " + count++);
});
make('<div hx-sync="this:replace"><button id="b1" hx-get="/test">Initial</button>' +
' <button id="b2" hx-get="/test">Initial</button></div>')
var b1 = byId("b1");
var b2 = byId("b2");
b1.click();
b2.click();
this.server.respond();
this.server.respond();
b1.innerHTML.should.equal('Initial');
b2.innerHTML.should.equal('Click 0');
});
it('can use queue all strategy', function()
{
var count = 0;
this.server.respondWith("GET", "/test", function(xhr){
xhr.respond(200, {}, "Click " + count++);
});
make('<div hx-sync="this:queue all"><button id="b1" hx-get="/test">Initial</button>' +
' <button id="b2" hx-get="/test">Initial</button>' +
' <button id="b3" hx-get="/test">Initial</button></div>')
var b1 = byId("b1");
b1.click();
var b2 = byId("b2");
b2.click();
var b3 = byId("b3");
b3.click();
this.server.respond();
b1.innerHTML.should.equal('Click 0');
b2.innerHTML.should.equal('Initial');
b3.innerHTML.should.equal('Initial');
this.server.respond();
b1.innerHTML.should.equal('Click 0');
b2.innerHTML.should.equal('Click 1');
b3.innerHTML.should.equal('Initial');
this.server.respond();
b1.innerHTML.should.equal('Click 0');
b2.innerHTML.should.equal('Click 1');
b3.innerHTML.should.equal('Click 2');
});
it('can use queue last strategy', function()
{
var count = 0;
this.server.respondWith("GET", "/test", function(xhr){
xhr.respond(200, {}, "Click " + count++);
});
make('<div hx-sync="this:queue last"><button id="b1" hx-get="/test">Initial</button>' +
' <button id="b2" hx-get="/test">Initial</button>' +
' <button id="b3" hx-get="/test">Initial</button></div>')
var b1 = byId("b1");
b1.click();
var b2 = byId("b2");
b2.click();
var b3 = byId("b3");
b3.click();
this.server.respond();
b1.innerHTML.should.equal('Click 0');
b2.innerHTML.should.equal('Initial');
b3.innerHTML.should.equal('Initial');
this.server.respond();
b1.innerHTML.should.equal('Click 0');
b2.innerHTML.should.equal('Initial');
b3.innerHTML.should.equal('Click 1');
this.server.respond();
b1.innerHTML.should.equal('Click 0');
b2.innerHTML.should.equal('Initial');
b3.innerHTML.should.equal('Click 1');
});
it('can use queue first strategy', function()
{
var count = 0;
this.server.respondWith("GET", "/test", function(xhr){
xhr.respond(200, {}, "Click " + count++);
});
make('<div hx-sync="this:queue first"><button id="b1" hx-get="/test">Initial</button>' +
' <button id="b2" hx-get="/test">Initial</button>' +
' <button id="b3" hx-get="/test">Initial</button></div>')
var b1 = byId("b1");
b1.click();
var b2 = byId("b2");
b2.click();
var b3 = byId("b3");
b3.click();
this.server.respond();
b1.innerHTML.should.equal('Click 0');
b2.innerHTML.should.equal('Initial');
b3.innerHTML.should.equal('Initial');
this.server.respond();
b1.innerHTML.should.equal('Click 0');
b2.innerHTML.should.equal('Click 1');
b3.innerHTML.should.equal('Initial');
this.server.respond();
b1.innerHTML.should.equal('Click 0');
b2.innerHTML.should.equal('Click 1');
b3.innerHTML.should.equal('Initial');
});
it('can use abort strategy to end existing abortable request', function()
{
var count = 0;
this.server.respondWith("GET", "/test", function(xhr){
xhr.respond(200, {}, "Click " + count++);
});
make('<div hx-sync="this"><button hx-sync="closest div:abort" id="b1" hx-get="/test">Initial</button>' +
' <button id="b2" hx-get="/test">Initial</button></div>')
var b1 = byId("b1");
var b2 = byId("b2");
b1.click();
b2.click();
this.server.respond();
this.server.respond();
b1.innerHTML.should.equal('Initial');
b2.innerHTML.should.equal('Click 0');
});
it('can use abort strategy to drop abortable request when one is in flight', function()
{
var count = 0;
this.server.respondWith("GET", "/test", function(xhr){
xhr.respond(200, {}, "Click " + count++);
});
make('<div hx-sync="this"><button hx-sync="closest div:abort" id="b1" hx-get="/test">Initial</button>' +
' <button id="b2" hx-get="/test">Initial</button></div>')
var b1 = byId("b1");
var b2 = byId("b2");
b2.click();
b1.click();
this.server.respond();
this.server.respond();
b1.innerHTML.should.equal('Initial');
b2.innerHTML.should.equal('Click 0');
});
it('can abort a request programmatically', function()
{
var count = 0;
this.server.respondWith("GET", "/test", function(xhr){
xhr.respond(200, {}, "Click " + count++);
});
make('<div><button id="b1" hx-get="/test">Initial</button>' +
' <button id="b2" hx-get="/test">Initial</button></div>')
var b1 = byId("b1");
var b2 = byId("b2");
b1.click();
b2.click();
htmx.trigger(b1, "htmx:abort");
this.server.respond();
this.server.respond();
b1.innerHTML.should.equal('Initial');
b2.innerHTML.should.equal('Click 0');
});
})

View File

@@ -0,0 +1,134 @@
describe("hx-target attribute", function(){
beforeEach(function() {
this.server = sinon.fakeServer.create();
clearWorkArea();
});
afterEach(function() {
this.server.restore();
clearWorkArea();
});
it('targets an adjacent element properly', function()
{
this.server.respondWith("GET", "/test", "Clicked!");
var btn = make('<button hx-target="#d1" hx-get="/test">Click Me!</button>')
var div1 = make('<div id="d1"></div>')
btn.click();
this.server.respond();
div1.innerHTML.should.equal("Clicked!");
});
it('targets a parent element properly', function()
{
this.server.respondWith("GET", "/test", "Clicked!");
var div1 = make('<div id="d1"><button id="b1" hx-target="#d1" hx-get="/test">Click Me!</button></div>')
var btn = byId("b1")
btn.click();
this.server.respond();
div1.innerHTML.should.equal("Clicked!");
});
it('targets a `this` element properly', function()
{
this.server.respondWith("GET", "/test", "Clicked!");
var div1 = make('<div hx-target="this"><button id="b1" hx-get="/test">Click Me!</button></div>')
var btn = byId("b1")
btn.click();
this.server.respond();
div1.innerHTML.should.equal("Clicked!");
});
it('targets a `closest` element properly', function()
{
this.server.respondWith("GET", "/test", "Clicked!");
var div1 = make('<div><p><i><button id="b1" hx-target="closest div" hx-get="/test">Click Me!</button></i></p></div>')
var btn = byId("b1")
btn.click();
this.server.respond();
div1.innerHTML.should.equal("Clicked!");
});
it('targets a `find` element properly', function()
{
this.server.respondWith("GET", "/test", "Clicked!");
var div1 = make('<div hx-target="find span" hx-get="/test">Click Me! <div><span id="s1"></span><span id="s2"></span></div></div>')
div1.click();
this.server.respond();
var span1 = byId("s1")
var span2 = byId("s2")
span1.innerHTML.should.equal("Clicked!");
span2.innerHTML.should.equal("");
});
it('targets an inner element properly', function()
{
this.server.respondWith("GET", "/test", "Clicked!");
var btn = make('<button hx-target="#d1" hx-get="/test">Click Me!<div id="d1"></div></button>')
var div1 = byId("d1")
btn.click();
this.server.respond();
div1.innerHTML.should.equal("Clicked!");
});
it('handles bad target gracefully', function()
{
this.server.respondWith("GET", "/test", "Clicked!");
var btn = make('<button hx-target="bad" hx-get="/test">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Click Me!");
});
it('targets an adjacent element properly w/ data-* prefix', function()
{
this.server.respondWith("GET", "/test", "Clicked!");
var btn = make('<button data-hx-target="#d1" data-hx-get="/test">Click Me!</button>')
var div1 = make('<div id="d1"></div>')
btn.click();
this.server.respond();
div1.innerHTML.should.equal("Clicked!");
});
it('targets a `next` element properly', function()
{
this.server.respondWith("GET", "/test", "Clicked!");
make('<div>' +
' <div id="d3"></div>' +
' <button id="b1" hx-target="next div" hx-get="/test">Click Me!</button>' +
' <div id="d1"></div>' +
' <div id="d2"></div>' +
'</div>')
var btn = byId("b1")
var div1 = byId("d1")
var div2 = byId("d2")
var div3 = byId("d3")
btn.click();
this.server.respond();
div1.innerHTML.should.equal("Clicked!");
div2.innerHTML.should.equal("");
div3.innerHTML.should.equal("");
});
it('targets a `previous` element properly', function()
{
this.server.respondWith("GET", "/test", "Clicked!");
make('<div>' +
' <div id="d3"></div>' +
' <button id="b1" hx-target="previous div" hx-get="/test">Click Me!</button>' +
' <div id="d1"></div>' +
' <div id="d2"></div>' +
'</div>')
var btn = byId("b1")
var div1 = byId("d1")
var div2 = byId("d2")
var div3 = byId("d3")
btn.click();
this.server.respond();
div1.innerHTML.should.equal("");
div2.innerHTML.should.equal("");
div3.innerHTML.should.equal("Clicked!");
});
})

View File

@@ -0,0 +1,760 @@
describe("hx-trigger attribute", function(){
beforeEach(function() {
this.server = sinon.fakeServer.create();
clearWorkArea();
});
afterEach(function() {
this.server.restore();
clearWorkArea();
});
it('non-default value works', function()
{
this.server.respondWith("GET", "/test", "Clicked!");
var form = make('<form hx-get="/test" hx-trigger="click">Click Me!</form>');
form.click();
form.innerHTML.should.equal("Click Me!");
this.server.respond();
form.innerHTML.should.equal("Clicked!");
});
it('changed modifier works', function()
{
var requests = 0;
this.server.respondWith("GET", "/test", function (xhr) {
requests++;
xhr.respond(200, {}, "Requests: " + requests);
});
var input = make('<input hx-trigger="click changed" hx-target="#d1" hx-get="/test"/>');
var div = make('<div id="d1"></div>');
input.click();
this.server.respond();
div.innerHTML.should.equal("");
input.click();
this.server.respond();
div.innerHTML.should.equal("");
input.value = "bar";
input.click();
this.server.respond();
div.innerHTML.should.equal("Requests: 1");
input.click();
this.server.respond();
div.innerHTML.should.equal("Requests: 1");
});
it('once modifier works', function()
{
var requests = 0;
this.server.respondWith("GET", "/test", function (xhr) {
requests++;
xhr.respond(200, {}, "Requests: " + requests);
});
var input = make('<input hx-trigger="click once" hx-target="#d1" hx-get="/test" value="foo"/>');
var div = make('<div id="d1"></div>');
input.click();
this.server.respond();
div.innerHTML.should.equal("Requests: 1");
input.click();
this.server.respond();
div.innerHTML.should.equal("Requests: 1");
input.value = "bar";
input.click();
this.server.respond();
div.innerHTML.should.equal("Requests: 1");
input.click();
this.server.respond();
div.innerHTML.should.equal("Requests: 1");
});
it('once modifier works with multiple triggers', function()
{
var requests = 0;
this.server.respondWith("GET", "/test", function (xhr) {
requests++;
xhr.respond(200, {}, "Requests: " + requests);
});
var input = make('<input hx-trigger="click once, foo" hx-target="#d1" hx-get="/test" value="foo"/>');
var div = make('<div id="d1"></div>');
input.click();
this.server.respond();
div.innerHTML.should.equal("Requests: 1");
input.click();
this.server.respond();
div.innerHTML.should.equal("Requests: 1");
input.value = "bar";
input.click();
this.server.respond();
div.innerHTML.should.equal("Requests: 1");
input.click();
this.server.respond();
div.innerHTML.should.equal("Requests: 1");
htmx.trigger(input, "foo");
this.server.respond();
div.innerHTML.should.equal("Requests: 2");
});
it('polling works', function(complete)
{
var requests = 0;
this.server.respondWith("GET", "/test", function (xhr) {
requests++;
if (requests > 5) {
complete();
// cancel polling with a
xhr.respond(286, {}, "Requests: " + requests);
} else {
xhr.respond(200, {}, "Requests: " + requests);
}
});
this.server.autoRespond = true;
this.server.autoRespondAfter = 0;
make('<div hx-trigger="every 10ms" hx-get="/test"/>');
});
it('non-default value works w/ data-* prefix', function()
{
this.server.respondWith("GET", "/test", "Clicked!");
var form = make('<form data-hx-get="/test" data-hx-trigger="click">Click Me!</form>');
form.click();
form.innerHTML.should.equal("Click Me!");
this.server.respond();
form.innerHTML.should.equal("Clicked!");
});
it('works with multiple events', function()
{
var requests = 0;
this.server.respondWith("GET", "/test", function (xhr) {
requests++;
xhr.respond(200, {}, "Requests: " + requests);
});
var div = make('<div hx-trigger="load,click" hx-get="/test">Requests: 0</div>');
div.innerHTML.should.equal("Requests: 0");
this.server.respond();
div.innerHTML.should.equal("Requests: 1");
div.click();
this.server.respond();
div.innerHTML.should.equal("Requests: 2");
});
it("parses spec strings", function()
{
var specExamples = {
"": [{trigger: 'click'}],
"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}],
"event1,": [{trigger: 'event1'}],
" ": [{trigger: 'click'}],
}
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);
}
});
it('sets default trigger for forms', function()
{
var form = make('<form></form>');
var spec = htmx._('getTriggerSpecs')(form);
spec.should.deep.equal([{trigger: 'submit'}]);
})
it('sets default trigger for form elements', function()
{
var form = make('<input></input>');
var spec = htmx._('getTriggerSpecs')(form);
spec.should.deep.equal([{trigger: 'change'}]);
})
it('filters properly with false filter spec', function(){
this.server.respondWith("GET", "/test", "Called!");
var form = make('<form hx-get="/test" hx-trigger="evt[foo]">Not Called</form>');
form.click();
form.innerHTML.should.equal("Not Called");
var event = htmx._("makeEvent")('evt');
form.dispatchEvent(event);
this.server.respond();
form.innerHTML.should.equal("Not Called");
})
it('filters properly with true filter spec', function(){
this.server.respondWith("GET", "/test", "Called!");
var form = make('<form hx-get="/test" hx-trigger="evt[foo]">Not Called</form>');
form.click();
form.innerHTML.should.equal("Not Called");
var event = htmx._("makeEvent")('evt');
event.foo = true;
form.dispatchEvent(event);
this.server.respond();
form.innerHTML.should.equal("Called!");
})
it('filters properly compound filter spec', function(){
this.server.respondWith("GET", "/test", "Called!");
var div = make('<div hx-get="/test" hx-trigger="evt[foo&&bar]">Not Called</div>');
var event = htmx._("makeEvent")('evt');
event.foo = true;
div.dispatchEvent(event);
this.server.respond();
div.innerHTML.should.equal("Not Called");
event.bar = true;
div.dispatchEvent(event);
this.server.respond();
div.innerHTML.should.equal("Called!");
})
it('can refer to target element in condition', function(){
this.server.respondWith("GET", "/test", "Called!");
var div = make('<div hx-get="/test" hx-trigger="evt[target.classList.contains(\'doIt\')]">Not Called</div>');
var event = htmx._("makeEvent")('evt');
div.dispatchEvent(event);
this.server.respond();
div.innerHTML.should.equal("Not Called");
div.classList.add("doIt");
div.dispatchEvent(event);
this.server.respond();
div.innerHTML.should.equal("Called!");
})
it('can refer to target element in condition w/ equality', function(){
this.server.respondWith("GET", "/test", "Called!");
var div = make('<div hx-get="/test" hx-trigger="evt[target.id==\'foo\']">Not Called</div>');
var event = htmx._("makeEvent")('evt');
div.dispatchEvent(event);
this.server.respond();
div.innerHTML.should.equal("Not Called");
div.id = "foo";
div.dispatchEvent(event);
this.server.respond();
div.innerHTML.should.equal("Called!");
})
it('negative condition', function(){
this.server.respondWith("GET", "/test", "Called!");
var div = make('<div hx-get="/test" hx-trigger="evt[!target.classList.contains(\'disabled\')]">Not Called</div>');
div.classList.add("disabled");
var event = htmx._("makeEvent")('evt');
div.dispatchEvent(event);
this.server.respond();
div.innerHTML.should.equal("Not Called");
div.classList.remove("disabled");
div.dispatchEvent(event);
this.server.respond();
div.innerHTML.should.equal("Called!");
})
it('global function call works', function(){
window.globalFun = function(evt) {
return evt.bar;
}
try {
this.server.respondWith("GET", "/test", "Called!");
var div = make('<div hx-get="/test" hx-trigger="evt[globalFun(event)]">Not Called</div>');
var event = htmx._("makeEvent")('evt');
event.bar = false;
div.dispatchEvent(event);
this.server.respond();
div.innerHTML.should.equal("Not Called");
event.bar = true;
div.dispatchEvent(event);
this.server.respond();
div.innerHTML.should.equal("Called!");
} finally {
delete window.globalFun;
}
})
it('global property event filter works', function(){
window.foo = {
bar:false
}
try {
this.server.respondWith("GET", "/test", "Called!");
var div = make('<div hx-get="/test" hx-trigger="evt[foo.bar]">Not Called</div>');
var event = htmx._("makeEvent")('evt');
div.dispatchEvent(event);
this.server.respond();
div.innerHTML.should.equal("Not Called");
foo.bar = true;
div.dispatchEvent(event);
this.server.respond();
div.innerHTML.should.equal("Called!");
} finally {
delete window.foo;
}
})
it('global variable filter works', function(){
try {
this.server.respondWith("GET", "/test", "Called!");
var div = make('<div hx-get="/test" hx-trigger="evt[foo]">Not Called</div>');
var event = htmx._("makeEvent")('evt');
div.dispatchEvent(event);
this.server.respond();
div.innerHTML.should.equal("Not Called");
foo = true;
div.dispatchEvent(event);
this.server.respond();
div.innerHTML.should.equal("Called!");
} finally {
delete window.foo;
}
})
it('can filter polling', function(complete){
this.server.respondWith("GET", "/test", "Called!");
window.foo = false;
var div = make('<div hx-get="/test" hx-trigger="every 5ms[foo]">Not Called</div>');
var div2 = make('<div hx-get="/test" hx-trigger="every 5ms">Not Called</div>');
this.server.autoRespond = true;
this.server.autoRespondAfter = 0;
setTimeout(function () {
div.innerHTML.should.equal("Not Called");
div2.innerHTML.should.equal("Called!");
delete window.foo;
complete();
}, 100);
})
it('bad condition issues error', function(){
this.server.respondWith("GET", "/test", "Called!");
var div = make('<div hx-get="/test" hx-trigger="evt[a.b]">Not Called</div>');
var errorEvent = null;
var handler = htmx.on("htmx:eventFilter:error", function (event) {
errorEvent = event;
});
try {
var event = htmx._("makeEvent")('evt');
div.dispatchEvent(event);
should.not.equal(null, errorEvent);
should.not.equal(null, errorEvent.detail.source);
console.log(errorEvent.detail.source);
} finally {
htmx.off("htmx:eventFilter:error", handler);
}
})
it('from clause works', function()
{
var requests = 0;
this.server.respondWith("GET", "/test", function (xhr) {
requests++;
xhr.respond(200, {}, "Requests: " + requests);
});
var div2 = make('<div id="d2"></div>');
var div1 = make('<div hx-trigger="click from:#d2" hx-get="/test">Requests: 0</div>');
div1.innerHTML.should.equal("Requests: 0");
div1.click();
this.server.respond();
div1.innerHTML.should.equal("Requests: 0");
div2.click();
this.server.respond();
div1.innerHTML.should.equal("Requests: 1");
});
it('from clause works with body selector', function()
{
var requests = 0;
this.server.respondWith("GET", "/test", function (xhr) {
requests++;
xhr.respond(200, {}, "Requests: " + requests);
});
var div1 = make('<div hx-trigger="click from:body" hx-get="/test">Requests: 0</div>');
div1.innerHTML.should.equal("Requests: 0");
document.body.click();
this.server.respond();
div1.innerHTML.should.equal("Requests: 1");
});
it('from clause works with document selector', function()
{
var requests = 0;
this.server.respondWith("GET", "/test", function (xhr) {
requests++;
xhr.respond(200, {}, "Requests: " + requests);
});
var div1 = make('<div hx-trigger="foo from:document" hx-get="/test">Requests: 0</div>');
div1.innerHTML.should.equal("Requests: 0");
htmx.trigger(document, 'foo');
this.server.respond();
div1.innerHTML.should.equal("Requests: 1");
});
it('from clause works with window selector', function()
{
var requests = 0;
this.server.respondWith("GET", "/test", function (xhr) {
requests++;
xhr.respond(200, {}, "Requests: " + requests);
});
var div1 = make('<div hx-trigger="foo from:window" hx-get="/test">Requests: 0</div>');
div1.innerHTML.should.equal("Requests: 0");
htmx.trigger(window, 'foo');
this.server.respond();
div1.innerHTML.should.equal("Requests: 1");
});
it('from clause works with closest clause', function()
{
var requests = 0;
this.server.respondWith("GET", "/test", function (xhr) {
requests++;
xhr.respond(200, {}, "Requests: " + requests);
});
var div1 = make('<div><a id="a1" hx-trigger="click from:closest div" hx-get="/test">Requests: 0</a></div>');
var a1 = byId('a1');
a1.innerHTML.should.equal("Requests: 0");
div1.click();
this.server.respond();
a1.innerHTML.should.equal("Requests: 1");
});
it('from clause works with find clause', function()
{
var requests = 0;
this.server.respondWith("GET", "/test", function (xhr) {
requests++;
xhr.respond(200, {}, "Requests: " + requests);
});
var div1 = make('<div hx-trigger="click from:find a" hx-target="#a1" hx-get="/test"><a id="a1">Requests: 0</a></div>');
var a1 = byId('a1');
a1.innerHTML.should.equal("Requests: 0");
a1.click();
this.server.respond();
a1.innerHTML.should.equal("Requests: 1");
});
it('event listeners on other elements are removed when an element is swapped out', function()
{
var requests = 0;
this.server.respondWith("GET", "/test", function (xhr) {
requests++;
xhr.respond(200, {}, "Requests: " + requests);
});
this.server.respondWith("GET", "/test2", "Clicked");
var div1 = make('<div hx-get="/test2">' +
'<div id="d2" hx-trigger="click from:body" hx-get="/test">Requests: 0</div>' +
'</div>');
var div2 = byId("d2");
div2.innerHTML.should.equal("Requests: 0");
document.body.click();
this.server.respond();
requests.should.equal(1);
requests.should.equal(1);
div1.click();
this.server.respond();
div1.innerHTML.should.equal("Clicked");
requests.should.equal(2);
document.body.click();
this.server.respond();
requests.should.equal(2);
});
it('multiple triggers with from clauses mixed in work', function()
{
var requests = 0;
this.server.respondWith("GET", "/test", function (xhr) {
requests++;
xhr.respond(200, {}, "Requests: " + requests);
});
var div2 = make('<div id="d2"></div>');
var div1 = make('<div hx-trigger="click from:#d2, click" hx-get="/test">Requests: 0</div>');
div1.innerHTML.should.equal("Requests: 0");
div1.click();
this.server.respond();
div1.innerHTML.should.equal("Requests: 1");
div2.click();
this.server.respond();
div1.innerHTML.should.equal("Requests: 2");
});
it('event listeners can filter on target', function()
{
var requests = 0;
this.server.respondWith("GET", "/test", function (xhr) {
requests++;
xhr.respond(200, {}, "Requests: " + requests);
});
var div1 = make('<div>' +
'<div id="d1" hx-trigger="click from:body target:#d3" hx-get="/test">Requests: 0</div>' +
'<div id="d2"></div>' +
'<div id="d3"></div>' +
'</div>');
var div1 = byId("d1");
var div2 = byId("d2");
var div3 = byId("d3");
div1.innerHTML.should.equal("Requests: 0");
document.body.click();
this.server.respond();
requests.should.equal(0);
div1.click();
this.server.respond();
requests.should.equal(0);
div2.click();
this.server.respond();
requests.should.equal(0);
div3.click();
this.server.respond();
requests.should.equal(1);
});
it('consume prevents event propogation', function()
{
this.server.respondWith("GET", "/foo", "foo");
this.server.respondWith("GET", "/bar", "bar");
var div = make("<div hx-trigger='click' hx-get='/foo'>" +
" <div id='d1' hx-trigger='click consume' hx-get='/bar'></div>" +
"</div>");
byId("d1").click();
this.server.respond();
// should not have been replaced by click
byId("d1").parentElement.should.equal(div);
byId("d1").innerText.should.equal("bar");
});
it('throttle prevents 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:10ms' hx-get='/test'></div>");
div.click();
server.respond();
div.click();
server.respond();
div.click();
server.respond();
div.click();
server.respond();
// should not have been replaced by click
div.innerText.should.equal("Requests: 1");
setTimeout(function () {
div.click();
server.respond();
div.innerText.should.equal("Requests: 2");
div.click();
server.respond();
div.innerText.should.equal("Requests: 2");
done();
}, 50);
});
it('delay delays the request', function(done)
{
var requests = 0;
var server = this.server;
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:10ms' hx-get='/test'></div>");
div.click();
this.server.respond();
div.click();
this.server.respond();
div.click();
this.server.respond();
div.click();
this.server.respond();
div.innerText.should.equal("");
setTimeout(function () {
server.respond();
div.innerText.should.equal("Requests: 1");
div.click();
server.respond();
div.innerText.should.equal("Requests: 1");
done();
}, 50);
});
it('requests are queued with last one winning by default', function()
{
var requests = 0;
var server = this.server;
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' hx-get='/test'></div>");
div.click();
div.click();
div.click();
this.server.respond();
div.innerText.should.equal("Requests: 1");
this.server.respond();
div.innerText.should.equal("Requests: 2");
this.server.respond();
div.innerText.should.equal("Requests: 2");
});
it('queue:all queues all requests', function()
{
var requests = 0;
var server = this.server;
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 queue:all' hx-get='/test'></div>");
div.click();
div.click();
div.click();
this.server.respond();
div.innerText.should.equal("Requests: 1");
this.server.respond();
div.innerText.should.equal("Requests: 2");
this.server.respond();
div.innerText.should.equal("Requests: 3");
});
it('queue:first queues first request', function()
{
var requests = 0;
var server = this.server;
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 queue:first' hx-get='/test'></div>");
div.click();
div.click();
div.click();
this.server.respond();
div.innerText.should.equal("Requests: 1");
this.server.respond();
div.innerText.should.equal("Requests: 2");
this.server.respond();
div.innerText.should.equal("Requests: 2");
});
it('queue:none queues no requests', function()
{
var requests = 0;
var server = this.server;
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 queue:none' hx-get='/test'></div>");
div.click();
div.click();
div.click();
this.server.respond();
div.innerText.should.equal("Requests: 1");
this.server.respond();
div.innerText.should.equal("Requests: 1");
this.server.respond();
div.innerText.should.equal("Requests: 1");
});
it('load event works w/ positive filters', function()
{
this.server.respondWith("GET", "/test", "Loaded!");
var div = make('<div hx-get="/test" hx-trigger="load[true]">Load Me!</div>');
div.innerHTML.should.equal("Load Me!");
this.server.respond();
div.innerHTML.should.equal("Loaded!");
});
it('load event works w/ negative filters', function()
{
this.server.respondWith("GET", "/test", "Loaded!");
var div = make('<div hx-get="/test" hx-trigger="load[false]">Load Me!</div>');
div.innerHTML.should.equal("Load Me!");
this.server.respond();
div.innerHTML.should.equal("Load Me!");
});
it('reveal event works on two elements', function()
{
this.server.respondWith("GET", "/test1", "test 1");
this.server.respondWith("GET", "/test2", "test 2");
var div = make('<div hx-get="/test1" hx-trigger="revealed"></div>');
var div2 = make('<div hx-get="/test2" hx-trigger="revealed"></div>');
div.innerHTML.should.equal("");
div2.innerHTML.should.equal("");
htmx.trigger(div, 'revealed')
htmx.trigger(div2, 'revealed')
this.server.respondAll();
div.innerHTML.should.equal("test 1");
div2.innerHTML.should.equal("test 2");
});
it('reveal event works when triggered by window', function()
{
this.server.respondWith("GET", "/test1", "test 1");
var div = make('<div hx-get="/test1" hx-trigger="revealed" style="position: fixed; top: 1px; left: 1px; border: 3px solid red">foo</div>');
div.innerHTML.should.equal("foo");
this.server.respondAll();
div.innerHTML.should.equal("test 1");
});
})

View File

@@ -0,0 +1,255 @@
describe("hx-vals attribute", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('basic hx-vals works', function () {
this.server.respondWith("POST", "/vars", function (xhr) {
var params = getParameters(xhr);
params['i1'].should.equal("test");
xhr.respond(200, {}, "Clicked!")
});
var div = make("<div hx-post='/vars' hx-vals='\"i1\":\"test\"'></div>")
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('basic hx-vals works with braces', function () {
this.server.respondWith("POST", "/vars", function (xhr) {
var params = getParameters(xhr);
params['i1'].should.equal("test");
xhr.respond(200, {}, "Clicked!")
});
var div = make("<div hx-post='/vars' hx-vals='{\"i1\":\"test\"}'></div>")
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('multiple hx-vals works', function () {
this.server.respondWith("POST", "/vars", function (xhr) {
var params = getParameters(xhr);
params['v1'].should.equal("test");
params['v2'].should.equal("42");
xhr.respond(200, {}, "Clicked!")
});
var div = make("<div hx-post='/vars' hx-vals='\"v1\":\"test\", \"v2\":42'></div>")
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('hx-vals can be on parents', function () {
this.server.respondWith("POST", "/vars", function (xhr) {
var params = getParameters(xhr);
params['i1'].should.equal("test");
xhr.respond(200, {}, "Clicked!")
});
make("<div hx-vals='\"i1\":\"test\"'><div id='d1' hx-post='/vars'></div></div>");
var div = byId("d1");
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('hx-vals can override parents', function () {
this.server.respondWith("POST", "/vars", function (xhr) {
var params = getParameters(xhr);
params['i1'].should.equal("best");
xhr.respond(200, {}, "Clicked!")
});
make("<div hx-vals='\"i1\":\"test\"'><div id='d1' hx-vals='\"i1\":\"best\"' hx-post='/vars'></div></div>");
var div = byId("d1");
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('hx-vals overrides inputs', function () {
this.server.respondWith("POST", "/include", function (xhr) {
var params = getParameters(xhr);
params['i1'].should.equal("best");
xhr.respond(200, {}, "Clicked!")
});
var div = make("<div hx-target='this'><input hx-post='/include' hx-vals='\"i1\":\"best\"' hx-trigger='click' id='i1' name='i1' value='test'/></div>")
var input = byId("i1")
input.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('hx-vals overrides hx-vars', function () {
this.server.respondWith("POST", "/vars", function (xhr) {
var params = getParameters(xhr);
params['i1'].should.equal("test");
xhr.respond(200, {}, "Clicked!")
});
var div = make("<div hx-post='/vars' hx-vals='\"i1\":\"test\"' hx-vars='\"i1\":\"best\"'></div>")
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('basic hx-vals javascript: works', function () {
this.server.respondWith("POST", "/vars", function (xhr) {
var params = getParameters(xhr);
params['i1'].should.equal("test");
xhr.respond(200, {}, "Clicked!")
});
var div = make('<div hx-post="/vars" hx-vals="javascript:i1:\'test\'"></div>')
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('hx-vals works with braces', function () {
this.server.respondWith("POST", "/vars", function (xhr) {
var params = getParameters(xhr);
params['i1'].should.equal("test");
xhr.respond(200, {}, "Clicked!")
});
var div = make('<div hx-post="/vars" hx-vals="javascript:{i1:\'test\'}"></div>')
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('multiple hx-vals works', function () {
this.server.respondWith("POST", "/vars", function (xhr) {
var params = getParameters(xhr);
params['v1'].should.equal("test");
params['v2'].should.equal("42");
xhr.respond(200, {}, "Clicked!")
});
var div = make('<div hx-post="/vars" hx-vals="javascript:v1:\'test\', v2:42"></div>')
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('hx-vals can be on parents', function () {
this.server.respondWith("POST", "/vars", function (xhr) {
var params = getParameters(xhr);
params['i1'].should.equal("test");
xhr.respond(200, {}, "Clicked!")
});
make('<div hx-vals="javascript:i1:\'test\'"><div id="d1" hx-post="/vars"></div></div>')
var div = byId("d1");
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('hx-vals can override parents', function () {
this.server.respondWith("POST", "/vars", function (xhr) {
var params = getParameters(xhr);
params['i1'].should.equal("best");
xhr.respond(200, {}, "Clicked!")
});
make('<div hx-vals="javascript:i1:\'test\'"><div id="d1" hx-vals="javascript:i1:\'best\'" hx-post="/vars"></div></div>')
var div = byId("d1");
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('hx-vals overrides inputs', function () {
this.server.respondWith("POST", "/include", function (xhr) {
var params = getParameters(xhr);
params['i1'].should.equal("best");
xhr.respond(200, {}, "Clicked!")
});
var div = make('<div hx-target="this"><input hx-post="/include" hx-vals="javascript:i1:\'best\'" hx-trigger="click" id="i1" name="i1" value="test"/></div>')
var input = byId("i1")
input.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('hx-vals treats objects as JSON', function () {
this.server.respondWith("POST", "/vars", function (xhr) {
var params = getParameters(xhr);
params['i1'].should.equal("{\"i2\":\"test\"}");
xhr.respond(200, {}, "Clicked!")
});
var div = make("<div hx-post='/vars' hx-vals='\"i1\":{\"i2\" : \"test\"}'></div>")
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('basic hx-vals can be unset', function () {
this.server.respondWith("POST", "/vars", function (xhr) {
var params = getParameters(xhr);
params.should.be.empty;
xhr.respond(200, {}, "Clicked!")
});
make(
"<div hx-vals='\"i1\":\"test\"'>\
<div id='d1' hx-post='/vars' hx-vals='unset'></div>\
</div>"
);
var div = byId("d1");
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('basic hx-vals with braces can be unset', function () {
this.server.respondWith("POST", "/vars", function (xhr) {
var params = getParameters(xhr);
params.should.be.empty;
xhr.respond(200, {}, "Clicked!")
});
make(
"<div hx-vals='{\"i1\":\"test\"}'>\
<div id='d1' hx-post='/vars' hx-vals='unset'></div>\
</div>"
);
var div = byId("d1");
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('multiple hx-vals can be unset', function () {
this.server.respondWith("POST", "/vars", function (xhr) {
var params = getParameters(xhr);
params.should.be.empty;
xhr.respond(200, {}, "Clicked!")
});
make(
"<div hx-vals='\"v1\":\"test\", \"v2\":42'>\
<div id='d1' hx-post='/vars' hx-vals='unset'></div>\
</div>"
);
var div = byId("d1");
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('unsetting hx-vals maintains input values', function () {
this.server.respondWith("POST", "/include", function (xhr) {
var params = getParameters(xhr);
params['i1'].should.equal("test");
xhr.respond(200, {}, "Clicked!")
});
var div = make(
"<div hx-target='this' hx-vals='\"i1\":\"best\"'>\
<input hx-post='/include' hx-vals='unset' hx-trigger='click' id='i1' name='i1' value='test'/>\
</div>"
)
var input = byId("i1")
input.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
});

View File

@@ -0,0 +1,155 @@
describe("hx-vars attribute", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('basic hx-vars works', function () {
this.server.respondWith("POST", "/vars", function (xhr) {
var params = getParameters(xhr);
params['i1'].should.equal("test");
xhr.respond(200, {}, "Clicked!")
});
var div = make('<div hx-post="/vars" hx-vars="i1:\'test\'"></div>')
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('hx-vars works with braces', function () {
this.server.respondWith("POST", "/vars", function (xhr) {
var params = getParameters(xhr);
params['i1'].should.equal("test");
xhr.respond(200, {}, "Clicked!")
});
var div = make('<div hx-post="/vars" hx-vars="{i1:\'test\'}"></div>')
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('multiple hx-vars works', function () {
this.server.respondWith("POST", "/vars", function (xhr) {
var params = getParameters(xhr);
params['v1'].should.equal("test");
params['v2'].should.equal("42");
xhr.respond(200, {}, "Clicked!")
});
var div = make('<div hx-post="/vars" hx-vars="v1:\'test\', v2:42"></div>')
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('hx-vars can be on parents', function () {
this.server.respondWith("POST", "/vars", function (xhr) {
var params = getParameters(xhr);
params['i1'].should.equal("test");
xhr.respond(200, {}, "Clicked!")
});
make('<div hx-vars="i1:\'test\'"><div id="d1" hx-post="/vars"></div></div>')
var div = byId("d1");
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('hx-vars can override parents', function () {
this.server.respondWith("POST", "/vars", function (xhr) {
var params = getParameters(xhr);
params['i1'].should.equal("best");
xhr.respond(200, {}, "Clicked!")
});
make('<div hx-vars="i1:\'test\'"><div id="d1" hx-vars="i1:\'best\'" hx-post="/vars"></div></div>')
var div = byId("d1");
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('hx-vars overrides inputs', function () {
this.server.respondWith("POST", "/include", function (xhr) {
var params = getParameters(xhr);
params['i1'].should.equal("best");
xhr.respond(200, {}, "Clicked!")
});
var div = make('<div hx-target="this"><input hx-post="/include" hx-vars="i1:\'best\'" hx-trigger="click" id="i1" name="i1" value="test"/></div>')
var input = byId("i1")
input.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('basic hx-vars can be unset', function () {
this.server.respondWith("POST", "/vars", function (xhr) {
var params = getParameters(xhr);
params.should.be.empty;
xhr.respond(200, {}, "Clicked!")
});
make(
"<div hx-vars='i1:\"test\"'>\
<div id='d1' hx-post='/vars' hx-vars='unset'></div>\
</div>"
);
var div = byId("d1");
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('basic hx-vars with braces can be unset', function () {
this.server.respondWith("POST", "/vars", function (xhr) {
var params = getParameters(xhr);
params.should.be.empty;
xhr.respond(200, {}, "Clicked!")
});
make(
"<div hx-vars='{i1:\"test\"}'>\
<div id='d1' hx-post='/vars' hx-vars='unset'></div>\
</div>"
);
var div = byId("d1");
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('multiple hx-vars can be unset', function () {
this.server.respondWith("POST", "/vars", function (xhr) {
var params = getParameters(xhr);
params.should.be.empty;
xhr.respond(200, {}, "Clicked!")
});
make(
"<div hx-vars='v1:\"test\", v2:42'>\
<div id='d1' hx-post='/vars' hx-vars='unset'></div>\
</div>"
);
var div = byId("d1");
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('unsetting hx-vars maintains input values', function () {
this.server.respondWith("POST", "/include", function (xhr) {
var params = getParameters(xhr);
params['i1'].should.equal("test");
xhr.respond(200, {}, "Clicked!")
});
var div = make(
"<div hx-target='this' hx-vars='i1:\"best\"'>\
<input hx-post='/include' hx-vars='unset' hx-trigger='click' id='i1' name='i1' value='test'/>\
</div>"
)
var input = byId("i1")
input.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
});

View File

@@ -0,0 +1,77 @@
describe("hx-ws attribute", function() {
function mockWebsocket() {
var listener;
var lastSent;
var wasClosed = false;
var mockSocket = {
addEventListener : function(message, l) {
listener = l;
},
write : function(content) {
return listener({data:content});
},
send : function(data) {
lastSent = data;
},
getLastSent : function() {
return lastSent;
},
close : function() {
wasClosed = true;
},
wasClosed : function () {
return wasClosed;
}
};
return mockSocket;
}
beforeEach(function () {
this.server = makeServer();
var socket = mockWebsocket();
this.socket = socket;
clearWorkArea();
this.oldCreateWebSocket = htmx.createWebSocket;
htmx.createWebSocket = function(){
return socket
};
});
afterEach(function () {
this.server.restore();
clearWorkArea();
htmx.createWebSocket = this.oldCreateWebSocket;
});
it('handles a basic call back', function () {
var div = make('<div hx-ws="connect:/foo"><div id="d1">div1</div><div id="d2">div2</div></div>');
this.socket.write("<div id=\"d1\">replaced</div>")
byId("d1").innerHTML.should.equal("replaced");
byId("d2").innerHTML.should.equal("div2");
})
it('handles a basic send', function () {
var div = make('<div hx-ws="connect:/foo"><div hx-ws="send" id="d1">div1</div></div>');
byId("d1").click();
var lastSent = this.socket.getLastSent();
var data = JSON.parse(lastSent);
data.HEADERS["HX-Request"].should.equal("true");
})
it('is closed after removal', function () {
this.server.respondWith("GET", "/test", "Clicked!");
var div = make('<div hx-get="/test" hx-swap="outerHTML" hx-ws="connect:wss:/foo"></div>');
div.click();
this.server.respond();
this.socket.wasClosed().should.equal(true)
})
it('is closed after removal with no close and activity', function () {
var div = make('<div hx-ws="connect:/foo"></div>');
div.parentElement.removeChild(div);
this.socket.write("<div id=\"d1\">replaced</div>")
this.socket.wasClosed().should.equal(true)
})
});

View File

@@ -0,0 +1,982 @@
describe("Core htmx AJAX Tests", function(){
beforeEach(function() {
this.server = makeServer();
clearWorkArea();
});
afterEach(function() {
this.server.restore();
clearWorkArea();
});
// bootstrap test
it('issues a GET request on click and swaps content', function()
{
this.server.respondWith("GET", "/test", "Clicked!");
var btn = make('<button hx-get="/test">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Clicked!");
});
it('processes inner content properly', function()
{
this.server.respondWith("GET", "/test", '<a hx-get="/test2">Click Me</a>');
this.server.respondWith("GET", "/test2", "Clicked!");
var div = make('<div hx-get="/test"></div>')
div.click();
this.server.respond();
div.innerHTML.should.equal('<a hx-get="/test2">Click Me</a>');
var a = div.querySelector('a');
a.click();
this.server.respond();
a.innerHTML.should.equal('Clicked!');
});
it('handles swap outerHTML properly', function()
{
this.server.respondWith("GET", "/test", '<a id="a1" hx-get="/test2">Click Me</a>');
this.server.respondWith("GET", "/test2", "Clicked!");
var div = make('<div id="d1" hx-get="/test" hx-swap="outerHTML"></div>')
div.click();
should.equal(byId("d1"), div);
this.server.respond();
should.equal(byId("d1"), null);
byId("a1").click();
this.server.respond();
byId("a1").innerHTML.should.equal('Clicked!');
});
it('handles beforebegin properly', function()
{
var i = 0;
this.server.respondWith("GET", "/test", function(xhr){
i++;
xhr.respond(200, {}, '<a id="a' + i + '" hx-get="/test2" hx-swap="innerHTML">' + i + '</a>');
});
this.server.respondWith("GET", "/test2", "*");
var div = make('<div hx-get="/test" hx-swap="beforebegin">*</div>')
var parent = div.parentElement;
div.click();
this.server.respond();
div.innerText.should.equal("*");
removeWhiteSpace(parent.innerText).should.equal("1*");
byId("a1").click();
this.server.respond();
removeWhiteSpace(parent.innerText).should.equal("**");
div.click();
this.server.respond();
div.innerText.should.equal("*");
removeWhiteSpace(parent.innerText).should.equal("*2*");
byId("a2").click();
this.server.respond();
removeWhiteSpace(parent.innerText).should.equal("***");
});
it('handles afterbegin properly', function()
{
var i = 0;
this.server.respondWith("GET", "/test", function(xhr){
i++;
xhr.respond(200, {}, "" + i);
});
var div = make('<div hx-get="/test" hx-swap="afterbegin">*</div>')
div.click();
this.server.respond();
div.innerText.should.equal("1*");
div.click();
this.server.respond();
div.innerText.should.equal("21*");
div.click();
this.server.respond();
div.innerText.should.equal("321*");
});
it('handles afterbegin properly with no initial content', function()
{
var i = 0;
this.server.respondWith("GET", "/test", function(xhr){
i++;
xhr.respond(200, {}, "" + i);
});
var div = make('<div hx-get="/test" hx-swap="afterbegin"></div>')
div.click();
this.server.respond();
div.innerText.should.equal("1");
div.click();
this.server.respond();
div.innerText.should.equal("21");
div.click();
this.server.respond();
div.innerText.should.equal("321");
});
it('handles afterend properly', function()
{
var i = 0;
this.server.respondWith("GET", "/test", function(xhr){
i++;
xhr.respond(200, {}, '<a id="a' + i + '" hx-get="/test2" hx-swap="innerHTML">' + i + '</a>');
});
this.server.respondWith("GET", "/test2", "*");
var div = make('<div hx-get="/test" hx-swap="afterend">*</div>')
var parent = div.parentElement;
div.click();
this.server.respond();
div.innerText.should.equal("*");
removeWhiteSpace(parent.innerText).should.equal("*1");
byId("a1").click();
this.server.respond();
removeWhiteSpace(parent.innerText).should.equal("**");
div.click();
this.server.respond();
div.innerText.should.equal("*");
removeWhiteSpace(parent.innerText).should.equal("*2*");
byId("a2").click();
this.server.respond();
removeWhiteSpace(parent.innerText).should.equal("***");
});
it('handles beforeend properly', function()
{
var i = 0;
this.server.respondWith("GET", "/test", function(xhr){
i++;
xhr.respond(200, {}, "" + i);
});
var div = make('<div hx-get="/test" hx-swap="beforeend">*</div>')
div.click();
this.server.respond();
div.innerText.should.equal("*1");
div.click();
this.server.respond();
div.innerText.should.equal("*12");
div.click();
this.server.respond();
div.innerText.should.equal("*123");
});
it('handles beforeend properly with no initial content', function()
{
var i = 0;
this.server.respondWith("GET", "/test", function(xhr){
i++;
xhr.respond(200, {}, "" + i);
});
var div = make('<div hx-get="/test" hx-swap="beforeend"></div>')
div.click();
this.server.respond();
div.innerText.should.equal("1");
div.click();
this.server.respond();
div.innerText.should.equal("12");
div.click();
this.server.respond();
div.innerText.should.equal("123");
});
it('handles hx-target properly', function()
{
this.server.respondWith("GET", "/test", "Clicked!");
var btn = make('<button hx-get="/test" hx-target="#s1">Click Me!</button>');
var target = make('<span id="s1">Initial</span>');
btn.click();
target.innerHTML.should.equal("Initial");
this.server.respond();
target.innerHTML.should.equal("Clicked!");
});
it('handles 204 NO CONTENT responses properly', function()
{
this.server.respondWith("GET", "/test", [204, {}, "No Content!"]);
var btn = make('<button hx-get="/test">Click Me!</button>');
btn.click();
btn.innerHTML.should.equal("Click Me!");
this.server.respond();
btn.innerHTML.should.equal("Click Me!");
});
it('handles 304 NOT MODIFIED responses properly', function()
{
this.server.respondWith("GET", "/test-1", [200, {}, "Content for Tab 1"]);
this.server.respondWith("GET", "/test-2", [200, {}, "Content for Tab 2"]);
var target = make('<div id="target"></div>')
var btn1 = make('<button hx-get="/test-1" hx-target="#target">Tab 1</button>');
var btn2 = make('<button hx-get="/test-2" hx-target="#target">Tab 2</button>');
btn1.click();
target.innerHTML.should.equal("");
this.server.respond();
target.innerHTML.should.equal("Content for Tab 1");
btn2.click();
this.server.respond();
target.innerHTML.should.equal("Content for Tab 2");
this.server.respondWith("GET", "/test-1", [304, {}, "Content for Tab 1"]);
this.server.respondWith("GET", "/test-2", [304, {}, "Content for Tab 2"]);
btn1.click();
this.server.respond();
target.innerHTML.should.equal("Content for Tab 1");
btn2.click();
this.server.respond();
target.innerHTML.should.equal("Content for Tab 2");
});
it('handles hx-trigger with non-default value', function()
{
this.server.respondWith("GET", "/test", "Clicked!");
var form = make('<form hx-get="/test" hx-trigger="click">Click Me!</form>');
form.click();
form.innerHTML.should.equal("Click Me!");
this.server.respond();
form.innerHTML.should.equal("Clicked!");
});
it('handles hx-trigger with load event', function()
{
this.server.respondWith("GET", "/test", "Loaded!");
var div = make('<div hx-get="/test" hx-trigger="load">Load Me!</div>');
div.innerHTML.should.equal("Load Me!");
this.server.respond();
div.innerHTML.should.equal("Loaded!");
});
it('sets the content type of the request properly', function (done) {
this.server.respondWith("GET", "/test", function(xhr){
xhr.respond(200, {}, "done");
xhr.overriddenMimeType.should.equal("text/html");
done();
});
var div = make('<div hx-get="/test">Click Me!</div>');
div.click();
this.server.respond();
});
it('issues two requests when clicked twice before response', function()
{
var i = 1;
this.server.respondWith("GET", "/test", function (xhr) {
xhr.respond(200, {}, "click " + i);
i++
});
var div = make('<div hx-get="/test"></div>');
div.click();
div.click();
this.server.respond();
div.innerHTML.should.equal("click 1");
this.server.respond();
div.innerHTML.should.equal("click 2");
});
it('issues two requests when clicked three times before response', function()
{
var i = 1;
this.server.respondWith("GET", "/test", function (xhr) {
xhr.respond(200, {}, "click " + i);
i++
});
var div = make('<div hx-get="/test"></div>');
div.click();
div.click();
div.click();
this.server.respondAll();
div.innerHTML.should.equal("click 2");
});
it('properly handles hx-select for basic situation', function()
{
var i = 1;
this.server.respondWith("GET", "/test", "<div id='d1'>foo</div><div id='d2'>bar</div>");
var div = make('<div hx-get="/test" hx-select="#d1"></div>');
div.click();
this.server.respond();
div.innerHTML.should.equal("<div id=\"d1\">foo</div>");
});
it('properly handles hx-select for full html document situation', function()
{
this.server.respondWith("GET", "/test", "<html><body><div id='d1'>foo</div><div id='d2'>bar</div></body></html>");
var div = make('<div hx-get="/test" hx-select="#d1"></div>');
div.click();
this.server.respond();
div.innerHTML.should.equal("<div id=\"d1\">foo</div>");
});
it('properly settles attributes on interior elements', function(done)
{
this.server.respondWith("GET", "/test", "<div hx-get='/test'><div width='bar' id='d1'></div></div>");
var div = make("<div hx-get='/test' hx-swap='outerHTML settle:10ms'><div id='d1'></div></div>");
div.click();
this.server.respond();
should.equal(byId("d1").getAttribute("width"), null);
setTimeout(function () {
should.equal(byId("d1").getAttribute("width"), "bar");
done();
}, 20);
});
it('properly settles attributes elements with single quotes in id', function(done)
{
this.server.respondWith("GET", "/test", "<div hx-get='/test'><div width='bar' id=\"d1'\"></div></div>");
var div = make("<div hx-get='/test' hx-swap='outerHTML settle:10ms'><div id=\"d1'\"></div></div>");
div.click();
this.server.respond();
should.equal(byId("d1'").getAttribute("width"), null);
setTimeout(function () {
should.equal(byId("d1'").getAttribute("width"), "bar");
done();
}, 20);
});
it('properly settles attributes elements with double quotes in id', function(done)
{
this.server.respondWith("GET", "/test", "<div hx-get='/test'><div width='bar' id='d1\"'></div></div>");
var div = make("<div hx-get='/test' hx-swap='outerHTML settle:10ms'><div id='d1\"'></div></div>");
div.click();
this.server.respond();
should.equal(byId("d1\"").getAttribute("width"), null);
setTimeout(function () {
should.equal(byId("d1\"").getAttribute("width"), "bar");
done();
}, 20);
});
it('properly handles multiple select input', function()
{
var values;
this.server.respondWith("Post", "/test", function (xhr) {
values = getParameters(xhr);
xhr.respond(204, {}, "");
});
var form = make('<form hx-post="/test" hx-trigger="click">' +
'<select id="multiSelect" name="multiSelect" multiple="multiple">'+
'<option id="m1" value="m1">m1</option>'+
'<option id="m2" value="m2">m2</option>'+
'<option id="m3" value="m3">m3</option>'+
'<option id="m4" value="m4">m4</option>'+
'</select>'+
'</form>');
form.click();
this.server.respond();
values.should.deep.equal({});
byId("m1").selected = true;
form.click();
this.server.respond();
values.should.deep.equal({multiSelect:"m1"});
byId("m1").selected = true;
byId("m3").selected = true;
form.click();
this.server.respond();
values.should.deep.equal({multiSelect:["m1", "m3"]});
});
it('properly handles multiple select input when "multiple" attribute is empty string', function()
{
var values;
this.server.respondWith("Post", "/test", function (xhr) {
values = getParameters(xhr);
xhr.respond(204, {}, "");
});
var form = make('<form hx-post="/test" hx-trigger="click">' +
'<select name="multiSelect" id="id_question_list" multiple="" tabindex="-1" aria-hidden="true">' +
'<option id="m1" value="m1">m1</option>'+
'<option id="m2" value="m2">m2</option>'+
'<option id="m3" value="m3">m3</option>'+
'<option id="m4" value="m4">m4</option>'+
'</select>' +
'</form>');
form.click();
this.server.respond();
values.should.deep.equal({});
byId("m1").selected = true;
form.click();
this.server.respond();
values.should.deep.equal({multiSelect:"m1"});
byId("m1").selected = true;
byId("m3").selected = true;
form.click();
this.server.respond();
values.should.deep.equal({multiSelect:["m1", "m3"]});
});
it('properly handles two multiple select inputs w/ same name', function()
{
var values;
this.server.respondWith("Post", "/test", function (xhr) {
values = getParameters(xhr);
xhr.respond(204, {}, "");
});
var form = make('<form hx-post="/test" hx-trigger="click">' +
'<select id="multiSelect" name="multiSelect" multiple="multiple">'+
'<option id="m1" value="m1">m1</option>'+
'<option id="m2" value="m2">m2</option>'+
'<option id="m3" value="m3">m3</option>'+
'<option id="m4" value="m4">m4</option>'+
'</select>'+
'<select id="multiSelect" name="multiSelect" multiple="multiple">'+
'<option id="m5" value="m5">m1</option>'+
'<option id="m6" value="m6">m2</option>'+
'<option id="m7" value="m7">m3</option>'+
'<option id="m8" value="m8">m4</option>'+
'</select>'+
'</form>');
form.click();
this.server.respond();
values.should.deep.equal({});
byId("m1").selected = true;
form.click();
this.server.respond();
values.should.deep.equal({multiSelect:"m1"});
byId("m1").selected = true;
byId("m3").selected = true;
byId("m7").selected = true;
byId("m8").selected = true;
form.click();
this.server.respond();
values.should.deep.equal({multiSelect:["m1", "m3", "m7", "m8"]});
});
it('properly handles checkbox inputs', function()
{
var values;
this.server.respondWith("Post", "/test", function (xhr) {
values = getParameters(xhr);
xhr.respond(204, {}, "");
});
var form = make('<form hx-post="/test" hx-trigger="click">' +
'<input id="cb1" name="c1" value="cb1" type="checkbox">'+
'<input id="cb2" name="c1" value="cb2" type="checkbox">'+
'<input id="cb3" name="c1" value="cb3" type="checkbox">'+
'<input id="cb4" name="c2" value="cb4" type="checkbox">'+
'<input id="cb5" name="c2" value="cb5" type="checkbox">'+
'<input id="cb6" name="c3" value="cb6" type="checkbox">'+
'</form>');
form.click();
this.server.respond();
values.should.deep.equal({});
byId("cb1").checked = true;
form.click();
this.server.respond();
values.should.deep.equal({c1:"cb1"});
byId("cb1").checked = true;
byId("cb2").checked = true;
form.click();
this.server.respond();
values.should.deep.equal({c1:["cb1", "cb2"]});
byId("cb1").checked = true;
byId("cb2").checked = true;
byId("cb3").checked = true;
form.click();
this.server.respond();
values.should.deep.equal({c1:["cb1", "cb2", "cb3"]});
byId("cb1").checked = true;
byId("cb2").checked = true;
byId("cb3").checked = true;
byId("cb4").checked = true;
form.click();
this.server.respond();
values.should.deep.equal({c1:["cb1", "cb2", "cb3"], c2:"cb4"});
byId("cb1").checked = true;
byId("cb2").checked = true;
byId("cb3").checked = true;
byId("cb4").checked = true;
byId("cb5").checked = true;
form.click();
this.server.respond();
values.should.deep.equal({c1:["cb1", "cb2", "cb3"], c2:["cb4", "cb5"]});
byId("cb1").checked = true;
byId("cb2").checked = true;
byId("cb3").checked = true;
byId("cb4").checked = true;
byId("cb5").checked = true;
byId("cb6").checked = true;
form.click();
this.server.respond();
values.should.deep.equal({c1:["cb1", "cb2", "cb3"], c2:["cb4", "cb5"], c3:"cb6"});
byId("cb1").checked = true;
byId("cb2").checked = false;
byId("cb3").checked = true;
byId("cb4").checked = false;
byId("cb5").checked = true;
byId("cb6").checked = true;
form.click();
this.server.respond();
values.should.deep.equal({c1:["cb1", "cb3"], c2:"cb5", c3:"cb6"});
});
it('text nodes dont screw up settling via variable capture', function()
{
this.server.respondWith("GET", "/test", "<div id='d1' hx-trigger='click consume' hx-get='/test2'></div>fooo");
this.server.respondWith("GET", "/test2", "clicked");
var div = make("<div hx-get='/test'/>");
div.click();
this.server.respond();
byId("d1").click();
this.server.respond();
byId("d1").innerHTML.should.equal("clicked");
});
it('script nodes evaluate', function()
{
var globalWasCalled = false;
window.callGlobal = function() {
globalWasCalled = true;
}
try {
this.server.respondWith("GET", "/test", "<div></div><script type='text/javascript'>callGlobal()</script>");
var div = make("<div hx-get='/test'></div>");
div.click();
this.server.respond();
globalWasCalled.should.equal(true);
} finally {
delete window.callGlobal;
}
});
it('stand alone script nodes evaluate', function()
{
var globalWasCalled = false;
window.callGlobal = function() {
globalWasCalled = true;
}
try {
this.server.respondWith("GET", "/test", "<script type='text/javascript'>callGlobal()</script>");
var div = make("<div hx-get='/test'></div>");
div.click();
this.server.respond();
globalWasCalled.should.equal(true);
} finally {
delete window.callGlobal;
}
});
it('script nodes can define global functions', function()
{
try {
window.foo = {}
this.server.respondWith("GET", "/test", "<script type='text/javascript'>foo.bar = function() { return 42 }</script>");
var div = make("<div hx-get='/test'></div>");
div.click();
this.server.respond();
foo.bar().should.equal(42);
} finally {
delete foo;
}
});
it('child script nodes evaluate when children', function()
{
var globalWasCalled = false;
window.callGlobal = function() {
globalWasCalled = true;
}
try {
this.server.respondWith("GET", "/test", "<div><script type='text/javascript'>callGlobal()</script></div>");
var div = make("<div hx-get='/test'></div>");
div.click();
this.server.respond();
globalWasCalled.should.equal(true);
} finally {
delete window.callGlobal;
}
});
it('child script nodes evaluate when first child', function()
{
var globalWasCalled = false;
window.callGlobal = function() {
globalWasCalled = true;
}
try {
this.server.respondWith("GET", "/test", "<script type='text/javascript'>callGlobal()</script><div></div>");
var div = make("<div hx-get='/test'></div>");
div.click();
this.server.respond();
globalWasCalled.should.equal(true);
} finally {
delete window.callGlobal;
}
});
it('child script nodes evaluate when not explicitly marked javascript', function()
{
var globalWasCalled = false;
window.callGlobal = function() {
globalWasCalled = true;
}
try {
this.server.respondWith("GET", "/test", "<div><script>callGlobal()</script></div>");
var div = make("<div hx-get='/test'></div>");
div.click();
this.server.respond();
globalWasCalled.should.equal(true);
} finally {
delete window.callGlobal;
}
});
it('script nodes do not evaluate when explicity marked as something other than javascript', function()
{
var globalWasCalled = false;
window.callGlobal = function() {
globalWasCalled = true;
}
try {
this.server.respondWith("GET", "/test", "<div><script type='text/samplescript'>callGlobal()</script></div>");
var div = make("<div hx-get='/test'></div>");
div.click();
this.server.respond();
globalWasCalled.should.equal(false);
} finally {
delete window.callGlobal;
}
});
it('script nodes evaluate after swap', function()
{
window.callGlobal = function() {
console.log("Here...");
window.tempVal = byId("d1").innerText
}
try {
this.server.respondWith("GET", "/test", "<div><script>callGlobal()</script><div id='d1'>After settle...</div> </div>");
var div = make("<div hx-get='/test'></div>");
div.click();
this.server.respond();
window.tempVal.should.equal("After settle...");
} finally {
delete window.callGlobal;
delete window.tempVal;
}
});
it('script node exceptions do not break rendering', function()
{
this.skip("Rendering does not break, but the exception bubbles up and mocha reports it");
this.server.respondWith("GET", "/test", "clicked<script type='text/javascript'>throw 'foo';</script>");
var div = make("<div hx-get='/test'></div>");
div.click();
this.server.respond();
div.innerText.should.equal("clicked");
console.log(div.innerText);
console.log("here");
});
it('allows empty verb values', function()
{
var path = null;
var div = make("<div hx-get=''/>");
htmx.on(div, "htmx:configRequest", function (evt) {
path = evt.detail.path;
return false;
});
div.click();
this.server.respond();
path.should.not.be.null;
});
it('allows blank verb values', function()
{
var path = null;
var div = make("<div hx-get/>");
htmx.on(div, "htmx:configRequest", function (evt) {
path = evt.detail.path;
return false;
});
div.click();
this.server.respond();
path.should.not.be.null;
});
it('input values are not settle swapped (causes flicker)', function()
{
this.server.respondWith("GET", "/test", "<input id='i1' value='bar'/>");
var input = make("<input id='i1' hx-get='/test' value='foo' hx-swap='outerHTML settle:50' hx-trigger='click'/>");
input.click();
this.server.respond();
input = byId('i1');
input.value.should.equal('bar');
});
it('autofocus attribute works properly', function()
{
this.server.respondWith("GET", "/test", "<input id='i2' value='bar' autofocus/>");
var input = make("<input id='i1' hx-get='/test' value='foo' hx-swap='afterend' hx-trigger='click'/>");
input.focus();
input.click();
document.activeElement.should.equal(input);
this.server.respond();
var input2 = byId('i2');
document.activeElement.should.equal(input2);
});
it('autofocus attribute works properly w/ child', function()
{
this.server.respondWith("GET", "/test", "<div><input id='i2' value='bar' autofocus/></div>");
var input = make("<input id='i1' hx-get='/test' value='foo' hx-swap='afterend' hx-trigger='click'/>");
input.focus();
input.click();
document.activeElement.should.equal(input);
this.server.respond();
var input2 = byId('i2');
document.activeElement.should.equal(input2);
});
it('autofocus attribute works properly w/ true value', function()
{
this.server.respondWith("GET", "/test", "<div><input id='i2' value='bar' autofocus='true'/></div>");
var input = make("<input id='i1' hx-get='/test' value='foo' hx-swap='afterend' hx-trigger='click'/>");
input.focus();
input.click();
document.activeElement.should.equal(input);
this.server.respond();
var input2 = byId('i2');
document.activeElement.should.equal(input2);
});
it('multipart/form-data encoding works', function()
{
this.server.respondWith("POST", "/test", function(xhr){
should.equal(xhr.requestHeaders['Content-Type'], undefined);
if (xhr.requestBody.get) { //IE 11 does not support
xhr.requestBody.get("i1").should.equal('foo');
}
xhr.respond(200, {}, "body: " + xhr.requestBody);
});
var form = make("<form hx-post='/test' hx-encoding='multipart/form-data' hx-trigger='click'>" +
"<input name='i1' id='i1' value='foo'/>" +
"</form>");
form.focus();
form.click();
this.server.respond();
});
it('removed elements do not issue requests', function()
{
var count = 0;
this.server.respondWith("GET", "/test", function (xhr) {
count++;
xhr.respond(200, {}, "");
});
var btn = make('<button hx-get="/test">Click Me!</button>')
htmx.remove(btn);
btn.click();
this.server.respond();
count.should.equal(0);
});
it('title tags update title', function()
{
this.server.respondWith("GET", "/test", function (xhr) {
xhr.respond(200, {}, "<title class=''>htmx rocks!</title>Clicked!");
});
var btn = make('<button hx-get="/test">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerText.should.equal("Clicked!");
window.document.title.should.equal("htmx rocks!");
});
it('svg title tags do not update title', function()
{
var originalTitle = window.document.title
this.server.respondWith("GET", "/test", function (xhr) {
xhr.respond(200, {}, "<svg class=''><title>" + originalTitle + "UPDATE" + "</title></svg>Clicked!");
});
var btn = make('<button hx-get="/test">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerText.should.equal("Clicked!");
window.document.title.should.equal(originalTitle);
});
it('first title tag outside svg title tags updates title', function()
{
var originalTitle = window.document.title
var newTitle = originalTitle + "!!!";
this.server.respondWith("GET", "/test", function (xhr) {
xhr.respond(200, {}, "<title class=''>" + newTitle + "</title><svg class=''><title>foo</title></svg>Clicked!<title class=''>x</title>");
});
var btn = make('<button hx-get="/test">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerText.should.equal("Clicked!");
window.document.title.should.equal(newTitle);
});
it('title update does not URL escape', function()
{
this.server.respondWith("GET", "/test", function (xhr) {
xhr.respond(200, {}, "<title>&lt;/> htmx rocks!</title>Clicked!");
});
var btn = make('<button hx-get="/test">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerText.should.equal("Clicked!");
window.document.title.should.equal("</> htmx rocks!");
});
it('by default 400 content is not swapped', function()
{
this.server.respondWith("GET", "/test", function (xhr) {
xhr.respond(400, {}, "Clicked!");
});
var btn = make('<button hx-get="/test">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerText.should.equal("Click Me!");
});
it('400 content can be swapped if configured to do so', function()
{
var handler = htmx.on("htmx:beforeSwap", function (event) {
if (event.detail.xhr.status === 400) {
event.detail.shouldSwap = true;
}
});
this.server.respondWith("GET", "/test", function (xhr) {
xhr.respond(400, {}, "Clicked!");
});
var btn = make('<button hx-get="/test">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerText.should.equal("Clicked!");
htmx.off("htmx:beforeSwap", handler);
});
it('400 content can be retargeted if configured to do so', function()
{
var handler = htmx.on("htmx:beforeSwap", function (event) {
if (event.detail.xhr.status === 400) {
event.detail.shouldSwap = true;
event.detail.target = byId('d1')
}
});
this.server.respondWith("GET", "/test", function (xhr) {
xhr.respond(400, {}, "Clicked!");
});
var btn = make('<button hx-get="/test">Click Me!</button>')
var div = make('<div id="d1"></div>')
btn.click();
this.server.respond();
div.innerText.should.equal("Clicked!");
htmx.off("htmx:beforeSwap", handler);
});
it('errors are triggered only on 400+', function()
{
var errors = 0;
var handler = htmx.on("htmx:responseError", function(){
errors++;
})
this.server.respondWith("GET", "/test1", function (xhr) {
xhr.respond(204, {}, "Clicked!");
});
this.server.respondWith("GET", "/test2", function (xhr) {
xhr.respond(400, {}, "Clicked!");
});
var btn1 = make('<button hx-get="/test1">Click Me!</button>')
var btn2 = make('<button hx-get="/test2">Click Me!</button>')
btn1.click();
btn2.click();
this.server.respond();
this.server.respond();
errors.should.equal(1);
htmx.off("htmx:responseError", handler);
});
it('content can be modified if configured to do so', function()
{
var handler = htmx.on("htmx:beforeSwap", function (event) {
if (event.detail.xhr.status === 400) {
event.detail.shouldSwap = true;
event.detail.serverResponse = event.detail.serverResponse + "!!";
}
});
this.server.respondWith("GET", "/test", function (xhr) {
xhr.respond(400, {}, "Clicked!");
});
var btn = make('<button hx-get="/test">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerText.should.equal("Clicked!!!");
htmx.off("htmx:beforeSwap", handler);
});
it('scripts w/ src attribute are properly loaded', function(done)
{
try {
this.server.respondWith("GET", "/test", "<script src='setGlobal.js'></script>");
var div = make("<div hx-get='/test'></div>");
div.click();
this.server.respond();
setTimeout(function () {
window.globalWasCalled.should.equal(true);
delete window.globalWasCalled;
done();
}, 400);
} finally {
delete window.globalWasCalled;
}
});
})

View File

@@ -0,0 +1,343 @@
describe("Core htmx API test", function(){
beforeEach(function() {
this.server = makeServer();
clearWorkArea();
});
afterEach(function() {
this.server.restore();
clearWorkArea();
});
it('onLoad is called... onLoad', function(done){
// also tests on/off
this.server.respondWith("GET", "/test", "<div id='d1' hx-get='/test'></div>")
var helper = htmx.onLoad(function (elt) {
elt.setAttribute("foo", "bar");
});
var server = this.server;
setTimeout(function() {
try {
var div = make("<div id='d1' hx-get='/test' hx-swap='outerHTML'></div>");
div.click();
server.respond();
byId("d1").getAttribute("foo").should.equal("bar");
done();
} finally {
htmx.off("htmx:load", helper);
}
}, 10)
});
it('triggers properly', function () {
var div = make("<div/>");
var myEventCalled = false;
var detailStr = "";
htmx.on("myEvent", function(evt){
myEventCalled = true;
detailStr = evt.detail.str;
})
htmx.trigger(div, "myEvent", {str:"foo"})
myEventCalled.should.equal(true);
detailStr.should.equal("foo");
});
it('triggers properly w/ selector', function () {
var div = make("<div id='div1'/>");
var myEventCalled = false;
var detailStr = "";
htmx.on("myEvent", function(evt){
myEventCalled = true;
detailStr = evt.detail.str;
})
htmx.trigger("#div1", "myEvent", {str:"foo"})
myEventCalled.should.equal(true);
detailStr.should.equal("foo");
});
it('triggers with no details properly', function () {
var div = make("<div/>");
var myEventCalled = false;
htmx.on("myEvent", function(evt){
myEventCalled = true;
})
htmx.trigger(div, "myEvent")
myEventCalled.should.equal(true);
});
it('should find properly', function(){
var div = make("<div id='d1' class='c1 c2'>");
div.should.equal(htmx.find("#d1"));
div.should.equal(htmx.find(".c1"));
div.should.equal(htmx.find(".c2"));
div.should.equal(htmx.find(".c1.c2"));
});
it('should find properly from elt', function(){
var div = make("<div><a id='a1'></a><a id='a2'></a></div>");
htmx.find(div, "a").id.should.equal('a1');
});
it('should find all properly', function(){
var div = make("<div class='c1 c2 c3'><div class='c1 c2'><div class='c1'>");
htmx.findAll(".c1").length.should.equal(3);
htmx.findAll(".c2").length.should.equal(2);
htmx.findAll(".c3").length.should.equal(1);
});
it('should find all properly from elt', function(){
var div = make("<div><div class='c1 c2 c3'><div class='c1 c2'><div class='c1'></div>");
htmx.findAll(div, ".c1").length.should.equal(3);
htmx.findAll(div, ".c2").length.should.equal(2);
htmx.findAll(div,".c3").length.should.equal(1);
});
it('should find closest element properly', function () {
var div = make("<div><a id='a1'></a><a id='a2'></a></div>");
var a = htmx.find(div, "a");
htmx.closest(a, "div").should.equal(div);
});
it('should remove element properly', function () {
var div = make("<div><a></a></div>");
var a = htmx.find(div, "a");
htmx.remove(a);
div.innerHTML.should.equal("");
});
it('should remove element properly w/ selector', function () {
var div = make("<div><a id='a1'></a></div>");
var a = htmx.find(div, "a");
htmx.remove("#a1");
div.innerHTML.should.equal("");
});
it('should add class properly', function () {
var div = make("<div></div>");
div.classList.contains("foo").should.equal(false);
htmx.addClass(div, "foo");
div.classList.contains("foo").should.equal(true);
});
it('should add class properly w/ selector', function () {
var div = make("<div id='div1'></div>");
div.classList.contains("foo").should.equal(false);
htmx.addClass("#div1", "foo");
div.classList.contains("foo").should.equal(true);
});
it('should add class properly after delay', function (done) {
var div = make("<div></div>");
div.classList.contains("foo").should.equal(false);
htmx.addClass(div, "foo", 10);
div.classList.contains("foo").should.equal(false);
setTimeout(function () {
div.classList.contains("foo").should.equal(true);
done();
}, 20);
});
it('should remove class properly', function () {
var div = make("<div></div>");
htmx.addClass(div, "foo");
div.classList.contains("foo").should.equal(true);
htmx.removeClass(div, "foo");
div.classList.contains("foo").should.equal(false);
});
it('should remove class properly w/ selector', function () {
var div = make("<div id='div1'></div>");
htmx.addClass(div, "foo");
div.classList.contains("foo").should.equal(true);
htmx.removeClass("#div1", "foo");
div.classList.contains("foo").should.equal(false);
});
it('should add class properly after delay', function (done) {
var div = make("<div></div>");
htmx.addClass(div, "foo");
div.classList.contains("foo").should.equal(true);
htmx.removeClass(div, "foo", 10);
div.classList.contains("foo").should.equal(true);
setTimeout(function () {
div.classList.contains("foo").should.equal(false);
done();
}, 20);
});
it('should toggle class properly', function () {
var div = make("<div></div>");
div.classList.contains("foo").should.equal(false);
htmx.toggleClass(div, "foo");
div.classList.contains("foo").should.equal(true);
htmx.toggleClass(div, "foo");
div.classList.contains("foo").should.equal(false);
});
it('should toggle class properly w/ selector', function () {
var div = make("<div id='div1'></div>");
div.classList.contains("foo").should.equal(false);
htmx.toggleClass("#div1", "foo");
div.classList.contains("foo").should.equal(true);
htmx.toggleClass("#div1", "foo");
div.classList.contains("foo").should.equal(false);
});
it('should take class properly', function () {
var div1 = make("<div></div>");
var div2 = make("<div></div>");
var div3 = make("<div></div>");
div1.classList.contains("foo").should.equal(false);
div2.classList.contains("foo").should.equal(false);
div3.classList.contains("foo").should.equal(false);
htmx.takeClass(div1, "foo");
div1.classList.contains("foo").should.equal(true);
div2.classList.contains("foo").should.equal(false);
div3.classList.contains("foo").should.equal(false);
htmx.takeClass(div2, "foo");
div1.classList.contains("foo").should.equal(false);
div2.classList.contains("foo").should.equal(true);
div3.classList.contains("foo").should.equal(false);
htmx.takeClass(div3, "foo");
div1.classList.contains("foo").should.equal(false);
div2.classList.contains("foo").should.equal(false);
div3.classList.contains("foo").should.equal(true);
});
it('should take class properly w/ selector', function () {
var div1 = make("<div id='div1'></div>");
var div2 = make("<div id='div2'></div>");
var div3 = make("<div id='div3'></div>");
div1.classList.contains("foo").should.equal(false);
div2.classList.contains("foo").should.equal(false);
div3.classList.contains("foo").should.equal(false);
htmx.takeClass("#div1", "foo");
div1.classList.contains("foo").should.equal(true);
div2.classList.contains("foo").should.equal(false);
div3.classList.contains("foo").should.equal(false);
htmx.takeClass("#div2", "foo");
div1.classList.contains("foo").should.equal(false);
div2.classList.contains("foo").should.equal(true);
div3.classList.contains("foo").should.equal(false);
htmx.takeClass("#div3", "foo");
div1.classList.contains("foo").should.equal(false);
div2.classList.contains("foo").should.equal(false);
div3.classList.contains("foo").should.equal(true);
});
it('logAll works', function () {
var initialLogger = htmx.config.logger
try {
htmx.logAll();
} finally {
htmx.config.logger = initialLogger;
}
});
it('eval can be suppressed', function () {
var calledEvent = false;
var handler = htmx.on("htmx:evalDisallowedError", function(){
calledEvent = true;
});
try {
htmx.config.allowEval = false;
should.equal(htmx._("tokenizeString"), undefined);
} finally {
htmx.config.allowEval = true;
htmx.off("htmx:evalDisallowedError", handler);
}
calledEvent.should.equal(true);
});
it('ajax api works', function()
{
this.server.respondWith("GET", "/test", "foo!");
var div = make("<div></div>");
htmx.ajax("GET", "/test", div)
this.server.respond();
div.innerHTML.should.equal("foo!");
});
it('ajax api works by ID', function()
{
this.server.respondWith("GET", "/test", "foo!");
var div = make("<div id='d1'></div>");
htmx.ajax("GET", "/test", "#d1")
this.server.respond();
div.innerHTML.should.equal("foo!");
});
it('ajax api works with swapSpec', function()
{
this.server.respondWith("GET", "/test", "<p class='test'>foo!</p>");
var div = make("<div><div id='target'></div></div>");
htmx.ajax("GET", "/test", {target: "#target", swap:"outerHTML"});
this.server.respond();
div.innerHTML.should.equal('<p class="test">foo!</p>');
});
it('ajax returns a promise', function(done)
{
// in IE we do not return a promise
if (typeof Promise !== "undefined") {
this.server.respondWith("GET", "/test", "foo!");
var div = make("<div id='d1'></div>");
var promise = htmx.ajax("GET", "/test", "#d1");
this.server.respond();
div.innerHTML.should.equal("foo!");
promise.then(function(){
done();
})
} else {
done();
}
});
it('ajax api can pass parameters', function()
{
this.server.respondWith("POST", "/test", function (xhr) {
var params = getParameters(xhr);
params['i1'].should.equal("test");
xhr.respond(200, {}, "Clicked!")
});
var div = make("<div id='d1'></div>");
htmx.ajax("POST", "/test", {target:"#d1", values:{i1: 'test'}})
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('can re-init with new attributes', function () {
this.server.respondWith("PATCH", "/test", "patch");
this.server.respondWith("DELETE", "/test", "delete");
var div = make('<div hx-patch="/test">click me</div>');
div.click();
this.server.respond();
div.innerHTML.should.equal("patch");
div.removeAttribute("hx-patch");
div.setAttribute("hx-delete", "/test");
htmx.process(div);
div.click();
this.server.respond();
div.innerHTML.should.equal("delete");
})
})

View File

@@ -0,0 +1,657 @@
describe("Core htmx Events", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it("htmx:load fires properly", function () {
var called = false;
var handler = htmx.on("htmx:load", function (evt) {
called = true;
});
try {
this.server.respondWith("GET", "/test", "");
this.server.respondWith("GET", "/test", "<div></div>");
var div = make("<div hx-get='/test'></div>");
div.click();
this.server.respond();
should.equal(called, true);
} finally {
htmx.off("htmx:load", handler);
}
});
it("htmx:configRequest allows attribute addition", function () {
var handler = htmx.on("htmx:configRequest", function (evt) {
evt.detail.parameters['param'] = "true";
});
try {
var param = null;
this.server.respondWith("POST", "/test", function (xhr) {
param = getParameters(xhr)['param'];
xhr.respond(200, {}, "");
});
var div = make("<div hx-post='/test'></div>");
div.click();
this.server.respond();
param.should.equal("true");
} finally {
htmx.off("htmx:configRequest", handler);
}
});
it("htmx:configRequest is also dispatched in kebab-case", function () {
var handler = htmx.on("htmx:config-request", function (evt) {
evt.detail.parameters['param'] = "true";
});
try {
var param = null;
this.server.respondWith("POST", "/test", function (xhr) {
param = getParameters(xhr)['param'];
xhr.respond(200, {}, "");
});
var div = make("<div hx-post='/test'></div>");
div.click();
this.server.respond();
param.should.equal("true");
} finally {
htmx.off("htmx:config-request", handler);
}
});
it("events are only dispatched once if kebab and camel case match", function () {
var invoked = 0;
var handler = htmx.on("custom", function () {
invoked = invoked + 1
});
try {
var div = make("<div hx-post='/test'></div>");
htmx.trigger(div, "custom");
invoked.should.equal(1);
} finally {
htmx.off("custom", handler);
}
});
it("htmx:configRequest allows attribute removal", function () {
var param = "foo";
var handler = htmx.on("htmx:configRequest", function (evt) {
delete evt.detail.parameters['param'];
});
try {
this.server.respondWith("POST", "/test", function (xhr) {
param = getParameters(xhr)['param'];
xhr.respond(200, {}, "");
});
var div = make("<form hx-trigger='click' hx-post='/test'><input name='param' value='foo'></form>");
div.click();
this.server.respond();
should.equal(param, undefined);
} finally {
htmx.off("htmx:configRequest", handler);
}
});
it("htmx:configRequest allows header tweaking", function () {
var header = "foo";
var handler = htmx.on("htmx:configRequest", function (evt) {
evt.detail.headers['X-My-Header'] = "bar";
});
try {
this.server.respondWith("POST", "/test", function (xhr) {
header = xhr.requestHeaders['X-My-Header'];
xhr.respond(200, {}, "");
});
var div = make("<form hx-trigger='click' hx-post='/test'><input name='param' value='foo'></form>");
div.click();
this.server.respond();
should.equal(header, "bar");
} finally {
htmx.off("htmx:configRequest", handler);
}
});
it("htmx:configRequest on form gives access to submit event", function () {
var submitterId;
var handler = htmx.on("htmx:configRequest", function (evt) {
evt.detail.headers['X-Submitter-Id'] = evt.detail.triggeringEvent.submitter.id;
});
try {
this.server.respondWith("POST", "/test", function (xhr) {
submitterId = xhr.requestHeaders['X-Submitter-Id']
xhr.respond(200, {}, "");
});
make('<div hx-target="this" hx-boost="true"><form action="/test" method="post"><button type="submit" id="b1">Submit</button><button type="submit" id="b2">Submit</button></form></div>');
var btn = byId('b1');
btn.click();
this.server.respond();
should.equal(submitterId, "b1")
} finally {
htmx.off("htmx:configRequest", handler);
}
});
it("htmx:afterSwap is called when replacing outerHTML", function () {
var called = false;
var handler = htmx.on("htmx:afterSwap", function (evt) {
called = true;
});
try {
this.server.respondWith("POST", "/test", function (xhr) {
xhr.respond(200, {}, "<button>Bar</button>");
});
var div = make("<button hx-post='/test' hx-swap='outerHTML'>Foo</button>");
div.click();
this.server.respond();
should.equal(called, true);
} finally {
htmx.off("htmx:afterSwap", handler);
}
});
it("htmx:oobBeforeSwap is called before swap", function () {
var called = false;
var handler = htmx.on("htmx:oobBeforeSwap", function (evt) {
called = true;
});
try {
this.server.respondWith("POST", "/test", function (xhr) {
xhr.respond(200, {}, "<button>Bar</button><div hx-swap-oob='true' id='d1'>Baz</div>");
});
var oob = make('<div id="d1">Blip</div>');
var div = make("<button hx-post='/test' hx-swap='outerHTML'>Foo</button>");
div.click();
this.server.respond();
byId("d1").innerHTML.should.equal("Baz");
should.equal(called, true);
} finally {
htmx.off("htmx:oobBeforeSwap", handler);
}
});
it("htmx:oobBeforeSwap can abort a swap", function () {
var called = false;
var handler = htmx.on("htmx:oobBeforeSwap", function (evt) {
called = true;
evt.preventDefault();
});
try {
this.server.respondWith("POST", "/test", function (xhr) {
xhr.respond(200, {}, "<button>Bar</button><div hx-swap-oob='true' id='d1'>Baz</div>");
});
var oob = make('<div id="d1">Blip</div>');
var div = make("<button hx-post='/test' hx-swap='outerHTML'>Foo</button>");
div.click();
this.server.respond();
byId("d1").innerHTML.should.equal("Blip");
should.equal(called, true);
} finally {
htmx.off("htmx:oobBeforeSwap", handler);
}
});
it("htmx:oobBeforeSwap is not called on an oob miss", function () {
var called = false;
var handler = htmx.on("htmx:oobBeforeSwap", function (evt) {
called = true;
});
try {
this.server.respondWith("POST", "/test", function (xhr) {
xhr.respond(200, {}, "<button>Bar</button><div hx-swap-oob='true' id='test'>Baz</div>");
});
var div = make("<button hx-post='/test' hx-swap='outerHTML'>Foo</button>");
div.click();
this.server.respond();
should.equal(called, false);
} finally {
htmx.off("htmx:oobBeforeSwap", handler);
}
});
it("htmx:oobAfterSwap is called after swap", function () {
var called = false;
var handler = htmx.on("htmx:oobAfterSwap", function (evt) {
called = true;
});
try {
this.server.respondWith("POST", "/test", function (xhr) {
xhr.respond(200, {}, "<button>Bar</button><div hx-swap-oob='true' id='d1'>Baz</div>");
});
var oob = make('<div id="d1">Blip</div>');
var div = make("<button hx-post='/test' hx-swap='outerHTML'>Foo</button>");
div.click();
this.server.respond();
byId("d1").innerHTML.should.equal("Baz");
should.equal(called, true);
} finally {
htmx.off("htmx:oobAfterSwap", handler);
}
});
it("htmx:oobAfterSwap is not called on an oob miss", function () {
var called = false;
var handler = htmx.on("htmx:oobAfterSwap", function (evt) {
called = true;
});
try {
this.server.respondWith("POST", "/test", function (xhr) {
xhr.respond(200, {}, "<button>Bar</button><div hx-swap-oob='true' id='test'>Baz</div>");
});
var div = make("<button hx-post='/test' hx-swap='outerHTML'>Foo</button>");
div.click();
this.server.respond();
should.equal(called, false);
} finally {
htmx.off("htmx:oobAfterSwap", handler);
}
});
it("htmx:afterSettle is called once when replacing outerHTML", function () {
var called = 0;
var handler = htmx.on("htmx:afterSettle", function (evt) {
called++;
});
try {
this.server.respondWith("POST", "/test", function (xhr) {
xhr.respond(200, {}, "<button>Bar</button>");
});
var div = make("<button hx-post='/test' hx-swap='outerHTML'>Foo</button>");
div.click();
this.server.respond();
should.equal(called, 1);
} finally {
htmx.off("htmx:afterSettle", handler);
}
});
it("htmx:afterSettle is called once when replacing outerHTML with whitespace", function () {
var called = 0;
var handler = htmx.on("htmx:afterSettle", function (evt) {
called++;
});
try {
this.server.respondWith("POST", "/test", function (xhr) {
xhr.respond(200, {}, "<button>Bar</button>\n");
});
var div = make("<button hx-post='/test' hx-swap='outerHTML'>Foo</button>");
div.click();
this.server.respond();
should.equal(called, 1);
} finally {
htmx.off("htmx:afterSettle", handler);
}
});
it("htmx:afterSettle is called twice when replacing outerHTML with whitespace separated elements", function () {
var called = 0;
var handler = htmx.on("htmx:afterSettle", function (evt) {
called++;
});
try {
this.server.respondWith("POST", "/test", function (xhr) {
xhr.respond(200, {}, "<button>Bar</button>\n <a>Foo</a>");
});
var div = make("<button hx-post='/test' hx-swap='outerHTML'>Foo</button>");
div.click();
this.server.respond();
should.equal(called, 2);
} finally {
htmx.off("htmx:afterSettle", handler);
}
});
it("htmx:afterRequest is called after a successful request", function () {
var called = false;
var handler = htmx.on("htmx:afterRequest", function (evt) {
called = true;
});
try {
this.server.respondWith("POST", "/test", function (xhr) {
xhr.respond(200, {}, "");
});
var div = make("<button hx-post='/test'>Foo</button>");
div.click();
this.server.respond();
should.equal(called, true);
} finally {
htmx.off("htmx:afterRequest", handler);
}
});
it("htmx:afterOnLoad is called after a successful request", function () {
var called = false;
var handler = htmx.on("htmx:afterOnLoad", function (evt) {
called = true;
});
try {
this.server.respondWith("POST", "/test", function (xhr) {
xhr.respond(200, {}, "");
});
var div = make("<button hx-post='/test'>Foo</button>");
div.click();
this.server.respond();
should.equal(called, true);
} finally {
htmx.off("htmx:afterOnLoad", handler);
}
});
it("htmx:afterRequest is called after a failed request", function () {
var called = false;
var handler = htmx.on("htmx:afterRequest", function (evt) {
called = true;
});
try {
this.server.respondWith("POST", "/test", function (xhr) {
xhr.respond(500, {}, "");
});
var div = make("<button hx-post='/test'>Foo</button>");
div.click();
this.server.respond();
should.equal(called, true);
} finally {
htmx.off("htmx:afterRequest", handler);
}
});
it("htmx:sendError is called after a failed request", function (done) {
var called = false;
var handler = htmx.on("htmx:sendError", function (evt) {
called = true;
});
this.server.restore(); // turn off server mock so connection doesn't work
var div = make("<button hx-post='file://foo'>Foo</button>");
div.click();
setTimeout(function () {
htmx.off("htmx:sendError", handler);
should.equal(called, true);
done();
}, 30);
});
it("htmx:afterRequest is called when replacing outerHTML", function () {
var called = false;
var handler = htmx.on("htmx:afterRequest", function (evt) {
called = true;
});
try {
this.server.respondWith("POST", "/test", function (xhr) {
xhr.respond(200, {}, "<button>Bar</button>");
});
var div = make("<button hx-post='/test' hx-swap='outerHTML'>Foo</button>");
div.click();
this.server.respond();
should.equal(called, true);
} finally {
htmx.off("htmx:afterRequest", handler);
}
});
it("htmx:afterOnLoad is called when replacing outerHTML", function () {
var called = false;
var handler = htmx.on("htmx:afterOnLoad", function (evt) {
called = true;
});
try {
this.server.respondWith("POST", "/test", function (xhr) {
xhr.respond(200, {}, "<button>Bar</button>");
});
var div = make("<button hx-post='/test' hx-swap='outerHTML'>Foo</button>");
div.click();
this.server.respond();
should.equal(called, true);
} finally {
htmx.off("htmx:afterOnLoad", handler);
}
});
it("htmx:beforeProcessNode is called when replacing outerHTML", function () {
var called = false;
var handler = htmx.on("htmx:beforeProcessNode", function (evt) {
called = true;
});
try {
this.server.respondWith("POST", "/test", function (xhr) {
xhr.respond(200, {}, "<button>Bar</button>");
});
var div = make("<button hx-post='/test' hx-swap='outerHTML'>Foo</button>");
div.click();
this.server.respond();
should.equal(called, true);
} finally {
htmx.off("htmx:beforeProcessNode", handler);
}
});
it("htmx:beforeProcessNode allows htmx attribute tweaking", function () {
var called = false;
var handler = htmx.on("htmx:beforeProcessNode", function (evt) {
evt.target.setAttribute("hx-post", "/success")
called = true;
});
try {
this.server.respondWith("POST", "/success", function (xhr) {
xhr.respond(200, {}, "<button>Bar</button>");
});
var div = make("<button hx-post='/fail' hx-swap='outerHTML'>Foo</button>");
div.click();
this.server.respond();
should.equal(called, true);
} finally {
htmx.off("htmx:beforeProcessNode", handler);
}
});
it("htmx:afterProcessNode is called after replacing outerHTML", function () {
var called = false;
var handler = htmx.on("htmx:afterProcessNode", function (evt) {
called = true;
});
try {
this.server.respondWith("POST", "/test", function (xhr) {
xhr.respond(200, {}, "<button>Bar</button>");
});
var div = make("<button hx-post='/test' hx-swap='outerHTML'>Foo</button>");
div.click();
this.server.respond();
should.equal(called, true);
} finally {
htmx.off("htmx:afterProcessNode", handler);
}
});
it("htmx:afterRequest is called when targeting a parent div", function () {
var called = false;
var handler = htmx.on("htmx:afterRequest", function (evt) {
called = true;
});
try {
this.server.respondWith("POST", "/test", function (xhr) {
xhr.respond(200, {}, "<button>Bar</button>");
});
var div = make("<div hx-target='this'><button id='b1' hx-post='/test' hx-swap='outerHTML'>Foo</button></div>");
var button = byId('b1');
button.click();
this.server.respond();
should.equal(called, true);
} finally {
htmx.off("htmx:afterRequest", handler);
}
});
it("adding an error in htmx:configRequest stops the request", function () {
try {
var handler = htmx.on("htmx:configRequest", function (evt) {
evt.detail.errors.push("An error");
});
var request = false;
this.server.respondWith("POST", "/test", function (xhr) {
request = true;
xhr.respond(200, {}, "<button>Bar</button>");
});
var div = make("<button hx-post='/test' hx-swap='outerHTML'>Foo</button>");
div.click();
this.server.respond();
should.equal(request, false);
} finally {
htmx.off("htmx:configRequest", handler);
}
});
it("preventDefault() in htmx:configRequest stops the request", function () {
try {
var handler = htmx.on("htmx:configRequest", function (evt) {
evt.detail.errors.push("An error");
});
var request = false;
this.server.respondWith("POST", "/test", function (xhr) {
request = true;
xhr.respond(200, {}, "<button>Bar</button>");
});
var div = make("<button hx-post='/test' hx-swap='outerHTML'>Foo</button>");
div.click();
this.server.respond();
should.equal(request, false);
} finally {
htmx.off("htmx:configRequest", handler);
}
});
it("preventDefault() in the htmx:beforeRequest event cancels the request", function () {
try {
var handler = htmx.on("htmx:beforeRequest", function (evt) {
evt.preventDefault();
});
var request = false;
this.server.respondWith("POST", "/test", function (xhr) {
request = true;
xhr.respond(200, {}, "<button>Bar</button>");
});
var div = make("<button hx-post='/test' hx-swap='outerHTML'>Foo</button>");
div.click();
this.server.respond();
should.equal(request, false);
} finally {
htmx.off("htmx:beforeRequest", handler);
}
});
it("preventDefault() in the htmx:beforeOnLoad event cancels the swap", function () {
try {
var handler = htmx.on("htmx:beforeOnLoad", function (evt) {
evt.preventDefault();
});
var request = false;
this.server.respondWith("POST", "/test", function (xhr) {
request = true;
xhr.respond(200, {}, "Bar");
});
var div = make("<button hx-post='/test'>Foo</button>");
div.click();
this.server.respond();
should.equal(request, true);
div.innerText.should.equal("Foo");
} finally {
htmx.off("htmx:beforeOnLoad", handler);
}
});
it("htmx:afterRequest event contains 'successful' and 'failed' properties indicating success after successful request", function () {
var successful = false;
var failed = true;
var handler = htmx.on("htmx:afterRequest", function (evt) {
successful = evt.detail.successful;
failed = evt.detail.failed;
});
try {
this.server.respondWith("POST", "/test", function (xhr) {
xhr.respond(200, {}, "");
});
var div = make("<button hx-post='/test'>Foo</button>");
div.click();
this.server.respond();
should.equal(successful, true);
should.equal(failed, false);
} finally {
htmx.off("htmx:afterRequest", handler);
}
});
it("htmx:afterRequest event contains 'successful' and 'failed' properties indicating failure after failed request", function () {
var successful = true;
var failed = false;
var handler = htmx.on("htmx:afterRequest", function (evt) {
successful = evt.detail.successful;
failed = evt.detail.failed;
});
try {
this.server.respondWith("POST", "/test", function (xhr) {
xhr.respond(500, {}, "");
});
var div = make("<button hx-post='/test'>Foo</button>");
div.click();
this.server.respond();
should.equal(successful, false);
should.equal(failed, true);
} finally {
htmx.off("htmx:afterRequest", handler);
}
});
it("htmx:confirm can cancel request", function () {
var allow = false;
var handler = htmx.on("htmx:confirm", function (evt) {
evt.preventDefault();
if (allow) {
evt.detail.issueRequest();
}
});
try {
this.server.respondWith("GET", "/test", "updated");
var div = make("<div hx-get='/test'></div>");
div.click();
this.server.respond();
div.innerHTML.should.equal("");
allow = true;
div.click();
this.server.respond();
div.innerHTML.should.equal("updated");
} finally {
htmx.off("htmx:load", handler);
}
});
it("has updated target available when target set via htmx:beforeSwap", function () {
var targetWasUpdatedInAfterSwapHandler = false;
var beforeSwapHandler = htmx.on("htmx:beforeSwap", function (evt) {
console.log("beforeSwap", evt.detail.target, byId('d2'));
evt.detail.target = byId('d2');
});
var afterSwapHandler = htmx.on("htmx:afterSwap", function (evt) {
console.log("afterSwap", evt.detail.target, byId('d2'));
targetWasUpdatedInAfterSwapHandler = evt.detail.target === byId('d2');
});
try {
this.server.respondWith("GET", "/test", "updated");
make("<div id='d0' hx-get='/test' hx-target='#d1'></div><div id='d1'></div><div id='d2'></div>");
var div = byId('d0');
div.click();
this.server.respond();
targetWasUpdatedInAfterSwapHandler.should.equal(true);
} finally {
htmx.off("htmx:beforeSwap", beforeSwapHandler);
htmx.off("htmx:afterSwap", afterSwapHandler);
}
});
});

View File

@@ -0,0 +1,229 @@
describe("Core htmx AJAX headers", function () {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it("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, {}, "");
});
var div = make('<div hx-get="/test"></div>');
div.click();
this.server.respond();
})
it("should include the HX-Trigger header", function () {
this.server.respondWith("GET", "/test", function (xhr) {
xhr.requestHeaders['HX-Trigger'].should.equal('d1');
xhr.respond(200, {}, "");
});
var div = make('<div id="d1" hx-get="/test"></div>');
div.click();
this.server.respond();
})
it("should include the HX-Trigger-Name header", function () {
this.server.respondWith("GET", "/test", function (xhr) {
xhr.requestHeaders['HX-Trigger-Name'].should.equal('n1');
xhr.respond(200, {}, "");
});
var div = make('<button name="n1" hx-get="/test"></button>');
div.click();
this.server.respond();
})
it("should include the HX-Target header", function () {
this.server.respondWith("GET", "/test", function (xhr) {
xhr.requestHeaders['HX-Target'].should.equal('d1');
xhr.respond(200, {}, "");
});
var div = make('<div hx-target="#d1" hx-get="/test"></div><div id="d1" ></div>');
div.click();
this.server.respond();
})
it("should handle simple string HX-Trigger response header properly", function () {
this.server.respondWith("GET", "/test", [200, {"HX-Trigger": "foo"}, ""]);
var div = make('<div hx-get="/test"></div>');
var invokedEvent = false;
div.addEventListener("foo", function (evt) {
invokedEvent = true;
});
div.click();
this.server.respond();
invokedEvent.should.equal(true);
})
it("should handle dot path HX-Trigger response header properly", function () {
this.server.respondWith("GET", "/test", [200, {"HX-Trigger": "foo.bar"}, ""]);
var div = make('<div hx-get="/test"></div>');
var invokedEvent = false;
div.addEventListener("foo.bar", function (evt) {
invokedEvent = true;
});
div.click();
this.server.respond();
invokedEvent.should.equal(true);
})
it("should handle simple string HX-Trigger response header in different case properly", function () {
this.server.respondWith("GET", "/test", [200, {"hx-trigger": "foo"}, ""]);
var div = make('<div hx-get="/test"></div>');
var invokedEvent = false;
div.addEventListener("foo", function (evt) {
invokedEvent = true;
});
div.click();
this.server.respond();
invokedEvent.should.equal(true);
})
it("should handle a namespaced HX-Trigger response header properly", function () {
this.server.respondWith("GET", "/test", [200, {"HX-Trigger": "namespace:foo"}, ""]);
var div = make('<div hx-get="/test"></div>');
var invokedEvent = false;
div.addEventListener("namespace:foo", function (evt) {
invokedEvent = true;
});
div.click();
this.server.respond();
invokedEvent.should.equal(true);
})
it("should handle basic JSON HX-Trigger response header properly", function () {
this.server.respondWith("GET", "/test", [200, {"HX-Trigger": "{\"foo\":null}"}, ""]);
var div = make('<div hx-get="/test"></div>');
var invokedEvent = false;
div.addEventListener("foo", function (evt) {
invokedEvent = true;
should.equal(null, evt.detail.value);
evt.detail.elt.should.equal(div);
});
div.click();
this.server.respond();
invokedEvent.should.equal(true);
})
it("should handle JSON with array arg HX-Trigger response header properly", function () {
this.server.respondWith("GET", "/test", [200, {"HX-Trigger": "{\"foo\":[1, 2, 3]}"}, ""]);
var div = make('<div hx-get="/test"></div>');
var invokedEvent = false;
div.addEventListener("foo", function (evt) {
invokedEvent = true;
evt.detail.elt.should.equal(div);
evt.detail.value.should.deep.equal([1, 2, 3]);
});
div.click();
this.server.respond();
invokedEvent.should.equal(true);
})
it("should handle JSON with array arg HX-Trigger response header properly", function () {
this.server.respondWith("GET", "/test", [200, {"HX-Trigger": "{\"foo\":{\"a\":1, \"b\":2}}"}, ""]);
var div = make('<div hx-get="/test"></div>');
var invokedEvent = false;
div.addEventListener("foo", function (evt) {
invokedEvent = true;
evt.detail.elt.should.equal(div);
evt.detail.a.should.equal(1);
evt.detail.b.should.equal(2);
});
div.click();
this.server.respond();
invokedEvent.should.equal(true);
})
it("should survive malformed JSON in HX-Trigger response header", function () {
this.server.respondWith("GET", "/test", [200, {"HX-Trigger": "{not: valid}"}, ""]);
var div = make('<div hx-get="/test"></div>');
div.click();
this.server.respond();
})
it("should handle simple string HX-Trigger response header properly w/ outerHTML swap", function () {
this.server.respondWith("GET", "/test", [200, {"HX-Trigger": "foo"}, ""]);
var div = make('<div hx-swap="outerHTML" hx-get="/test"></div>');
var invokedEvent = false;
var handler = htmx.on('foo', function (evt) {
invokedEvent = true;
});
div.click();
this.server.respond();
invokedEvent.should.equal(true);
htmx.off('foo', handler);
})
it("should handle HX-Retarget", function () {
this.server.respondWith("GET", "/test", [200, {"HX-Retarget": "#d2"}, "Result"]);
var div1 = make('<div id="d1" hx-get="/test"></div>');
var div2 = make('<div id="d2"></div>');
div1.click();
this.server.respond();
div1.innerHTML.should.equal("");
div2.innerHTML.should.equal("Result");
})
it("should handle HX-Reswap", function () {
this.server.respondWith("GET", "/test", [200, {"HX-Reswap": "innerHTML"}, "Result"]);
var div1 = make('<div id="d1" hx-get="/test" hx-swap="outerHTML"></div>');
div1.click();
this.server.respond();
div1.innerHTML.should.equal("Result");
})
it("should handle simple string HX-Trigger-After-Swap response header properly w/ outerHTML swap", function () {
this.server.respondWith("GET", "/test", [200, {"HX-Trigger-After-Swap": "foo"}, ""]);
var div = make('<div hx-swap="outerHTML" hx-get="/test"></div>');
var invokedEvent = false;
var handler = htmx.on('foo', function (evt) {
invokedEvent = true;
});
div.click();
this.server.respond();
invokedEvent.should.equal(true);
htmx.off('foo', handler);
})
it("should handle simple string HX-Trigger-After-Settle response header properly w/ outerHTML swap", function () {
this.server.respondWith("GET", "/test", [200, {"HX-Trigger-After-Settle": "foo"}, ""]);
var div = make('<div hx-swap="outerHTML" hx-get="/test"></div>');
var invokedEvent = false;
var handler = htmx.on('foo', function (evt) {
invokedEvent = true;
});
div.click();
this.server.respond();
invokedEvent.should.equal(true);
htmx.off('foo', handler);
})
it("should change body content on HX-Location", function () {
this.server.respondWith("GET", "/test", [200, {"HX-Location": '{"path":"/test2", "target":"#testdiv"}'}, ""]);
this.server.respondWith("GET", "/test2", [200, {}, "<div>Yay! Welcome</div>"]);
var div = make('<div id="testdiv" hx-trigger="click" hx-get="/test"></div>');
div.click();
this.server.respond();
this.server.respond();
div.innerHTML.should.equal('<div>Yay! Welcome</div>');
})
});

View File

@@ -0,0 +1,135 @@
describe("Core htmx internals Tests", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it("makeFragment works with janky stuff", function(){
htmx._("makeFragment")("<html></html>").tagName.should.equal("BODY");
htmx._("makeFragment")("<html><body></body></html>").tagName.should.equal("BODY");
//NB - the tag name should be the *parent* element hosting the HTML since we use the fragment children
// for the swap
htmx._("makeFragment")("<td></td>").tagName.should.equal("TR");
htmx._("makeFragment")("<thead></thead>").tagName.should.equal("TABLE");
htmx._("makeFragment")("<col></col>").tagName.should.equal("COLGROUP");
htmx._("makeFragment")("<tr></tr>").tagName.should.equal("TBODY");
})
it("makeFragment works with template wrapping", function(){
htmx.config.useTemplateFragments = true;
try {
htmx._("makeFragment")("<html></html>").children.length.should.equal(0);
htmx._("makeFragment")("<html><body></body></html>").children.length.should.equal(0);
var fragment = htmx._("makeFragment")("<td></td>");
fragment.firstElementChild.tagName.should.equal("TD");
fragment = htmx._("makeFragment")("<thead></thead>");
fragment.firstElementChild.tagName.should.equal("THEAD");
fragment = htmx._("makeFragment")("<col></col>");
fragment.firstElementChild.tagName.should.equal("COL");
fragment = htmx._("makeFragment")("<tr></tr>");
fragment.firstElementChild.tagName.should.equal("TR");
} finally {
htmx.config.useTemplateFragments = false;
}
})
it("makeFragment works with template wrapping and funky combos", function(){
htmx.config.useTemplateFragments = true;
try {
var fragment = htmx._("makeFragment")("<td></td><div></div>");
fragment.children[0].tagName.should.equal("TD");
fragment.children[1].tagName.should.equal("DIV");
} finally {
htmx.config.useTemplateFragments = false;
}
})
it("set header works with non-ASCII values", function(){
var xhr = new XMLHttpRequest();
xhr.open("GET", "/dummy");
htmx._("safelySetHeaderValue")(xhr, "Example", "привет");
// unfortunately I can't test the value :/
// https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest
})
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(null)).to.be.undefined
chai.expect(htmx.parseInterval("")).to.be.undefined
chai.expect(htmx.parseInterval("undefined")).to.be.undefined
chai.expect(htmx.parseInterval("true")).to.be.undefined
chai.expect(htmx.parseInterval("false")).to.be.undefined
})
it("tokenizes correctly", function() {
chai.expect(htmx._("tokenizeString")("a,")).to.be.deep.equal(['a', ',']);
chai.expect(htmx._("tokenizeString")("aa,")).to.be.deep.equal(['aa', ',']);
chai.expect(htmx._("tokenizeString")("aa,aa")).to.be.deep.equal(['aa', ',', 'aa']);
chai.expect(htmx._("tokenizeString")("aa.aa")).to.be.deep.equal(['aa', '.', 'aa']);
})
it("tags respond correctly to shouldCancel", function() {
var anchorThatShouldCancel = make("<a href='/foo'></a>");
htmx._("shouldCancel")({type:'click'}, anchorThatShouldCancel).should.equal(true);
var anchorThatShouldCancel = make("<a href='#'></a>");
htmx._("shouldCancel")({type:'click'}, anchorThatShouldCancel).should.equal(true);
var anchorThatShouldNotCancel = make("<a href='#foo'></a>");
htmx._("shouldCancel")({type:'click'}, anchorThatShouldNotCancel).should.equal(false);
var form = make("<form></form>");
htmx._("shouldCancel")({type:'submit'}, form).should.equal(true);
var form = make("<form><input id='i1' type='submit'></form>");
var input = byId("i1");
htmx._("shouldCancel")({type:'click'}, input).should.equal(true);
var form = make("<form><button id='b1' type='submit'></form>");
var button = byId("b1");
htmx._("shouldCancel")({type:'click'}, button).should.equal(true);
})
it("unset properly unsets a given attribute", function(){
make("<div foo='1'><div foo='2'><div foo='unset' id='d1'></div></div></div>");
var div = byId("d1");
should.equal(undefined, htmx._("getClosestAttributeValue")(div, "foo"));
})
it("unset properly unsets a given attribute on a parent", function(){
make("<div foo='1'><div foo='unset'><div id='d1'></div></div></div>");
var div = byId("d1");
should.equal(undefined, htmx._("getClosestAttributeValue")(div, "foo"));
})
it("unset does not unset a value below it in the hierarchy", function(){
make("<div foo='unset'><div foo='2'><div id='d1'></div></div></div>");
var div = byId("d1");
should.equal("2", htmx._("getClosestAttributeValue")(div, "foo"));
})
it("encoding values respects enctype on forms", function(){
var form = make("<form enctype='multipart/form-data'></form>");
var value = htmx._("encodeParamsForBody")(null, form, {});
(value instanceof FormData).should.equal(true);
})
});

View File

@@ -0,0 +1,181 @@
describe("Core htmx Parameter Handling", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('Input includes value', function () {
var input = make('<input name="foo" value="bar"/>');
var vals = htmx._('getInputValues')(input).values;
vals['foo'].should.equal('bar');
})
it('Input includes value on get', function () {
var input = make('<input name="foo" value="bar"/>');
var vals = htmx._('getInputValues')(input, "get").values;
vals['foo'].should.equal('bar');
})
it('Input includes form', function () {
var form = make('<form><input id="i1" name="foo" value="bar"/><input id="i2" name="do" value="rey"/></form>');
var input = byId('i1');
var vals = htmx._('getInputValues')(input).values;
vals['foo'].should.equal('bar');
vals['do'].should.equal('rey');
})
it('Input doesnt include form on get', function () {
var form = make('<form><input id="i1" name="foo" value="bar"/><input id="i2" name="do" value="rey"/></form>');
var input = byId('i1');
var vals = htmx._('getInputValues')(input, 'get').values;
vals['foo'].should.equal('bar');
should.equal(vals['do'], undefined);
})
it('non-input includes form', function () {
var form = make('<form><div id="d1"/><input id="i2" name="do" value="rey"/></form>');
var div = byId('d1');
var vals = htmx._('getInputValues')(div, "post").values;
vals['do'].should.equal('rey');
})
it('non-input doesnt include form on get', function () {
var form = make('<form><div id="d1"/><input id="i2" name="do" value="rey"/></form>');
var div = byId('d1');
var vals = htmx._('getInputValues')(div, "get").values;
should.equal(vals['do'], undefined);
})
it('Basic form works on get', function () {
var form = make('<form><input id="i1" name="foo" value="bar"/><input id="i2" name="do" value="rey"/></form>');
var vals = htmx._('getInputValues')(form, 'get').values;
vals['foo'].should.equal('bar');
vals['do'].should.equal('rey');
})
it('Basic form works on non-get', function () {
var form = make('<form><input id="i1" name="foo" value="bar"/><input id="i2" name="do" value="rey"/></form>');
var vals = htmx._('getInputValues')(form, 'post').values;
vals['foo'].should.equal('bar');
vals['do'].should.equal('rey');
})
it('Double values are included as array', function () {
var form = make('<form><input id="i1" name="foo" value="bar"/><input id="i2" name="do" value="rey"/><input id="i2" name="do" value="rey"/></form>');
var vals = htmx._('getInputValues')(form).values;
vals['foo'].should.equal('bar');
vals['do'].should.deep.equal(['rey', 'rey']);
})
it('Double values are included as array in correct order', function () {
var form = make('<form><input id="i1" name="foo" value="bar"/><input id="i2" name="do" value="rey1"/><input id="i3" name="do" value="rey2"/></form>');
var vals = htmx._('getInputValues')(byId("i3")).values;
vals['foo'].should.equal('bar');
vals['do'].should.deep.equal(['rey1', 'rey2']);
})
it('Double empty values are included as array in correct order', function () {
var form = make('<form><input id="i1" name="do" value=""/><input id="i2" name="do" value="rey"/><input id="i3" name="do" value=""/></form>');
var vals = htmx._('getInputValues')(byId("i3")).values;
vals['do'].should.deep.equal(['', 'rey', '']);
})
it('hx-include works with form', function () {
var form = make('<form id="f1"><input id="i1" name="foo" value="bar"/><input id="i2" name="do" value="rey"/><input id="i2" name="do" value="rey"/></form>');
var div = make('<div hx-include="#f1"></div>');
var vals = htmx._('getInputValues')(div).values;
vals['foo'].should.equal('bar');
vals['do'].should.deep.equal(['rey', 'rey']);
})
it('hx-include works with input', function () {
var form = make('<form id="f1"><input id="i1" name="foo" value="bar"/><input id="i2" name="do" value="rey"/><input id="i2" name="do" value="rey"/></form>');
var div = make('<div hx-include="#i1"></div>');
var vals = htmx._('getInputValues')(div).values;
vals['foo'].should.equal('bar');
should.equal(vals['do'], undefined);
})
it('hx-include works with two inputs', function () {
var form = make('<form id="f1"><input id="i1" name="foo" value="bar"/><input id="i2" name="do" value="rey"/><input id="i2" name="do" value="rey"/></form>');
var div = make('<div hx-include="#i1, #i2"></div>');
var vals = htmx._('getInputValues')(div).values;
vals['foo'].should.equal('bar');
vals['do'].should.deep.equal(['rey', 'rey']);
})
it('hx-include works with two inputs, plus form', function () {
var form = make('<form id="f1"><input id="i1" name="foo" value="bar"/><input id="i2" name="do" value="rey"/><input id="i2" name="do" value="rey"/></form>');
var div = make('<div hx-include="#i1, #i2, #f1"></div>');
var vals = htmx._('getInputValues')(div).values;
vals['foo'].should.equal('bar');
vals['do'].should.deep.equal(['rey', 'rey']);
})
it('correctly URL escapes values', function () {
htmx._("urlEncode")({}).should.equal("");
htmx._("urlEncode")({"foo": "bar"}).should.equal("foo=bar");
htmx._("urlEncode")({"foo": "bar", "do" : "rey"}).should.equal("foo=bar&do=rey");
htmx._("urlEncode")({"foo": "bar", "do" : ["rey", "blah"]}).should.equal("foo=bar&do=rey&do=blah");
});
it('form includes last focused button', function () {
var form = make('<form hx-get="/foo"><input id="i1" name="foo" value="bar"/><input id="i2" name="do" value="rey"/><button id="b1" name="btn" value="bar"></button></form>');
var input = byId('i1');
var button = byId('b1');
button.focus();
var vals = htmx._('getInputValues')(form).values;
vals['foo'].should.equal('bar');
vals['do'].should.equal('rey');
vals['btn'].should.equal('bar');
})
it('form includes last focused submit', function () {
var form = make('<form hx-get="/foo"><input id="i1" name="foo" value="bar"/><input id="i2" name="do" value="rey"/><input type="submit" id="s1" name="s1" value="bar"/></form>');
var input = byId('i1');
var button = byId('s1');
button.focus();
var vals = htmx._('getInputValues')(form).values;
vals['foo'].should.equal('bar');
vals['do'].should.equal('rey');
vals['s1'].should.equal('bar');
})
it('form does not include button when focus is lost', function () {
var form = make('<form hx-get="/foo"><input id="i1" name="foo" value="bar"/><input id="i2" name="do" value="rey"/><input type="submit" id="s1" name="s1" value="bar"/></form>');
var input = byId('i1');
var button = byId('s1');
button.focus();
input.focus();
var vals = htmx._('getInputValues')(form).values;
vals['foo'].should.equal('bar');
vals['do'].should.equal('rey');
should.equal(vals['s1'], undefined);
})
it('form does not include button when focus is lost outside of form', function () {
var form = make('<form hx-get="/foo"><input id="i1" name="foo" value="bar"/><input id="i2" name="do" value="rey"/><input type="submit" id="s1" name="s1" value="bar"/></form>');
var anchor = make('<button id="a1"></button>');
var button = byId('s1');
button.focus();
anchor.focus();
var vals = htmx._('getInputValues')(form).values;
vals['foo'].should.equal('bar');
vals['do'].should.equal('rey');
should.equal(vals['s1'], undefined);
})
it('form includes button name and value if button has nested elements when clicked', function () {
var form = make('<form hx-get="/foo"><input id="i1" name="foo" value="bar"/><button type="submit" id="btn1" name="do" value="rey"><div id="div1"><span id="span1"></span></div></button></form>');
var nestedElt = byId('span1');
nestedElt.click();
var vals = htmx._('getInputValues')(form).values;
vals['do'].should.equal('rey');
})
});

View File

@@ -0,0 +1,64 @@
describe("Core htmx perf Tests", function() {
var HTMX_HISTORY_CACHE_NAME = "htmx-history-cache";
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
localStorage.removeItem(HTMX_HISTORY_CACHE_NAME);
});
afterEach(function () {
this.server.restore();
clearWorkArea();
localStorage.removeItem(HTMX_HISTORY_CACHE_NAME);
});
function stringRepeat(str, num) {
num = Number(num);
var result = '';
while (true) {
if (num & 1) { // (1)
result += str;
}
num >>>= 1; // (2)
if (num <= 0) break;
str += str;
}
return result;
}
it("history implementation should be fast", function(){
// create an entry with a large content string (256k) and see how fast we can write and read it
// to local storage as a single entry
var entry = {url: stringRepeat("x", 32), content:stringRepeat("x", 256*1024)}
var array = [];
for (var i = 0; i < 10; i++) {
array.push(entry);
}
var start = performance.now();
var string = JSON.stringify(array);
localStorage.setItem(HTMX_HISTORY_CACHE_NAME, string);
var reReadString = localStorage.getItem(HTMX_HISTORY_CACHE_NAME);
var finalJson = JSON.parse(reReadString);
var end = performance.now();
var timeInMs = end - start;
chai.assert(timeInMs < 300, "Should take less than 300ms on most platforms");
})
it("history snapshot cleaning should be fast", function(){
//
var workArea = getWorkArea();
var html = "<div class='foo bar'>Yay, really large HTML documents are fun!</div>\n";
html = stringRepeat(html, 5 * 1024); // ~350K in size, about the size of CNN's body tag :p
workArea.insertAdjacentHTML("beforeend", html)
var start = performance.now();
htmx._("cleanInnerHtmlForHistory")(workArea);
var end = performance.now();
var timeInMs = end - start;
chai.assert(timeInMs < 50, "Should take less than 50ms on most platforms");
})
})

View File

@@ -0,0 +1,211 @@
describe("Core htmx Regression Tests", function(){
beforeEach(function() {
this.server = makeServer();
clearWorkArea();
});
afterEach(function() {
this.server.restore();
clearWorkArea();
});
it('SVGs process properly in IE11', function()
{
var btn = make('<svg onclick="document.getElementById(\'contents\').classList.toggle(\'show\')" class="hamburger" viewBox="0 0 100 80" width="25" height="25" style="margin-bottom:-5px">\n' +
'<rect width="100" height="20" style="fill:rgb(52, 101, 164)" rx="10"></rect>\n' +
'<rect y="30" width="100" height="20" style="fill:rgb(52, 101, 164)" rx="10"></rect>\n' +
'<rect y="60" width="100" height="20" style="fill:rgb(52, 101, 164)" rx="10"></rect>\n' +
'</svg>')
});
it ('Handles https://github.com/bigskysoftware/htmx/issues/4 properly', function() {
this.server.respondWith("GET", "/index2a.php",
"<div id='message' hx-swap-oob='true'>I came from message oob swap I should be second</div>" +
"<div id='message2' hx-swap-oob='true'>I came from a message2 oob swap I should be third but I am in the wrong spot</div>" +
"I'm page2 content (non-swap) I should be first")
var h1 = make("" +
"<div id='page2' ></div>" +
"<div id='message'></div>" +
"<div id='message2'></div>" +
"<h1 hx-get='/index2a.php' hx-target='#page2' hx-trigger='click'>Kutty CLICK ME</h1>")
h1.click();
this.server.respond();
htmx.find("#page2").innerHTML.should.equal("I'm page2 content (non-swap) I should be first")
htmx.find("#message").innerHTML.should.equal("I came from message oob swap I should be second")
htmx.find("#message2").innerHTML.should.equal("I came from a message2 oob swap I should be third but I am in the wrong spot")
});
it ('Handles https://github.com/bigskysoftware/htmx/issues/33 "empty values" properly', function() {
this.server.respondWith("POST", "/htmx.php", function (xhr) {
xhr.respond(200, {}, xhr.requestBody);
});
var form = make('<form hx-trigger="click" hx-post="/htmx.php">\n' +
'<input type="text" name="variable" value="">\n' +
'<button type="submit">Submit</button>\n' +
'</form>')
form.click();
this.server.respond();
form.innerHTML.should.equal("variable=")
});
it ('name=id doesnt cause an error', function(){
this.server.respondWith("GET", "/test", "Foo<form><input name=\"id\"/></form>")
var div = make('<div hx-get="/test">Get It</div>')
div.click();
this.server.respond();
div.innerText.should.contain("Foo")
});
it ('empty id doesnt cause an error', function(){
this.server.respondWith("GET", "/test", "Foo\n<div id=''></div>")
var div = make('<div hx-get="/test">Get It</div>')
div.click();
this.server.respond();
div.innerText.should.contain("Foo")
});
it ('id with dot in value doesnt cause an error', function(){
this.server.respondWith("GET", "/test", "Foo <div id='ViewModel.Test'></div>");
var div = make('<div hx-get="/test">Get It</div>');
div.click();
this.server.respond();
div.innerText.should.contain("Foo");
});
it ('@ symbol in attributes does not break requests', function(){
this.server.respondWith("GET", "/test", "<div id='d1' @foo='bar'>Foo</div>");
var div = make('<div hx-get="/test">Get It</div>');
div.click();
this.server.respond();
byId("d1").getAttribute('@foo').should.equal('bar');
});
it ('@ symbol in attributes does not break attribute settling requests', function(){
this.server.respondWith("GET", "/test", "<div id='d1' @foo='bar'>Foo</div>");
var div = make('<div hx-get="/test"><div id="d1">Foo</div></div>');
div.click();
this.server.respond();
byId("d1").getAttribute('@foo').should.equal('bar');
});
it ('selected element with ID does not cause NPE when it disappears', function(){
this.server.respondWith("GET", "/test", "<div id='d1'>Replaced</div>");
var input = make('<input hx-trigger="click" hx-get="/test" id="i1" hx-swap="outerHTML">');
input.focus();
input.click();
this.server.respond();
byId("d1").innerText.should.equal('Replaced');
});
it('does not submit with a false condition on a form', function() {
this.server.respondWith("POST", "/test", "Submitted");
var defaultPrevented = false;
htmx.on("click", function(evt) {
defaultPrevented = evt.defaultPrevented;
})
var form = make('<form hx-post="/test" hx-trigger="click[false]"></form>');
form.click()
this.server.respond();
defaultPrevented.should.equal(true);
})
it('two elements can listen for the same event on another element', function() {
this.server.respondWith("GET", "/test", "triggered");
make('<div id="d1" hx-trigger="click from:body" hx-get="/test"></div>' +
' <div id="d2" hx-trigger="click from:body" hx-get="/test"></div>');
var div1 = byId("d1");
var div2 = byId("d2");
document.body.click();
this.server.respond();
div2.innerHTML.should.equal("triggered");
div1.innerHTML.should.equal("triggered");
})
it('a form can reset based on the htmx:afterRequest event', function() {
this.server.respondWith("POST", "/test", "posted");
//htmx.logAll();
var form = make('<div id="d1"></div><form _="on htmx:afterRequest reset() me" hx-post="/test" hx-target="#d1">' +
' <input type="text" name="input" id="i1"/>' +
' <input type="submit" id="s1"/>' +
'</form>');
htmx.trigger(form, "htmx:load"); // have to manually trigger the load event for non-AJAX dynamic content
var div1 = byId("d1");
var input = byId("i1");
input.value = "foo";
var submit = byId("s1");
input.value.should.equal("foo");
submit.click();
this.server.respond();
div1.innerHTML.should.equal("posted");
input.value.should.equal(""); // form should be reset
})
it('supports image maps', function() {
this.server.respondWith("GET", "/test", "triggered");
make('<div>' +
' <div id="d1"></div>' +
' <img src="img/bars.svg" usemap="#workmap" width="400" height="379">' +
'' +
' <map name="workmap">' +
' <area shape="rect" coords="34,44,270,350" alt="Computer" hx-get="/test" hx-target="#d1">' +
' </map>' +
'</div>');
var div1 = byId("d1");
var area = document.getElementsByTagName('area')[0];
area.click();
this.server.respond();
div1.innerHTML.should.equal("triggered");
})
it("supports unset on hx-select", function(){
this.server.respondWith("GET", "/test", "Foo<span id='example'>Bar</span>");
htmx.logAll();
make('<form hx-select="#example">\n' +
' <button id="b1" hx-select="unset" hx-get="/test">Initial</button>\n' +
'</form>')
var btn = byId("b1");
btn.click()
this.server.respond();
btn.innerText.should.equal("FooBar");
})
it("can trigger swaps from fields that don't support setSelectionRange", function(){
const template = '<form id="formtest"> \n' +
'<input hx-get="/test" hx-target="#formtest" hx-trigger="click" type="text" id="id_email" value="test@test.com" />\n' +
'</form>';
const response = '<form id="formtest">\n' +
'<input hx-get="/test" hx-target="#formtest" hx-trigger="click" type="email" id="id_email" value="supertest@test.com" />\n' +
'</form>';
this.server.respondWith("GET", "/test", response);
make(template);
var input = byId("id_email");
// HTMX only attempts to restore the selection on inputs that have a current selection and are active.
// additionally we can't set the selection on email inputs (that's the whole bug) so start as a text input where you can set selection
// and replace with an email
input.focus();
input.selectionStart = 3;
input.selectionEnd = 3;
input.click();
this.server.respond();
var input = byId("id_email");
input.value.should.equal("supertest@test.com");
});
});

View File

@@ -0,0 +1,32 @@
describe("security options", function() {
beforeEach(function() {
this.server = makeServer();
clearWorkArea();
});
afterEach(function() {
this.server.restore();
clearWorkArea();
});
it("can disable a single elt", function(){
this.server.respondWith("GET", "/test", "Clicked!");
var btn = make('<button hx-disable hx-get="/test">Initial</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Initial");
})
it("can disable a parent elt", function(){
this.server.respondWith("GET", "/test", "Clicked!");
var div = make('<div hx-disable><button id="b1" hx-get="/test">Initial</button></div>')
var btn = byId("b1");
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Initial");
})
});

View File

@@ -0,0 +1,48 @@
describe("Core htmx tokenizer tests", function(){
beforeEach(function() {
this.server = makeServer();
clearWorkArea();
});
afterEach(function() {
this.server.restore();
clearWorkArea();
});
function tokenize(str) {
return htmx._("tokenizeString")(str);
}
function tokenizeTest(str, result) {
return tokenize(str).should.deep.equal(result);
}
it('tokenizes properly', function()
{
tokenizeTest("", []);
tokenizeTest(" ", [" ", " "]);
tokenizeTest("(", ["("]);
tokenizeTest("()", ["(", ")"]);
tokenizeTest("(,)", ["(", ",", ")"]);
tokenizeTest(" ( ) ", [" ", "(", " ", ")", " "]);
tokenizeTest(" && ) ", [" ", "&", "&", " ", ")", " "]);
tokenizeTest(" && ) 'asdf'", [" ", "&", "&", " ", ")", " ", "'asdf'"]);
tokenizeTest(" && ) ',asdf'", [" ", "&", "&", " ", ")", " ", "',asdf'"]);
tokenizeTest('",asdf"', ['",asdf"']);
tokenizeTest('&& ) ",asdf"', ["&", "&", " ", ")", " ", '",asdf"']);
});
it('generates conditionals property', function()
{
var tokens = tokenize("[code==4||(code==5&&foo==true)]");
var conditional = htmx._("maybeGenerateConditional")(null, tokens);
var func = eval(conditional);
func({code: 5, foo: true}).should.equal(true);
func({code: 5, foo: false}).should.equal(false);
func({code: 4, foo: false}).should.equal(true);
func({code: 3, foo: true}).should.equal(false);
});
})

View File

@@ -0,0 +1,198 @@
describe("Core htmx client side validation tests", function(){
beforeEach(function() {
this.server = makeServer();
clearWorkArea();
});
afterEach(function() {
this.server.restore();
clearWorkArea();
});
it('HTML5 required validation error prevents request', function()
{
this.server.respondWith("POST", "/test", "Clicked!");
var form = make('<form hx-post="/test" hx-trigger="click">' +
'No Request' +
'<input id="i1" name="i1" required>' +
'</form>');
form.textContent.should.equal("No Request");
form.click();
this.server.respond();
form.textContent.should.equal("No Request");
byId("i1").value = "foo";
form.click();
this.server.respond();
form.textContent.should.equal("Clicked!");
});
it('Novalidate skips form validation', function()
{
this.server.respondWith("POST", "/test", "Clicked!");
var form = make('<form hx-post="/test" hx-trigger="click" novalidate>' +
'No Request' +
'<input id="i1" name="i1" required>' +
'</form>');
form.textContent.should.equal("No Request");
form.click();
this.server.respond();
form.textContent.should.equal("Clicked!");
});
it('Validation skipped for indirect form submission', function()
{
this.server.respondWith("POST", "/test", "Clicked!");
var form = make('<form hx-post="/test" hx-trigger="click">' +
'No Request' +
'<input id="i1" name="i1" required>' +
'<button id="button" hx-post="/test" hx-target="form"></button>' +
'</form>');
form.textContent.should.equal("No Request");
byId("button").click();
this.server.respond();
form.textContent.should.equal("Clicked!");
});
it('Formnovalidate skips form validation', function()
{
this.server.respondWith("POST", "/test", "Clicked!");
var form = make('<form hx-post="/test">' +
'No Request' +
'<input id="i1" name="i1" required>' +
'<button id="button" type="submit" formnovalidate></button>' +
'</form>');
form.textContent.should.equal("No Request");
byId("button").click();
this.server.respond();
form.textContent.should.equal("Clicked!");
});
it('HTML5 pattern validation error prevents request', function()
{
this.server.respondWith("POST", "/test", "Clicked!");
var form = make('<form hx-post="/test" hx-trigger="click">' +
'No Request' +
'<input id="i1" name="i1" pattern="abc" value="xyz">' +
'</form>');
byId("i1").value = "xyz";
form.textContent.should.equal("No Request");
form.click();
this.server.respond();
form.textContent.should.equal("No Request");
byId("i1").value = "abc";
form.click();
this.server.respond();
form.textContent.should.equal("Clicked!");
});
it('Custom validation error prevents request', function()
{
this.server.respondWith("POST", "/test", "Clicked!");
var form = make('<form hx-post="/test" hx-trigger="click">' +
'No Request' +
'<input id="i1" name="i1">' +
'</form>');
byId("i1").setCustomValidity("Nope");
form.textContent.should.equal("No Request");
form.click();
this.server.respond();
form.textContent.should.equal("No Request");
byId("i1").setCustomValidity("");
form.click();
this.server.respond();
form.textContent.should.equal("Clicked!");
});
it('hyperscript validation error prevents request', function()
{
this.server.respondWith("POST", "/test", "Clicked!");
var form = make('<form hx-post="/test" hx-trigger="click">' +
'No Request' +
'<input _="on htmx:validation:validate if my.value != \'foo\' call me.setCustomValidity(\'Nope\') ' +
' else call me.setCustomValidity(\'\')" id="i1" name="i1">' +
'</form>');
htmx.trigger(form, "htmx:load");
byId("i1").value = "boo";
form.textContent.should.equal("No Request");
form.click();
this.server.respond();
form.textContent.should.equal("No Request");
byId("i1").value = "foo";
form.click();
this.server.respond();
form.textContent.should.equal("Clicked!");
});
it('calls htmx:validation:failed on failure', function()
{
var form = make('<form hx-post="/test" hx-trigger="click">' +
'No Request' +
'<input id="i1" name="i1" required>' +
'</form>');
var calledEvent = false;
var handler = htmx.on(form, "htmx:validation:failed", function(){
calledEvent = true;
});
try {
form.click();
this.server.respond();
} finally {
htmx.off(form, handler);
}
calledEvent.should.equal(true);
});
it('calls htmx:validation:halted on failure', function()
{
var form = make('<form hx-post="/test" hx-trigger="click">' +
'No Request' +
'<input id="i1" name="i1" required>' +
'</form>');
var errors = null;
var handler = htmx.on(form, "htmx:validation:halted", function(evt){
errors = evt.detail.errors;
});
try {
form.click();
this.server.respond();
} finally {
htmx.off(form, handler);
}
errors.length.should.equal(1);
byId("i1").should.equal(errors[0].elt);
errors[0].validity.valueMissing.should.equal(true);
});
it('hx-validate can prevent a single input from submitting', function()
{
this.server.respondWith("POST", "/test", "Clicked!");
var div = make("<div id='d1'>No Request</div>")
var form = make('<form><input type="text" hx-target="#d1" hx-post="/test" hx-trigger="click" id="i1" name="i1" pattern="[0-9]+" hx-validate="true"/></form>');
var input = byId("i1");
div.textContent.should.equal("No Request");
input.value = "abc";
input.click();
this.server.respond();
div.textContent.should.equal("No Request");
input.value = "1bc";
input.click();
this.server.respond();
div.textContent.should.equal("No Request");
input.value = "123";
input.click();
this.server.respond();
div.textContent.should.equal("Clicked!");
});
})

View File

@@ -0,0 +1,44 @@
describe("Core htmx AJAX Verbs", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('handles basic posts properly', function () {
this.server.respondWith("POST", "/test", "post");
var div = make('<div hx-post="/test">click me</div>');
div.click();
this.server.respond();
div.innerHTML.should.equal("post");
})
it('handles basic put properly', function () {
this.server.respondWith("PUT", "/test", "put");
var div = make('<div hx-put="/test">click me</div>');
div.click();
this.server.respond();
div.innerHTML.should.equal("put");
})
it('handles basic patch properly', function () {
this.server.respondWith("PATCH", "/test", "patch");
var div = make('<div hx-patch="/test">click me</div>');
div.click();
this.server.respond();
div.innerHTML.should.equal("patch");
})
it('handles basic delete properly', function () {
this.server.respondWith("DELETE", "/test", "delete");
var div = make('<div hx-delete="/test">click me</div>');
div.click();
this.server.respond();
div.innerHTML.should.equal("delete");
})
});

View File

@@ -0,0 +1,21 @@
describe("ajax-header extension", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('Sends the X-Requested-With header', function () {
this.server.respondWith("GET", "/test", function (xhr) {
xhr.respond(200, {}, xhr.requestHeaders['X-Requested-With'])
});
var btn = make('<button hx-get="/test" hx-ext="ajax-header">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("XMLHttpRequest");
});
});

View File

@@ -0,0 +1,27 @@
describe("bad extension", function() {
htmx.defineExtension("bad-extension", {
onEvent : function(name, evt) {throw "onEvent"},
transformResponse : function(text, xhr, elt) {throw "transformRequest"},
isInlineSwap : function(swapStyle) {throw "isInlineSwap"},
handleSwap : function(swapStyle, target, fragment, settleInfo) {throw "handleSwap"},
encodeParameters : function(xhr, parameters, elt) {throw "encodeParmeters"}
}
)
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('does not blow up rendering', function () {
this.server.respondWith("GET", "/test", "clicked!");
var div = make('<div hx-get="/test" hx-ext="bad-extension">Click Me!</div>')
div.click();
this.server.respond();
div.innerHTML.should.equal("clicked!");
});
});

View File

@@ -0,0 +1,55 @@
describe("class-tools extension", function(){
beforeEach(function() {
this.server = makeServer();
clearWorkArea();
});
afterEach(function() {
this.server.restore();
clearWorkArea();
});
it('adds classes properly', function(done)
{
var div = make('<div hx-ext="class-tools" classes="add c1">Click Me!</div>')
should.equal(div.classList.length, 0);
setTimeout(function(){
should.equal(div.classList.contains("c1"), true);
done();
}, 100);
});
it('removes classes properly', function(done)
{
var div = make('<div class="foo bar" hx-ext="class-tools" classes="remove bar">Click Me!</div>')
should.equal(div.classList.contains("foo"), true);
should.equal(div.classList.contains("bar"), true);
setTimeout(function(){
should.equal(div.classList.contains("foo"), true);
should.equal(div.classList.contains("bar"), false);
done();
}, 100);
});
it('adds classes properly w/ data-* prefix', function(done)
{
var div = make('<div hx-ext="class-tools" data-classes="add c1">Click Me!</div>')
should.equal(div.classList.length, 0);
setTimeout(function(){
should.equal(div.classList.contains("c1"), true);
done();
}, 100);
});
it('extension can be on parent', function(done)
{
var div = make('<div hx-ext="class-tools"><div id="d1" classes="add c1">Click Me!</div></div>')
should.equal(div.classList.length, 0);
setTimeout(function(){
should.equal(div.classList.contains("c1"), false);
should.equal(byId("d1").classList.contains("c1"), true);
done();
}, 100);
});
})

View File

@@ -0,0 +1,30 @@
describe("client-side-templates extension", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('works on basic mustache template', function () {
this.server.respondWith("GET", "/test", '{"foo":"bar"}');
var btn = make('<button hx-get="/test" hx-ext="client-side-templates" mustache-template="mt1">Click Me!</button>')
make('<script id="mt1" type="x-tmpl-mustache">*{{foo}}*</script>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("*bar*");
});
it('works on basic handlebars template', function () {
this.server.respondWith("GET", "/test", '{"foo":"bar"}');
var btn = make('<button hx-get="/test" hx-ext="client-side-templates" handlebars-template="hb1">Click Me!</button>')
Handlebars.partials["hb1"] = Handlebars.compile("*{{foo}}*");
btn.click();
this.server.respond();
btn.innerHTML.should.equal("*bar*");
});
});

View File

@@ -0,0 +1,19 @@
describe("debug extension", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('works on basic request', function () {
this.server.respondWith("GET", "/test", "Clicked!");
var btn = make('<button hx-get="/test" hx-ext="debug">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Clicked!");
});
});

View File

@@ -0,0 +1,62 @@
describe("disable-element extension", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('disables the triggering element during htmx request', function () {
// GIVEN:
// - A button triggering an htmx request with disable-element extension
// - The button is enabled
this.server.respondWith("GET", "/test", function (xhr) {
xhr.respond(200, {})
});
var btn = make('<button hx-get="/test" hx-ext="disable-element" hx-disable-element="self">Click Me!</button>')
btn.disabled.should.equal(false);
// WHEN clicking
btn.click();
// THEN it's disabled
btn.disabled.should.equal(true);
// WHEN server response has arrived
this.server.respond();
// THEN it's re-enabled
btn.disabled.should.equal(false);
});
it('disables the designated element during htmx request', function () {
// GIVEN:
// - A button triggering an htmx request with disable-element extension
// - Another button that needs to be disabled during the htmx request
// - Both buttons are enabled
this.server.respondWith("GET", "/test", function (xhr) {
xhr.respond(200, {})
});
var btn = make('<button hx-get="/test" hx-ext="disable-element" hx-disable-element="#should-be-disabled">Click Me!</button>')
var btn2 = make('<button id="should-be-disabled">Should be disabled</button>')
btn.disabled.should.equal(false);
btn2.disabled.should.equal(false);
// WHEN clicking
btn.click();
// THEN it's not disabled, but the other one is
btn.disabled.should.equal(false);
btn2.disabled.should.equal(true);
// WHEN server response has arrived
this.server.respond();
// THEN both buttons are back enabled
btn.disabled.should.equal(false);
btn2.disabled.should.equal(false);
});
});

View File

@@ -0,0 +1,23 @@
describe("event-header extension", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('Sends the Triggering-Event header', function () {
this.server.respondWith("GET", "/test", function (xhr) {
xhr.respond(200, {}, xhr.requestHeaders['Triggering-Event'])
});
var btn = make('<button hx-get="/test" hx-ext="event-header">Click Me!</button>')
btn.click();
this.server.respond();
var json = JSON.parse(btn.innerText);
json.type.should.equal("click");
json.target.should.equal("button");
});
});

View File

@@ -0,0 +1,60 @@
describe("default extensions behavior", function() {
var loadCalls, afterSwapCalls, afterSettleCalls;
beforeEach(function () {
loadCalls = [];
this.server = makeServer();
clearWorkArea();
htmx.defineExtension("ext-testswap", {
onEvent : function(name, evt) {
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);
return [parentEl.appendChild(fragment)]; // return the newly added element
}
});
});
afterEach(function () {
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;
btn.click()
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.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(2);
loadCalls[1].textContent.should.equal('Loaded!'); // the new span is loaded
});
});

View File

@@ -0,0 +1,64 @@
describe("hyperscript integration", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('can trigger with a custom event', function () {
this.server.respondWith("GET", "/test", "Custom Event Sent!");
var btn = make('<button _="on click send customEvent" hx-trigger="customEvent" hx-get="/test">Click Me!</button>')
htmx.trigger(btn, "htmx:load"); // have to manually trigger the load event for non-AJAX dynamic content
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Custom Event Sent!");
});
it('can handle htmx driven events', function () {
this.server.respondWith("GET", "/test", "Clicked!");
var btn = make('<button _="on htmx:afterSettle add .afterSettle" hx-get="/test">Click Me!</button>')
htmx.trigger(btn, "htmx:load");
btn.classList.contains("afterSettle").should.equal(false);
btn.click();
this.server.respond();
btn.classList.contains("afterSettle").should.equal(true);
});
it('can handle htmx error events', function () {
this.server.respondWith("GET", "/test", [404, {}, "Bad request"]);
var div = make('<div id="d1"></div>')
var btn = make('<button _="on htmx:error(errorInfo) put errorInfo.error into #d1.innerHTML" hx-get="/test">Click Me!</button>')
htmx.trigger(btn, "htmx:load");
btn.click();
this.server.respond();
div.innerHTML.startsWith("Response Status Error Code 404 from");
});
it('hyperscript in non-htmx annotated nodes is evaluated', function () {
this.server.respondWith("GET", "/test", "<div><div><div id='d1' _='on click put \"Clicked...\" into my.innerHTML'></div></div></div>");
var btn = make('<button hx-get="/test">Click Me!</button>')
btn.click();
this.server.respond();
var newDiv = byId("d1");
newDiv.click();
newDiv.innerText.should.equal("Clicked...");
});
it('hyperscript removal example works', function (done) {
this.server.respondWith("GET", "/test", "<div id='d1' _='on load wait 20ms then remove me'>To Remove</div>");
var btn = make('<button hx-get="/test">Click Me!</button>')
btn.click();
this.server.respond();
var newDiv = byId("d1");
newDiv.innerText.should.equal("To Remove")
setTimeout(function(){
newDiv = byId("d1");
should.equal(newDiv, null);
done();
}, 100);
});
});

View File

@@ -0,0 +1,23 @@
describe("include-vals extension", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('Includes values properly', function () {
var params = {};
this.server.respondWith("POST", "/test", function (xhr) {
params = getParameters(xhr);
xhr.respond(200, {}, "clicked");
});
var btn = make('<button hx-post="/test" hx-ext="include-vals" include-vals="foo:\'bar\'">Click Me!</button>')
btn.click();
this.server.respond();
params['foo'].should.equal("bar");
});
});

View File

@@ -0,0 +1,136 @@
//
describe("json-enc extension", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('handles basic post properly', function () {
var jsonResponseBody = JSON.stringify({});
this.server.respondWith("POST", "/test", jsonResponseBody);
var div = make("<div hx-post='/test' hx-ext='json-enc'>click me</div>");
div.click();
this.server.respond();
this.server.lastRequest.response.should.equal("{}");
})
it('handles basic put properly', function () {
var jsonResponseBody = JSON.stringify({});
this.server.respondWith("PUT", "/test", jsonResponseBody);
var div = make('<div hx-put="/test" hx-ext="json-enc">click me</div>');
div.click();
this.server.respond();
this.server.lastRequest.response.should.equal("{}");
})
it('handles basic patch properly', function () {
var jsonResponseBody = JSON.stringify({});
this.server.respondWith("PATCH", "/test", jsonResponseBody);
var div = make('<div hx-patch="/test" hx-ext="json-enc">click me</div>');
div.click();
this.server.respond();
this.server.lastRequest.response.should.equal("{}");
})
it('handles basic delete properly', function () {
var jsonResponseBody = JSON.stringify({});
this.server.respondWith("DELETE", "/test", jsonResponseBody);
var div = make('<div hx-delete="/test" hx-ext="json-enc">click me</div>');
div.click();
this.server.respond();
this.server.lastRequest.response.should.equal("{}");
})
it('handles post with form parameters', function () {
this.server.respondWith("POST", "/test", function (xhr) {
var values = JSON.parse(xhr.requestBody);
values.should.have.keys("username","password");
values["username"].should.be.equal("joe");
values["password"].should.be.equal("123456");
var ans = { "passwordok": values["password"] == "123456"};
xhr.respond(200, {}, JSON.stringify(ans));
});
var html = make('<form hx-post="/test" hx-ext="json-enc" > ' +
'<input type="text" name="username" value="joe"> ' +
'<input type="password" name="password" value="123456"> ' +
'<button id="btnSubmit">Submit</button> ');
byId("btnSubmit").click();
this.server.respond();
this.server.lastRequest.response.should.equal('{"passwordok":true}');
})
it('handles put with form parameters', function () {
this.server.respondWith("PUT", "/test", function (xhr) {
var values = JSON.parse(xhr.requestBody);
values.should.have.keys("username","password");
values["username"].should.be.equal("joe");
values["password"].should.be.equal("123456");
var ans = { "passwordok": values["password"] == "123456"};
xhr.respond(200, {}, JSON.stringify(ans));
});
var html = make('<form hx-put="/test" hx-ext="json-enc" > ' +
'<input type="text" name="username" value="joe"> ' +
'<input type="password" name="password" value="123456"> ' +
'<button id="btnSubmit">Submit</button> ');
byId("btnSubmit").click();
this.server.respond();
this.server.lastRequest.response.should.equal('{"passwordok":true}');
})
it('handles patch with form parameters', function () {
this.server.respondWith("PATCH", "/test", function (xhr) {
var values = JSON.parse(xhr.requestBody);
values.should.have.keys("username","password");
values["username"].should.be.equal("joe");
values["password"].should.be.equal("123456");
var ans = { "passwordok": values["password"] == "123456"};
xhr.respond(200, {}, JSON.stringify(ans));
});
var html = make('<form hx-patch="/test" hx-ext="json-enc" > ' +
'<input type="text" name="username" value="joe"> ' +
'<input type="password" name="password" value="123456"> ' +
'<button id="btnSubmit">Submit</button> ');
byId("btnSubmit").click();
this.server.respond();
this.server.lastRequest.response.should.equal('{"passwordok":true}');
})
it('handles delete with form parameters', function () {
this.server.respondWith("DELETE", "/test", function (xhr) {
var values = JSON.parse(xhr.requestBody);
values.should.have.keys("username","password");
values["username"].should.be.equal("joe");
values["password"].should.be.equal("123456");
var ans = { "passwordok": values["password"] == "123456"};
xhr.respond(200, {}, JSON.stringify(ans));
});
var html = make('<form hx-delete="/test" hx-ext="json-enc" > ' +
'<input type="text" name="username" value="joe"> ' +
'<input type="password" name="password" value="123456"> ' +
'<button id="btnSubmit">Submit</button> ');
byId("btnSubmit").click();
this.server.respond();
this.server.lastRequest.response.should.equal('{"passwordok":true}');
})
});

View File

@@ -0,0 +1,167 @@
describe("loading states extension", function () {
beforeEach(function () {
this.server = makeServer();
this.clock = sinon.useFakeTimers();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
this.clock.restore();
clearWorkArea();
});
it('works on basic setup', function () {
this.server.respondWith("GET", "/test", "Clicked!");
var btn = make('<button hx-get="/test" hx-ext="loading-states">Click Me!</button>');
var element = make('<div data-loading>');
btn.click();
element.style.display.should.be.equal("inline-block");
this.server.respond();
element.style.display.should.be.equal("none");
btn.innerHTML.should.equal("Clicked!");
});
it('works with custom display', function () {
this.server.respondWith("GET", "/test", "Clicked!");
var btn = make('<button hx-get="/test" hx-ext="loading-states">Click Me!</button>');
var element = make('<div data-loading="flex">');
btn.click();
element.style.display.should.be.equal("flex");
this.server.respond();
element.style.display.should.be.equal("none");
btn.innerHTML.should.equal("Clicked!");
});
it('works with classes', function () {
this.server.respondWith("GET", "/test", "Clicked!");
var btn = make('<button hx-get="/test" hx-ext="loading-states">Click Me!</button>');
var element = make('<div data-loading-class="test">');
btn.click();
element.should.have.class("test");
this.server.respond();
element.should.not.have.class("test");
btn.innerHTML.should.equal("Clicked!");
});
it('works with classes removal', function () {
this.server.respondWith("GET", "/test", "Clicked!");
var btn = make('<button hx-get="/test" hx-ext="loading-states">Click Me!</button>');
var element = make('<div data-loading-class-remove="test" class="test">');
btn.click();
element.should.not.have.class("test");
this.server.respond();
element.should.have.class("test");
btn.innerHTML.should.equal("Clicked!");
});
it('works with disabling', function () {
this.server.respondWith("GET", "/test", "Clicked!");
var btn = make('<button hx-get="/test" hx-ext="loading-states">Click Me!</button>');
var element = make('<button data-loading-disable>');
btn.click();
element.disabled.should.be.true;
this.server.respond();
element.disabled.should.be.false;
btn.innerHTML.should.equal("Clicked!");
});
it('works with aria-busy', function () {
this.server.respondWith("GET", "/test", "Clicked!");
var btn = make('<button hx-get="/test" hx-ext="loading-states">Click Me!</button>');
var element = make('<button data-loading-aria-busy>');
btn.click();
element.should.have.attribute("aria-busy", "true");
this.server.respond();
element.should.not.have.attribute("aria-busy");
btn.innerHTML.should.equal("Clicked!");
});
it('works with multiple directives', function () {
this.server.respondWith("GET", "/test", "Clicked!");
var btn = make('<button hx-get="/test" hx-ext="loading-states">Click Me!</button>');
var element = make('<button data-loading-aria-busy data-loading-class="loading" data-loading-class-remove="not-loading" class="not-loading">');
btn.click();
element.should.have.attribute("aria-busy", "true");
element.should.have.class("loading")
element.should.not.have.class("not-loading")
this.server.respond();
element.should.not.have.attribute("aria-busy");
element.should.not.have.class("loading")
element.should.have.class("not-loading")
btn.innerHTML.should.equal("Clicked!");
});
it('works with delay', function () {
this.server.respondWith("GET", "/test", "Clicked!");
var btn = make('<button hx-get="/test" hx-ext="loading-states">Click Me!</button>');
var element = make('<div data-loading-class-remove="test" data-loading-delay="1s" class="test">');
btn.click();
element.should.have.class("test");
this.clock.tick(1000);
element.should.not.have.class("test");
this.server.respond();
element.should.have.class("test");
btn.innerHTML.should.equal("Clicked!");
});
it('works with custom targets', function () {
this.server.respondWith("GET", "/test", "Clicked!");
var btn = make('<button hx-get="/test" hx-ext="loading-states" data-loading-target="#loader" data-loading-class="test">Click Me!</button>');
var element = make('<div id="loader">');
btn.click();
element.should.have.class("test");
this.server.respond();
element.should.not.have.class("test");
btn.innerHTML.should.equal("Clicked!");
});
it('works with path filters', function () {
this.server.respondWith("GET", "/test", "Clicked!");
var btn = make('<button hx-get="/test" hx-ext="loading-states" >Click Me!</button>');
var matchingRequestElement = make('<div data-loading-class="test" data-loading-path="/test">');
var nonMatchingPathElement = make('<div data-loading-class="test" data-loading-path="/test1">');
btn.click();
matchingRequestElement.should.have.class("test");
nonMatchingPathElement.should.not.have.class("test");
this.server.respond();
matchingRequestElement.should.not.have.class("test");
nonMatchingPathElement.should.not.have.class("test");
btn.innerHTML.should.equal("Clicked!");
});
it('works with scopes', function () {
this.server.respondWith("GET", "/test", "Clicked!");
var btn = make('<div data-loading-states><button hx-get="/test" hx-ext="loading-states" >Click Me!</button></div>');
var element = make('<div data-loading-class="test">');
btn.getElementsByTagName("button")[0].click();
element.should.not.have.class("test");
this.server.respond();
element.should.not.have.class("test");
btn.getElementsByTagName("button")[0].innerHTML.should.equal("Clicked!");
});
it('history restore should not have loading states in content', function () {
// this test is based on test from test/attributes/hx-push-url.js:65
this.server.respondWith("GET", "/test1", '<button id="d2" hx-push-url="true" hx-get="/test2" hx-swap="outerHTML settle:0" data-loading-disable>test1</button>');
this.server.respondWith("GET", "/test2", '<button id="d3" hx-push-url="true" hx-get="/test3" hx-swap="outerHTML settle:0" data-loading-disable>test2</button>');
make('<div hx-ext="loading-states"><button id="d1" hx-push-url="true" hx-get="/test1" hx-swap="outerHTML settle:0" data-loading-disable>init</button></div>');
byId("d1").click();
byId("d1").disabled.should.be.true;
this.server.respond();
byId("d2").disabled.should.be.false;
var workArea = getWorkArea();
workArea.textContent.should.equal("test1");
byId("d2").click();
byId("d2").disabled.should.be.true;
this.server.respond();
workArea.textContent.should.equal("test2")
htmx._('restoreHistory')("/test1")
var el = byId("d2");
el.disabled.should.be.false;
})
});

View File

@@ -0,0 +1,53 @@
describe("method-override extension", function(){
beforeEach(function() {
this.server = makeServer();
clearWorkArea();
});
afterEach(function() {
this.server.restore();
clearWorkArea();
});
it('issues a DELETE request with proper headers', function()
{
this.server.respondWith("DELETE", "/test", function(xhr){
xhr.requestHeaders['X-HTTP-Method-Override'].should.equal('DELETE');
xhr.method.should.equal("POST")
xhr.respond(200, {}, "Deleted!");
});
var btn = make('<button hx-ext="method-override" hx-delete="/test">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Deleted!");
});
it('issues a PATCH request with proper headers', function()
{
this.server.respondWith("PATCH", "/test", function(xhr){
xhr.requestHeaders['X-HTTP-Method-Override'].should.equal('PATCH');
xhr.method.should.equal("POST")
xhr.respond(200, {}, "Patched!");
});
var btn = make('<button hx-ext="method-override" hx-patch="/test">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Patched!");
});
it('issues a PUT request with proper headers', function()
{
this.server.respondWith("PUT", "/test", function(xhr){
xhr.requestHeaders['X-HTTP-Method-Override'].should.equal('PUT');
xhr.method.should.equal("POST")
xhr.respond(200, {}, "Putted!");
});
var btn = make('<button hx-ext="method-override" hx-put="/test">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Putted!");
});
})

View File

@@ -0,0 +1,40 @@
describe("morphdom-swap extension", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('works on basic request', function () {
this.server.respondWith("GET", "/test", "<button>Clicked!</button>!");
var btn = make('<button hx-get="/test" hx-ext="morphdom-swap" hx-swap="morphdom" >Click Me!</button>')
btn.click();
should.equal(btn.getAttribute("hx-get"), "/test");
this.server.respond();
should.equal(btn.getAttribute("hx-get"), null);
btn.innerHTML.should.equal("Clicked!");
});
it('works with htmx elements in new content', function () {
this.server.respondWith("GET", "/test", '<button>Clicked!<span hx-get="/test-inner" hx-trigger="load" hx-swap="morphdom"></span></button>');
this.server.respondWith("GET", "/test-inner", 'Loaded!');
var btn = make('<div hx-ext="morphdom-swap"><button hx-get="/test" hx-swap="morphdom">Click Me!</button></div>').querySelector('button');
btn.click();
this.server.respond(); // call /test via button trigger=click
this.server.respond(); // call /test-inner via span trigger=load
btn.innerHTML.should.equal("Clicked!Loaded!");
});
it('works with hx-select', function () {
this.server.respondWith("GET", "/test", "<button>Clicked!</button>!");
var btn = make('<button hx-get="/test" hx-ext="morphdom-swap" hx-swap="morphdom" hx-select="button" >Click Me!</button>')
btn.click();
should.equal(btn.getAttribute("hx-get"), "/test");
this.server.respond();
should.equal(btn.getAttribute("hx-get"), null);
btn.innerHTML.should.equal("Clicked!");
});
});

View File

@@ -0,0 +1,52 @@
describe("multi-swap extension", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('swap only one element with default innerHTML', function () {
this.server.respondWith("GET", "/test", '<html><body><div class="dummy"><div id="a">New A</div></div></html>');
var content = make('<div>Foo <div id="a">Old A</div></div>');
var btn = make('<button hx-get="/test" hx-ext="multi-swap" hx-swap="multi:#a">Click Me!</button>');
btn.click();
this.server.respond();
should.equal(content.innerHTML, 'Foo <div id="a">New A</div>');
});
it('swap multiple elements with outerHTML, beforeend, afterend, beforebegin and delete methods', function () {
this.server.respondWith("GET", "/test",
'<html><body><div class="abc">' +
'<div id="a">New A</div> foo ' +
'<div id="b"><b>New B</b></div> bar ' +
'<div id="c">New C</div> dummy ' +
'<div id="d">New D</div> lorem ' +
'<div id="e">TO DELETE</div>' +
'</div></html>'
);
var content = make(
'<div>Foo ' +
' <div id="a">Old A</div> A ' +
' <div id="b">Old B</div> B ' +
' <div id="c">Old C</div> C ' +
' <div id="d">Old D</div> D ' +
' <div id="e">Old E</div> E ' +
'</div>'
);
var btn = make('<button hx-get="/test" hx-ext="multi-swap" hx-swap="multi:#a:outerHTML,#b:beforeend,#c:afterend,#d:beforebegin,#e:delete">Click Me!</button>');
btn.click();
this.server.respond();
should.equal(content.outerHTML,
'<div>Foo ' +
' <div id="a">New A</div> A ' +
' <div id="b">Old B<b>New B</b></div> B ' +
' <div id="c">Old C</div>New C C ' +
' New D<div id="d">Old D</div> D ' +
' E ' +
'</div>'
);
});
});

View File

@@ -0,0 +1,173 @@
describe("path-deps extension", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('path-deps basic case works', function () {
this.server.respondWith("POST", "/test", "Clicked!");
this.server.respondWith("GET", "/test2", "Deps fired!");
var btn = make('<button hx-post="/test" hx-ext="path-deps">Click Me!</button>')
var div = make('<div hx-get="/test2" hx-trigger="path-deps" path-deps="/test">FOO</div>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Clicked!");
div.innerHTML.should.equal("FOO");
this.server.respond();
div.innerHTML.should.equal("Deps fired!");
});
it('path-deps works with trailing slash', function () {
this.server.respondWith("POST", "/test", "Clicked!");
this.server.respondWith("GET", "/test2", "Deps fired!");
var btn = make('<button hx-post="/test" hx-ext="path-deps">Click Me!</button>')
var div = make('<div hx-get="/test2" hx-trigger="path-deps" path-deps="/test/">FOO</div>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Clicked!");
div.innerHTML.should.equal("FOO");
this.server.respond();
div.innerHTML.should.equal("Deps fired!");
});
it('path-deps GET does not trigger', function () {
this.server.respondWith("GET", "/test", "Clicked!");
this.server.respondWith("GET", "/test2", "Deps fired!");
var btn = make('<button hx-get="/test" hx-ext="path-deps">Click Me!</button>')
var div = make('<div hx-get="/test2" hx-trigger="path-deps" path-deps="/test">FOO</div>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Clicked!");
div.innerHTML.should.equal("FOO");
this.server.respond();
div.innerHTML.should.equal("FOO");
});
it('path-deps dont trigger on path mismatch', function () {
this.server.respondWith("POST", "/test", "Clicked!");
this.server.respondWith("GET", "/test2", "Deps fired!");
var btn = make('<button hx-post="/test" hx-ext="path-deps">Click Me!</button>')
var div = make('<div hx-get="/test2" hx-trigger="path-deps" path-deps="/test2">FOO</div>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Clicked!");
div.innerHTML.should.equal("FOO");
this.server.respond();
div.innerHTML.should.equal("FOO");
});
it('path-deps dont trigger on path longer than request', function () {
this.server.respondWith("POST", "/test", "Clicked!");
this.server.respondWith("GET", "/test2", "Deps fired!");
var btn = make('<button hx-post="/test" hx-ext="path-deps">Click Me!</button>')
var div = make('<div hx-get="/test2" hx-trigger="path-deps" path-deps="/test/child">FOO</div>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Clicked!");
div.innerHTML.should.equal("FOO");
this.server.respond();
div.innerHTML.should.equal("FOO");
});
it('path-deps trigger on path shorter than request', function () {
this.server.respondWith("POST", "/test/child", "Clicked!");
this.server.respondWith("GET", "/test2", "Deps fired!");
var btn = make('<button hx-post="/test/child" hx-ext="path-deps">Click Me!</button>')
var div = make('<div hx-get="/test2" hx-trigger="path-deps" path-deps="/test">FOO</div>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Clicked!");
div.innerHTML.should.equal("FOO");
this.server.respond();
div.innerHTML.should.equal("Deps fired!");
});
it('path-deps trigger on *-at-start path', function () {
this.server.respondWith("POST", "/test/child/test", "Clicked!");
this.server.respondWith("GET", "/test2", "Deps fired!");
var btn = make('<button hx-post="/test/child/test" hx-ext="path-deps">Click Me!</button>')
var div = make('<div hx-get="/test2" hx-trigger="path-deps" path-deps="/*/child/test">FOO</div>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Clicked!");
div.innerHTML.should.equal("FOO");
this.server.respond();
div.innerHTML.should.equal("Deps fired!");
});
it('path-deps trigger on *-in-middle path', function () {
this.server.respondWith("POST", "/test/child/test", "Clicked!");
this.server.respondWith("GET", "/test2", "Deps fired!");
var btn = make('<button hx-post="/test/child/test" hx-ext="path-deps">Click Me!</button>')
var div = make('<div hx-get="/test2" hx-trigger="path-deps" path-deps="/test/*/test">FOO</div>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Clicked!");
div.innerHTML.should.equal("FOO");
this.server.respond();
div.innerHTML.should.equal("Deps fired!");
});
it('path-deps trigger on *-at-end path', function () {
this.server.respondWith("POST", "/test/child/test", "Clicked!");
this.server.respondWith("GET", "/test2", "Deps fired!");
var btn = make('<button hx-post="/test/child/test" hx-ext="path-deps">Click Me!</button>')
var div = make('<div hx-get="/test2" hx-trigger="path-deps" path-deps="/test/child/*">FOO</div>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Clicked!");
div.innerHTML.should.equal("FOO");
this.server.respond();
div.innerHTML.should.equal("Deps fired!");
});
it('path-deps trigger all *s path', function () {
this.server.respondWith("POST", "/test/child/test", "Clicked!");
this.server.respondWith("GET", "/test2", "Deps fired!");
var btn = make('<button hx-post="/test/child/test" hx-ext="path-deps">Click Me!</button>')
var div = make('<div hx-get="/test2" hx-trigger="path-deps" path-deps="/*/*/*">FOO</div>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Clicked!");
div.innerHTML.should.equal("FOO");
this.server.respond();
div.innerHTML.should.equal("Deps fired!");
});
it('path-deps api basic refresh case works', function () {
this.server.respondWith("GET", "/test", "Path deps fired!");
var div = make('<div hx-get="/test" hx-trigger="path-deps" path-deps="/test">FOO</div>')
PathDeps.refresh("/test");
this.server.respond();
div.innerHTML.should.equal("Path deps fired!");
});
it('path-deps api parent path case works', function () {
this.server.respondWith("GET", "/test1", "Path deps 1 fired!");
this.server.respondWith("GET", "/test2", "Path deps 2 fired!");
var div = make('<div hx-get="/test1" hx-trigger="path-deps" path-deps="/test/child">FOO</div>')
var div2 = make('<div hx-get="/test2" hx-trigger="path-deps" path-deps="/test">BAR</div>')
PathDeps.refresh("/test/child");
this.server.respond();
div.innerHTML.should.equal("Path deps 1 fired!");
this.server.respond();
div2.innerHTML.should.equal("Path deps 2 fired!");
});
it('path-deps replacing containing element fires event', function () {
this.server.respondWith("POST", "/test", "Clicked!");
this.server.respondWith("GET", "/test2", "Deps fired!");
var div1 = make('<div><button id="buttonSubmit" hx-post="/test" hx-swap="outerHTML" hx-ext="path-deps" >Click Me!</button></div>')
var div2 = make('<div hx-get="/test2" hx-trigger="path-deps" path-deps="/test">FOO</div>')
byId("buttonSubmit").click();
this.server.respond();
div1.innerHTML.should.equal('Clicked!');
div2.innerHTML.should.equal("FOO");
this.server.respond();
div2.innerHTML.should.equal("Deps fired!");
});
});

View File

@@ -0,0 +1,53 @@
describe("remove-me extension", function(){
beforeEach(function() {
this.server = makeServer();
clearWorkArea();
});
afterEach(function() {
this.server.restore();
clearWorkArea();
});
it('removes elements properly', function(done)
{
var div = make('<div id="d1" hx-ext="remove-me" remove-me="20ms">Click Me!</div>')
byId("d1").should.equal(div)
setTimeout(function(){
should.equal(byId("d1"), null);
done();
}, 40);
});
it('removes properly w/ data-* prefix', function(done)
{
var div = make('<div hx-ext="remove-me" data-remove-me="20ms">Click Me!</div>')
should.equal(div.classList.length, 0);
setTimeout(function(){
should.equal(div.parentElement, null);
done();
}, 100);
});
it('extension can be on parent', function(done)
{
var div = make('<div hx-ext="remove-me"><div id="d1" remove-me="20ms">Click Me!</div></div>')
should.equal(div.classList.length, 0);
setTimeout(function(){
should.equal(byId("d1"), null);
done();
}, 100);
});
it('extension can be on a child', function(done)
{
var div = make('<div><div hx-ext="remove-me" id="d1" remove-me="20ms">Click Me!</div></div>')
should.equal(div.classList.length, 0);
setTimeout(function(){
should.equal(byId("d1"), null);
done();
}, 100);
});
})

View File

@@ -0,0 +1,319 @@
describe("web-sockets extension", function () {
beforeEach(function () {
this.server = makeServer();
this.socketServer = new Mock.Server('ws://localhost:8080');
this.messages = [];
this.clock = sinon.useFakeTimers();
this.socketServer.on('connection', function (socket) {
socket.on('message', function (event) {
this.messages.push(event)
}.bind(this))
}.bind(this))
/* Mock socket library is cool, but it uses setTimeout to emulate asynchronous nature of the network.
* To avoid unexpected behavior, make sure to call this method whenever socket would have a network communication,
* e.g., when connecting, disconnecting, sending messages. */
this.tickMock = function () {
this.clock.tick(5);
}
clearWorkArea();
});
afterEach(function () {
clearWorkArea();
this.socketServer.close();
this.socketServer.stop();
this.clock.restore();
});
it('can establish connection with the server', function () {
this.socketServer.clients().length.should.equal(0);
make('<div hx-ext="ws" ws-connect="ws://localhost:8080">');
this.socketServer.clients().length.should.equal(1);
this.tickMock();
})
it('is closed after removal by swap', function () {
this.server.respondWith("GET", "/test", "Clicked!");
var div = make('<div hx-get="/test" hx-swap="outerHTML" hx-ext="ws" ws-connect="ws://localhost:8080">');
this.tickMock();
this.socketServer.clients().length.should.equal(1);
div.click();
this.server.respond();
this.tickMock();
this.socketServer.clients().length.should.equal(0);
})
it('is closed after removal by js when message is received', function () {
this.server.respondWith("GET", "/test", "Clicked!");
var div = make('<div hx-get="/test" hx-swap="outerHTML" hx-ext="ws" ws-connect="ws://localhost:8080">');
this.tickMock();
this.socketServer.clients().length.should.equal(1);
div.parentElement.removeChild(div);
this.socketServer.emit('message', 'foo');
this.tickMock();
this.socketServer.clients().length.should.equal(0);
})
it('sends data to the server', function () {
var div = make('<div hx-ext="ws" ws-connect="ws://localhost:8080"><div ws-send id="d1">div1</div></div>');
this.tickMock();
byId("d1").click();
this.tickMock();
this.messages.length.should.equal(1);
})
it('handles message from the server', function () {
var div = make('<div hx-ext="ws" ws-connect="ws://localhost:8080"><div id="d1">div1</div><div id="d2">div2</div></div>');
this.tickMock();
this.socketServer.emit('message', "<div id=\"d1\">replaced</div>");
this.tickMock();
byId("d1").innerHTML.should.equal("replaced");
byId("d2").innerHTML.should.equal("div2");
})
it('raises lifecycle events (connecting, open, close) in correct order', function () {
var handledEventTypes = [];
var handler = function (evt) { handledEventTypes.push(evt.detail.event.type) };
htmx.on("htmx:wsConnecting", handler);
var div = make('<div hx-get="/test" hx-swap="outerHTML" hx-ext="ws" ws-connect="ws://localhost:8080">');
htmx.on(div, "htmx:wsOpen", handler);
htmx.on(div, "htmx:wsClose", handler);
this.tickMock();
div.parentElement.removeChild(div);
this.socketServer.emit('message', 'foo');
this.tickMock();
handledEventTypes.should.eql(['connecting', 'open', 'close']);
this.tickMock();
htmx.off("htmx:wsConnecting", handler);
htmx.off(div, "htmx:wsOpen", handler);
htmx.off(div, "htmx:wsClose", handler);
})
it('raises htmx:wsConfigSend when sending, allows message modification', function () {
var myEventCalled = false;
function handle(evt) {
myEventCalled = true;
evt.detail.parameters.foo = "bar";
}
htmx.on("htmx:wsConfigSend", handle)
var div = make('<div hx-ext="ws" ws-connect="ws://localhost:8080"><div ws-send id="d1">div1</div></div>');
this.tickMock();
byId("d1").click();
this.tickMock();
myEventCalled.should.be.true;
this.messages.length.should.equal(1);
this.messages[0].should.contains('"foo":"bar"')
htmx.off("htmx:wsConfigSend", handle)
})
it('passes socketWrapper to htmx:wsConfigSend', function () {
var socketWrapper = null;
function handle(evt) {
evt.preventDefault();
socketWrapper = evt.detail.socketWrapper;
socketWrapper.send(JSON.stringify({foo: 'bar'}), evt.detail.elt)
}
htmx.on("htmx:wsConfigSend", handle)
var div = make('<div hx-ext="ws" ws-connect="ws://localhost:8080"><div ws-send id="d1">div1</div></div>');
this.tickMock();
byId("d1").click();
this.tickMock();
socketWrapper.should.not.be.null;
socketWrapper.send.should.be.a('function');
socketWrapper.sendImmediately.should.be.a('function');
socketWrapper.queue.should.be.an('array');
this.messages.length.should.equal(1);
this.messages[0].should.contains('"foo":"bar"')
htmx.off("htmx:wsConfigSend", handle);
})
it('cancels sending when htmx:wsConfigSend is cancelled', function () {
var myEventCalled = false;
function handle(evt) {
myEventCalled = true;
evt.preventDefault();
}
htmx.on("htmx:wsConfigSend", handle)
var div = make('<div hx-ext="ws" ws-connect="ws://localhost:8080"><div ws-send id="d1">div1</div></div>');
this.tickMock();
byId("d1").click();
this.messages.length.should.equal(0);
myEventCalled.should.be.true;
htmx.off("htmx:wsConfigSend", handle);
})
it('raises htmx:wsBeforeSend when sending', function () {
var myEventCalled = false;
function handle(evt) {
myEventCalled = true;
}
htmx.on("htmx:wsBeforeSend", handle)
var div = make('<div hx-ext="ws" ws-connect="ws://localhost:8080"><div ws-send id="d1">div1</div></div>');
this.tickMock();
byId("d1").click();
this.tickMock();
myEventCalled.should.be.true;
this.messages.length.should.equal(1);
htmx.off("htmx:wsBeforeSend", handle)
})
it('cancels sending when htmx:wsBeforeSend is cancelled', function () {
var myEventCalled = false;
function handle(evt) {
myEventCalled = true;
evt.preventDefault();
}
htmx.on("htmx:wsBeforeSend", handle)
var div = make('<div hx-ext="ws" ws-connect="ws://localhost:8080"><div ws-send id="d1">div1</div></div>');
this.tickMock();
byId("d1").click();
this.tickMock();
myEventCalled.should.be.true;
this.messages.length.should.equal(0);
htmx.off("htmx:wsBeforeSend", handle)
})
it('raises htmx:wsAfterSend when sending', function () {
var myEventCalled = false;
function handle(evt) {
myEventCalled = true;
}
htmx.on("htmx:wsAfterSend", handle)
var div = make('<div hx-ext="ws" ws-connect="ws://localhost:8080"><div ws-send id="d1">div1</div></div>');
this.tickMock();
byId("d1").click();
this.tickMock();
myEventCalled.should.be.true;
this.messages.length.should.equal(1);
htmx.off("htmx:wsAfterSend", handle)
})
it('raises htmx:wsBeforeMessage when receiving message from the server', function () {
var myEventCalled = false;
function handle(evt) {
myEventCalled = true;
}
htmx.on("htmx:wsBeforeMessage", handle)
var div = make('<div hx-ext="ws" ws-connect="ws://localhost:8080"><div id="d1">div1</div><div id="d2">div2</div></div>');
this.tickMock();
this.socketServer.emit('message', "<div id=\"d1\">replaced</div>")
this.tickMock();
myEventCalled.should.be.true;
htmx.off("htmx:wsBeforeMessage", handle)
})
it('cancels swap when htmx:wsBeforeMessage was cancelled', function () {
var myEventCalled = false;
function handle(evt) {
myEventCalled = true;
evt.preventDefault();
}
htmx.on("htmx:wsBeforeMessage", handle)
var div = make('<div hx-ext="ws" ws-connect="ws://localhost:8080"><div id="d1">div1</div><div id="d2">div2</div></div>');
this.tickMock();
this.socketServer.emit('message', "<div id=\"d1\">replaced</div>")
this.tickMock();
myEventCalled.should.be.true;
byId("d1").innerHTML.should.equal("div1");
byId("d2").innerHTML.should.equal("div2");
htmx.off("htmx:wsBeforeMessage", handle)
})
it('raises htmx:wsAfterMessage when message was completely processed', function () {
var myEventCalled = false;
function handle(evt) {
myEventCalled = true;
}
htmx.on("htmx:wsAfterMessage", handle)
var div = make('<div hx-ext="ws" ws-connect="ws://localhost:8080"><div id="d1">div1</div><div id="d2">div2</div></div>');
this.tickMock();
this.socketServer.emit('message', "<div id=\"d1\">replaced</div>")
this.tickMock();
myEventCalled.should.be.true;
htmx.off("htmx:wsAfterMessage", handle)
})
});

View File

@@ -0,0 +1,52 @@
<svg width="16" height="16" viewBox="0 0 135 140" xmlns="http://www.w3.org/2000/svg" fill="#494949">
<rect y="10" width="15" height="120" rx="6">
<animate attributeName="height"
begin="0.5s" dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="y"
begin="0.5s" dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
repeatCount="indefinite" />
</rect>
<rect x="30" y="10" width="15" height="120" rx="6">
<animate attributeName="height"
begin="0.25s" dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="y"
begin="0.25s" dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
repeatCount="indefinite" />
</rect>
<rect x="60" width="15" height="140" rx="6">
<animate attributeName="height"
begin="0s" dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="y"
begin="0s" dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
repeatCount="indefinite" />
</rect>
<rect x="90" y="10" width="15" height="120" rx="6">
<animate attributeName="height"
begin="0.25s" dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="y"
begin="0.25s" dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
repeatCount="indefinite" />
</rect>
<rect x="120" y="10" width="15" height="120" rx="6">
<animate attributeName="height"
begin="0.5s" dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="y"
begin="0.5s" dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
repeatCount="indefinite" />
</rect>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1,158 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Mocha Tests</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="../node_modules/mocha/mocha.css" />
<meta http-equiv="cache-control" content="no-cache, must-revalidate, post-check=0, pre-check=0" />
<meta http-equiv="cache-control" content="max-age=0" />
<meta http-equiv="expires" content="0" />
<meta http-equiv="expires" content="Tue, 01 Jan 1980 1:00:00 GMT" />
<meta http-equiv="pragma" content="no-cache" />
<meta name="htmx-config" content='{"historyEnabled":false,"defaultSettleDelay":0}'>
</head>
<body style="padding:20px;font-family: sans-serif">
<h1 style="margin-top: 40px">htmx.js test suite</h1>
<h2>Scratch Page</h2>
<ul>
<li>
<a href="scratch/scratch.html">Scratch Page</a>
</li>
</ul>
<h2>Manual Tests</h2>
<a href="manual">Here</a>
<h2>Mocha Test Suite</h2>
<a href="index.html">[ALL]</a>
<script src="../node_modules/chai/chai.js"></script>
<script src="../node_modules/chai-dom/chai-dom.js"></script>
<script src="../node_modules/mocha/mocha.js"></script>
<script src="../node_modules/mocha-webdriver-runner/dist/mocha-webdriver-client.js"></script>
<script src="../node_modules/sinon/pkg/sinon.js"></script>
<script src="../node_modules/mock-socket/dist/mock-socket.js"></script>
<script src="../src/htmx.js"></script>
<script class="mocha-init">
mocha.setup('bdd');
mocha.checkLeaks();
should = chai.should();
</script>
<script src="util/util.js"></script>
<!-- core tests -->
<script src="core/internals.js"></script>
<script src="core/api.js"></script>
<script src="core/ajax.js"></script>
<script src="core/verbs.js"></script>
<script src="core/parameters.js"></script>
<script src="core/headers.js"></script>
<script src="core/regressions.js"></script>
<script src="core/security.js"></script>
<script src="core/perf.js"></script>
<script src="core/validation.js"></script>
<script src="core/tokenizer.js"></script>
<!-- attribute tests -->
<script src="attributes/hx-boost.js"></script>
<script src="attributes/hx-delete.js"></script>
<script src="attributes/hx-ext.js"></script>
<script src="attributes/hx-get.js"></script>
<script src="attributes/hx-headers.js"></script>
<script src="attributes/hx-history.js"></script>
<script src="attributes/hx-include.js"></script>
<script src="attributes/hx-indicator.js"></script>
<script src="attributes/hx-disinherit.js"></script>
<script src="attributes/hx-params.js"></script>
<script src="attributes/hx-patch.js"></script>
<script src="attributes/hx-post.js"></script>
<script src="attributes/hx-preserve.js"></script>
<script src="attributes/hx-push-url.js"></script>
<script src="attributes/hx-put.js"></script>
<script src="attributes/hx-request.js"></script>
<script src="attributes/hx-select.js"></script>
<script src="attributes/hx-select-oob.js"></script>
<script src="attributes/hx-sse.js"></script>
<script src="attributes/hx-swap-oob.js"></script>
<script src="attributes/hx-swap.js"></script>
<script src="attributes/hx-sync.js"></script>
<script src="attributes/hx-target.js"></script>
<script src="attributes/hx-trigger.js"></script>
<script src="attributes/hx-vals.js"></script>
<script src="attributes/hx-vars.js"></script>
<script src="attributes/hx-ws.js"></script>
<!-- hyperscript integration -->
<script src="lib/_hyperscript.js"></script>
<script src="ext/hyperscript.js"></script>
<!-- extension tests -->
<script src="ext/extension-swap.js"></script>
<script src="../src/ext/method-override.js"></script>
<script src="ext/method-override.js"></script>
<script src="../src/ext/debug.js"></script>
<script src="ext/debug.js"></script>
<script src="lib/morphdom-umd.js"></script>
<script src="../src/ext/morphdom-swap.js"></script>
<script src="ext/morphdom-swap.js"></script>
<script src="../src/ext/json-enc.js"></script>
<script src="ext/json-enc.js"></script>
<script src="lib/handlebars-v4.7.6.js"></script>
<script src="lib/mustache.js"></script>
<script src="../src/ext/client-side-templates.js"></script>
<script src="ext/client-side-templates.js"></script>
<script src="../src/ext/path-deps.js"></script>
<script src="ext/path-deps.js"></script>
<script src="../src/ext/class-tools.js"></script>
<script src="ext/class-tools.js"></script>
<script src="../src/ext/loading-states.js"></script>
<script src="ext/loading-states.js"></script>
<script src="ext/bad-extension.js"></script>
<script src="../src/ext/remove-me.js"></script>
<script src="ext/remove-me.js"></script>
<script src="../src/ext/include-vals.js"></script>
<script src="ext/include-vals.js"></script>
<script src="../src/ext/ajax-header.js"></script>
<script src="ext/ajax-header.js"></script>
<script src="../src/ext/event-header.js"></script>
<script src="ext/event-header.js"></script>
<script src="../src/ext/disable-element.js"></script>
<script src="ext/disable-element.js"></script>
<script src="../src/ext/multi-swap.js"></script>
<script src="ext/multi-swap.js"></script>
<script src="../src/ext/ws.js"></script>
<script src="ext/ws.js"></script>
<!-- events last so they don't screw up other tests -->
<script src="core/events.js"></script>
<div id="mocha"></div>
<script class="mocha-exec">
mocha.run();
</script>
<em>Work Area</em>
<hr/>
<div id="work-area" hx-history-elt hx-ext="sse">
</div>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,763 @@
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = global || self, global.morphdom = factory());
}(this, function () { 'use strict';
var DOCUMENT_FRAGMENT_NODE = 11;
function morphAttrs(fromNode, toNode) {
var toNodeAttrs = toNode.attributes;
var attr;
var attrName;
var attrNamespaceURI;
var attrValue;
var fromValue;
// document-fragments dont have attributes so lets not do anything
if (toNode.nodeType === DOCUMENT_FRAGMENT_NODE || fromNode.nodeType === DOCUMENT_FRAGMENT_NODE) {
return;
}
// update attributes on original DOM element
for (var i = toNodeAttrs.length - 1; i >= 0; i--) {
attr = toNodeAttrs[i];
attrName = attr.name;
attrNamespaceURI = attr.namespaceURI;
attrValue = attr.value;
if (attrNamespaceURI) {
attrName = attr.localName || attrName;
fromValue = fromNode.getAttributeNS(attrNamespaceURI, attrName);
if (fromValue !== attrValue) {
if (attr.prefix === 'xmlns'){
attrName = attr.name; // It's not allowed to set an attribute with the XMLNS namespace without specifying the `xmlns` prefix
}
fromNode.setAttributeNS(attrNamespaceURI, attrName, attrValue);
}
} else {
fromValue = fromNode.getAttribute(attrName);
if (fromValue !== attrValue) {
fromNode.setAttribute(attrName, attrValue);
}
}
}
// Remove any extra attributes found on the original DOM element that
// weren't found on the target element.
var fromNodeAttrs = fromNode.attributes;
for (var d = fromNodeAttrs.length - 1; d >= 0; d--) {
attr = fromNodeAttrs[d];
attrName = attr.name;
attrNamespaceURI = attr.namespaceURI;
if (attrNamespaceURI) {
attrName = attr.localName || attrName;
if (!toNode.hasAttributeNS(attrNamespaceURI, attrName)) {
fromNode.removeAttributeNS(attrNamespaceURI, attrName);
}
} else {
if (!toNode.hasAttribute(attrName)) {
fromNode.removeAttribute(attrName);
}
}
}
}
var range; // Create a range object for efficently rendering strings to elements.
var NS_XHTML = 'http://www.w3.org/1999/xhtml';
var doc = typeof document === 'undefined' ? undefined : document;
var HAS_TEMPLATE_SUPPORT = !!doc && 'content' in doc.createElement('template');
var HAS_RANGE_SUPPORT = !!doc && doc.createRange && 'createContextualFragment' in doc.createRange();
function createFragmentFromTemplate(str) {
var template = doc.createElement('template');
template.innerHTML = str;
return template.content.childNodes[0];
}
function createFragmentFromRange(str) {
if (!range) {
range = doc.createRange();
range.selectNode(doc.body);
}
var fragment = range.createContextualFragment(str);
return fragment.childNodes[0];
}
function createFragmentFromWrap(str) {
var fragment = doc.createElement('body');
fragment.innerHTML = str;
return fragment.childNodes[0];
}
/**
* This is about the same
* var html = new DOMParser().parseFromString(str, 'text/html');
* return html.body.firstChild;
*
* @method toElement
* @param {String} str
*/
function toElement(str) {
str = str.trim();
if (HAS_TEMPLATE_SUPPORT) {
// avoid restrictions on content for things like `<tr><th>Hi</th></tr>` which
// createContextualFragment doesn't support
// <template> support not available in IE
return createFragmentFromTemplate(str);
} else if (HAS_RANGE_SUPPORT) {
return createFragmentFromRange(str);
}
return createFragmentFromWrap(str);
}
/**
* Returns true if two node's names are the same.
*
* NOTE: We don't bother checking `namespaceURI` because you will never find two HTML elements with the same
* nodeName and different namespace URIs.
*
* @param {Element} a
* @param {Element} b The target element
* @return {boolean}
*/
function compareNodeNames(fromEl, toEl) {
var fromNodeName = fromEl.nodeName;
var toNodeName = toEl.nodeName;
var fromCodeStart, toCodeStart;
if (fromNodeName === toNodeName) {
return true;
}
fromCodeStart = fromNodeName.charCodeAt(0);
toCodeStart = toNodeName.charCodeAt(0);
// If the target element is a virtual DOM node or SVG node then we may
// need to normalize the tag name before comparing. Normal HTML elements that are
// in the "http://www.w3.org/1999/xhtml"
// are converted to upper case
if (fromCodeStart <= 90 && toCodeStart >= 97) { // from is upper and to is lower
return fromNodeName === toNodeName.toUpperCase();
} else if (toCodeStart <= 90 && fromCodeStart >= 97) { // to is upper and from is lower
return toNodeName === fromNodeName.toUpperCase();
} else {
return false;
}
}
/**
* Create an element, optionally with a known namespace URI.
*
* @param {string} name the element name, e.g. 'div' or 'svg'
* @param {string} [namespaceURI] the element's namespace URI, i.e. the value of
* its `xmlns` attribute or its inferred namespace.
*
* @return {Element}
*/
function createElementNS(name, namespaceURI) {
return !namespaceURI || namespaceURI === NS_XHTML ?
doc.createElement(name) :
doc.createElementNS(namespaceURI, name);
}
/**
* Copies the children of one DOM element to another DOM element
*/
function moveChildren(fromEl, toEl) {
var curChild = fromEl.firstChild;
while (curChild) {
var nextChild = curChild.nextSibling;
toEl.appendChild(curChild);
curChild = nextChild;
}
return toEl;
}
function syncBooleanAttrProp(fromEl, toEl, name) {
if (fromEl[name] !== toEl[name]) {
fromEl[name] = toEl[name];
if (fromEl[name]) {
fromEl.setAttribute(name, '');
} else {
fromEl.removeAttribute(name);
}
}
}
var specialElHandlers = {
OPTION: function(fromEl, toEl) {
var parentNode = fromEl.parentNode;
if (parentNode) {
var parentName = parentNode.nodeName.toUpperCase();
if (parentName === 'OPTGROUP') {
parentNode = parentNode.parentNode;
parentName = parentNode && parentNode.nodeName.toUpperCase();
}
if (parentName === 'SELECT' && !parentNode.hasAttribute('multiple')) {
if (fromEl.hasAttribute('selected') && !toEl.selected) {
// Workaround for MS Edge bug where the 'selected' attribute can only be
// removed if set to a non-empty value:
// https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/12087679/
fromEl.setAttribute('selected', 'selected');
fromEl.removeAttribute('selected');
}
// We have to reset select element's selectedIndex to -1, otherwise setting
// fromEl.selected using the syncBooleanAttrProp below has no effect.
// The correct selectedIndex will be set in the SELECT special handler below.
parentNode.selectedIndex = -1;
}
}
syncBooleanAttrProp(fromEl, toEl, 'selected');
},
/**
* The "value" attribute is special for the <input> element since it sets
* the initial value. Changing the "value" attribute without changing the
* "value" property will have no effect since it is only used to the set the
* initial value. Similar for the "checked" attribute, and "disabled".
*/
INPUT: function(fromEl, toEl) {
syncBooleanAttrProp(fromEl, toEl, 'checked');
syncBooleanAttrProp(fromEl, toEl, 'disabled');
if (fromEl.value !== toEl.value) {
fromEl.value = toEl.value;
}
if (!toEl.hasAttribute('value')) {
fromEl.removeAttribute('value');
}
},
TEXTAREA: function(fromEl, toEl) {
var newValue = toEl.value;
if (fromEl.value !== newValue) {
fromEl.value = newValue;
}
var firstChild = fromEl.firstChild;
if (firstChild) {
// Needed for IE. Apparently IE sets the placeholder as the
// node value and vise versa. This ignores an empty update.
var oldValue = firstChild.nodeValue;
if (oldValue == newValue || (!newValue && oldValue == fromEl.placeholder)) {
return;
}
firstChild.nodeValue = newValue;
}
},
SELECT: function(fromEl, toEl) {
if (!toEl.hasAttribute('multiple')) {
var selectedIndex = -1;
var i = 0;
// We have to loop through children of fromEl, not toEl since nodes can be moved
// from toEl to fromEl directly when morphing.
// At the time this special handler is invoked, all children have already been morphed
// and appended to / removed from fromEl, so using fromEl here is safe and correct.
var curChild = fromEl.firstChild;
var optgroup;
var nodeName;
while(curChild) {
nodeName = curChild.nodeName && curChild.nodeName.toUpperCase();
if (nodeName === 'OPTGROUP') {
optgroup = curChild;
curChild = optgroup.firstChild;
} else {
if (nodeName === 'OPTION') {
if (curChild.hasAttribute('selected')) {
selectedIndex = i;
break;
}
i++;
}
curChild = curChild.nextSibling;
if (!curChild && optgroup) {
curChild = optgroup.nextSibling;
optgroup = null;
}
}
}
fromEl.selectedIndex = selectedIndex;
}
}
};
var ELEMENT_NODE = 1;
var DOCUMENT_FRAGMENT_NODE$1 = 11;
var TEXT_NODE = 3;
var COMMENT_NODE = 8;
function noop() {}
function defaultGetNodeKey(node) {
if (node) {
return (node.getAttribute && node.getAttribute('id')) || node.id;
}
}
function morphdomFactory(morphAttrs) {
return function morphdom(fromNode, toNode, options) {
if (!options) {
options = {};
}
if (typeof toNode === 'string') {
if (fromNode.nodeName === '#document' || fromNode.nodeName === 'HTML' || fromNode.nodeName === 'BODY') {
var toNodeHtml = toNode;
toNode = doc.createElement('html');
toNode.innerHTML = toNodeHtml;
} else {
toNode = toElement(toNode);
}
}
var getNodeKey = options.getNodeKey || defaultGetNodeKey;
var onBeforeNodeAdded = options.onBeforeNodeAdded || noop;
var onNodeAdded = options.onNodeAdded || noop;
var onBeforeElUpdated = options.onBeforeElUpdated || noop;
var onElUpdated = options.onElUpdated || noop;
var onBeforeNodeDiscarded = options.onBeforeNodeDiscarded || noop;
var onNodeDiscarded = options.onNodeDiscarded || noop;
var onBeforeElChildrenUpdated = options.onBeforeElChildrenUpdated || noop;
var childrenOnly = options.childrenOnly === true;
// This object is used as a lookup to quickly find all keyed elements in the original DOM tree.
var fromNodesLookup = Object.create(null);
var keyedRemovalList = [];
function addKeyedRemoval(key) {
keyedRemovalList.push(key);
}
function walkDiscardedChildNodes(node, skipKeyedNodes) {
if (node.nodeType === ELEMENT_NODE) {
var curChild = node.firstChild;
while (curChild) {
var key = undefined;
if (skipKeyedNodes && (key = getNodeKey(curChild))) {
// If we are skipping keyed nodes then we add the key
// to a list so that it can be handled at the very end.
addKeyedRemoval(key);
} else {
// Only report the node as discarded if it is not keyed. We do this because
// at the end we loop through all keyed elements that were unmatched
// and then discard them in one final pass.
onNodeDiscarded(curChild);
if (curChild.firstChild) {
walkDiscardedChildNodes(curChild, skipKeyedNodes);
}
}
curChild = curChild.nextSibling;
}
}
}
/**
* Removes a DOM node out of the original DOM
*
* @param {Node} node The node to remove
* @param {Node} parentNode The nodes parent
* @param {Boolean} skipKeyedNodes If true then elements with keys will be skipped and not discarded.
* @return {undefined}
*/
function removeNode(node, parentNode, skipKeyedNodes) {
if (onBeforeNodeDiscarded(node) === false) {
return;
}
if (parentNode) {
parentNode.removeChild(node);
}
onNodeDiscarded(node);
walkDiscardedChildNodes(node, skipKeyedNodes);
}
// // TreeWalker implementation is no faster, but keeping this around in case this changes in the future
// function indexTree(root) {
// var treeWalker = document.createTreeWalker(
// root,
// NodeFilter.SHOW_ELEMENT);
//
// var el;
// while((el = treeWalker.nextNode())) {
// var key = getNodeKey(el);
// if (key) {
// fromNodesLookup[key] = el;
// }
// }
// }
// // NodeIterator implementation is no faster, but keeping this around in case this changes in the future
//
// function indexTree(node) {
// var nodeIterator = document.createNodeIterator(node, NodeFilter.SHOW_ELEMENT);
// var el;
// while((el = nodeIterator.nextNode())) {
// var key = getNodeKey(el);
// if (key) {
// fromNodesLookup[key] = el;
// }
// }
// }
function indexTree(node) {
if (node.nodeType === ELEMENT_NODE || node.nodeType === DOCUMENT_FRAGMENT_NODE$1) {
var curChild = node.firstChild;
while (curChild) {
var key = getNodeKey(curChild);
if (key) {
fromNodesLookup[key] = curChild;
}
// Walk recursively
indexTree(curChild);
curChild = curChild.nextSibling;
}
}
}
indexTree(fromNode);
function handleNodeAdded(el) {
onNodeAdded(el);
var curChild = el.firstChild;
while (curChild) {
var nextSibling = curChild.nextSibling;
var key = getNodeKey(curChild);
if (key) {
var unmatchedFromEl = fromNodesLookup[key];
// if we find a duplicate #id node in cache, replace `el` with cache value
// and morph it to the child node.
if (unmatchedFromEl && compareNodeNames(curChild, unmatchedFromEl)) {
curChild.parentNode.replaceChild(unmatchedFromEl, curChild);
morphEl(unmatchedFromEl, curChild);
} else {
handleNodeAdded(curChild);
}
} else {
// recursively call for curChild and it's children to see if we find something in
// fromNodesLookup
handleNodeAdded(curChild);
}
curChild = nextSibling;
}
}
function cleanupFromEl(fromEl, curFromNodeChild, curFromNodeKey) {
// We have processed all of the "to nodes". If curFromNodeChild is
// non-null then we still have some from nodes left over that need
// to be removed
while (curFromNodeChild) {
var fromNextSibling = curFromNodeChild.nextSibling;
if ((curFromNodeKey = getNodeKey(curFromNodeChild))) {
// Since the node is keyed it might be matched up later so we defer
// the actual removal to later
addKeyedRemoval(curFromNodeKey);
} else {
// NOTE: we skip nested keyed nodes from being removed since there is
// still a chance they will be matched up later
removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */);
}
curFromNodeChild = fromNextSibling;
}
}
function morphEl(fromEl, toEl, childrenOnly) {
var toElKey = getNodeKey(toEl);
if (toElKey) {
// If an element with an ID is being morphed then it will be in the final
// DOM so clear it out of the saved elements collection
delete fromNodesLookup[toElKey];
}
if (!childrenOnly) {
// optional
if (onBeforeElUpdated(fromEl, toEl) === false) {
return;
}
// update attributes on original DOM element first
morphAttrs(fromEl, toEl);
// optional
onElUpdated(fromEl);
if (onBeforeElChildrenUpdated(fromEl, toEl) === false) {
return;
}
}
if (fromEl.nodeName !== 'TEXTAREA') {
morphChildren(fromEl, toEl);
} else {
specialElHandlers.TEXTAREA(fromEl, toEl);
}
}
function morphChildren(fromEl, toEl) {
var curToNodeChild = toEl.firstChild;
var curFromNodeChild = fromEl.firstChild;
var curToNodeKey;
var curFromNodeKey;
var fromNextSibling;
var toNextSibling;
var matchingFromEl;
// walk the children
outer: while (curToNodeChild) {
toNextSibling = curToNodeChild.nextSibling;
curToNodeKey = getNodeKey(curToNodeChild);
// walk the fromNode children all the way through
while (curFromNodeChild) {
fromNextSibling = curFromNodeChild.nextSibling;
if (curToNodeChild.isSameNode && curToNodeChild.isSameNode(curFromNodeChild)) {
curToNodeChild = toNextSibling;
curFromNodeChild = fromNextSibling;
continue outer;
}
curFromNodeKey = getNodeKey(curFromNodeChild);
var curFromNodeType = curFromNodeChild.nodeType;
// this means if the curFromNodeChild doesnt have a match with the curToNodeChild
var isCompatible = undefined;
if (curFromNodeType === curToNodeChild.nodeType) {
if (curFromNodeType === ELEMENT_NODE) {
// Both nodes being compared are Element nodes
if (curToNodeKey) {
// The target node has a key so we want to match it up with the correct element
// in the original DOM tree
if (curToNodeKey !== curFromNodeKey) {
// The current element in the original DOM tree does not have a matching key so
// let's check our lookup to see if there is a matching element in the original
// DOM tree
if ((matchingFromEl = fromNodesLookup[curToNodeKey])) {
if (fromNextSibling === matchingFromEl) {
// Special case for single element removals. To avoid removing the original
// DOM node out of the tree (since that can break CSS transitions, etc.),
// we will instead discard the current node and wait until the next
// iteration to properly match up the keyed target element with its matching
// element in the original tree
isCompatible = false;
} else {
// We found a matching keyed element somewhere in the original DOM tree.
// Let's move the original DOM node into the current position and morph
// it.
// NOTE: We use insertBefore instead of replaceChild because we want to go through
// the `removeNode()` function for the node that is being discarded so that
// all lifecycle hooks are correctly invoked
fromEl.insertBefore(matchingFromEl, curFromNodeChild);
// fromNextSibling = curFromNodeChild.nextSibling;
if (curFromNodeKey) {
// Since the node is keyed it might be matched up later so we defer
// the actual removal to later
addKeyedRemoval(curFromNodeKey);
} else {
// NOTE: we skip nested keyed nodes from being removed since there is
// still a chance they will be matched up later
removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */);
}
curFromNodeChild = matchingFromEl;
}
} else {
// The nodes are not compatible since the "to" node has a key and there
// is no matching keyed node in the source tree
isCompatible = false;
}
}
} else if (curFromNodeKey) {
// The original has a key
isCompatible = false;
}
isCompatible = isCompatible !== false && compareNodeNames(curFromNodeChild, curToNodeChild);
if (isCompatible) {
// We found compatible DOM elements so transform
// the current "from" node to match the current
// target DOM node.
// MORPH
morphEl(curFromNodeChild, curToNodeChild);
}
} else if (curFromNodeType === TEXT_NODE || curFromNodeType == COMMENT_NODE) {
// Both nodes being compared are Text or Comment nodes
isCompatible = true;
// Simply update nodeValue on the original node to
// change the text value
if (curFromNodeChild.nodeValue !== curToNodeChild.nodeValue) {
curFromNodeChild.nodeValue = curToNodeChild.nodeValue;
}
}
}
if (isCompatible) {
// Advance both the "to" child and the "from" child since we found a match
// Nothing else to do as we already recursively called morphChildren above
curToNodeChild = toNextSibling;
curFromNodeChild = fromNextSibling;
continue outer;
}
// No compatible match so remove the old node from the DOM and continue trying to find a
// match in the original DOM. However, we only do this if the from node is not keyed
// since it is possible that a keyed node might match up with a node somewhere else in the
// target tree and we don't want to discard it just yet since it still might find a
// home in the final DOM tree. After everything is done we will remove any keyed nodes
// that didn't find a home
if (curFromNodeKey) {
// Since the node is keyed it might be matched up later so we defer
// the actual removal to later
addKeyedRemoval(curFromNodeKey);
} else {
// NOTE: we skip nested keyed nodes from being removed since there is
// still a chance they will be matched up later
removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */);
}
curFromNodeChild = fromNextSibling;
} // END: while(curFromNodeChild) {}
// If we got this far then we did not find a candidate match for
// our "to node" and we exhausted all of the children "from"
// nodes. Therefore, we will just append the current "to" node
// to the end
if (curToNodeKey && (matchingFromEl = fromNodesLookup[curToNodeKey]) && compareNodeNames(matchingFromEl, curToNodeChild)) {
fromEl.appendChild(matchingFromEl);
// MORPH
morphEl(matchingFromEl, curToNodeChild);
} else {
var onBeforeNodeAddedResult = onBeforeNodeAdded(curToNodeChild);
if (onBeforeNodeAddedResult !== false) {
if (onBeforeNodeAddedResult) {
curToNodeChild = onBeforeNodeAddedResult;
}
if (curToNodeChild.actualize) {
curToNodeChild = curToNodeChild.actualize(fromEl.ownerDocument || doc);
}
fromEl.appendChild(curToNodeChild);
handleNodeAdded(curToNodeChild);
}
}
curToNodeChild = toNextSibling;
curFromNodeChild = fromNextSibling;
}
cleanupFromEl(fromEl, curFromNodeChild, curFromNodeKey);
var specialElHandler = specialElHandlers[fromEl.nodeName];
if (specialElHandler) {
specialElHandler(fromEl, toEl);
}
} // END: morphChildren(...)
var morphedNode = fromNode;
var morphedNodeType = morphedNode.nodeType;
var toNodeType = toNode.nodeType;
if (!childrenOnly) {
// Handle the case where we are given two DOM nodes that are not
// compatible (e.g. <div> --> <span> or <div> --> TEXT)
if (morphedNodeType === ELEMENT_NODE) {
if (toNodeType === ELEMENT_NODE) {
if (!compareNodeNames(fromNode, toNode)) {
onNodeDiscarded(fromNode);
morphedNode = moveChildren(fromNode, createElementNS(toNode.nodeName, toNode.namespaceURI));
}
} else {
// Going from an element node to a text node
morphedNode = toNode;
}
} else if (morphedNodeType === TEXT_NODE || morphedNodeType === COMMENT_NODE) { // Text or comment node
if (toNodeType === morphedNodeType) {
if (morphedNode.nodeValue !== toNode.nodeValue) {
morphedNode.nodeValue = toNode.nodeValue;
}
return morphedNode;
} else {
// Text node to something else
morphedNode = toNode;
}
}
}
if (morphedNode === toNode) {
// The "to node" was not compatible with the "from node" so we had to
// toss out the "from node" and use the "to node"
onNodeDiscarded(fromNode);
} else {
if (toNode.isSameNode && toNode.isSameNode(morphedNode)) {
return;
}
morphEl(morphedNode, toNode, childrenOnly);
// We now need to loop over any keyed nodes that might need to be
// removed. We only do the removal if we know that the keyed node
// never found a match. When a keyed node is matched up we remove
// it out of fromNodesLookup and we use fromNodesLookup to determine
// if a keyed node has been matched up or not
if (keyedRemovalList) {
for (var i=0, len=keyedRemovalList.length; i<len; i++) {
var elToRemove = fromNodesLookup[keyedRemovalList[i]];
if (elToRemove) {
removeNode(elToRemove, elToRemove.parentNode, false);
}
}
}
}
if (!childrenOnly && morphedNode !== fromNode && fromNode.parentNode) {
if (morphedNode.actualize) {
morphedNode = morphedNode.actualize(fromNode.ownerDocument || doc);
}
// If we had to swap out the from node with a new node because the old
// node was not compatible with the target node then we need to
// replace the old DOM node in the original DOM tree. This is only
// possible if the original DOM node was part of a DOM tree which
// we know is the case if it has a parent node.
fromNode.parentNode.replaceChild(morphedNode, fromNode);
}
return morphedNode;
};
}
var morphdom = morphdomFactory(morphAttrs);
return morphdom;
}));

View File

@@ -0,0 +1,740 @@
// This file has been generated from mustache.mjs
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = global || self, global.Mustache = factory());
}(this, (function () { 'use strict';
/*!
* mustache.js - Logic-less {{mustache}} templates with JavaScript
* http://github.com/janl/mustache.js
*/
var objectToString = Object.prototype.toString;
var isArray = Array.isArray || function isArrayPolyfill (object) {
return objectToString.call(object) === '[object Array]';
};
function isFunction (object) {
return typeof object === 'function';
}
/**
* More correct typeof string handling array
* which normally returns typeof 'object'
*/
function typeStr (obj) {
return isArray(obj) ? 'array' : typeof obj;
}
function escapeRegExp (string) {
return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&');
}
/**
* Null safe way of checking whether or not an object,
* including its prototype, has a given property
*/
function hasProperty (obj, propName) {
return obj != null && typeof obj === 'object' && (propName in obj);
}
/**
* Safe way of detecting whether or not the given thing is a primitive and
* whether it has the given property
*/
function primitiveHasOwnProperty (primitive, propName) {
return (
primitive != null
&& typeof primitive !== 'object'
&& primitive.hasOwnProperty
&& primitive.hasOwnProperty(propName)
);
}
// Workaround for https://issues.apache.org/jira/browse/COUCHDB-577
// See https://github.com/janl/mustache.js/issues/189
var regExpTest = RegExp.prototype.test;
function testRegExp (re, string) {
return regExpTest.call(re, string);
}
var nonSpaceRe = /\S/;
function isWhitespace (string) {
return !testRegExp(nonSpaceRe, string);
}
var entityMap = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
'/': '&#x2F;',
'`': '&#x60;',
'=': '&#x3D;'
};
function escapeHtml (string) {
return String(string).replace(/[&<>"'`=\/]/g, function fromEntityMap (s) {
return entityMap[s];
});
}
var whiteRe = /\s*/;
var spaceRe = /\s+/;
var equalsRe = /\s*=/;
var curlyRe = /\s*\}/;
var tagRe = /#|\^|\/|>|\{|&|=|!/;
/**
* Breaks up the given `template` string into a tree of tokens. If the `tags`
* argument is given here it must be an array with two string values: the
* opening and closing tags used in the template (e.g. [ "<%", "%>" ]). Of
* course, the default is to use mustaches (i.e. mustache.tags).
*
* A token is an array with at least 4 elements. The first element is the
* mustache symbol that was used inside the tag, e.g. "#" or "&". If the tag
* did not contain a symbol (i.e. {{myValue}}) this element is "name". For
* all text that appears outside a symbol this element is "text".
*
* The second element of a token is its "value". For mustache tags this is
* whatever else was inside the tag besides the opening symbol. For text tokens
* this is the text itself.
*
* The third and fourth elements of the token are the start and end indices,
* respectively, of the token in the original template.
*
* Tokens that are the root node of a subtree contain two more elements: 1) an
* array of tokens in the subtree and 2) the index in the original template at
* which the closing tag for that section begins.
*
* Tokens for partials also contain two more elements: 1) a string value of
* indendation prior to that tag and 2) the index of that tag on that line -
* eg a value of 2 indicates the partial is the third tag on this line.
*/
function parseTemplate (template, tags) {
if (!template)
return [];
var lineHasNonSpace = false;
var sections = []; // Stack to hold section tokens
var tokens = []; // Buffer to hold the tokens
var spaces = []; // Indices of whitespace tokens on the current line
var hasTag = false; // Is there a {{tag}} on the current line?
var nonSpace = false; // Is there a non-space char on the current line?
var indentation = ''; // Tracks indentation for tags that use it
var tagIndex = 0; // Stores a count of number of tags encountered on a line
// Strips all whitespace tokens array for the current line
// if there was a {{#tag}} on it and otherwise only space.
function stripSpace () {
if (hasTag && !nonSpace) {
while (spaces.length)
delete tokens[spaces.pop()];
} else {
spaces = [];
}
hasTag = false;
nonSpace = false;
}
var openingTagRe, closingTagRe, closingCurlyRe;
function compileTags (tagsToCompile) {
if (typeof tagsToCompile === 'string')
tagsToCompile = tagsToCompile.split(spaceRe, 2);
if (!isArray(tagsToCompile) || tagsToCompile.length !== 2)
throw new Error('Invalid tags: ' + tagsToCompile);
openingTagRe = new RegExp(escapeRegExp(tagsToCompile[0]) + '\\s*');
closingTagRe = new RegExp('\\s*' + escapeRegExp(tagsToCompile[1]));
closingCurlyRe = new RegExp('\\s*' + escapeRegExp('}' + tagsToCompile[1]));
}
compileTags(tags || mustache.tags);
var scanner = new Scanner(template);
var start, type, value, chr, token, openSection;
while (!scanner.eos()) {
start = scanner.pos;
// Match any text between tags.
value = scanner.scanUntil(openingTagRe);
if (value) {
for (var i = 0, valueLength = value.length; i < valueLength; ++i) {
chr = value.charAt(i);
if (isWhitespace(chr)) {
spaces.push(tokens.length);
indentation += chr;
} else {
nonSpace = true;
lineHasNonSpace = true;
indentation += ' ';
}
tokens.push([ 'text', chr, start, start + 1 ]);
start += 1;
// Check for whitespace on the current line.
if (chr === '\n') {
stripSpace();
indentation = '';
tagIndex = 0;
lineHasNonSpace = false;
}
}
}
// Match the opening tag.
if (!scanner.scan(openingTagRe))
break;
hasTag = true;
// Get the tag type.
type = scanner.scan(tagRe) || 'name';
scanner.scan(whiteRe);
// Get the tag value.
if (type === '=') {
value = scanner.scanUntil(equalsRe);
scanner.scan(equalsRe);
scanner.scanUntil(closingTagRe);
} else if (type === '{') {
value = scanner.scanUntil(closingCurlyRe);
scanner.scan(curlyRe);
scanner.scanUntil(closingTagRe);
type = '&';
} else {
value = scanner.scanUntil(closingTagRe);
}
// Match the closing tag.
if (!scanner.scan(closingTagRe))
throw new Error('Unclosed tag at ' + scanner.pos);
if (type == '>') {
token = [ type, value, start, scanner.pos, indentation, tagIndex, lineHasNonSpace ];
} else {
token = [ type, value, start, scanner.pos ];
}
tagIndex++;
tokens.push(token);
if (type === '#' || type === '^') {
sections.push(token);
} else if (type === '/') {
// Check section nesting.
openSection = sections.pop();
if (!openSection)
throw new Error('Unopened section "' + value + '" at ' + start);
if (openSection[1] !== value)
throw new Error('Unclosed section "' + openSection[1] + '" at ' + start);
} else if (type === 'name' || type === '{' || type === '&') {
nonSpace = true;
} else if (type === '=') {
// Set the tags for the next time around.
compileTags(value);
}
}
stripSpace();
// Make sure there are no open sections when we're done.
openSection = sections.pop();
if (openSection)
throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos);
return nestTokens(squashTokens(tokens));
}
/**
* Combines the values of consecutive text tokens in the given `tokens` array
* to a single token.
*/
function squashTokens (tokens) {
var squashedTokens = [];
var token, lastToken;
for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
token = tokens[i];
if (token) {
if (token[0] === 'text' && lastToken && lastToken[0] === 'text') {
lastToken[1] += token[1];
lastToken[3] = token[3];
} else {
squashedTokens.push(token);
lastToken = token;
}
}
}
return squashedTokens;
}
/**
* Forms the given array of `tokens` into a nested tree structure where
* tokens that represent a section have two additional items: 1) an array of
* all tokens that appear in that section and 2) the index in the original
* template that represents the end of that section.
*/
function nestTokens (tokens) {
var nestedTokens = [];
var collector = nestedTokens;
var sections = [];
var token, section;
for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
token = tokens[i];
switch (token[0]) {
case '#':
case '^':
collector.push(token);
sections.push(token);
collector = token[4] = [];
break;
case '/':
section = sections.pop();
section[5] = token[2];
collector = sections.length > 0 ? sections[sections.length - 1][4] : nestedTokens;
break;
default:
collector.push(token);
}
}
return nestedTokens;
}
/**
* A simple string scanner that is used by the template parser to find
* tokens in template strings.
*/
function Scanner (string) {
this.string = string;
this.tail = string;
this.pos = 0;
}
/**
* Returns `true` if the tail is empty (end of string).
*/
Scanner.prototype.eos = function eos () {
return this.tail === '';
};
/**
* Tries to match the given regular expression at the current position.
* Returns the matched text if it can match, the empty string otherwise.
*/
Scanner.prototype.scan = function scan (re) {
var match = this.tail.match(re);
if (!match || match.index !== 0)
return '';
var string = match[0];
this.tail = this.tail.substring(string.length);
this.pos += string.length;
return string;
};
/**
* Skips all text until the given regular expression can be matched. Returns
* the skipped string, which is the entire tail if no match can be made.
*/
Scanner.prototype.scanUntil = function scanUntil (re) {
var index = this.tail.search(re), match;
switch (index) {
case -1:
match = this.tail;
this.tail = '';
break;
case 0:
match = '';
break;
default:
match = this.tail.substring(0, index);
this.tail = this.tail.substring(index);
}
this.pos += match.length;
return match;
};
/**
* Represents a rendering context by wrapping a view object and
* maintaining a reference to the parent context.
*/
function Context (view, parentContext) {
this.view = view;
this.cache = { '.': this.view };
this.parent = parentContext;
}
/**
* Creates a new context using the given view with this context
* as the parent.
*/
Context.prototype.push = function push (view) {
return new Context(view, this);
};
/**
* Returns the value of the given name in this context, traversing
* up the context hierarchy if the value is absent in this context's view.
*/
Context.prototype.lookup = function lookup (name) {
var cache = this.cache;
var value;
if (cache.hasOwnProperty(name)) {
value = cache[name];
} else {
var context = this, intermediateValue, names, index, lookupHit = false;
while (context) {
if (name.indexOf('.') > 0) {
intermediateValue = context.view;
names = name.split('.');
index = 0;
/**
* Using the dot notion path in `name`, we descend through the
* nested objects.
*
* To be certain that the lookup has been successful, we have to
* check if the last object in the path actually has the property
* we are looking for. We store the result in `lookupHit`.
*
* This is specially necessary for when the value has been set to
* `undefined` and we want to avoid looking up parent contexts.
*
* In the case where dot notation is used, we consider the lookup
* to be successful even if the last "object" in the path is
* not actually an object but a primitive (e.g., a string, or an
* integer), because it is sometimes useful to access a property
* of an autoboxed primitive, such as the length of a string.
**/
while (intermediateValue != null && index < names.length) {
if (index === names.length - 1)
lookupHit = (
hasProperty(intermediateValue, names[index])
|| primitiveHasOwnProperty(intermediateValue, names[index])
);
intermediateValue = intermediateValue[names[index++]];
}
} else {
intermediateValue = context.view[name];
/**
* Only checking against `hasProperty`, which always returns `false` if
* `context.view` is not an object. Deliberately omitting the check
* against `primitiveHasOwnProperty` if dot notation is not used.
*
* Consider this example:
* ```
* Mustache.render("The length of a football field is {{#length}}{{length}}{{/length}}.", {length: "100 yards"})
* ```
*
* If we were to check also against `primitiveHasOwnProperty`, as we do
* in the dot notation case, then render call would return:
*
* "The length of a football field is 9."
*
* rather than the expected:
*
* "The length of a football field is 100 yards."
**/
lookupHit = hasProperty(context.view, name);
}
if (lookupHit) {
value = intermediateValue;
break;
}
context = context.parent;
}
cache[name] = value;
}
if (isFunction(value))
value = value.call(this.view);
return value;
};
/**
* A Writer knows how to take a stream of tokens and render them to a
* string, given a context. It also maintains a cache of templates to
* avoid the need to parse the same template twice.
*/
function Writer () {
this.templateCache = {
_cache: {},
set: function set (key, value) {
this._cache[key] = value;
},
get: function get (key) {
return this._cache[key];
},
clear: function clear () {
this._cache = {};
}
};
}
/**
* Clears all cached templates in this writer.
*/
Writer.prototype.clearCache = function clearCache () {
if (typeof this.templateCache !== 'undefined') {
this.templateCache.clear();
}
};
/**
* Parses and caches the given `template` according to the given `tags` or
* `mustache.tags` if `tags` is omitted, and returns the array of tokens
* that is generated from the parse.
*/
Writer.prototype.parse = function parse (template, tags) {
var cache = this.templateCache;
var cacheKey = template + ':' + (tags || mustache.tags).join(':');
var isCacheEnabled = typeof cache !== 'undefined';
var tokens = isCacheEnabled ? cache.get(cacheKey) : undefined;
if (tokens == undefined) {
tokens = parseTemplate(template, tags);
isCacheEnabled && cache.set(cacheKey, tokens);
}
return tokens;
};
/**
* High-level method that is used to render the given `template` with
* the given `view`.
*
* The optional `partials` argument may be an object that contains the
* names and templates of partials that are used in the template. It may
* also be a function that is used to load partial templates on the fly
* that takes a single argument: the name of the partial.
*
* If the optional `tags` argument is given here it must be an array with two
* string values: the opening and closing tags used in the template (e.g.
* [ "<%", "%>" ]). The default is to mustache.tags.
*/
Writer.prototype.render = function render (template, view, partials, tags) {
var tokens = this.parse(template, tags);
var context = (view instanceof Context) ? view : new Context(view, undefined);
return this.renderTokens(tokens, context, partials, template, tags);
};
/**
* Low-level method that renders the given array of `tokens` using
* the given `context` and `partials`.
*
* Note: The `originalTemplate` is only ever used to extract the portion
* of the original template that was contained in a higher-order section.
* If the template doesn't use higher-order sections, this argument may
* be omitted.
*/
Writer.prototype.renderTokens = function renderTokens (tokens, context, partials, originalTemplate, tags) {
var buffer = '';
var token, symbol, value;
for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
value = undefined;
token = tokens[i];
symbol = token[0];
if (symbol === '#') value = this.renderSection(token, context, partials, originalTemplate);
else if (symbol === '^') value = this.renderInverted(token, context, partials, originalTemplate);
else if (symbol === '>') value = this.renderPartial(token, context, partials, tags);
else if (symbol === '&') value = this.unescapedValue(token, context);
else if (symbol === 'name') value = this.escapedValue(token, context);
else if (symbol === 'text') value = this.rawValue(token);
if (value !== undefined)
buffer += value;
}
return buffer;
};
Writer.prototype.renderSection = function renderSection (token, context, partials, originalTemplate) {
var self = this;
var buffer = '';
var value = context.lookup(token[1]);
// This function is used to render an arbitrary template
// in the current context by higher-order sections.
function subRender (template) {
return self.render(template, context, partials);
}
if (!value) return;
if (isArray(value)) {
for (var j = 0, valueLength = value.length; j < valueLength; ++j) {
buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate);
}
} else if (typeof value === 'object' || typeof value === 'string' || typeof value === 'number') {
buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate);
} else if (isFunction(value)) {
if (typeof originalTemplate !== 'string')
throw new Error('Cannot use higher-order sections without the original template');
// Extract the portion of the original template that the section contains.
value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender);
if (value != null)
buffer += value;
} else {
buffer += this.renderTokens(token[4], context, partials, originalTemplate);
}
return buffer;
};
Writer.prototype.renderInverted = function renderInverted (token, context, partials, originalTemplate) {
var value = context.lookup(token[1]);
// Use JavaScript's definition of falsy. Include empty arrays.
// See https://github.com/janl/mustache.js/issues/186
if (!value || (isArray(value) && value.length === 0))
return this.renderTokens(token[4], context, partials, originalTemplate);
};
Writer.prototype.indentPartial = function indentPartial (partial, indentation, lineHasNonSpace) {
var filteredIndentation = indentation.replace(/[^ \t]/g, '');
var partialByNl = partial.split('\n');
for (var i = 0; i < partialByNl.length; i++) {
if (partialByNl[i].length && (i > 0 || !lineHasNonSpace)) {
partialByNl[i] = filteredIndentation + partialByNl[i];
}
}
return partialByNl.join('\n');
};
Writer.prototype.renderPartial = function renderPartial (token, context, partials, tags) {
if (!partials) return;
var value = isFunction(partials) ? partials(token[1]) : partials[token[1]];
if (value != null) {
var lineHasNonSpace = token[6];
var tagIndex = token[5];
var indentation = token[4];
var indentedValue = value;
if (tagIndex == 0 && indentation) {
indentedValue = this.indentPartial(value, indentation, lineHasNonSpace);
}
return this.renderTokens(this.parse(indentedValue, tags), context, partials, indentedValue, tags);
}
};
Writer.prototype.unescapedValue = function unescapedValue (token, context) {
var value = context.lookup(token[1]);
if (value != null)
return value;
};
Writer.prototype.escapedValue = function escapedValue (token, context) {
var value = context.lookup(token[1]);
if (value != null)
return typeof value === 'number' ? String(value) : mustache.escape(value);
};
Writer.prototype.rawValue = function rawValue (token) {
return token[1];
};
var mustache = {
name: 'mustache.js',
version: '4.0.1',
tags: [ '{{', '}}' ],
clearCache: undefined,
escape: undefined,
parse: undefined,
render: undefined,
Scanner: undefined,
Context: undefined,
Writer: undefined,
/**
* Allows a user to override the default caching strategy, by providing an
* object with set, get and clear methods. This can also be used to disable
* the cache by setting it to the literal `undefined`.
*/
set templateCache (cache) {
defaultWriter.templateCache = cache;
},
/**
* Gets the default or overridden caching object from the default writer.
*/
get templateCache () {
return defaultWriter.templateCache;
}
};
// All high-level mustache.* functions use this writer.
var defaultWriter = new Writer();
/**
* Clears all cached templates in the default writer.
*/
mustache.clearCache = function clearCache () {
return defaultWriter.clearCache();
};
/**
* Parses and caches the given template in the default writer and returns the
* array of tokens it contains. Doing this ahead of time avoids the need to
* parse templates on the fly as they are rendered.
*/
mustache.parse = function parse (template, tags) {
return defaultWriter.parse(template, tags);
};
/**
* Renders the `template` with the given `view` and `partials` using the
* default writer. If the optional `tags` argument is given here it must be an
* array with two string values: the opening and closing tags used in the
* template (e.g. [ "<%", "%>" ]). The default is to mustache.tags.
*/
mustache.render = function render (template, view, partials, tags) {
if (typeof template !== 'string') {
throw new TypeError('Invalid template! Template should be a "string" ' +
'but "' + typeStr(template) + '" was given as the first ' +
'argument for mustache#render(template, view, partials)');
}
return defaultWriter.render(template, view, partials, tags);
};
// Export the escaping function so that the user may override it.
// See https://github.com/janl/mustache.js/issues/244
mustache.escape = escapeHtml;
// Export these mainly for testing, but also for advanced usage.
mustache.Scanner = Scanner;
mustache.Context = Context;
mustache.Writer = Writer;
return mustache;
})));

View File

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Aborting Request Event Test</title>
</head>
<body style="padding: 20px; font-family: sans-serif;">
<h1>Aborting Request Event Tests</h1>
<p>Aborting a request during an HTMX event should not prevent future events from firing.</p>
<label for="block">Block Request?</label>
<input id="block" type="checkbox" checked>
<br><br>
<button hx-get="https://httpbin.org/html"
hx-target="#target">
Send Request
</button>
<br><br>
<h3>Event Messages:</h3>
<div id="target"></div>
<script src="../../src/htmx.js"></script>
<script>
var btn = document.querySelector('button');
var checkbox = document.querySelector('input');
var target = document.querySelector('#target');
btn.addEventListener('htmx:beforeRequest', function(e) {
var messages = target.innerHTML;
target.innerHTML = messages + "<p>Another Message!</p>";
if (checkbox.checked) {
e.detail.xhr.abort();
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,19 @@
<div style="height: 800px;background-color: blue">
</div>
<h1 id="anchor1">Anchor 1</h1>
<div style="height: 800px;background-color: blue">
</div>
<h1 id="anchor2">Anchor 2</h1>
<div style="height: 800px;background-color: blue">
</div>
<h1 id="anchor3">Anchor 3</h1>
<div style="height: 800px;background-color: blue">
</div>
<h1 id="anchor4">Anchor 4</h1>
<div style="height: 1800px;background-color: blue">
</div>

View File

@@ -0,0 +1,13 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Test if indicators are invisible by default</title>
<script src="../../../src/htmx.js"></script>
</head>
<body style="padding:20px;font-family: sans-serif">
<a hx-boost="true" href="has-anchors.html#anchor1">Anchor 1</a>
<button hx-push-url="true" hx-get="has-anchors.html#anchor2" hx-target="body">Anchor 2</button>
</body>

View File

@@ -0,0 +1,23 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<script type="application/javascript" src="../../../src/htmx.js"></script>
<meta name="htmx-config" content='{"getCacheBusterParam":true}'>
<title>Cache Buster - 1</title>
</head>
<body style="padding:20px;font-family: sans-serif" hx-boost="true">
<h1>Cache-Buster Test</h1>
<ul>
<li>
<a href="index.html">Index</a>
</li>
<li>
<a href="second.html">Second</a>
</li>
<li>
<a href="second.html?foo=bar">Second W/ Param</a>
</li>
</ul>
<h3>Index Page</h3>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More