mirror of
https://github.com/bigskysoftware/htmx.git
synced 2025-12-15 20:58:32 +00:00
Refactor and expose swap in public API (#2191)
* refactor and expose swapping as a single method * fixes after rebasing on latest 2.0 * fix optional parameters for swapping * add `fullSwap` to internal API * swap refactor polishing * make linter happy
This commit is contained in:
parent
4af2f75ae1
commit
7fe27dd275
16
src/htmx.d.ts
vendored
16
src/htmx.d.ts
vendored
@ -438,6 +438,22 @@ export interface HtmxConfig {
|
||||
triggerSpecsCache?: {[trigger: string]: HtmxTriggerSpecification[]};
|
||||
}
|
||||
|
||||
type HtmxSwapStyle = "innerHTML" | "outerHTML" | "beforebegin" | "afterbegin" | "beforeend" | "afterend" | "delete" | "none" | string
|
||||
|
||||
export interface HtmxSwapSpecification {
|
||||
swapStyle: HtmxSwapStyle;
|
||||
swapDelay?: number;
|
||||
settleDelay?: number;
|
||||
transition?: boolean;
|
||||
ignoreTitle?: boolean;
|
||||
head?: string;
|
||||
scroll?: string;
|
||||
scrollTarget?: string;
|
||||
show?: string;
|
||||
showTarget?: string;
|
||||
focusScroll?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* https://htmx.org/extensions/#defining
|
||||
*/
|
||||
|
||||
424
src/htmx.js
424
src/htmx.js
@ -4,12 +4,14 @@ var htmx = (function() {
|
||||
// Public API
|
||||
//* * @type {import("./htmx").HtmxApi} */
|
||||
const htmx = {
|
||||
/* Event processing */
|
||||
onLoad: onLoadHelper,
|
||||
process: processNode,
|
||||
on: addEventListenerImpl,
|
||||
off: removeEventListenerImpl,
|
||||
trigger: triggerEvent,
|
||||
ajax: ajaxHelper,
|
||||
/* DOM querying helpers */
|
||||
find,
|
||||
findAll,
|
||||
closest,
|
||||
@ -17,13 +19,17 @@ var htmx = (function() {
|
||||
const inputValues = getInputValues(elt, type || 'post')
|
||||
return inputValues.values
|
||||
},
|
||||
/* DOM manipulation helpers */
|
||||
remove: removeElement,
|
||||
addClass: addClassToElement,
|
||||
removeClass: removeClassFromElement,
|
||||
toggleClass: toggleClassOnElement,
|
||||
takeClass: takeClassForElement,
|
||||
swap,
|
||||
/* Extension entrypoints */
|
||||
defineExtension,
|
||||
removeExtension,
|
||||
/* Debugging */
|
||||
logAll,
|
||||
logNone,
|
||||
logger: null,
|
||||
@ -81,6 +87,7 @@ var htmx = (function() {
|
||||
canAccessLocalStorage,
|
||||
findThisElement,
|
||||
filterValues,
|
||||
swap,
|
||||
hasAttribute,
|
||||
getAttributeValue,
|
||||
getClosestAttributeValue,
|
||||
@ -97,7 +104,6 @@ var htmx = (function() {
|
||||
makeSettleInfo,
|
||||
oobSwap,
|
||||
querySelectorExt,
|
||||
selectAndSwap,
|
||||
settleImmediately,
|
||||
shouldCancel,
|
||||
triggerEvent,
|
||||
@ -145,7 +151,7 @@ var htmx = (function() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} elt
|
||||
* @param {Element} elt
|
||||
* @param {string} name
|
||||
* @returns {(string | null)}
|
||||
*/
|
||||
@ -285,7 +291,7 @@ var htmx = (function() {
|
||||
|
||||
/**
|
||||
* @param {string} response HTML
|
||||
* @returns {DocumentFragment & {string:title, head:Element}} a document fragment representing the response HTML, including
|
||||
* @returns {DocumentFragment & {title: string, head:Element}} a document fragment representing the response HTML, including
|
||||
* a `head` property for any head content found
|
||||
*/
|
||||
function makeFragment(response) {
|
||||
@ -392,6 +398,16 @@ var htmx = (function() {
|
||||
return returnArr
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @callback forEachCallback
|
||||
* @param {T} value
|
||||
*/
|
||||
/**
|
||||
* @template T
|
||||
* @param {{[index: number]: T, length: number}} arr
|
||||
* @param {forEachCallback<T>} func
|
||||
*/
|
||||
function forEach(arr, func) {
|
||||
if (arr) {
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
@ -662,10 +678,17 @@ var htmx = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string|Element} arg2
|
||||
* @param {Element} [context]
|
||||
* @returns {Element}
|
||||
*/
|
||||
function resolveTarget(arg2, context) {
|
||||
if (isType(arg2, 'String')) {
|
||||
return find(context || document, arg2)
|
||||
} else {
|
||||
// @ts-ignore
|
||||
return arg2
|
||||
}
|
||||
}
|
||||
@ -790,7 +813,7 @@ var htmx = (function() {
|
||||
/**
|
||||
*
|
||||
* @param {string} oobValue
|
||||
* @param {HTMLElement} oobElement
|
||||
* @param {Element} oobElement
|
||||
* @param {*} settleInfo
|
||||
* @returns
|
||||
*/
|
||||
@ -824,7 +847,7 @@ var htmx = (function() {
|
||||
|
||||
target = beforeSwapDetails.target // allow re-targeting
|
||||
if (beforeSwapDetails.shouldSwap) {
|
||||
swap(swapStyle, target, target, fragment, settleInfo)
|
||||
swapWithStyle(swapStyle, target, target, fragment, settleInfo)
|
||||
}
|
||||
forEach(settleInfo.elts, function(elt) {
|
||||
triggerEvent(elt, 'htmx:oobAfterSwap', beforeSwapDetails)
|
||||
@ -839,42 +862,6 @@ var htmx = (function() {
|
||||
return oobValue
|
||||
}
|
||||
|
||||
function findAndSwapOobElements(fragment, settleInfo) {
|
||||
forEach(findAll(fragment, '[hx-swap-oob], [data-hx-swap-oob]'), function(oobElement) {
|
||||
const oobValue = getAttributeValue(oobElement, 'hx-swap-oob')
|
||||
if (oobValue != null) {
|
||||
oobSwap(oobValue, oobElement, settleInfo)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleOutOfBandSwaps(elt, fragment, settleInfo) {
|
||||
const oobSelects = getClosestAttributeValue(elt, 'hx-select-oob')
|
||||
if (oobSelects) {
|
||||
const oobSelectValues = oobSelects.split(',')
|
||||
for (let i = 0; i < oobSelectValues.length; i++) {
|
||||
const oobSelectValue = oobSelectValues[i].split(':', 2)
|
||||
let id = oobSelectValue[0].trim()
|
||||
if (id.indexOf('#') === 0) {
|
||||
id = id.substring(1)
|
||||
}
|
||||
const oobValue = oobSelectValue[1] || 'true'
|
||||
const oobElement = fragment.querySelector('#' + id)
|
||||
if (oobElement) {
|
||||
oobSwap(oobValue, oobElement, settleInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
findAndSwapOobElements(fragment, settleInfo)
|
||||
forEach(findAll(fragment, 'template'), function(template) {
|
||||
findAndSwapOobElements(template.content, settleInfo)
|
||||
if (template.content.childElementCount === 0) {
|
||||
// Avoid polluting the DOM with empty templates that were only used to encapsulate oob swap
|
||||
template.remove()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handlePreservedElements(fragment) {
|
||||
forEach(findAll(fragment, '[hx-preserve], [data-hx-preserve]'), function(preservedElt) {
|
||||
const id = getAttributeValue(preservedElt, 'id')
|
||||
@ -1047,19 +1034,14 @@ var htmx = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
function maybeSelectFromResponse(elt, fragment, selectOverride) {
|
||||
const selector = selectOverride || getClosestAttributeValue(elt, 'hx-select')
|
||||
if (selector) {
|
||||
const newFragment = getDocument().createDocumentFragment()
|
||||
forEach(fragment.querySelectorAll(selector), function(node) {
|
||||
newFragment.appendChild(node)
|
||||
})
|
||||
fragment = newFragment
|
||||
}
|
||||
return fragment
|
||||
}
|
||||
|
||||
function swap(swapStyle, elt, target, fragment, settleInfo) {
|
||||
/**
|
||||
* @param {string} swapStyle
|
||||
* @param {HTMLElement} elt
|
||||
* @param {HTMLElement} target
|
||||
* @param {Node} fragment
|
||||
* @param {{ tasks: (() => void)[]; }} settleInfo
|
||||
*/
|
||||
function swapWithStyle(swapStyle, elt, target, fragment, settleInfo) {
|
||||
switch (swapStyle) {
|
||||
case 'none':
|
||||
return
|
||||
@ -1106,7 +1088,7 @@ var htmx = (function() {
|
||||
if (swapStyle === 'innerHTML') {
|
||||
swapInnerHTML(target, fragment, settleInfo)
|
||||
} else {
|
||||
swap(htmx.config.defaultSwapStyle, elt, target, fragment, settleInfo)
|
||||
swapWithStyle(htmx.config.defaultSwapStyle, elt, target, fragment, settleInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1126,16 +1108,171 @@ var htmx = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
function selectAndSwap(swapStyle, target, elt, responseText, settleInfo, selectOverride) {
|
||||
let fragment = makeFragment(responseText)
|
||||
if (fragment) {
|
||||
// ugly :/
|
||||
settleInfo.title = fragment.title
|
||||
settleInfo.head = fragment.head
|
||||
handleOutOfBandSwaps(elt, fragment, settleInfo)
|
||||
fragment = maybeSelectFromResponse(elt, fragment, selectOverride)
|
||||
handlePreservedElements(fragment)
|
||||
return swap(swapStyle, elt, target, fragment, settleInfo)
|
||||
function findAndSwapOobElements(fragment, settleInfo) {
|
||||
forEach(findAll(fragment, '[hx-swap-oob], [data-hx-swap-oob]'), function(oobElement) {
|
||||
const oobValue = getAttributeValue(oobElement, 'hx-swap-oob')
|
||||
if (oobValue != null) {
|
||||
oobSwap(oobValue, oobElement, settleInfo)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @callback swapCallback
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SwapOptions
|
||||
* @property {?string} select
|
||||
* @property {?string} selectOOB
|
||||
* @property {?*} eventInfo
|
||||
* @property {?*} anchor
|
||||
* @property {?HTMLElement} contextElement
|
||||
* @property {?swapCallback} afterSwapCallback
|
||||
* @property {?swapCallback} afterSettleCallback
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements complete swapping pipeline, including: focus and selection preservation,
|
||||
* title updates, head merging, scroll, OOB swapping, normal swapping and settling
|
||||
* @param {string|Element} target
|
||||
* @param {string} content
|
||||
* @param {import("./htmx").HtmxSwapSpecification} swapSpec
|
||||
* @param {SwapOptions} swapOptions
|
||||
*/
|
||||
function swap(target, content, swapSpec, swapOptions) {
|
||||
if (!swapOptions) {
|
||||
swapOptions = {}
|
||||
}
|
||||
|
||||
target = resolveTarget(target)
|
||||
|
||||
// preserve focus and selection
|
||||
const activeElt = document.activeElement
|
||||
let selectionInfo = {}
|
||||
try {
|
||||
selectionInfo = {
|
||||
elt: activeElt,
|
||||
// @ts-ignore
|
||||
start: activeElt ? activeElt.selectionStart : null,
|
||||
// @ts-ignore
|
||||
end: activeElt ? activeElt.selectionEnd : null
|
||||
}
|
||||
} catch (e) {
|
||||
// safari issue - see https://github.com/microsoft/playwright/issues/5894
|
||||
}
|
||||
const settleInfo = makeSettleInfo(target)
|
||||
|
||||
let fragment = makeFragment(content)
|
||||
|
||||
settleInfo.title = fragment.title
|
||||
settleInfo.head = fragment.head
|
||||
|
||||
// select-oob swaps
|
||||
if (swapOptions.selectOOB) {
|
||||
const oobSelectValues = swapOptions.selectOOB.split(',')
|
||||
for (let i = 0; i < oobSelectValues.length; i++) {
|
||||
const oobSelectValue = oobSelectValues[i].split(':', 2)
|
||||
let id = oobSelectValue[0].trim()
|
||||
if (id.indexOf('#') === 0) {
|
||||
id = id.substring(1)
|
||||
}
|
||||
const oobValue = oobSelectValue[1] || 'true'
|
||||
const oobElement = fragment.querySelector('#' + id)
|
||||
if (oobElement) {
|
||||
oobSwap(oobValue, oobElement, settleInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
// oob swaps
|
||||
findAndSwapOobElements(fragment, settleInfo)
|
||||
forEach(findAll(fragment, 'template'), function(template) {
|
||||
findAndSwapOobElements(template.content, settleInfo)
|
||||
if (template.content.childElementCount === 0) {
|
||||
// Avoid polluting the DOM with empty templates that were only used to encapsulate oob swap
|
||||
template.remove()
|
||||
}
|
||||
})
|
||||
|
||||
// normal swap
|
||||
if (swapOptions.select) {
|
||||
const newFragment = getDocument().createDocumentFragment()
|
||||
forEach(fragment.querySelectorAll(swapOptions.select), function(node) {
|
||||
newFragment.appendChild(node)
|
||||
})
|
||||
fragment = newFragment
|
||||
}
|
||||
handlePreservedElements(fragment)
|
||||
swapWithStyle(swapSpec.swapStyle, swapOptions.contextElement, target, fragment, settleInfo)
|
||||
|
||||
// apply saved focus and selection information to swapped content
|
||||
if (selectionInfo.elt &&
|
||||
!bodyContains(selectionInfo.elt) &&
|
||||
getRawAttribute(selectionInfo.elt, 'id')) {
|
||||
const newActiveElt = document.getElementById(getRawAttribute(selectionInfo.elt, 'id'))
|
||||
const focusOptions = { preventScroll: swapSpec.focusScroll !== undefined ? !swapSpec.focusScroll : !htmx.config.defaultFocusScroll }
|
||||
if (newActiveElt) {
|
||||
// @ts-ignore
|
||||
if (selectionInfo.start && newActiveElt.setSelectionRange) {
|
||||
try {
|
||||
// @ts-ignore
|
||||
newActiveElt.setSelectionRange(selectionInfo.start, selectionInfo.end)
|
||||
} catch (e) {
|
||||
// the setSelectionRange method is present on fields that don't support it, so just let this fail
|
||||
}
|
||||
}
|
||||
newActiveElt.focus(focusOptions)
|
||||
}
|
||||
}
|
||||
|
||||
target.classList.remove(htmx.config.swappingClass)
|
||||
forEach(settleInfo.elts, function(elt) {
|
||||
if (elt.classList) {
|
||||
elt.classList.add(htmx.config.settlingClass)
|
||||
}
|
||||
triggerEvent(elt, 'htmx:afterSwap', swapOptions.eventInfo)
|
||||
})
|
||||
if (swapOptions.afterSwapCallback) {
|
||||
swapOptions.afterSwapCallback()
|
||||
}
|
||||
|
||||
// merge in new head after swap but before settle
|
||||
if (!swapSpec.ignoreTitle) {
|
||||
handleTitle(settleInfo.title)
|
||||
}
|
||||
if (triggerEvent(document.body, 'htmx:beforeHeadMerge', { head: settleInfo.head })) {
|
||||
handleHeadTag(settleInfo.head, swapSpec.head)
|
||||
}
|
||||
|
||||
// settle
|
||||
const doSettle = function() {
|
||||
forEach(settleInfo.tasks, function(task) {
|
||||
task.call()
|
||||
})
|
||||
forEach(settleInfo.elts, function(elt) {
|
||||
if (elt.classList) {
|
||||
elt.classList.remove(htmx.config.settlingClass)
|
||||
}
|
||||
triggerEvent(elt, 'htmx:afterSettle', swapOptions.eventInfo)
|
||||
})
|
||||
|
||||
if (swapOptions.anchor) {
|
||||
const anchorTarget = resolveTarget(swapOptions.anchor)
|
||||
if (anchorTarget) {
|
||||
anchorTarget.scrollIntoView({ block: 'start', behavior: 'auto' })
|
||||
}
|
||||
}
|
||||
|
||||
updateScrollState(settleInfo.elts, swapSpec)
|
||||
if (swapOptions.afterSettleCallback) {
|
||||
swapOptions.afterSettleCallback()
|
||||
}
|
||||
}
|
||||
|
||||
if (swapSpec.settleDelay > 0) {
|
||||
setTimeout(doSettle, swapSpec.settleDelay)
|
||||
} else {
|
||||
doSettle()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1213,7 +1350,7 @@ var htmx = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
function handleTrigger(xhr, header, elt) {
|
||||
function handleTriggerHeader(xhr, header, elt) {
|
||||
const triggerBody = xhr.getResponseHeader(header)
|
||||
if (triggerBody.indexOf('{') === 0) {
|
||||
const triggers = parseJSON(triggerBody)
|
||||
@ -2548,11 +2685,12 @@ var htmx = (function() {
|
||||
/**
|
||||
*
|
||||
* @param {HTMLElement} elt
|
||||
* @param {string} swapInfoOverride
|
||||
* @param {import("./htmx").HtmxSwapStyle} swapInfoOverride
|
||||
* @returns {import("./htmx").HtmxSwapSpecification}
|
||||
*/
|
||||
function getSwapSpecification(elt, swapInfoOverride) {
|
||||
const swapInfo = swapInfoOverride || getClosestAttributeValue(elt, 'hx-swap')
|
||||
/** @type import("./htmx").HtmxSwapSpecification */
|
||||
const swapSpec = {
|
||||
swapStyle: getInternalData(elt).boosted ? 'innerHTML' : htmx.config.defaultSwapStyle,
|
||||
swapDelay: htmx.config.defaultSwapDelay,
|
||||
@ -3320,12 +3458,12 @@ var htmx = (function() {
|
||||
const xhr = responseInfo.xhr
|
||||
let target = responseInfo.target
|
||||
const etc = responseInfo.etc
|
||||
const select = responseInfo.select
|
||||
const responseInfoSelect = responseInfo.select
|
||||
|
||||
if (!triggerEvent(elt, 'htmx:beforeOnLoad', responseInfo)) return
|
||||
|
||||
if (hasHeader(xhr, /HX-Trigger:/i)) {
|
||||
handleTrigger(xhr, 'HX-Trigger', elt)
|
||||
handleTriggerHeader(xhr, 'HX-Trigger', elt)
|
||||
}
|
||||
|
||||
if (hasHeader(xhr, /HX-Location:/i)) {
|
||||
@ -3437,11 +3575,11 @@ var htmx = (function() {
|
||||
}
|
||||
var swapSpec = getSwapSpecification(elt, swapOverride)
|
||||
|
||||
if (swapSpec.hasOwnProperty('ignoreTitle')) {
|
||||
ignoreTitle = swapSpec.ignoreTitle
|
||||
if (!swapSpec.hasOwnProperty('ignoreTitle')) {
|
||||
swapSpec.ignoreTitle = ignoreTitle
|
||||
}
|
||||
if (swapSpec.hasOwnProperty('head')) {
|
||||
head = swapSpec.head
|
||||
if (!swapSpec.hasOwnProperty('head')) {
|
||||
swapSpec.head = head
|
||||
}
|
||||
|
||||
target.classList.add(htmx.config.swappingClass)
|
||||
@ -3450,30 +3588,19 @@ var htmx = (function() {
|
||||
let settleResolve = null
|
||||
let settleReject = null
|
||||
|
||||
if (responseInfoSelect) {
|
||||
selectOverride = responseInfoSelect
|
||||
}
|
||||
|
||||
if (hasHeader(xhr, /HX-Reselect:/i)) {
|
||||
selectOverride = xhr.getResponseHeader('HX-Reselect')
|
||||
}
|
||||
|
||||
const selectOOB = getClosestAttributeValue(elt, 'hx-select-oob')
|
||||
const select = getClosestAttributeValue(elt, 'hx-select')
|
||||
|
||||
let doSwap = function() {
|
||||
try {
|
||||
const activeElt = document.activeElement
|
||||
let selectionInfo = {}
|
||||
try {
|
||||
selectionInfo = {
|
||||
elt: activeElt,
|
||||
// @ts-ignore
|
||||
start: activeElt ? activeElt.selectionStart : null,
|
||||
// @ts-ignore
|
||||
end: activeElt ? activeElt.selectionEnd : null
|
||||
}
|
||||
} catch (e) {
|
||||
// safari issue - see https://github.com/microsoft/playwright/issues/5894
|
||||
}
|
||||
|
||||
if (select) {
|
||||
selectOverride = select
|
||||
}
|
||||
|
||||
if (hasHeader(xhr, /HX-Reselect:/i)) {
|
||||
selectOverride = xhr.getResponseHeader('HX-Reselect')
|
||||
}
|
||||
|
||||
// if we need to save history, do so, before swapping so that relative resources have the correct base URL
|
||||
if (historyUpdate.type) {
|
||||
triggerEvent(getDocument().body, 'htmx:beforeHistoryUpdate', mergeObjects({ history: historyUpdate }, responseInfo))
|
||||
@ -3486,89 +3613,32 @@ var htmx = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
const settleInfo = makeSettleInfo(target)
|
||||
selectAndSwap(swapSpec.swapStyle, target, elt, serverResponse, settleInfo, selectOverride)
|
||||
|
||||
if (selectionInfo.elt &&
|
||||
!bodyContains(selectionInfo.elt) &&
|
||||
getRawAttribute(selectionInfo.elt, 'id')) {
|
||||
const newActiveElt = document.getElementById(getRawAttribute(selectionInfo.elt, 'id'))
|
||||
const focusOptions = { preventScroll: swapSpec.focusScroll !== undefined ? !swapSpec.focusScroll : !htmx.config.defaultFocusScroll }
|
||||
if (newActiveElt) {
|
||||
// @ts-ignore
|
||||
if (selectionInfo.start && newActiveElt.setSelectionRange) {
|
||||
// @ts-ignore
|
||||
try {
|
||||
newActiveElt.setSelectionRange(selectionInfo.start, selectionInfo.end)
|
||||
} catch (e) {
|
||||
// the setSelectionRange method is present on fields that don't support it, so just let this fail
|
||||
swap(target, serverResponse, swapSpec, {
|
||||
select: selectOverride || select,
|
||||
selectOOB,
|
||||
eventInfo: responseInfo,
|
||||
anchor: responseInfo.pathInfo.anchor,
|
||||
contextElement: elt,
|
||||
afterSwapCallback: function() {
|
||||
if (hasHeader(xhr, /HX-Trigger-After-Swap:/i)) {
|
||||
let finalElt = elt
|
||||
if (!bodyContains(elt)) {
|
||||
finalElt = getDocument().body
|
||||
}
|
||||
handleTriggerHeader(xhr, 'HX-Trigger-After-Swap', finalElt)
|
||||
}
|
||||
newActiveElt.focus(focusOptions)
|
||||
},
|
||||
afterSettleCallback: function() {
|
||||
if (hasHeader(xhr, /HX-Trigger-After-Settle:/i)) {
|
||||
let finalElt = elt
|
||||
if (!bodyContains(elt)) {
|
||||
finalElt = getDocument().body
|
||||
}
|
||||
handleTriggerHeader(xhr, 'HX-Trigger-After-Settle', finalElt)
|
||||
}
|
||||
maybeCall(settleResolve)
|
||||
}
|
||||
}
|
||||
|
||||
target.classList.remove(htmx.config.swappingClass)
|
||||
forEach(settleInfo.elts, function(elt) {
|
||||
if (elt.classList) {
|
||||
elt.classList.add(htmx.config.settlingClass)
|
||||
}
|
||||
triggerEvent(elt, 'htmx:afterSwap', responseInfo)
|
||||
})
|
||||
|
||||
if (!ignoreTitle) {
|
||||
handleTitle(settleInfo.title)
|
||||
}
|
||||
|
||||
console.log('Here', head)
|
||||
// merge in new head after swap but before settle
|
||||
if (triggerEvent(document.body, 'htmx:beforeHeadMerge', { head: settleInfo.head })) {
|
||||
handleHeadTag(settleInfo.head, head)
|
||||
}
|
||||
|
||||
if (hasHeader(xhr, /HX-Trigger-After-Swap:/i)) {
|
||||
let finalElt = elt
|
||||
if (!bodyContains(elt)) {
|
||||
finalElt = getDocument().body
|
||||
}
|
||||
handleTrigger(xhr, 'HX-Trigger-After-Swap', finalElt)
|
||||
}
|
||||
|
||||
const doSettle = function() {
|
||||
forEach(settleInfo.tasks, function(task) {
|
||||
task.call()
|
||||
})
|
||||
forEach(settleInfo.elts, function(elt) {
|
||||
if (elt.classList) {
|
||||
elt.classList.remove(htmx.config.settlingClass)
|
||||
}
|
||||
triggerEvent(elt, 'htmx:afterSettle', responseInfo)
|
||||
})
|
||||
|
||||
if (responseInfo.pathInfo.anchor) {
|
||||
const anchorTarget = getDocument().getElementById(responseInfo.pathInfo.anchor)
|
||||
if (anchorTarget) {
|
||||
anchorTarget.scrollIntoView({ block: 'start', behavior: 'auto' })
|
||||
}
|
||||
}
|
||||
|
||||
updateScrollState(settleInfo.elts, swapSpec)
|
||||
|
||||
if (hasHeader(xhr, /HX-Trigger-After-Settle:/i)) {
|
||||
let finalElt = elt
|
||||
if (!bodyContains(elt)) {
|
||||
finalElt = getDocument().body
|
||||
}
|
||||
handleTrigger(xhr, 'HX-Trigger-After-Settle', finalElt)
|
||||
}
|
||||
maybeCall(settleResolve)
|
||||
}
|
||||
|
||||
if (swapSpec.settleDelay > 0) {
|
||||
setTimeout(doSettle, swapSpec.settleDelay)
|
||||
} else {
|
||||
doSettle()
|
||||
}
|
||||
} catch (e) {
|
||||
triggerErrorEvent(elt, 'htmx:swapError', responseInfo)
|
||||
maybeCall(settleReject)
|
||||
@ -3662,12 +3732,12 @@ var htmx = (function() {
|
||||
* @param {import("./htmx").HtmxExtension[]=} extensionsToIgnore
|
||||
*/
|
||||
function getExtensions(elt, extensionsToReturn, extensionsToIgnore) {
|
||||
if (elt == undefined) {
|
||||
return extensionsToReturn
|
||||
}
|
||||
if (extensionsToReturn == undefined) {
|
||||
extensionsToReturn = []
|
||||
}
|
||||
if (elt == undefined) {
|
||||
return extensionsToReturn
|
||||
}
|
||||
if (extensionsToIgnore == undefined) {
|
||||
extensionsToIgnore = []
|
||||
}
|
||||
|
||||
@ -375,4 +375,32 @@ describe('Core htmx API test', function() {
|
||||
htmx.trigger(div, 'myEvent')
|
||||
myEventCalled.should.equal(true)
|
||||
})
|
||||
|
||||
it('swaps content properly (basic)', function() {
|
||||
var output = make('<output id="output"/>')
|
||||
htmx.swap('#output', '<div>Swapped!</div>', { swapStyle: 'innerHTML' })
|
||||
output.innerHTML.should.be.equal('<div>Swapped!</div>')
|
||||
})
|
||||
|
||||
it('swaps content properly (with select)', function() {
|
||||
var output = make('<output id="output"/>')
|
||||
htmx.swap('#output', '<div><p id="select-me">Swapped!</p></div>', { swapStyle: 'innerHTML' }, { select: '#select-me' })
|
||||
output.innerHTML.should.be.equal('<p id="select-me">Swapped!</p>')
|
||||
})
|
||||
|
||||
it('swaps content properly (with oob)', function() {
|
||||
var output = make('<output id="output"/>')
|
||||
var oobDiv = make('<div id="oob"/>')
|
||||
htmx.swap('#output', '<div id="oob" hx-swap-oob="innerHTML">OOB Swapped!</div><div>Swapped!</div>', { swapStyle: 'innerHTML' })
|
||||
output.innerHTML.should.be.equal('<div>Swapped!</div>')
|
||||
oobDiv.innerHTML.should.be.equal('OOB Swapped!')
|
||||
})
|
||||
|
||||
it('swaps content properly (with select oob)', function() {
|
||||
var output = make('<output id="output"/>')
|
||||
var oobDiv = make('<div id="oob"/>')
|
||||
htmx.swap('#output', '<div id="oob">OOB Swapped!</div><div>Swapped!</div>', { swapStyle: 'innerHTML' }, { selectOOB: '#oob:innerHTML' })
|
||||
output.innerHTML.should.be.equal('<div>Swapped!</div>')
|
||||
oobDiv.innerHTML.should.be.equal('OOB Swapped!')
|
||||
})
|
||||
})
|
||||
|
||||
@ -448,6 +448,37 @@ Removes the given extension from htmx
|
||||
htmx.removeExtension("my-extension");
|
||||
```
|
||||
|
||||
### Method - `htmx.swap()` {#swap}
|
||||
|
||||
Performs swapping (and settling) of HTML content
|
||||
|
||||
##### Parameters
|
||||
|
||||
* `target` - the HTML element or string selector of swap target
|
||||
* `content` - string representation of content to be swapped
|
||||
* `swapSpec` - swapping specification, representing parameters from `hx-swap`
|
||||
* `swapStyle` (required) - swapping style (`innerHTML`, `outerHTML`, `beforebegin` etc)
|
||||
* `swapDelay`, `settleDelay` (number) - delays before swapping and settling respectively
|
||||
* `transition` (bool) - whether to use HTML transitions for swap
|
||||
* `ignoreTitle` (bool) - disables page title updates
|
||||
* `head` (string) - specifies `head` tag handling strategy (`merge` or `append`). Leave empty to disable head handling
|
||||
* `scroll`, `scrollTarget`, `show`, `showTarget`, `focusScroll` - specifies scroll handling after swap
|
||||
* `swapOptions` - additional *optional* parameters for swapping
|
||||
* `select` - selector for the content to be swapped (equivalent of `hx-select`)
|
||||
* `selectOOB` - selector for the content to be swapped out-of-band (equivalent of `hx-select-oob`)
|
||||
* `eventInfo` - an object to be attached to `htmx:afterSwap` and `htmx:afterSettle` elements
|
||||
* `anchor` - an anchor element that triggered scroll, will be scrolled into view on settle. Provides simple alternative to full scroll handling
|
||||
* `contextElement` - DOM element that serves as context to swapping operation. Currently used to find extensions enabled for specific element
|
||||
* `afterSwapCallback`, `afterSettleCallback` - callback functions called after swap and settle respectively. Take no arguments
|
||||
|
||||
|
||||
##### Example
|
||||
|
||||
```js
|
||||
// swap #output element inner HTML with div element with "Swapped!" text
|
||||
htmx.swap("#output", "<div>Swapped!</div>", {swapStyle: 'innerHTML'});
|
||||
```
|
||||
|
||||
### Method - `htmx.takeClass()` {#takeClass}
|
||||
|
||||
Takes the given class from its siblings, so that among its siblings, only the given element will have the class.
|
||||
|
||||
@ -202,6 +202,7 @@ The table below lists all other attributes available in htmx.
|
||||
| [`htmx.remove()`](@/api.md#remove) | Removes the given element
|
||||
| [`htmx.removeClass()`](@/api.md#removeClass) | Removes a class from the given element
|
||||
| [`htmx.removeExtension()`](@/api.md#removeExtension) | Removes an htmx [extension](@/extensions/_index.md)
|
||||
| [`htmx.swap()`](@/api.md#swap) | Performs swapping (and settling) of HTML content
|
||||
| [`htmx.takeClass()`](@/api.md#takeClass) | Takes a class from other elements for the given element
|
||||
| [`htmx.toggleClass()`](@/api.md#toggleClass) | Toggles a class from the given element
|
||||
| [`htmx.trigger()`](@/api.md#trigger) | Triggers an event on an element
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user