From 5363b79926fa84ac944960e2f2b842e445f60800 Mon Sep 17 00:00:00 2001 From: Michael Kennedy Date: Mon, 1 Jul 2024 11:46:53 -0700 Subject: [PATCH 1/6] Update to the latest dependencies. --- requirements.txt | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/requirements.txt b/requirements.txt index 959604b..7c7d2f7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,30 +1,38 @@ # This file was autogenerated by uv via the following command: # uv pip compile requirements.piptools --output-file requirements.txt -annotated-types==0.6.0 +annotated-types==0.7.0 # via pydantic -blinker==1.7.0 +blinker==1.8.2 # via flask click==8.1.7 # via flask -flask==3.0.2 -itsdangerous==2.1.2 +flask==3.0.3 + # via -r requirements.piptools +itsdangerous==2.2.0 # via flask -jinja-partials==0.2.0 -jinja2==3.1.3 +jinja-partials==0.2.1 + # via -r requirements.piptools +jinja2==3.1.4 # via + # -r requirements.piptools # flask # jinja-partials markupsafe==2.1.5 # via + # -r requirements.piptools # jinja2 # werkzeug -more-itertools==10.2.0 -pydantic==2.6.3 -pydantic-core==2.16.3 +more-itertools==10.3.0 + # via -r requirements.piptools +pydantic==2.8.0 + # via -r requirements.piptools +pydantic-core==2.20.0 # via pydantic -typing-extensions==4.10.0 +typing-extensions==4.12.2 # via # pydantic # pydantic-core -werkzeug==3.0.1 - # via flask +werkzeug==3.0.3 + # via + # -r requirements.piptools + # flask From ba5d8ae63c3160ddec42c4d3d471d7b96621b61a Mon Sep 17 00:00:00 2001 From: Michael Kennedy Date: Mon, 1 Jul 2024 11:53:28 -0700 Subject: [PATCH 2/6] Upgrade to the 2.0 release of HTMX (no code changes needed) --- .../static/js/htmx.d.ts | 195 + .../static/js/htmx.js | 8890 ++++++++++------- .../static/js/htmx.min.js | 6 +- .../templates/shared/_layout.html | 2 +- .../static/js/htmx.d.ts | 195 + .../static/js/htmx.js | 8890 ++++++++++------- .../static/js/htmx.min.js | 6 +- .../static/js/htmx.d.ts | 195 + .../static/js/htmx.js | 8890 ++++++++++------- .../static/js/htmx.min.js | 6 +- .../templates/shared/_layout.html | 2 +- .../static/js/htmx.d.ts | 195 + .../static/js/htmx.js | 8890 ++++++++++------- .../static/js/htmx.min.js | 6 +- .../templates/shared/_layout.html | 2 +- .../static/js/htmx.d.ts | 195 + .../static/js/htmx.js | 8890 ++++++++++------- .../static/js/htmx.min.js | 6 +- .../templates/shared/_layout.html | 2 +- .../static/js/htmx.d.ts | 195 + .../static/js/htmx.js | 8890 ++++++++++------- .../static/js/htmx.min.js | 6 +- .../templates/shared/_layout.html | 2 +- .../static/js/htmx.d.ts | 195 + .../static/js/htmx.js | 8890 ++++++++++------- .../static/js/htmx.min.js | 6 +- .../templates/shared/_layout.html | 2 +- .../static/js/htmx.d.ts | 195 + .../static/js/htmx.js | 8890 ++++++++++------- .../static/js/htmx.min.js | 6 +- .../templates/shared/_layout.html | 2 +- .../static/js/htmx.d.ts | 195 + .../starter_video_collector/static/js/htmx.js | 8890 ++++++++++------- .../static/js/htmx.min.js | 6 +- 34 files changed, 47284 insertions(+), 34549 deletions(-) create mode 100644 code/ch4_app/ch4_final_video_collector/static/js/htmx.d.ts create mode 100644 code/ch4_app/ch4_starter_video_collector/static/js/htmx.d.ts create mode 100644 code/ch5_partials/ch5_final_video_collector/static/js/htmx.d.ts create mode 100644 code/ch5_partials/ch5_starter_video_collector/static/js/htmx.d.ts create mode 100644 code/ch6_active_search/ch6_final_video_collector/static/js/htmx.d.ts create mode 100644 code/ch6_active_search/ch6_starter_video_collector/static/js/htmx.d.ts create mode 100644 code/ch7_infinite_scroll/ch7_final_video_collector/static/js/htmx.d.ts create mode 100644 code/ch7_infinite_scroll/ch7_starter_video_collector/static/js/htmx.d.ts create mode 100644 code/starter_video_collector/static/js/htmx.d.ts diff --git a/code/ch4_app/ch4_final_video_collector/static/js/htmx.d.ts b/code/ch4_app/ch4_final_video_collector/static/js/htmx.d.ts new file mode 100644 index 0000000..3775459 --- /dev/null +++ b/code/ch4_app/ch4_final_video_collector/static/js/htmx.d.ts @@ -0,0 +1,195 @@ +declare namespace htmx { + const onLoad: (callback: (elt: Node) => void) => EventListener; + const process: (elt: string | Element) => void; + const on: (arg1: string | EventTarget, arg2: string | EventListener, arg3?: EventListener) => EventListener; + const off: (arg1: string | EventTarget, arg2: string | EventListener, arg3?: EventListener) => EventListener; + const trigger: (elt: string | EventTarget, eventName: string, detail?: any) => boolean; + const ajax: (verb: HttpVerb, path: string, context: string | Element | HtmxAjaxHelperContext) => Promise; + const find: (eltOrSelector: string | ParentNode, selector?: string) => Element; + const findAll: (eltOrSelector: string | ParentNode, selector?: string) => NodeListOf; + const closest: (elt: string | Element, selector: string) => Element; + function values(elt: Element, type: HttpVerb): any; + const remove: (elt: Node, delay?: number) => void; + const addClass: (elt: string | Element, clazz: string, delay?: number) => void; + const removeClass: (node: string | Node, clazz: string, delay?: number) => void; + const toggleClass: (elt: string | Element, clazz: string) => void; + const takeClass: (elt: string | Node, clazz: string) => void; + const swap: (target: string | Element, content: string, swapSpec: HtmxSwapSpecification, swapOptions?: SwapOptions) => void; + const defineExtension: (name: string, extension: any) => void; + const removeExtension: (name: string) => void; + const logAll: () => void; + const logNone: () => void; + const logger: any; + namespace config { + const historyEnabled: boolean; + const historyCacheSize: number; + const refreshOnHistoryMiss: boolean; + const defaultSwapStyle: HtmxSwapStyle; + const defaultSwapDelay: number; + const defaultSettleDelay: number; + const includeIndicatorStyles: boolean; + const indicatorClass: string; + const requestClass: string; + const addedClass: string; + const settlingClass: string; + const swappingClass: string; + const allowEval: boolean; + const allowScriptTags: boolean; + const inlineScriptNonce: string; + const inlineStyleNonce: string; + const attributesToSettle: string[]; + const withCredentials: boolean; + const timeout: number; + const wsReconnectDelay: "full-jitter" | ((retryCount: number) => number); + const wsBinaryType: BinaryType; + const disableSelector: string; + const scrollBehavior: 'auto' | 'instant' | 'smooth'; + const defaultFocusScroll: boolean; + const getCacheBusterParam: boolean; + const globalViewTransitions: boolean; + const methodsThatUseUrlParams: (HttpVerb)[]; + const selfRequestsOnly: boolean; + const ignoreTitle: boolean; + const scrollIntoViewOnBoost: boolean; + const triggerSpecsCache: any | null; + const disableInheritance: boolean; + const responseHandling: HtmxResponseHandlingConfig[]; + const allowNestedOobSwaps: boolean; + } + const parseInterval: (str: string) => number; + const _: (str: string) => any; + const version: string; +} +type HttpVerb = 'get' | 'head' | 'post' | 'put' | 'delete' | 'connect' | 'options' | 'trace' | 'patch'; +type SwapOptions = { + select?: string; + selectOOB?: string; + eventInfo?: any; + anchor?: string; + contextElement?: Element; + afterSwapCallback?: swapCallback; + afterSettleCallback?: swapCallback; +}; +type swapCallback = () => any; +type HtmxSwapStyle = 'innerHTML' | 'outerHTML' | 'beforebegin' | 'afterbegin' | 'beforeend' | 'afterend' | 'delete' | 'none' | string; +type HtmxSwapSpecification = { + swapStyle: HtmxSwapStyle; + swapDelay: number; + settleDelay: number; + transition?: boolean; + ignoreTitle?: boolean; + head?: string; + scroll?: 'top' | 'bottom'; + scrollTarget?: string; + show?: string; + showTarget?: string; + focusScroll?: boolean; +}; +type ConditionalFunction = ((this: Node, evt: Event) => boolean) & { + source: string; +}; +type HtmxTriggerSpecification = { + trigger: string; + pollInterval?: number; + eventFilter?: ConditionalFunction; + changed?: boolean; + once?: boolean; + consume?: boolean; + delay?: number; + from?: string; + target?: string; + throttle?: number; + queue?: string; + root?: string; + threshold?: string; +}; +type HtmxElementValidationError = { + elt: Element; + message: string; + validity: ValidityState; +}; +type HtmxHeaderSpecification = Record; +type HtmxAjaxHelperContext = { + source?: Element | string; + event?: Event; + handler?: HtmxAjaxHandler; + target: Element | string; + swap?: HtmxSwapStyle; + values?: any | FormData; + headers?: Record; + select?: string; +}; +type HtmxRequestConfig = { + boosted: boolean; + useUrlParams: boolean; + formData: FormData; + /** + * formData proxy + */ + parameters: any; + unfilteredFormData: FormData; + /** + * unfilteredFormData proxy + */ + unfilteredParameters: any; + headers: HtmxHeaderSpecification; + target: Element; + verb: HttpVerb; + errors: HtmxElementValidationError[]; + withCredentials: boolean; + timeout: number; + path: string; + triggeringEvent: Event; +}; +type HtmxResponseInfo = { + xhr: XMLHttpRequest; + target: Element; + requestConfig: HtmxRequestConfig; + etc: HtmxAjaxEtc; + boosted: boolean; + select: string; + pathInfo: { + requestPath: string; + finalRequestPath: string; + responsePath: string | null; + anchor: string; + }; + failed?: boolean; + successful?: boolean; +}; +type HtmxAjaxEtc = { + returnPromise?: boolean; + handler?: HtmxAjaxHandler; + select?: string; + targetOverride?: Element; + swapOverride?: HtmxSwapStyle; + headers?: Record; + values?: any | FormData; + credentials?: boolean; + timeout?: number; +}; +type HtmxResponseHandlingConfig = { + code?: string; + swap: boolean; + error?: boolean; + ignoreTitle?: boolean; + select?: string; + target?: string; + swapOverride?: string; + event?: string; +}; +type HtmxBeforeSwapDetails = HtmxResponseInfo & { + shouldSwap: boolean; + serverResponse: any; + isError: boolean; + ignoreTitle: boolean; + selectOverride: string; +}; +type HtmxAjaxHandler = (elt: Element, responseInfo: HtmxResponseInfo) => any; +type HtmxSettleTask = (() => void); +type HtmxSettleInfo = { + tasks: HtmxSettleTask[]; + elts: Element[]; + title?: string; +}; +type HtmxExtension = any; diff --git a/code/ch4_app/ch4_final_video_collector/static/js/htmx.js b/code/ch4_app/ch4_final_video_collector/static/js/htmx.js index 86e7668..c57bcd7 100644 --- a/code/ch4_app/ch4_final_video_collector/static/js/htmx.js +++ b/code/ch4_app/ch4_final_video_collector/static/js/htmx.js @@ -1,3909 +1,5131 @@ -// /////////////////////////////////////////////////////////////////// -// HTMX v1.9.10 from https://unpkg.com/htmx.org@1.9.10/dist/htmx.js -// - -// UMD insanity -// This code sets up support for (in order) AMD, ES6 modules, and globals. -(function (root, factory) { - //@ts-ignore - if (typeof define === 'function' && define.amd) { - // AMD. Register as an anonymous module. - //@ts-ignore - define([], factory); - } else if (typeof module === 'object' && module.exports) { - // Node. Does not work with strict CommonJS, but - // only CommonJS-like environments that support module.exports, - // like Node. - module.exports = factory(); - } else { - // Browser globals - root.htmx = root.htmx || factory(); - } -}(typeof self !== 'undefined' ? self : this, function () { -return (function () { - 'use strict'; - - // Public API - //** @type {import("./htmx").HtmxApi} */ - // TODO: list all methods in public API - var htmx = { - onLoad: onLoadHelper, - process: processNode, - on: addEventListenerImpl, - off: removeEventListenerImpl, - trigger : triggerEvent, - ajax : ajaxHelper, - find : find, - findAll : findAll, - closest : closest, - values : function(elt, type){ - var inputValues = getInputValues(elt, type || "post"); - return inputValues.values; - }, - remove : removeElement, - addClass : addClassToElement, - removeClass : removeClassFromElement, - toggleClass : toggleClassOnElement, - takeClass : takeClassForElement, - defineExtension : defineExtension, - removeExtension : removeExtension, - logAll : logAll, - logNone : logNone, - logger : null, - config : { - historyEnabled:true, - historyCacheSize:10, - refreshOnHistoryMiss:false, - defaultSwapStyle:'innerHTML', - defaultSwapDelay:0, - defaultSettleDelay:20, - includeIndicatorStyles:true, - indicatorClass:'htmx-indicator', - requestClass:'htmx-request', - addedClass:'htmx-added', - settlingClass:'htmx-settling', - swappingClass:'htmx-swapping', - allowEval:true, - allowScriptTags:true, - inlineScriptNonce:'', - attributesToSettle:["class", "style", "width", "height"], - withCredentials:false, - timeout:0, - wsReconnectDelay: 'full-jitter', - wsBinaryType: 'blob', - disableSelector: "[hx-disable], [data-hx-disable]", - useTemplateFragments: false, - scrollBehavior: 'smooth', - defaultFocusScroll: false, - getCacheBusterParam: false, - globalViewTransitions: false, - methodsThatUseUrlParams: ["get"], - selfRequestsOnly: false, - ignoreTitle: false, - scrollIntoViewOnBoost: true, - triggerSpecsCache: null, - }, - parseInterval:parseInterval, - _:internalEval, - createEventSource: function(url){ - return new EventSource(url, {withCredentials:true}) - }, - createWebSocket: function(url){ - var sock = new WebSocket(url, []); - sock.binaryType = htmx.config.wsBinaryType; - return sock; - }, - version: "1.9.10" - }; - - /** @type {import("./htmx").HtmxInternalApi} */ - var internalAPI = { - addTriggerHandler: addTriggerHandler, - bodyContains: bodyContains, - canAccessLocalStorage: canAccessLocalStorage, - findThisElement: findThisElement, - filterValues: filterValues, - hasAttribute: hasAttribute, - getAttributeValue: getAttributeValue, - getClosestAttributeValue: getClosestAttributeValue, - getClosestMatch: getClosestMatch, - getExpressionVars: getExpressionVars, - getHeaders: getHeaders, - getInputValues: getInputValues, - getInternalData: getInternalData, - getSwapSpecification: getSwapSpecification, - getTriggerSpecs: getTriggerSpecs, - getTarget: getTarget, - makeFragment: makeFragment, - mergeObjects: mergeObjects, - makeSettleInfo: makeSettleInfo, - oobSwap: oobSwap, - querySelectorExt: querySelectorExt, - selectAndSwap: selectAndSwap, - settleImmediately: settleImmediately, - shouldCancel: shouldCancel, - triggerEvent: triggerEvent, - triggerErrorEvent: triggerErrorEvent, - withExtensions: withExtensions, - } - - var VERBS = ['get', 'post', 'put', 'delete', 'patch']; - var VERB_SELECTOR = VERBS.map(function(verb){ - return "[hx-" + verb + "], [data-hx-" + verb + "]" - }).join(", "); - - var HEAD_TAG_REGEX = makeTagRegEx('head'), - TITLE_TAG_REGEX = makeTagRegEx('title'), - SVG_TAGS_REGEX = makeTagRegEx('svg', true); - - //==================================================================== - // 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'); - } - - function parseInterval(str) { - if (str == undefined) { - return undefined; - } - - let interval = NaN; - if (str.slice(-2) == "ms") { - interval = parseFloat(str.slice(0, -2)); - } else if (str.slice(-1) == "s") { - interval = parseFloat(str.slice(0, -1)) * 1000; - } else if (str.slice(-1) == "m") { - interval = parseFloat(str.slice(0, -1)) * 1000 * 60; - } else { - interval = parseFloat(str); - } - return isNaN(interval) ? undefined : interval; - } - - /** - * @param {HTMLElement} elt - * @param {string} name - * @returns {(string | null)} - */ - function getRawAttribute(elt, name) { - return elt.getAttribute && elt.getAttribute(name); - } - - // resolve with both hx and data-hx prefixes - function hasAttribute(elt, qualifiedName) { - return elt.hasAttribute && (elt.hasAttribute(qualifiedName) || - elt.hasAttribute("data-" + qualifiedName)); - } - - /** - * - * @param {HTMLElement} elt - * @param {string} qualifiedName - * @returns {(string | null)} - */ - function getAttributeValue(elt, qualifiedName) { - return getRawAttribute(elt, qualifiedName) || getRawAttribute(elt, "data-" + qualifiedName); - } - - /** - * @param {HTMLElement} elt - * @returns {HTMLElement | null} - */ - function parentElt(elt) { - return elt.parentElement; - } - - /** - * @returns {Document} - */ - function getDocument() { - return document; - } - - /** - * @param {HTMLElement} elt - * @param {(e:HTMLElement) => boolean} condition - * @returns {HTMLElement | null} - */ - function getClosestMatch(elt, condition) { - while (elt && !condition(elt)) { - elt = parentElt(elt); - } - - return elt ? elt : null; - } - - function getAttributeValueWithDisinheritance(initialElement, ancestor, attributeName){ - var attributeValue = getAttributeValue(ancestor, attributeName); - var disinherit = getAttributeValue(ancestor, "hx-disinherit"); - if (initialElement !== ancestor && disinherit && (disinherit === "*" || disinherit.split(" ").indexOf(attributeName) >= 0)) { - return "unset"; - } else { - return attributeValue - } - } - - /** - * @param {HTMLElement} elt - * @param {string} attributeName - * @returns {string | null} - */ - function getClosestAttributeValue(elt, attributeName) { - var closestAttr = null; - getClosestMatch(elt, function (e) { - return closestAttr = getAttributeValueWithDisinheritance(elt, e, attributeName); - }); - if (closestAttr !== "unset") { - return closestAttr; - } - } - - /** - * @param {HTMLElement} elt - * @param {string} selector - * @returns {boolean} - */ - function matches(elt, selector) { - // @ts-ignore: non-standard properties for browser compatibility - // noinspection JSUnresolvedVariable - var matchesFunction = elt.matches || elt.matchesSelector || elt.msMatchesSelector || elt.mozMatchesSelector || elt.webkitMatchesSelector || elt.oMatchesSelector; - return matchesFunction && matchesFunction.call(elt, selector); - } - - /** - * @param {string} str - * @returns {string} - */ - function getStartTag(str) { - var tagMatcher = /<([a-z][^\/\0>\x20\t\r\n\f]*)/i - var match = tagMatcher.exec( str ); - if (match) { - return match[1].toLowerCase(); - } else { - return ""; - } - } - - /** - * - * @param {string} resp - * @param {number} depth - * @returns {Element} - */ - function parseHTML(resp, depth) { - var parser = new DOMParser(); - var responseDoc = parser.parseFromString(resp, "text/html"); - - /** @type {Element} */ - var responseNode = responseDoc.body; - while (depth > 0) { - depth--; - // @ts-ignore - responseNode = responseNode.firstChild; - } - if (responseNode == null) { - // @ts-ignore - responseNode = getDocument().createDocumentFragment(); - } - return responseNode; - } - - function aFullPageResponse(resp) { - return / number)} + * @default "full-jitter" + */ + wsReconnectDelay: 'full-jitter', + /** + * The type of binary data being received over the WebSocket connection + * @type BinaryType + * @default 'blob' + */ + wsBinaryType: 'blob', + /** + * @type string + * @default '[hx-disable], [data-hx-disable]' + */ + disableSelector: '[hx-disable], [data-hx-disable]', + /** + * @type {'auto' | 'instant' | 'smooth'} + * @default 'smooth' + */ + scrollBehavior: 'instant', + /** + * If the focused element should be scrolled into view. + * @type boolean + * @default false + */ + defaultFocusScroll: false, + /** + * If set to true htmx will include a cache-busting parameter in GET requests to avoid caching partial responses by the browser + * @type boolean + * @default false + */ + getCacheBusterParam: false, + /** + * If set to true, htmx will use the View Transition API when swapping in new content. + * @type boolean + * @default false + */ + globalViewTransitions: false, + /** + * htmx will format requests with these methods by encoding their parameters in the URL, not the request body + * @type {(HttpVerb)[]} + * @default ['get', 'delete'] + */ + methodsThatUseUrlParams: ['get', 'delete'], + /** + * If set to true, disables htmx-based requests to non-origin hosts. + * @type boolean + * @default false + */ + selfRequestsOnly: true, + /** + * If set to true htmx will not update the title of the document when a title tag is found in new content + * @type boolean + * @default false + */ + ignoreTitle: false, + /** + * Whether the target of a boosted element is scrolled into the viewport. + * @type boolean + * @default true + */ + scrollIntoViewOnBoost: true, + /** + * The cache to store evaluated trigger specifications into. + * You may define a simple object to use a never-clearing cache, or implement your own system using a [proxy object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Proxy) + * @type {Object|null} + * @default null + */ + triggerSpecsCache: null, + /** @type boolean */ + disableInheritance: false, + /** @type HtmxResponseHandlingConfig[] */ + responseHandling: [ + { code: '204', swap: false }, + { code: '[23]..', swap: true }, + { code: '[45]..', swap: false, error: true } + ], + /** + * Whether to process OOB swaps on elements that are nested within the main response element. + * @type boolean + * @default true + */ + allowNestedOobSwaps: true + }, + /** @type {typeof parseInterval} */ + parseInterval: null, + /** @type {typeof internalEval} */ + _: null, + version: '2.0.0' + } + // Tsc madness part 2 + htmx.onLoad = onLoadHelper + htmx.process = processNode + htmx.on = addEventListenerImpl + htmx.off = removeEventListenerImpl + htmx.trigger = triggerEvent + htmx.ajax = ajaxHelper + htmx.find = find + htmx.findAll = findAll + htmx.closest = closest + htmx.remove = removeElement + htmx.addClass = addClassToElement + htmx.removeClass = removeClassFromElement + htmx.toggleClass = toggleClassOnElement + htmx.takeClass = takeClassForElement + htmx.swap = swap + htmx.defineExtension = defineExtension + htmx.removeExtension = removeExtension + htmx.logAll = logAll + htmx.logNone = logNone + htmx.parseInterval = parseInterval + htmx._ = internalEval + + const internalAPI = { + addTriggerHandler, + bodyContains, + canAccessLocalStorage, + findThisElement, + filterValues, + swap, + hasAttribute, + getAttributeValue, + getClosestAttributeValue, + getClosestMatch, + getExpressionVars, + getHeaders, + getInputValues, + getInternalData, + getSwapSpecification, + getTriggerSpecs, + getTarget, + makeFragment, + mergeObjects, + makeSettleInfo, + oobSwap, + querySelectorExt, + settleImmediately, + shouldCancel, + triggerEvent, + triggerErrorEvent, + withExtensions + } + + const VERBS = ['get', 'post', 'put', 'delete', 'patch'] + const VERB_SELECTOR = VERBS.map(function(verb) { + 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. + * + * Caution: Accepts an int followed by either **s** or **ms**. All other values use **parseFloat** + * + * @see https://htmx.org/api/#parseInterval + * + * @param {string} str timing string + * @returns {number|undefined} + */ + function parseInterval(str) { + if (str == undefined) { + return undefined + } - /** - * - * @param {string} response - * @returns {Element} - */ - function makeFragment(response) { - var partialResponse = !aFullPageResponse(response); - var startTag = getStartTag(response); - var content = response; - if (startTag === 'head') { - content = content.replace(HEAD_TAG_REGEX, ''); - } - if (htmx.config.useTemplateFragments && partialResponse) { - var documentFragment = parseHTML("", 0); - // @ts-ignore type mismatch between DocumentFragment and Element. - // TODO: Are these close enough for htmx to use interchangeably? - return documentFragment.querySelector('template').content; - } - switch (startTag) { - case "thead": - case "tbody": - case "tfoot": - case "colgroup": - case "caption": - return parseHTML("" + content + "
", 1); - case "col": - return parseHTML("" + content + "
", 2); - case "tr": - return parseHTML("" + content + "
", 2); - case "td": - case "th": - return parseHTML("" + content + "
", 3); - case "script": - case "style": - return parseHTML("
" + content + "
", 1); - default: - return parseHTML(content, 0); - } - } + let interval = NaN + if (str.slice(-2) == 'ms') { + interval = parseFloat(str.slice(0, -2)) + } else if (str.slice(-1) == 's') { + interval = parseFloat(str.slice(0, -1)) * 1000 + } else if (str.slice(-1) == 'm') { + interval = parseFloat(str.slice(0, -1)) * 1000 * 60 + } else { + interval = parseFloat(str) + } + return isNaN(interval) ? undefined : interval + } + + /** + * @param {Node} elt + * @param {string} name + * @returns {(string | null)} + */ + function getRawAttribute(elt, name) { + return elt instanceof Element && elt.getAttribute(name) + } + + /** + * @param {Element} elt + * @param {string} qualifiedName + * @returns {boolean} + */ + // resolve with both hx and data-hx prefixes + function hasAttribute(elt, qualifiedName) { + return !!elt.hasAttribute && (elt.hasAttribute(qualifiedName) || + elt.hasAttribute('data-' + qualifiedName)) + } + + /** + * + * @param {Node} elt + * @param {string} qualifiedName + * @returns {(string | null)} + */ + function getAttributeValue(elt, qualifiedName) { + return getRawAttribute(elt, qualifiedName) || getRawAttribute(elt, 'data-' + qualifiedName) + } + + /** + * @param {Node} elt + * @returns {Node | null} + */ + function parentElt(elt) { + const parent = elt.parentElement + if (!parent && elt.parentNode instanceof ShadowRoot) return elt.parentNode + return parent + } + + /** + * @returns {Document} + */ + function getDocument() { + return document + } + + /** + * @param {Node} elt + * @param {boolean} global + * @returns {Node|Document} + */ + function getRootNode(elt, global) { + return elt.getRootNode ? elt.getRootNode({ composed: global }) : getDocument() + } + + /** + * @param {Node} elt + * @param {(e:Node) => boolean} condition + * @returns {Node | null} + */ + function getClosestMatch(elt, condition) { + while (elt && !condition(elt)) { + elt = parentElt(elt) + } - /** - * @param {Function} func - */ - function maybeCall(func){ - if(func) { - func(); - } + return elt || null + } + + /** + * @param {Element} initialElement + * @param {Element} ancestor + * @param {string} attributeName + * @returns {string|null} + */ + function getAttributeValueWithDisinheritance(initialElement, ancestor, attributeName) { + const attributeValue = getAttributeValue(ancestor, attributeName) + const disinherit = getAttributeValue(ancestor, 'hx-disinherit') + var inherit = getAttributeValue(ancestor, 'hx-inherit') + if (initialElement !== ancestor) { + if (htmx.config.disableInheritance) { + if (inherit && (inherit === '*' || inherit.split(' ').indexOf(attributeName) >= 0)) { + return attributeValue + } else { + return null + } + } + if (disinherit && (disinherit === '*' || disinherit.split(' ').indexOf(attributeName) >= 0)) { + return 'unset' + } + } + return attributeValue + } + + /** + * @param {Element} elt + * @param {string} attributeName + * @returns {string | null} + */ + function getClosestAttributeValue(elt, attributeName) { + let closestAttr = null + getClosestMatch(elt, function(e) { + return !!(closestAttr = getAttributeValueWithDisinheritance(elt, asElement(e), attributeName)) + }) + if (closestAttr !== 'unset') { + return closestAttr + } + } + + /** + * @param {Node} elt + * @param {string} selector + * @returns {boolean} + */ + function matches(elt, selector) { + // @ts-ignore: non-standard properties for browser compatibility + // noinspection JSUnresolvedVariable + const matchesFunction = elt instanceof Element && (elt.matches || elt.matchesSelector || elt.msMatchesSelector || elt.mozMatchesSelector || elt.webkitMatchesSelector || elt.oMatchesSelector) + return !!matchesFunction && matchesFunction.call(elt, selector) + } + + /** + * @param {string} str + * @returns {string} + */ + function getStartTag(str) { + const tagMatcher = /<([a-z][^\/\0>\x20\t\r\n\f]*)/i + const match = tagMatcher.exec(str) + if (match) { + return match[1].toLowerCase() + } else { + return '' + } + } + + /** + * @param {string} resp + * @returns {Document} + */ + function parseHTML(resp) { + const parser = new DOMParser() + return parser.parseFromString(resp, 'text/html') + } + + /** + * @param {DocumentFragment} fragment + * @param {Node} elt + */ + function takeChildrenFor(fragment, elt) { + while (elt.childNodes.length > 0) { + fragment.append(elt.childNodes[0]) + } + } + + /** + * @param {HTMLScriptElement} script + * @returns {HTMLScriptElement} + */ + function duplicateScript(script) { + const newScript = getDocument().createElement('script') + forEach(script.attributes, function(attr) { + newScript.setAttribute(attr.name, attr.value) + }) + newScript.textContent = script.textContent + newScript.async = false + if (htmx.config.inlineScriptNonce) { + newScript.nonce = htmx.config.inlineScriptNonce + } + return newScript + } + + /** + * @param {HTMLScriptElement} script + * @returns {boolean} + */ + function isJavaScriptScriptNode(script) { + return script.matches('script') && (script.type === 'text/javascript' || script.type === 'module' || script.type === '') + } + + /** + * we have to make new copies of script tags that we are going to insert because + * SOME browsers (not saying who, but it involves an element and an animal) don't + * execute scripts created in