Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/dev' into dev
Browse files Browse the repository at this point in the history
  • Loading branch information
1cg committed Oct 3, 2024
2 parents b8c92b8 + 958fef2 commit 1537833
Show file tree
Hide file tree
Showing 12 changed files with 509 additions and 51 deletions.
107 changes: 61 additions & 46 deletions src/htmx.js
Original file line number Diff line number Diff line change
Expand Up @@ -337,22 +337,10 @@ var htmx = (function() {
return '[hx-' + verb + '], [data-hx-' + verb + ']'
}).join(', ')

const HEAD_TAG_REGEX = makeTagRegEx('head')

//= ===================================================================
// Utilities
//= ===================================================================

/**
* @param {string} tag
* @param {boolean} global
* @returns {RegExp}
*/
function makeTagRegEx(tag, global = false) {
return new RegExp(`<${tag}(\\s[^>]*>|>)([\\s\\S]*?)<\\/${tag}>`,
global ? 'gim' : 'im')
}

/**
* Parses an interval string consistent with the way htmx does. Useful for plugins that have timing-related attributes.
*
Expand Down Expand Up @@ -595,7 +583,7 @@ var htmx = (function() {
*/
function makeFragment(response) {
// strip head tag to determine shape of response we are dealing with
const responseWithNoHead = response.replace(HEAD_TAG_REGEX, '')
const responseWithNoHead = response.replace(/<head(\s[^>]*)?>.*?<\/head>/is, '')
const startTag = getStartTag(responseWithNoHead)
/** @type DocumentFragmentWithTitle */
let fragment
Expand Down Expand Up @@ -695,7 +683,7 @@ var htmx = (function() {
* @property {boolean} [triggeredOnce]
* @property {number} [delayed]
* @property {number|null} [throttle]
* @property {string} [lastValue]
* @property {WeakMap<HtmxTriggerSpecification,WeakMap<EventTarget,string>>} [lastValue]
* @property {boolean} [loaded]
* @property {string} [path]
* @property {string} [verb]
Expand Down Expand Up @@ -1161,6 +1149,8 @@ var htmx = (function() {
return [document.body]
} else if (selector === 'root') {
return [getRootNode(elt, !!global)]
} else if (selector === 'host') {
return [(/** @type ShadowRoot */(elt.getRootNode())).host]
} else if (selector.indexOf('global ') === 0) {
return querySelectorAllExt(elt, selector.slice(7), true)
} else {
Expand Down Expand Up @@ -1417,9 +1407,11 @@ var htmx = (function() {
* @param {string} oobValue
* @param {Element} oobElement
* @param {HtmxSettleInfo} settleInfo
* @param {Node|Document} [rootNode]
* @returns
*/
function oobSwap(oobValue, oobElement, settleInfo) {
function oobSwap(oobValue, oobElement, settleInfo, rootNode) {
rootNode = rootNode || getDocument()
let selector = '#' + getRawAttribute(oobElement, 'id')
/** @type HtmxSwapStyle */
let swapStyle = 'outerHTML'
Expand All @@ -1431,8 +1423,10 @@ var htmx = (function() {
} else {
swapStyle = oobValue
}
oobElement.removeAttribute('hx-swap-oob')
oobElement.removeAttribute('data-hx-swap-oob')

const targets = getDocument().querySelectorAll(selector)
const targets = querySelectorAllExt(rootNode, selector, false)
if (targets) {
forEach(
targets,
Expand All @@ -1450,7 +1444,9 @@ var htmx = (function() {

target = beforeSwapDetails.target // allow re-targeting
if (beforeSwapDetails.shouldSwap) {
handlePreservedElements(fragment)
swapWithStyle(swapStyle, target, target, fragment, settleInfo)
restorePreservedElements()
}
forEach(settleInfo.elts, function(elt) {
triggerEvent(elt, 'htmx:oobAfterSwap', beforeSwapDetails)
Expand Down Expand Up @@ -1479,7 +1475,7 @@ var htmx = (function() {
}

/**
* @param {DocumentFragment} fragment
* @param {DocumentFragment|ParentNode} fragment
*/
function handlePreservedElements(fragment) {
forEach(findAll(fragment, '[hx-preserve], [data-hx-preserve]'), function(preservedElt) {
Expand Down Expand Up @@ -1661,9 +1657,13 @@ var htmx = (function() {
/** @type {Node} */
let newElt
const eltBeforeNewContent = target.previousSibling
insertNodesBefore(parentElt(target), target, fragment, settleInfo)
const parentNode = parentElt(target)
if (!parentNode) { // when parent node disappears, we can't do anything
return
}
insertNodesBefore(parentNode, target, fragment, settleInfo)
if (eltBeforeNewContent == null) {
newElt = parentElt(target).firstChild
newElt = parentNode.firstChild
} else {
newElt = eltBeforeNewContent.nextSibling
}
Expand Down Expand Up @@ -1725,7 +1725,10 @@ var htmx = (function() {
*/
function swapDelete(target) {
cleanUpElement(target)
return parentElt(target).removeChild(target)
const parent = parentElt(target)
if (parent) {
return parent.removeChild(target)
}
}

/**
Expand Down Expand Up @@ -1808,14 +1811,15 @@ var htmx = (function() {
/**
* @param {DocumentFragment} fragment
* @param {HtmxSettleInfo} settleInfo
* @param {Node|Document} [rootNode]
*/
function findAndSwapOobElements(fragment, settleInfo) {
function findAndSwapOobElements(fragment, settleInfo, rootNode) {
var oobElts = findAll(fragment, '[hx-swap-oob], [data-hx-swap-oob]')
forEach(oobElts, function(oobElement) {
if (htmx.config.allowNestedOobSwaps || oobElement.parentElement === null) {
const oobValue = getAttributeValue(oobElement, 'hx-swap-oob')
if (oobValue != null) {
oobSwap(oobValue, oobElement, settleInfo)
oobSwap(oobValue, oobElement, settleInfo, rootNode)
}
} else {
oobElement.removeAttribute('hx-swap-oob')
Expand All @@ -1839,6 +1843,7 @@ var htmx = (function() {
}

target = resolveTarget(target)
const rootNode = swapOptions.contextElement ? getRootNode(swapOptions.contextElement, false) : getDocument()

// preserve focus and selection
const activeElt = document.activeElement
Expand Down Expand Up @@ -1877,14 +1882,14 @@ var htmx = (function() {
const oobValue = oobSelectValue[1] || 'true'
const oobElement = fragment.querySelector('#' + id)
if (oobElement) {
oobSwap(oobValue, oobElement, settleInfo)
oobSwap(oobValue, oobElement, settleInfo, rootNode)
}
}
}
// oob swaps
findAndSwapOobElements(fragment, settleInfo)
findAndSwapOobElements(fragment, settleInfo, rootNode)
forEach(findAll(fragment, 'template'), /** @param {HTMLTemplateElement} template */function(template) {
if (findAndSwapOobElements(template.content, settleInfo)) {
if (findAndSwapOobElements(template.content, settleInfo, rootNode)) {
// Avoid polluting the DOM with empty templates that were only used to encapsulate oob swap
template.remove()
}
Expand Down Expand Up @@ -2171,8 +2176,8 @@ var htmx = (function() {
if (eventFilter) {
triggerSpec.eventFilter = eventFilter
}
consumeUntil(tokens, NOT_WHITESPACE)
while (tokens.length > 0 && tokens[0] !== ',') {
consumeUntil(tokens, NOT_WHITESPACE)
const token = tokens.shift()
if (token === 'changed') {
triggerSpec.changed = true
Expand Down Expand Up @@ -2217,6 +2222,7 @@ var htmx = (function() {
} else {
triggerErrorEvent(elt, 'htmx:syntax:error', { token: tokens.shift() })
}
consumeUntil(tokens, NOT_WHITESPACE)
}
triggerSpecs.push(triggerSpec)
}
Expand Down Expand Up @@ -2316,9 +2322,10 @@ var htmx = (function() {
} else {
const rawAttribute = getRawAttribute(elt, 'method')
verb = (/** @type HttpVerb */(rawAttribute ? rawAttribute.toLowerCase() : 'get'))
if (verb === 'get') {
}
path = getRawAttribute(elt, 'action')
if (verb === 'get' && path.includes('?')) {
path = path.replace(/\?[^#]+/, '')
}
}
triggerSpecs.forEach(function(triggerSpec) {
addEventListener(elt, function(node, evt) {
Expand Down Expand Up @@ -2407,10 +2414,15 @@ var htmx = (function() {
}
// store the initial values of the elements, so we can tell if they change
if (triggerSpec.changed) {
if (!('lastValue' in elementData)) {
elementData.lastValue = new WeakMap()
}
eltsToListenOn.forEach(function(eltToListenOn) {
const eltToListenOnData = getInternalData(eltToListenOn)
if (!elementData.lastValue.has(triggerSpec)) {
elementData.lastValue.set(triggerSpec, new WeakMap())
}
// @ts-ignore value will be undefined for non-input elements, which is fine
eltToListenOnData.lastValue = eltToListenOn.value
elementData.lastValue.get(triggerSpec).set(eltToListenOn, eltToListenOn.value)
})
}
forEach(eltsToListenOn, function(eltToListenOn) {
Expand Down Expand Up @@ -2452,13 +2464,14 @@ var htmx = (function() {
}
}
if (triggerSpec.changed) {
const eltToListenOnData = getInternalData(eltToListenOn)
const node = event.target
// @ts-ignore value will be undefined for non-input elements, which is fine
const value = eltToListenOn.value
if (eltToListenOnData.lastValue === value) {
const value = node.value
const lastValue = elementData.lastValue.get(triggerSpec)
if (lastValue.has(node) && lastValue.get(node) === value) {
return
}
eltToListenOnData.lastValue = value
lastValue.set(node, value)
}
if (elementData.delayed) {
clearTimeout(elementData.delayed)
Expand Down Expand Up @@ -2836,12 +2849,6 @@ var htmx = (function() {

triggerEvent(elt, 'htmx:beforeProcessNode')

// @ts-ignore value will be undefined for non-input elements, which is fine
if (elt.value) {
// @ts-ignore
nodeData.lastValue = elt.value
}

const triggerSpecs = getTriggerSpecs(elt)
const hasExplicitHttpAction = processVerbs(elt, nodeData, triggerSpecs)

Expand Down Expand Up @@ -3269,16 +3276,18 @@ var htmx = (function() {
* @param {Element[]} disabled
*/
function removeRequestIndicators(indicators, disabled) {
forEach(indicators.concat(disabled), function(ele) {
const internalData = getInternalData(ele)
internalData.requestCount = (internalData.requestCount || 1) - 1
})
forEach(indicators, function(ic) {
const internalData = getInternalData(ic)
internalData.requestCount = (internalData.requestCount || 0) - 1
if (internalData.requestCount === 0) {
ic.classList.remove.call(ic.classList, htmx.config.requestClass)
}
})
forEach(disabled, function(disabledElement) {
const internalData = getInternalData(disabledElement)
internalData.requestCount = (internalData.requestCount || 0) - 1
if (internalData.requestCount === 0) {
disabledElement.removeAttribute('disabled')
disabledElement.removeAttribute('data-disabled-by-htmx')
Expand Down Expand Up @@ -3891,16 +3900,22 @@ var htmx = (function() {
if (context) {
if (context instanceof Element || typeof context === 'string') {
return issueAjaxRequest(verb, path, null, null, {
targetOverride: resolveTarget(context),
targetOverride: resolveTarget(context) || DUMMY_ELT,
returnPromise: true
})
} else {
let resolvedTarget = resolveTarget(context.target)
// If target is supplied but can't resolve OR both target and source can't be resolved
// then use DUMMY_ELT to abort the request with htmx:targetError to avoid it replacing body by mistake
if ((context.target && !resolvedTarget) || (!resolvedTarget && !resolveTarget(context.source))) {
resolvedTarget = DUMMY_ELT
}
return issueAjaxRequest(verb, path, resolveTarget(context.source), context.event,
{
handler: context.handler,
headers: context.headers,
values: context.values,
targetOverride: resolveTarget(context.target),
targetOverride: resolvedTarget,
swapOverride: context.swap,
select: context.select,
returnPromise: true
Expand Down Expand Up @@ -3962,7 +3977,7 @@ var htmx = (function() {
const formData = new FormData()
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
if (typeof obj[key].forEach === 'function') {
if (obj[key] && typeof obj[key].forEach === 'function') {
obj[key].forEach(function(v) { formData.append(key, v) })
} else if (typeof obj[key] === 'object' && !(obj[key] instanceof Blob)) {
formData.append(key, JSON.stringify(obj[key]))
Expand Down Expand Up @@ -4055,7 +4070,7 @@ var htmx = (function() {
return false
}
target.delete(name)
if (typeof value.forEach === 'function') {
if (value && typeof value.forEach === 'function') {
value.forEach(function(v) { target.append(name, v) })
} else if (typeof value === 'object' && !(value instanceof Blob)) {
target.append(name, JSON.stringify(value))
Expand Down
25 changes: 25 additions & 0 deletions test/attributes/hx-boost.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,29 @@ describe('hx-boost attribute', function() {
this.server.respond()
btn.innerHTML.should.equal('Boosted!')
})

it('form get w/ search params in action property excludes search params', function() {
this.server.respondWith('GET', /\/test.*/, function(xhr) {
should.equal(undefined, getParameters(xhr).foo)
xhr.respond(200, {}, 'Boosted!')
})

var div = make('<div hx-target="this" hx-boost="true"><form id="f1" action="/test?foo=bar" method="get"><button id="b1">Submit</button></form></div>')
var btn = byId('b1')
btn.click()
this.server.respond()
div.innerHTML.should.equal('Boosted!')
})

it('form post w/ query params in action property uses full url', function() {
this.server.respondWith('POST', /\/test.*/, function(xhr) {
should.equal(undefined, getParameters(xhr).foo)
xhr.respond(200, {}, 'Boosted!')
})
var div = make('<div hx-target="this" hx-boost="true"><form id="f1" action="/test?foo=bar" method="post"><button id="b1">Submit</button></form></div>')
var btn = byId('b1')
btn.click()
this.server.respond()
div.innerHTML.should.equal('Boosted!')
})
})
12 changes: 12 additions & 0 deletions test/attributes/hx-disabled-elt.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,16 @@ describe('hx-disabled-elt attribute', function() {
b2.hasAttribute('disabled').should.equal(false)
b3.hasAttribute('disabled').should.equal(false)
})

it('load trigger does not prevent disabled element working', function() {
this.server.respondWith('GET', '/test', 'Loaded!')
var div1 = make('<div id="d1" hx-get="/test" hx-disabled-elt="#b1" hx-trigger="load">Load Me!</div><button id="b1">Demo</button>')
var div = byId('d1')
var btn = byId('b1')
div.innerHTML.should.equal('Load Me!')
btn.hasAttribute('disabled').should.equal(true)
this.server.respond()
div.innerHTML.should.equal('Loaded!')
btn.hasAttribute('disabled').should.equal(false)
})
})
34 changes: 34 additions & 0 deletions test/attributes/hx-preserve.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,38 @@ describe('hx-preserve attribute', function() {
byId('d1').innerHTML.should.equal('Old Content')
byId('d2').innerHTML.should.equal('New Content')
})

it('preserved element should not be swapped if it is part of a oob swap', function() {
this.server.respondWith('GET', '/test', "Normal Content<div id='d2' hx-swap-oob='true'><div id='d3' hx-preserve>New oob Content</div><div id='d4'>New oob Content</div></div>")
var div1 = make("<div id='d1' hx-get='/test'>Click Me!</div>")
var div2 = make("<div id='d2'><div id='d3' hx-preserve>Old Content</div></div>")
div1.click()
this.server.respond()
byId('d1').innerHTML.should.equal('Normal Content')
byId('d3').innerHTML.should.equal('Old Content')
byId('d4').innerHTML.should.equal('New oob Content')
})

it('preserved element should not be swapped if it is part of a hx-select-oob swap', function() {
this.server.respondWith('GET', '/test', "Normal Content<div id='d2'><div id='d3' hx-preserve>New oob Content</div><div id='d4'>New oob Content</div></div>")
var div1 = make("<div id='d1' hx-get='/test' hx-select-oob='#d2'>Click Me!</div>")
var div2 = make("<div id='d2'><div id='d3' hx-preserve>Old Content</div></div>")
div1.click()
this.server.respond()
byId('d1').innerHTML.should.equal('Normal Content')
byId('d3').innerHTML.should.equal('Old Content')
byId('d4').innerHTML.should.equal('New oob Content')
})

it('preserved element should relocated unchanged if it is part of a oob swap targeting a different loction', function() {
this.server.respondWith('GET', '/test', "Normal Content<div id='d2' hx-swap-oob='innerHTML:#d5'><div id='d3' hx-preserve>New oob Content</div><div id='d4'>New oob Content</div></div>")
var div1 = make("<div id='d1' hx-get='/test'>Click Me!</div>")
var div2 = make("<div id='d2'><div id='d3' hx-preserve>Old Content</div></div>")
var div5 = make("<div id='d5'></div>")
div1.click()
this.server.respond()
byId('d1').innerHTML.should.equal('Normal Content')
byId('d2').innerHTML.should.equal('')
byId('d5').innerHTML.should.equal('<div id="d3" hx-preserve="">Old Content</div><div id="d4">New oob Content</div>')
})
})
Loading

0 comments on commit 1537833

Please sign in to comment.