diff --git a/package.json b/package.json index ebe0b74c..ffaf834e 100644 --- a/package.json +++ b/package.json @@ -27,8 +27,9 @@ "scripts": { "dist": "./scripts/dist.sh", "lint": "eslint src/htmx.js test/attributes/ test/core/ test/util/", + "lint-fix": "eslint src/htmx.js test/attributes/ test/core/ test/util/ --fix", "format": "eslint --fix src/htmx.js test/attributes/ test/core/ test/util/", - "test": "mocha-chrome test/index.html", + "test": "npm run lint && mocha-chrome test/index.html", "test-types": "tsc --project ./jsconfig.json", "ws-tests": "cd ./test/ws-sse && node ./server.js", "www": "bash ./scripts/www.sh" diff --git a/src/htmx.d.ts b/src/htmx.d.ts index 197ac527..631af6ec 100644 --- a/src/htmx.d.ts +++ b/src/htmx.d.ts @@ -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 */ diff --git a/src/htmx.js b/src/htmx.js index ca833e5a..118e340c 100644 --- a/src/htmx.js +++ b/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, @@ -59,9 +65,9 @@ var htmx = (function() { scrollIntoViewOnBoost: true, triggerSpecsCache: null, disableInheritance: false, - head : { - boost : "merge", - other: "none", + head: { + boost: 'merge', + other: 'none' }, responseHandling: [ { code: '204', swap: false }, @@ -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)} */ @@ -279,57 +285,53 @@ var htmx = (function() { function takeChildrenFor(fragment, elt) { while (elt.childNodes.length > 0) { - fragment.append(elt.childNodes[0]); + fragment.append(elt.childNodes[0]) } } /** * @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) { // strip head tag to determine shape of response we are dealing with - let head = (HEAD_TAG_REGEX.exec(response) || [""])[0] - let responseWithNoHead = response.replace(HEAD_TAG_REGEX, '') + const head = (HEAD_TAG_REGEX.exec(response) || [''])[0] + const responseWithNoHead = response.replace(HEAD_TAG_REGEX, '') const startTag = getStartTag(responseWithNoHead) if (startTag === 'html') { - // if it is a full document, parse it and return the body - const fragment = new DocumentFragment(); - let doc = parseHTML(response); + const fragment = new DocumentFragment() + const doc = parseHTML(response) takeChildrenFor(fragment, doc.body) - fragment.head = doc.head; - fragment.title = doc.title; - return fragment; + fragment.head = doc.head + fragment.title = doc.title + return fragment } else if (startTag === 'body') { - // body w/ a potential head, parse head & body w/o wrapping in template - const fragment = new DocumentFragment(); - let doc = parseHTML(head + responseWithNoHead); + const fragment = new DocumentFragment() + const doc = parseHTML(head + responseWithNoHead) takeChildrenFor(fragment, doc.body) - fragment.head = doc.head; - fragment.title = doc.title; - return fragment; - + fragment.head = doc.head + fragment.title = doc.title + return fragment } else { - // otherwise we have non-body content, so wrap it in a template and insert the head before the content const doc = parseHTML(head + '
' + responseWithNoHead + '') - var fragment = doc.querySelector('template').content; + var fragment = doc.querySelector('template').content // extract head into fragment for later processing - fragment.head = doc.head; - fragment.title = doc.title; + fragment.head = doc.head + fragment.title = doc.title // for legacy reasons we support a title tag at the root level of non-body responses, so we need to handle it - var rootTitleElt = fragment.querySelector(":scope title"); + var rootTitleElt = fragment.querySelector(':scope title') if (rootTitleElt) { - rootTitleElt.remove(); - fragment.title = rootTitleElt.innerText; + rootTitleElt.remove() + fragment.title = rootTitleElt.innerText } - return fragment; + return fragment } } @@ -396,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