├── .prettierignore ├── .gitignore ├── bun.lockb ├── htmx-offline-app.png ├── public ├── images │ ├── logo.png │ ├── spinner.gif │ ├── logo-192.png │ └── logo-512.png ├── icons │ ├── apple-icon-180.png │ ├── apple-splash-1125-2436.jpg │ ├── apple-splash-1136-640.jpg │ ├── apple-splash-1170-2532.jpg │ ├── apple-splash-1179-2556.jpg │ ├── apple-splash-1242-2208.jpg │ ├── apple-splash-1242-2688.jpg │ ├── apple-splash-1284-2778.jpg │ ├── apple-splash-1290-2796.jpg │ ├── apple-splash-1334-750.jpg │ ├── apple-splash-1536-2048.jpg │ ├── apple-splash-1620-2160.jpg │ ├── apple-splash-1668-2224.jpg │ ├── apple-splash-1668-2388.jpg │ ├── apple-splash-1792-828.jpg │ ├── apple-splash-2048-1536.jpg │ ├── apple-splash-2048-2732.jpg │ ├── apple-splash-2160-1620.jpg │ ├── apple-splash-2208-1242.jpg │ ├── apple-splash-2224-1668.jpg │ ├── apple-splash-2388-1668.jpg │ ├── apple-splash-2436-1125.jpg │ ├── apple-splash-2532-1170.jpg │ ├── apple-splash-2556-1179.jpg │ ├── apple-splash-2688-1242.jpg │ ├── apple-splash-2732-2048.jpg │ ├── apple-splash-2778-1284.jpg │ ├── apple-splash-2796-1290.jpg │ ├── apple-splash-640-1136.jpg │ ├── apple-splash-750-1334.jpg │ ├── apple-splash-828-1792.jpg │ ├── manifest-icon-192.maskable.png │ └── manifest-icon-512.maskable.png ├── manifest.json ├── types.d.ts ├── sw-setup.js ├── styles.css ├── index.html ├── js2htmlstr.js ├── service-worker.js ├── dog-router.js ├── idb-easy.js ├── tiny-request-router.mjs └── htmx.min.js ├── .prettierrc ├── src └── server.tsx ├── tsconfig.json ├── package.json └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | public/*.min.js -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .env 4 | .DS_Store -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvolkmann/htmx-offline/HEAD/bun.lockb -------------------------------------------------------------------------------- /htmx-offline-app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvolkmann/htmx-offline/HEAD/htmx-offline-app.png -------------------------------------------------------------------------------- /public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvolkmann/htmx-offline/HEAD/public/images/logo.png -------------------------------------------------------------------------------- /public/images/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvolkmann/htmx-offline/HEAD/public/images/spinner.gif -------------------------------------------------------------------------------- /public/images/logo-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvolkmann/htmx-offline/HEAD/public/images/logo-192.png -------------------------------------------------------------------------------- /public/images/logo-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvolkmann/htmx-offline/HEAD/public/images/logo-512.png -------------------------------------------------------------------------------- /public/icons/apple-icon-180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvolkmann/htmx-offline/HEAD/public/icons/apple-icon-180.png -------------------------------------------------------------------------------- /public/icons/apple-splash-1125-2436.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvolkmann/htmx-offline/HEAD/public/icons/apple-splash-1125-2436.jpg -------------------------------------------------------------------------------- /public/icons/apple-splash-1136-640.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvolkmann/htmx-offline/HEAD/public/icons/apple-splash-1136-640.jpg -------------------------------------------------------------------------------- /public/icons/apple-splash-1170-2532.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvolkmann/htmx-offline/HEAD/public/icons/apple-splash-1170-2532.jpg -------------------------------------------------------------------------------- /public/icons/apple-splash-1179-2556.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvolkmann/htmx-offline/HEAD/public/icons/apple-splash-1179-2556.jpg -------------------------------------------------------------------------------- /public/icons/apple-splash-1242-2208.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvolkmann/htmx-offline/HEAD/public/icons/apple-splash-1242-2208.jpg -------------------------------------------------------------------------------- /public/icons/apple-splash-1242-2688.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvolkmann/htmx-offline/HEAD/public/icons/apple-splash-1242-2688.jpg -------------------------------------------------------------------------------- /public/icons/apple-splash-1284-2778.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvolkmann/htmx-offline/HEAD/public/icons/apple-splash-1284-2778.jpg -------------------------------------------------------------------------------- /public/icons/apple-splash-1290-2796.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvolkmann/htmx-offline/HEAD/public/icons/apple-splash-1290-2796.jpg -------------------------------------------------------------------------------- /public/icons/apple-splash-1334-750.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvolkmann/htmx-offline/HEAD/public/icons/apple-splash-1334-750.jpg -------------------------------------------------------------------------------- /public/icons/apple-splash-1536-2048.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvolkmann/htmx-offline/HEAD/public/icons/apple-splash-1536-2048.jpg -------------------------------------------------------------------------------- /public/icons/apple-splash-1620-2160.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvolkmann/htmx-offline/HEAD/public/icons/apple-splash-1620-2160.jpg -------------------------------------------------------------------------------- /public/icons/apple-splash-1668-2224.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvolkmann/htmx-offline/HEAD/public/icons/apple-splash-1668-2224.jpg -------------------------------------------------------------------------------- /public/icons/apple-splash-1668-2388.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvolkmann/htmx-offline/HEAD/public/icons/apple-splash-1668-2388.jpg -------------------------------------------------------------------------------- /public/icons/apple-splash-1792-828.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvolkmann/htmx-offline/HEAD/public/icons/apple-splash-1792-828.jpg -------------------------------------------------------------------------------- /public/icons/apple-splash-2048-1536.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvolkmann/htmx-offline/HEAD/public/icons/apple-splash-2048-1536.jpg -------------------------------------------------------------------------------- /public/icons/apple-splash-2048-2732.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvolkmann/htmx-offline/HEAD/public/icons/apple-splash-2048-2732.jpg -------------------------------------------------------------------------------- /public/icons/apple-splash-2160-1620.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvolkmann/htmx-offline/HEAD/public/icons/apple-splash-2160-1620.jpg -------------------------------------------------------------------------------- /public/icons/apple-splash-2208-1242.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvolkmann/htmx-offline/HEAD/public/icons/apple-splash-2208-1242.jpg -------------------------------------------------------------------------------- /public/icons/apple-splash-2224-1668.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvolkmann/htmx-offline/HEAD/public/icons/apple-splash-2224-1668.jpg -------------------------------------------------------------------------------- /public/icons/apple-splash-2388-1668.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvolkmann/htmx-offline/HEAD/public/icons/apple-splash-2388-1668.jpg -------------------------------------------------------------------------------- /public/icons/apple-splash-2436-1125.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvolkmann/htmx-offline/HEAD/public/icons/apple-splash-2436-1125.jpg -------------------------------------------------------------------------------- /public/icons/apple-splash-2532-1170.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvolkmann/htmx-offline/HEAD/public/icons/apple-splash-2532-1170.jpg -------------------------------------------------------------------------------- /public/icons/apple-splash-2556-1179.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvolkmann/htmx-offline/HEAD/public/icons/apple-splash-2556-1179.jpg -------------------------------------------------------------------------------- /public/icons/apple-splash-2688-1242.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvolkmann/htmx-offline/HEAD/public/icons/apple-splash-2688-1242.jpg -------------------------------------------------------------------------------- /public/icons/apple-splash-2732-2048.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvolkmann/htmx-offline/HEAD/public/icons/apple-splash-2732-2048.jpg -------------------------------------------------------------------------------- /public/icons/apple-splash-2778-1284.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvolkmann/htmx-offline/HEAD/public/icons/apple-splash-2778-1284.jpg -------------------------------------------------------------------------------- /public/icons/apple-splash-2796-1290.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvolkmann/htmx-offline/HEAD/public/icons/apple-splash-2796-1290.jpg -------------------------------------------------------------------------------- /public/icons/apple-splash-640-1136.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvolkmann/htmx-offline/HEAD/public/icons/apple-splash-640-1136.jpg -------------------------------------------------------------------------------- /public/icons/apple-splash-750-1334.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvolkmann/htmx-offline/HEAD/public/icons/apple-splash-750-1334.jpg -------------------------------------------------------------------------------- /public/icons/apple-splash-828-1792.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvolkmann/htmx-offline/HEAD/public/icons/apple-splash-828-1792.jpg -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": false, 4 | "singleQuote": true, 5 | "trailingComma": "none" 6 | } 7 | -------------------------------------------------------------------------------- /public/icons/manifest-icon-192.maskable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvolkmann/htmx-offline/HEAD/public/icons/manifest-icon-192.maskable.png -------------------------------------------------------------------------------- /public/icons/manifest-icon-512.maskable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvolkmann/htmx-offline/HEAD/public/icons/manifest-icon-512.maskable.png -------------------------------------------------------------------------------- /src/server.tsx: -------------------------------------------------------------------------------- 1 | import {Hono} from 'hono'; 2 | import {serveStatic} from 'hono/bun'; 3 | 4 | const app = new Hono(); 5 | 6 | // Serve static files from the public directory. 7 | app.use('/*', serveStatic({root: './public'})); 8 | 9 | export default app; 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "checkJs": true, 5 | "esModuleInterop": true, 6 | "jsx": "react-jsx", 7 | "jsxImportSource": "hono/jsx", 8 | "lib": ["esnext"], 9 | "module": "ESNext", 10 | "moduleResolution": "node", 11 | "noEmit": true, 12 | "resolveJsonModule": true, 13 | "skipLibCheck": true, 14 | "strict": true, 15 | "target": "ESNext" 16 | }, 17 | "include": ["./public", "./src"] 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "htmx-offline", 3 | "type": "module", 4 | "scripts": { 5 | "check": "tsc --noEmit", 6 | "dev": "bun run --watch src/server.tsx", 7 | "format": "prettier --write '**/*.{css,html,js,ts,tsx}'", 8 | "reinstall": "rm -rf node_modules bun.lockb && bun install" 9 | }, 10 | "dependencies": { 11 | "hono": "^3.12.7", 12 | "js2htmlstr": "^1.0.0", 13 | "tiny-request-router": "^1.2.2" 14 | }, 15 | "devDependencies": { 16 | "@types/bun": "^1.0.0", 17 | "typescript": "^5.3.3" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "PWA demo app that runs in a Cloudflare Worker", 3 | "name": "PWA demo app that runs in a Cloudflare Worker", 4 | "short_name": "PWA Demo", 5 | "start_url": "/", 6 | "background_color": "#0000ff", 7 | "display": "standalone", 8 | "icons": [ 9 | { 10 | "src": "icons/manifest-icon-192.maskable.png", 11 | "sizes": "192x192", 12 | "type": "image/png", 13 | "purpose": "any" 14 | }, 15 | { 16 | "src": "icons/manifest-icon-192.maskable.png", 17 | "sizes": "192x192", 18 | "type": "image/png", 19 | "purpose": "maskable" 20 | }, 21 | { 22 | "src": "icons/manifest-icon-512.maskable.png", 23 | "sizes": "512x512", 24 | "type": "image/png", 25 | "purpose": "any" 26 | }, 27 | { 28 | "src": "icons/manifest-icon-512.maskable.png", 29 | "sizes": "512x512", 30 | "type": "image/png", 31 | "purpose": "maskable" 32 | } 33 | ], 34 | "theme_color": "#ff0000" 35 | } 36 | -------------------------------------------------------------------------------- /public/types.d.ts: -------------------------------------------------------------------------------- 1 | export declare type Attributes = Object; 2 | 3 | export declare type Child = string | number; 4 | 5 | export declare type ContentFn = ( 6 | attrs: Attributes | Children, 7 | children?: Children 8 | ) => string; 9 | 10 | export declare type SelfClosingFn = (attrs?: Attributes) => string; 11 | 12 | export declare type Dog = { 13 | id: number; 14 | name: string; 15 | breed: string; 16 | }; 17 | 18 | export declare type StringToAny = {[key: string]: any}; 19 | 20 | export declare type StringToString = {[key: string]: string}; 21 | 22 | type RouteCallback = ( 23 | params: StringToAny, 24 | request: Request 25 | ) => Promise; 26 | 27 | type RouteHandler = ( 28 | path: string, 29 | handler: RouteCallback, 30 | options?: StringToAny 31 | ) => void; 32 | 33 | export declare type RouteMatch = { 34 | handler: RouteCallback; 35 | params: StringToAny; 36 | }; 37 | 38 | type RouterMatchFunction = ( 39 | method: string, 40 | pathname: string 41 | ) => RouteMatch | undefined; 42 | 43 | export declare type MyRouter = { 44 | delete: RouteHandler; 45 | get: RouteHandler; 46 | match: RouterMatchFunction; 47 | patch: RouteHandler; 48 | post: RouteHandler; 49 | put: RouteHandler; 50 | }; 51 | -------------------------------------------------------------------------------- /public/sw-setup.js: -------------------------------------------------------------------------------- 1 | // All modern browsers support service workers. 2 | if ('serviceWorker' in navigator) { 3 | try { 4 | await navigator.serviceWorker.register('service-worker.js', { 5 | type: 'module' 6 | }); 7 | } catch (error) { 8 | console.error('sw-setup.js registerServiceWorker:', error); 9 | } 10 | } else { 11 | console.error('Your browser does not support service workers'); 12 | } 13 | 14 | // Register to receive messages from the service worker. 15 | // These are sent with "client.postMessage" in the service worker. 16 | // They are not push notifications. 17 | navigator.serviceWorker.addEventListener('message', event => { 18 | const message = event.data; 19 | if (message === 'ready') { 20 | // Determine if a service worker is already controlling this page. 21 | const haveServiceWorker = Boolean(navigator.serviceWorker.controller); 22 | // If not then we must have just installed a new service worker. 23 | if (!haveServiceWorker) { 24 | // Give the new service worker time to really be ready. 25 | // Then reload the page so a GET to /dog will work. 26 | // This is needed to populate the table of dogs. 27 | setTimeout(() => { 28 | location.reload(); 29 | }, 100); 30 | } 31 | } 32 | }); 33 | 34 | // Including this line makes this a module, 35 | // which is needed to use "await" in the top-level code. 36 | export {}; 37 | -------------------------------------------------------------------------------- /public/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: cornflowerblue; 3 | font-family: sans-serif; 4 | } 5 | 6 | button { 7 | background-color: lightgreen; 8 | border: none; 9 | border-radius: 0.5rem; 10 | padding: 0.5rem; 11 | 12 | &:disabled { 13 | background-color: gray; 14 | } 15 | } 16 | 17 | .buttons { 18 | display: flex; 19 | gap: 1rem; 20 | 21 | background-color: transparent; 22 | } 23 | 24 | h1 { 25 | color: orange; 26 | } 27 | 28 | .htmx-indicator { 29 | height: 1.5rem; 30 | margin-top: 0.5rem; 31 | width: 1.5rem; 32 | } 33 | 34 | input { 35 | background-color: white; 36 | border: none; 37 | border-radius: 0.5rem; 38 | margin-bottom: 1rem; 39 | padding: 0.5rem; 40 | } 41 | 42 | label { 43 | display: inline-block; 44 | font-weight: bold; 45 | margin-right: 0.5rem; 46 | text-align: right; 47 | width: 3rem; 48 | } 49 | 50 | .show-on-hover { 51 | transform: scale(2.5) translate(0.2rem, 0); 52 | visibility: hidden; 53 | } 54 | 55 | .on-hover:hover .show-on-hover { 56 | visibility: visible; 57 | } 58 | 59 | table { 60 | border-collapse: collapse; 61 | margin-top: 1rem; 62 | } 63 | 64 | td, 65 | th { 66 | border: 1px solid cornflowerblue; 67 | padding: 0.5rem; 68 | } 69 | 70 | td { 71 | background-color: white; 72 | 73 | & button { 74 | background-color: transparent; 75 | color: white; 76 | } 77 | } 78 | 79 | th { 80 | background-color: orange; 81 | } 82 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | htmx Offline 5 | 6 | 7 | 9 | 17 | 18 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |

Dogs

31 | 32 |
33 | 34 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 |
NameBreed
45 | 46 | 47 | -------------------------------------------------------------------------------- /public/js2htmlstr.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This defines functions that make it easy to 3 | * generate strings of HTML from JavaScript. 4 | */ 5 | 6 | /** @typedef {import('./types.js').Attributes} Attributes } */ 7 | /** @typedef {import('./types.js').Child} Child } */ 8 | /** @typedef {import('./types.js').ContentFn} ContentFn } */ 9 | /** @typedef {import('./types.js').SelfClosingFn} SelfClosingFn } */ 10 | 11 | /** 12 | * Generates an HTML string for an element with a close tag. 13 | * @param {string} name 14 | * @param {Attributes | Child[]} [attrs] 15 | * @param {Child[]} children 16 | * @returns string - the HTML 17 | */ 18 | export function el(name, attrs, ...children) { 19 | // Begin the opening tag. 20 | /** @type {string} */ 21 | let html = '<' + name; 22 | 23 | if (typeof attrs === 'object' && !Array.isArray(attrs)) { 24 | // Add attributes to the opening tag. 25 | for (const key of Object.keys(attrs).sort()) { 26 | html += ` ${key}="${attrs[key]}"`; 27 | } 28 | } else { 29 | // Assume attrs holds the first child. 30 | const child = /** @type {Child} */ (/** @type {unknown} */ (attrs)); 31 | children.unshift(child); 32 | } 33 | 34 | // Close the opening tag. 35 | html += '>'; 36 | 37 | // Add child elements. 38 | for (const child of children) { 39 | html += child; 40 | } 41 | 42 | // Add the closing tag. 43 | html += ``; 44 | 45 | return html; 46 | } 47 | 48 | /** 49 | * Generates an HTML string for a self-closing element. 50 | * @param {string} name 51 | * @param {Attributes} [attrs] 52 | * @returns string - the HTML 53 | */ 54 | export function elc(name, attrs) { 55 | // Begin the tag. 56 | /** @type {string} */ 57 | let html = '<' + name; 58 | 59 | if (typeof attrs === 'object' && !Array.isArray(attrs)) { 60 | // Add attributes to the opening tag. 61 | for (const key of Object.keys(attrs).sort()) { 62 | html += ` ${key}="${attrs[key]}"`; 63 | } 64 | } 65 | 66 | // Close the tag. 67 | html += ' />'; 68 | 69 | return html; 70 | } 71 | 72 | const contentElements = [ 73 | 'a', 74 | 'body', 75 | 'button', 76 | 'div', 77 | 'form', 78 | 'head', 79 | 'html', 80 | 'label', 81 | 'li', 82 | 'ol', 83 | 'option', 84 | 'p', 85 | 'script', 86 | 'section', 87 | 'select', 88 | 'span', 89 | 'table', 90 | 'tbody', 91 | 'td', 92 | 'tfoot', 93 | 'th', 94 | 'thead', 95 | 'tr', 96 | 'ul' 97 | ]; 98 | const selfClosingElements = ['br', 'hr', 'img', 'input', 'link', 'meta']; 99 | 100 | /** @type {{[name: string]: (ContentFn | SelfClosingFn)}} */ 101 | const elements = {}; 102 | 103 | for (const name of contentElements) { 104 | elements[name] = /** @type {ContentFn} */ ( 105 | (attrs, ...children) => el(name, attrs, ...children) 106 | ); 107 | } 108 | 109 | for (const name of selfClosingElements) { 110 | elements[name] = /** @type {SelfClosingFn} */ (attrs => elc(name, attrs)); 111 | } 112 | 113 | export default elements; 114 | -------------------------------------------------------------------------------- /public/service-worker.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /** @typedef {import('./types.d.ts').RouteMatch} RouteMatch */ 4 | /** @typedef {import('./types.d.ts').StringToAny} StringToAny */ 5 | 6 | // This is tedious, but necessary to force type assertion. 7 | const self = /** @type {ServiceWorkerGlobalScope} */ ( 8 | /** @type {unknown} */ (globalThis.self) 9 | ); 10 | 11 | import {getRouteMatch} from './dog-router.js'; 12 | 13 | const cacheName = 'pwa-demo-v1'; 14 | 15 | // We aren't currently caching .css files and certain .js files 16 | // because we want changes to be reflected without clearing the cache. 17 | const fileExtensionsToCache = ['jpg', 'js', 'json', 'png', 'webp']; 18 | 19 | /** 20 | * This attempts to get a resource from the cache. 21 | * If it is not found in the cache, it is retrieved from the network. 22 | * If it is a kind of resource we want to cache, it is added to the cache. 23 | * @param {Request} request 24 | * @returns {Promise} that contains the resource 25 | */ 26 | async function getResource(request) { 27 | const debug = false; // set to true for debugging 28 | const url = new URL(request.url); 29 | const {href, pathname} = url; 30 | 31 | // Attempt to get the resource from the cache. 32 | /** @type {Response | undefined} */ 33 | let resource = await caches.match(request); 34 | 35 | if (resource) { 36 | if (debug) console.debug('service worker got', href, 'from cache'); 37 | } else { 38 | try { 39 | // Get the resource from the network. 40 | resource = await fetch(request); 41 | if (debug) console.debug('service worker got', href, 'from network'); 42 | 43 | if (shouldCache(pathname)) { 44 | // Save in the cache to avoid unnecessary future network requests 45 | // and supports offline use. 46 | const cache = await caches.open(cacheName); 47 | await cache.add(url); 48 | if (debug) console.debug('service worker cached', href); 49 | } 50 | } catch (error) { 51 | console.error('service-worker.js getResource:', error); 52 | console.error('service worker failed to fetch', url); 53 | resource = new Response('', {status: 404}); 54 | } 55 | } 56 | 57 | return resource; 58 | } 59 | 60 | /** 61 | * This determines whether the file at a given path should be cached 62 | * based on its file extension. 63 | * @param {string} pathname 64 | * @returns {boolean} true to cache; false otherwise 65 | */ 66 | function shouldCache(pathname) { 67 | if (pathname.endsWith('service-worker.js')) return false; 68 | if (pathname.endsWith('sw-setup.js')) return false; 69 | const index = pathname.lastIndexOf('.'); 70 | const extension = index === -1 ? '' : pathname.substring(index + 1); 71 | return fileExtensionsToCache.includes(extension); 72 | } 73 | 74 | /** 75 | * This registers a listener for the "install" event of this service worker. 76 | */ 77 | addEventListener('install', event => { 78 | console.info('service-worker.js: installing'); 79 | // This allows existing browser tabs to use an 80 | // updated version of this service worker. 81 | self.skipWaiting(); 82 | }); 83 | 84 | /** 85 | * This registers a listener for the "activate" event of this service worker. 86 | */ 87 | addEventListener('activate', async event => { 88 | console.info('service-worker.js: activating'); 89 | 90 | try { 91 | // Let browser clients know that the service worker is ready. 92 | const matches = await self.clients.matchAll({includeUncontrolled: true}); 93 | for (const client of matches) { 94 | // setup.js listens for this message. 95 | client.postMessage('ready'); 96 | } 97 | } catch (error) { 98 | console.error('service-worker.js:', error); 99 | } 100 | }); 101 | 102 | /** 103 | * @callback RouteCallback 104 | * @param {StringToAny} [params] 105 | * @param {Request} [request] 106 | * @returns {Promise} 107 | */ 108 | 109 | /** 110 | * This registers a listener for the "fetch" event of this service worker. 111 | * It responds with a resource for accessing data at a requested URL. 112 | */ 113 | addEventListener('fetch', async event => { 114 | const {request} = event; 115 | const url = new URL(request.url); 116 | const {pathname} = url; 117 | 118 | const match = /** @type {RouteMatch} */ ( 119 | getRouteMatch(request.method, pathname) 120 | ); 121 | const promise = /** @type {Promise} */ ( 122 | match ? match.handler(match.params, request) : getResource(request) 123 | ); 124 | event.respondWith(promise); 125 | }); 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # htmx-offline 2 | 3 | ## Overview 4 | 5 | It is a widely held belief that htmx cannot be used for apps that require 6 | offline functionality. This is a reasonable assumption given that htmx 7 | is all about sending HTTP requests. However, a service worker can intercept 8 | HTTP requests and process them. The processing could include interacting 9 | with an IndexedDB database. 10 | For some web applications, this enables all the functionality to work offline. 11 | 12 | This repository implements a Progressive Web App (PWA) 13 | that demonstrates the approach described above. 14 | It displays descriptions of dogs in a table. 15 | Dogs can be added, modified, and deleted. 16 | 17 | ![app screenshot](htmx-offline-app.png) 18 | 19 | To modify a dog, hover over its table row and click the pencil icon that appears to the right of the row. 20 | This will populate the form fields at the top and change the "Add" button to "Update". 21 | Modify the input values and click the "Update" button to save the changes. 22 | 23 | To delete a dog, hover over its table row and click the "X" that appears to the right of the row. 24 | This will prompt for confirmation before deleting the dog. 25 | 26 | ## Steps to Run 27 | 28 | To run the app locally: 29 | 30 | - Install [Bun](https://bun.sh) if not already installed. 31 | - Enter `bun install` 32 | - Enter `bun dev` 33 | - Browse localhost:3000 34 | 35 | To reset the state of the application in Chrome, 36 | including unregistering the service worker, 37 | clearing the IndexedDB database, and clearing the cache: 38 | 39 | - Open the DevTools. 40 | - Click the "Application" tab. 41 | - Click "Storage" in the left nav. 42 | - Click the "Clear site data" button. 43 | 44 | ## Limitations 45 | 46 | One of the benefits of htmx is that it enables using any programming language 47 | to implement endpoints that return snippets of HTML. 48 | This benefit is lost when a service worker is used to process HTTP requests. 49 | The reason is that service workers must be implemented in JavaScript. 50 | 51 | ## Endpoint Routes 52 | 53 | The file [public/service-worker.js](/public/service-worker.js) 54 | adds an event listener that intercepts all "fetch" events. 55 | This includes all HTTP requests sent by htmx attributes. 56 | 57 | The function `getRouteMatch` defined at the bottom of 58 | [public/dog-router.js](/public/dog-router.js) 59 | returns either a handler function or `undefined`. 60 | When a handler function is returned, 61 | the request is handled by the service worker. 62 | When `undefined` is returned, the requests is forwarded to the network. 63 | 64 | The function `getResource` defined near the top of 65 | [public/service-worker.js](/public/service-worker.js) 66 | is called to handle requests to the network. 67 | This checks the cache for a previously cached response. 68 | If one is found, that is returned. 69 | Otherwise, a network request is sent, the response is cached, 70 | and the response is returned. 71 | 72 | For requests handled by the service worker, the small library (15 KB) 73 | [tiny-request-router](https://github.com/berstend/tiny-request-router) is used. 74 | This associates HTTP verbs and URL paths 75 | with functions that handle matching requests. 76 | The file [public/dog-router.js](/public/dog-router.js) 77 | defines all these endpoints. 78 | 79 | ## HTML Generation 80 | 81 | There are many approaches that endpoints can use to generate HTML. 82 | Each programming language tends to support 83 | templating libraries that are unique to the language. 84 | 85 | Some JavaScript run-times such as Bun support using JSX to generate HTML. 86 | Unfortunately we can't use JSX in a service worker because 87 | that relies on the browser JavaScript run-time and not Bun. 88 | 89 | This application uses a custom library for HTML generation 90 | that is very small (3 KB) and quite easy to use. 91 | See [https://www.npmjs.com/package/js2htmlstr](js2htmlstr) 92 | I copied the file `node_modules/js2htmlstr/src/js2htmlstr.js` 93 | to the `public` directory so I could use it in a service worker. 94 | I also copied the types from `node_modules/js2htmlstr/src/types.d.ts` 95 | into `public/types.d.ts`. 96 | 97 | This provides a function for each supported HTML element. 98 | These functions can be passed an optional object describing HTML attributes 99 | and the element content. 100 | 101 | For example, suppose we want to generate the following HTML string. 102 | 103 | ```html 104 |

Hello, World!\

105 | ``` 106 | 107 | The `p` function can be used as follows to do this. 108 | 109 | ```js 110 | const html = p({id: 'p1', class: 'greet'}, 'Hello, World!'); 111 | ``` 112 | 113 | ## Persistence 114 | 115 | All modern web browsers support using 116 | [IndexedDB](https://mvolkmann.github.io/blog/topics/#/blog/indexeddb/) 117 | to persist data locally. 118 | The IndexedDB API is a bit tedious to use. 119 | This application uses a custom library for interacting with IndexedDB databases 120 | that is small (10 KB) and easier to use. 121 | See [idb-easy.js](/public/idb-easy.js). 122 | 123 | ## Type Checking 124 | 125 | One goal of this application is to avoid having a build step. 126 | This precludes the use of TypeScript. 127 | However, we can still get type checking by using JSDoc comments. 128 | 129 | Type issues are flagged in code editors like VS Code. 130 | 131 | Type errors can be reported by running the command `tsx --noEmit`. 132 | The [package.json](/package.json) file defines the script "check" 133 | which can be run by entering `bun check`. 134 | 135 | Most of the custom types used in this application 136 | are defined in the file [public/types.d.ts](/public/types.d.ts). 137 | 138 | There are several places in the code that require type casting. 139 | A JSDoc typecast always has the form 140 | 141 | ```text 142 | /** @type {some-type} */ (some-expression); 143 | ``` 144 | 145 | For example, the following code queries an IndexedDB database collection 146 | named "dogs" and obtains an array of objects. 147 | A typecast is necessary to inform TypeScript 148 | that it is actually an array of `Dog` objects. 149 | 150 | ```js 151 | const dogs = /** @type {Dog[]} */ (await idbEasy.getAllRecords('dogs')); 152 | ``` 153 | -------------------------------------------------------------------------------- /public/dog-router.js: -------------------------------------------------------------------------------- 1 | // This file defines the API routes that the service worker will handle. 2 | // These are not implemented by a real HTTP server. 3 | 4 | import elements from './js2htmlstr.js'; 5 | const {button, div, form, input, label, td, tr} = elements; 6 | import IDBEasy from './idb-easy.js'; 7 | import {Router} from './tiny-request-router.mjs'; 8 | 9 | /** @typedef {import('./types.d.ts').Dog} Dog } */ 10 | /** @typedef {import('./types.d.ts').MyRouter} MyRouter */ 11 | /** @typedef {import('./types.d.ts').RouteMatch} RouteMatch */ 12 | /** @typedef {import('./types.d.ts').StringToString} StringToString */ 13 | 14 | // These are for an IndexedDB store. 15 | const dbName = 'myDB'; 16 | const storeName = 'dogs'; 17 | const version = 1; 18 | 19 | /** @type {IDBEasy} */ 20 | let idbEasy; // set in setupDB 21 | 22 | let selectedId = 0; 23 | 24 | setupDB(); 25 | 26 | /** 27 | * Converts a Dog object to an HTML string. 28 | * @param {Dog} dog 29 | * @param {boolean} updating 30 | * @returns 31 | */ 32 | function dogToTableRow(dog, updating = false) { 33 | const {breed, id, name} = dog; 34 | 35 | /** @type {StringToString} */ 36 | const attrs = { 37 | class: 'on-hover', 38 | id: `row-${id}` 39 | }; 40 | if (updating) attrs['hx-swap-oob'] = 'true'; 41 | 42 | return tr( 43 | attrs, 44 | td(name), 45 | td(breed), 46 | td( 47 | {class: 'buttons'}, 48 | button( 49 | { 50 | class: 'show-on-hover', 51 | 'hx-confirm': 'Are you sure?', 52 | 'hx-delete': `/dog/${id}`, 53 | 'hx-target': 'closest tr', 54 | 'hx-swap': 'outerHTML', 55 | type: 'button' 56 | }, 57 | '✕' 58 | ), 59 | // This selects the dog which triggers a selection-change event 60 | // which causes the form to update. 61 | button( 62 | { 63 | class: 'show-on-hover', 64 | 'hx-get': '/select/' + dog.id, 65 | 'hx-swap': 'none', 66 | type: 'button' 67 | }, 68 | '✎' 69 | ) 70 | ) 71 | ); 72 | } 73 | 74 | /** 75 | * This creates a Dog object from the FormData in a Request. 76 | * @param {Request} request 77 | * @returns {Promise} 78 | */ 79 | async function requestToDog(request) { 80 | const formData = await request.formData(); 81 | // @ts-ignore 82 | return /** @type {Dog} */ (Object.fromEntries(formData)); 83 | } 84 | 85 | /** 86 | * This initializes the dogs store with sample data if it is currently empty. 87 | * @param {IDBTransaction} [txn] 88 | * @returns {Promise} 89 | */ 90 | async function initializeDB(txn) { 91 | try { 92 | // Determine if the store is empty. 93 | const count = await idbEasy.getRecordCount(storeName, txn); 94 | if (count > 0) return; 95 | 96 | await idbEasy.createRecord( 97 | storeName, 98 | {name: 'Comet', breed: 'Whippet'}, 99 | txn 100 | ); 101 | await idbEasy.createRecord( 102 | storeName, 103 | { 104 | name: 'Oscar', 105 | breed: 'German Shorthaired Pointer' 106 | }, 107 | txn 108 | ); 109 | 110 | // This code is only here to provide an example of 111 | // updating an existing record using the upsertRecord method. 112 | const dogs = /** @type {Dog[]} */ ( 113 | await idbEasy.getAllRecords(storeName, txn) 114 | ); 115 | const comet = dogs.find(dog => dog.name === 'Comet'); 116 | if (comet) { 117 | comet.name = 'Fireball'; 118 | await idbEasy.upsertRecord(storeName, comet, txn); 119 | } 120 | 121 | // This code is only here to provide an example of 122 | // adding a new record using the upsertRecord method. 123 | await idbEasy.upsertRecord( 124 | storeName, 125 | { 126 | name: 'Clarice', 127 | breed: 'Whippet' 128 | }, 129 | txn 130 | ); 131 | } catch (error) { 132 | console.error('dogs.js initialize: error =', error); 133 | } 134 | } 135 | 136 | /** 137 | * This sets the idbEasy variable that is used to 138 | * simplify interacting with an IndexedDB database. 139 | * I tried for a couple of hours to simplify this code 140 | * and couldn't arrive at an alternative that works. 141 | */ 142 | function setupDB() { 143 | const promise = IDBEasy.openDB(dbName, version, (db, event) => { 144 | // const {newVersion, oldVersion} = event; 145 | 146 | idbEasy = new IDBEasy(db); 147 | 148 | // If the "dogs" store already exists, delete it. 149 | const request = /** @type {IDBOpenDBRequest} */ (event.target); 150 | const txn = request.transaction; 151 | if (txn) { 152 | const names = Array.from(txn.objectStoreNames); 153 | if (names.includes(storeName)) idbEasy.deleteStore(storeName); 154 | } 155 | 156 | // Create the "dogs" store and its indexes. 157 | const store = idbEasy.createStore(storeName, 'id', true); 158 | idbEasy.createIndex(store, 'breed-index', 'breed'); 159 | idbEasy.createIndex(store, 'name-index', 'name'); 160 | 161 | initializeDB(txn ?? undefined); 162 | }); 163 | 164 | // Top-level await is not allowed in service workers. 165 | promise.then(upgradedDB => { 166 | idbEasy = new IDBEasy(upgradedDB); 167 | }); 168 | } 169 | 170 | //----------------------------------------------------------------------------- 171 | 172 | const router = /** @type {MyRouter} */ (new Router()); 173 | 174 | // This deletes the dog with a given id. 175 | router.delete('/dog/:id', async params => { 176 | // params will always be present here since the route has a parameter. 177 | const id = params ? Number(params['id']) : 0; 178 | await idbEasy.deleteRecordByKey('dogs', id); 179 | return new Response(''); 180 | }); 181 | 182 | // This deselects the currently selected dog. 183 | router.get('/deselect', async () => { 184 | selectedId = 0; 185 | return new Response('', {headers: {'HX-Trigger': 'selection-change'}}); 186 | }); 187 | 188 | // This gets an HTML form that is used to add and update dogs. 189 | router.get('/form', async () => { 190 | const selectedDog = /** @type {Dog | undefined} */ ( 191 | await idbEasy.getRecordByKey('dogs', selectedId) 192 | ); 193 | 194 | /** @type {StringToString}} */ 195 | const attrs = { 196 | 'hx-on:htmx:after-request': 'this.reset()' 197 | }; 198 | 199 | if (selectedId) { 200 | // Update an existing row. 201 | attrs['hx-put'] = '/dog/' + selectedId; 202 | } else { 203 | // Add a new row. 204 | attrs['hx-post'] = '/dog'; 205 | attrs['hx-target'] = 'tbody'; 206 | attrs['hx-swap'] = 'afterbegin'; 207 | } 208 | 209 | const buttons = [button({id: 'submit-btn'}, selectedId ? 'Update' : 'Add')]; 210 | if (selectedId) { 211 | buttons.push( 212 | button( 213 | {'hx-get': '/deselect', 'hx-swap': 'none', type: 'button'}, 214 | 'Cancel' 215 | ) 216 | ); 217 | } 218 | 219 | const html = form( 220 | {'hx-disabled-elt': '#submit-btn', ...attrs}, 221 | div( 222 | label({for: 'name'}, 'Name'), 223 | input({ 224 | id: 'name', 225 | name: 'name', 226 | required: true, 227 | size: 30, 228 | type: 'text', 229 | value: selectedDog?.name ?? '' 230 | }) 231 | ), 232 | div( 233 | label({for: 'breed'}, 'Breed'), 234 | input({ 235 | id: 'breed', 236 | name: 'breed', 237 | required: true, 238 | size: 30, 239 | type: 'text', 240 | value: selectedDog?.breed ?? '' 241 | }) 242 | ), 243 | div({class: 'buttons'}, ...buttons) 244 | ); 245 | 246 | return new Response(html, { 247 | headers: {'Content-Type': 'application/html'} 248 | }); 249 | }); 250 | 251 | // This gets table rows for all the dogs. 252 | router.get('/rows', async () => { 253 | const dogs = /** @type {Dog[]} */ (await idbEasy.getAllRecords('dogs')); 254 | const sortedDogs = Array.from(dogs.values()).sort((a, b) => 255 | a.name.localeCompare(b.name) 256 | ); 257 | const html = sortedDogs.map(dog => dogToTableRow(dog)).join(''); 258 | return new Response(html, { 259 | headers: {'Content-Type': 'application/html'} 260 | }); 261 | }); 262 | 263 | // This selects a dog with a given id. 264 | 265 | router.get('/select/:id', async params => { 266 | // params will always be present here since the route has a parameter. 267 | selectedId = params ? Number(params['id']) : 0; 268 | return new Response('', {headers: {'HX-Trigger': 'selection-change'}}); 269 | }); 270 | 271 | // This creates a new dog. 272 | router.post('/dog', async (params, request) => { 273 | const dog = await requestToDog(request); 274 | const id = await idbEasy.createRecord('dogs', dog); 275 | dog.id = Number(id); 276 | const html = dogToTableRow(dog); 277 | return new Response(html, { 278 | headers: {'Content-Type': 'application/html'} 279 | }); 280 | }); 281 | 282 | // This updates an existing dog. 283 | router.put('/dog/:id', async (params, request) => { 284 | const dog = await requestToDog(request); 285 | dog.id = Number(params['id']); 286 | 287 | selectedId = 0; 288 | 289 | await idbEasy.upsertRecord('dogs', dog); 290 | const html = dogToTableRow(dog, true); 291 | return new Response(html, { 292 | headers: { 293 | 'Content-Type': 'application/html', 294 | 'HX-Trigger': 'selection-change' 295 | } 296 | }); 297 | }); 298 | 299 | /** 300 | * This function is used by the "fetch" handler in service-worker.js. 301 | * @param {string} method 302 | * @param {string} pathname 303 | * @returns {RouteMatch | undefined} 304 | */ 305 | export function getRouteMatch(method, pathname) { 306 | return router.match(method, pathname); 307 | } 308 | -------------------------------------------------------------------------------- /public/idb-easy.js: -------------------------------------------------------------------------------- 1 | /** @typedef {import('./types.d.ts').StringToString} StringToString */ 2 | 3 | /** 4 | * This returns a Promise that resolves to the result of a given request. 5 | * @param {IDBRequest} request 6 | * @param {string} action - for debugging 7 | * @param {boolean} [suppliedTxn] - indicates whether a transaction was supplied 8 | * @returns {Promise} 9 | */ 10 | function requestToPromise(request, action, suppliedTxn = false) { 11 | return new Promise((resolve, reject) => { 12 | request.onsuccess = event => { 13 | // console.log('succeeded to', action); 14 | if (!suppliedTxn) request.transaction?.commit(); 15 | resolve(request.result); 16 | }; 17 | request.onerror = event => { 18 | console.error('failed to', action); 19 | request.transaction?.abort(); 20 | reject(event); 21 | }; 22 | }); 23 | } 24 | 25 | /** 26 | * Instances of this class make it easier to work with IndexedDB. 27 | */ 28 | export default class IDBEasy { 29 | /** 30 | * This creates an instance of IDBEasy. 31 | * @param {IDBDatabase} db 32 | */ 33 | constructor(db) { 34 | this.db = db; 35 | } 36 | 37 | /** 38 | * This deletes all the records in a given store. 39 | * @param {string} storeName 40 | * @param {IDBTransaction} [txn] 41 | * @returns {Promise} 42 | */ 43 | clearStore(storeName, txn) { 44 | // TODO: Apply this same approach all over! 45 | const definiteTxn = this.ensureTxn(txn, storeName, 'readwrite'); 46 | const store = definiteTxn.objectStore(storeName); 47 | const request = store.clear(); 48 | return requestToPromise(request, 'clear store', Boolean(txn)); 49 | } 50 | 51 | /** 52 | * This creates an index on a given store. 53 | * @param {IDBObjectStore} store 54 | * @param {string} indexName 55 | * @param {string} keyPath 56 | * @param {boolean} [unique] 57 | * @returns {void} 58 | */ 59 | createIndex(store, indexName, keyPath, unique = false) { 60 | store.createIndex(indexName, keyPath, {unique}); 61 | } 62 | 63 | /** 64 | * This creates a record in a given store. 65 | * @param {string} storeName 66 | * @param {object} object 67 | * @param {IDBTransaction} [txn] 68 | * @returns {Promise} key of new record 69 | */ 70 | createRecord(storeName, object, txn) { 71 | const definiteTxn = this.ensureTxn(txn, storeName, 'readwrite'); 72 | const store = definiteTxn.objectStore(storeName); 73 | const request = store.add(object); 74 | return requestToPromise(request, 'create record', Boolean(txn)); 75 | } 76 | 77 | /** 78 | * This creates a store in the current database. 79 | * It must be called within a "versionchange" transaction. 80 | * @param {string} storeName 81 | * @param {string} keyPath 82 | * @param {boolean} [autoIncrement] 83 | * @returns {IDBObjectStore} 84 | */ 85 | createStore(storeName, keyPath, autoIncrement = false) { 86 | return this.db.createObjectStore(storeName, {autoIncrement, keyPath}); 87 | } 88 | 89 | /** 90 | * This deletes a given database 91 | * @param {string} dbName 92 | * @returns {Promise} 93 | */ 94 | static deleteDB(dbName) { 95 | const request = indexedDB.deleteDatabase(dbName); 96 | return requestToPromise(request, 'delete database'); 97 | } 98 | 99 | /** 100 | * This deletes all the records in a given store 101 | * that have a given value in a given index. 102 | * @param {string} storeName 103 | * @param {string} indexName 104 | * @param {any} indexValue 105 | * @param {IDBTransaction} [txn] 106 | * @returns {Promise} 107 | */ 108 | deleteRecordsByIndex(storeName, indexName, indexValue, txn) { 109 | const definiteTxn = this.ensureTxn(txn, storeName, 'readwrite'); 110 | return new Promise((resolve, reject) => { 111 | const store = definiteTxn.objectStore(storeName); 112 | const keyPath = Array.isArray(store.keyPath) 113 | ? store.keyPath[0] 114 | : store.keyPath; 115 | const index = store.index(indexName); 116 | const request = index.getAll(indexValue); 117 | request.onsuccess = event => { 118 | const request = /** @type {IDBRequest} */ (event.target); 119 | const records = request.result ?? []; 120 | for (const record of records) { 121 | store.delete(record[keyPath]); 122 | } 123 | if (!txn) definiteTxn.commit(); 124 | resolve(); 125 | }; 126 | request.onerror = event => { 127 | console.error('failed to delete records by index'); 128 | definiteTxn.abort(); 129 | reject(event); 130 | }; 131 | }); 132 | } 133 | 134 | /** 135 | * This delete the record in a given store 136 | * that has a given key value. 137 | * @param {string} storeName 138 | * @param {any} key 139 | * @param {IDBTransaction} [txn] 140 | * @returns {Promise} 141 | */ 142 | deleteRecordByKey(storeName, key, txn) { 143 | const definiteTxn = this.ensureTxn(txn, storeName, 'readwrite'); 144 | const store = definiteTxn.objectStore(storeName); 145 | const request = store.delete(key); 146 | return requestToPromise(request, 'delete dog', Boolean(txn)); 147 | } 148 | 149 | /** 150 | * This deletes a given store. 151 | * @param {string} storeName 152 | * @returns {void} 153 | */ 154 | deleteStore(storeName) { 155 | this.db.deleteObjectStore(storeName); 156 | } 157 | 158 | /** 159 | * @param {IDBTransaction | undefined} txn 160 | * @param {string} storeName 161 | * @param {IDBTransactionMode} mode 162 | * @returns {IDBTransaction} 163 | */ 164 | ensureTxn(txn, storeName, mode) { 165 | if (!txn) txn = this.db.transaction(storeName, mode); 166 | return /** @type {IDBTransaction} */ (txn); 167 | } 168 | 169 | /** 170 | * This gets all the record in a given store. 171 | * @param {string} storeName 172 | * @param {IDBTransaction} [txn] 173 | * @returns {Promise} 174 | */ 175 | getAllRecords(storeName, txn) { 176 | const definiteTxn = this.ensureTxn(txn, storeName, 'readonly'); 177 | const store = definiteTxn.objectStore(storeName); 178 | const request = store.getAll(); 179 | return requestToPromise(request, 'get all records', Boolean(txn)); 180 | } 181 | 182 | /** 183 | * This gets the record in a given store with a given key value. 184 | * @param {string} storeName 185 | * @param {any} key 186 | * @param {IDBTransaction} [txn] 187 | * @returns {Promise} 188 | */ 189 | getRecordByKey(storeName, key, txn) { 190 | const definiteTxn = this.ensureTxn(txn, storeName, 'readonly'); 191 | const store = definiteTxn.objectStore(storeName); 192 | const request = store.get(key); 193 | return requestToPromise(request, 'get record by key', Boolean(txn)); 194 | } 195 | 196 | /** 197 | * This gets the number of records in a given store. 198 | * @param {string} storeName 199 | * @param {IDBTransaction} [txn] 200 | * @returns {Promise} 201 | */ 202 | getRecordCount(storeName, txn) { 203 | const definiteTxn = this.ensureTxn(txn, storeName, 'readonly'); 204 | const store = definiteTxn.objectStore(storeName); 205 | const request = store.count(); 206 | return requestToPromise(request, 'get record count', Boolean(txn)); 207 | } 208 | 209 | /** 210 | * This gets all the records in a given store 211 | * that have a given value in a given index. 212 | * @param {string} storeName 213 | * @param {string} indexName 214 | * @param {any} indexValue 215 | * @param {IDBTransaction} [txn] 216 | * @returns {Promise} 217 | */ 218 | getRecordsByIndex(storeName, indexName, indexValue, txn) { 219 | const definiteTxn = this.ensureTxn(txn, storeName, 'readonly'); 220 | const store = definiteTxn.objectStore(storeName); 221 | const index = store.index(indexName); 222 | const request = index.getAll(indexValue); 223 | return requestToPromise(request, 'get records by index', Boolean(txn)); 224 | } 225 | 226 | /** 227 | * @callback UpgradeCallback 228 | * @param {IDBDatabase} db 229 | * @param {IDBVersionChangeEvent} event 230 | */ 231 | 232 | /** 233 | * This opens a given database. 234 | * @param {string} dbName 235 | * @param {number} version 236 | * @param {UpgradeCallback} upgrade 237 | * @returns {Promise} 238 | */ 239 | static openDB(dbName, version, upgrade) { 240 | return new Promise((resolve, reject) => { 241 | const request = indexedDB.open(dbName, version); 242 | 243 | // This is getting called twice, which seems to be a bug. 244 | request.onsuccess = event => { 245 | const db = request.result; 246 | resolve(db); 247 | }; 248 | 249 | request.onerror = event => { 250 | console.error(`failed to open database ${dbName}`); 251 | reject(event); 252 | }; 253 | 254 | request.onupgradeneeded = event => { 255 | const db = request.result; 256 | upgrade(db, event); 257 | }; 258 | }); 259 | } 260 | 261 | /** 262 | * This updates all records in a given store 263 | * that have a given value for a given index 264 | * to a new value. 265 | * @param {string} storeName 266 | * @param {string} indexName 267 | * @param {any} oldValue 268 | * @param {any} newValue 269 | * @param {IDBTransaction} [txn] 270 | * @returns {Promise} 271 | */ 272 | updateRecordsByIndex(storeName, indexName, oldValue, newValue, txn) { 273 | const definiteTxn = this.ensureTxn(txn, storeName, 'readwrite'); 274 | return new Promise((resolve, reject) => { 275 | const store = definiteTxn.objectStore(storeName); 276 | const index = store.index(indexName); 277 | const request = index.getAll(oldValue); 278 | request.onsuccess = event => { 279 | const request = /** @type {IDBRequest} */ (event.target); 280 | const records = request.result ?? []; 281 | for (const record of records) { 282 | let {keyPath} = index; 283 | if (Array.isArray(keyPath)) keyPath = keyPath[0]; 284 | record[keyPath] = newValue; 285 | store.put(record); 286 | } 287 | if (!txn) definiteTxn.commit(); 288 | resolve(); 289 | }; 290 | request.onerror = event => { 291 | console.error('failed to update records by index'); 292 | definiteTxn.abort(); 293 | reject(event); 294 | }; 295 | }); 296 | } 297 | 298 | /** 299 | * This inserts or updates a record in a given store. 300 | * @param {string} storeName 301 | * @param {object} object 302 | * @param {IDBTransaction} [txn] 303 | * @returns {Promise} 304 | */ 305 | upsertRecord(storeName, object, txn) { 306 | const definiteTxn = this.ensureTxn(txn, storeName, 'readwrite'); 307 | const store = definiteTxn.objectStore(storeName); 308 | const request = store.put(object); 309 | return requestToPromise(request, 'update dog', Boolean(txn)); 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /public/tiny-request-router.mjs: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | /*! 3 | * tiny-request-router v1.2.2 by berstend 4 | * https://github.com/berstend/tiny-request-router#readme 5 | * @license MIT 6 | */ 7 | /*! ***************************************************************************** 8 | Copyright (c) Microsoft Corporation. All rights reserved. 9 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 10 | this file except in compliance with the License. You may obtain a copy of the 11 | License at http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED 15 | WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, 16 | MERCHANTABLITY OR NON-INFRINGEMENT. 17 | 18 | See the Apache Version 2.0 License for specific language governing permissions 19 | and limitations under the License. 20 | ***************************************************************************** */ 21 | 22 | var __assign = function () { 23 | __assign = 24 | Object.assign || 25 | function __assign(t) { 26 | for (var s, i = 1, n = arguments.length; i < n; i++) { 27 | s = arguments[i]; 28 | for (var p in s) 29 | if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; 30 | } 31 | return t; 32 | }; 33 | return __assign.apply(this, arguments); 34 | }; 35 | 36 | /** 37 | * Tokenize input string. 38 | */ 39 | function lexer(str) { 40 | var tokens = []; 41 | var i = 0; 42 | while (i < str.length) { 43 | var char = str[i]; 44 | if (char === '*' || char === '+' || char === '?') { 45 | tokens.push({type: 'MODIFIER', index: i, value: str[i++]}); 46 | continue; 47 | } 48 | if (char === '\\') { 49 | tokens.push({type: 'ESCAPED_CHAR', index: i++, value: str[i++]}); 50 | continue; 51 | } 52 | if (char === '{') { 53 | tokens.push({type: 'OPEN', index: i, value: str[i++]}); 54 | continue; 55 | } 56 | if (char === '}') { 57 | tokens.push({type: 'CLOSE', index: i, value: str[i++]}); 58 | continue; 59 | } 60 | if (char === ':') { 61 | var name = ''; 62 | var j = i + 1; 63 | while (j < str.length) { 64 | var code = str.charCodeAt(j); 65 | if ( 66 | // `0-9` 67 | (code >= 48 && code <= 57) || 68 | // `A-Z` 69 | (code >= 65 && code <= 90) || 70 | // `a-z` 71 | (code >= 97 && code <= 122) || 72 | // `_` 73 | code === 95 74 | ) { 75 | name += str[j++]; 76 | continue; 77 | } 78 | break; 79 | } 80 | if (!name) throw new TypeError('Missing parameter name at ' + i); 81 | tokens.push({type: 'NAME', index: i, value: name}); 82 | i = j; 83 | continue; 84 | } 85 | if (char === '(') { 86 | var count = 1; 87 | var pattern = ''; 88 | var j = i + 1; 89 | if (str[j] === '?') { 90 | throw new TypeError('Pattern cannot start with "?" at ' + j); 91 | } 92 | while (j < str.length) { 93 | if (str[j] === '\\') { 94 | pattern += str[j++] + str[j++]; 95 | continue; 96 | } 97 | if (str[j] === ')') { 98 | count--; 99 | if (count === 0) { 100 | j++; 101 | break; 102 | } 103 | } else if (str[j] === '(') { 104 | count++; 105 | if (str[j + 1] !== '?') { 106 | throw new TypeError('Capturing groups are not allowed at ' + j); 107 | } 108 | } 109 | pattern += str[j++]; 110 | } 111 | if (count) throw new TypeError('Unbalanced pattern at ' + i); 112 | if (!pattern) throw new TypeError('Missing pattern at ' + i); 113 | tokens.push({type: 'PATTERN', index: i, value: pattern}); 114 | i = j; 115 | continue; 116 | } 117 | tokens.push({type: 'CHAR', index: i, value: str[i++]}); 118 | } 119 | tokens.push({type: 'END', index: i, value: ''}); 120 | return tokens; 121 | } 122 | /** 123 | * Parse a string for the raw tokens. 124 | */ 125 | function parse(str, options) { 126 | if (options === void 0) { 127 | options = {}; 128 | } 129 | var tokens = lexer(str); 130 | var _a = options.prefixes, 131 | prefixes = _a === void 0 ? './' : _a; 132 | var defaultPattern = '[^' + escapeString(options.delimiter || '/#?') + ']+?'; 133 | var result = []; 134 | var key = 0; 135 | var i = 0; 136 | var path = ''; 137 | var tryConsume = function (type) { 138 | if (i < tokens.length && tokens[i].type === type) return tokens[i++].value; 139 | }; 140 | var mustConsume = function (type) { 141 | var value = tryConsume(type); 142 | if (value !== undefined) return value; 143 | var _a = tokens[i], 144 | nextType = _a.type, 145 | index = _a.index; 146 | throw new TypeError( 147 | 'Unexpected ' + nextType + ' at ' + index + ', expected ' + type 148 | ); 149 | }; 150 | var consumeText = function () { 151 | var result = ''; 152 | var value; 153 | // tslint:disable-next-line 154 | while ((value = tryConsume('CHAR') || tryConsume('ESCAPED_CHAR'))) { 155 | result += value; 156 | } 157 | return result; 158 | }; 159 | while (i < tokens.length) { 160 | var char = tryConsume('CHAR'); 161 | var name = tryConsume('NAME'); 162 | var pattern = tryConsume('PATTERN'); 163 | if (name || pattern) { 164 | var prefix = char || ''; 165 | if (prefixes.indexOf(prefix) === -1) { 166 | path += prefix; 167 | prefix = ''; 168 | } 169 | if (path) { 170 | result.push(path); 171 | path = ''; 172 | } 173 | result.push({ 174 | name: name || key++, 175 | prefix: prefix, 176 | suffix: '', 177 | pattern: pattern || defaultPattern, 178 | modifier: tryConsume('MODIFIER') || '' 179 | }); 180 | continue; 181 | } 182 | var value = char || tryConsume('ESCAPED_CHAR'); 183 | if (value) { 184 | path += value; 185 | continue; 186 | } 187 | if (path) { 188 | result.push(path); 189 | path = ''; 190 | } 191 | var open = tryConsume('OPEN'); 192 | if (open) { 193 | var prefix = consumeText(); 194 | var name_1 = tryConsume('NAME') || ''; 195 | var pattern_1 = tryConsume('PATTERN') || ''; 196 | var suffix = consumeText(); 197 | mustConsume('CLOSE'); 198 | result.push({ 199 | name: name_1 || (pattern_1 ? key++ : ''), 200 | pattern: name_1 && !pattern_1 ? defaultPattern : pattern_1, 201 | prefix: prefix, 202 | suffix: suffix, 203 | modifier: tryConsume('MODIFIER') || '' 204 | }); 205 | continue; 206 | } 207 | mustConsume('END'); 208 | } 209 | return result; 210 | } 211 | /** 212 | * Escape a regular expression string. 213 | */ 214 | function escapeString(str) { 215 | return str.replace(/([.+*?=^!:${}()[\]|/\\])/g, '\\$1'); 216 | } 217 | /** 218 | * Get the flags for a regexp from the options. 219 | */ 220 | function flags(options) { 221 | return options && options.sensitive ? '' : 'i'; 222 | } 223 | /** 224 | * Pull out keys from a regexp. 225 | */ 226 | function regexpToRegexp(path, keys) { 227 | if (!keys) return path; 228 | // Use a negative lookahead to match only capturing groups. 229 | var groups = path.source.match(/\((?!\?)/g); 230 | if (groups) { 231 | for (var i = 0; i < groups.length; i++) { 232 | keys.push({ 233 | name: i, 234 | prefix: '', 235 | suffix: '', 236 | modifier: '', 237 | pattern: '' 238 | }); 239 | } 240 | } 241 | return path; 242 | } 243 | /** 244 | * Transform an array into a regexp. 245 | */ 246 | function arrayToRegexp(paths, keys, options) { 247 | var parts = paths.map(function (path) { 248 | return pathToRegexp(path, keys, options).source; 249 | }); 250 | return new RegExp('(?:' + parts.join('|') + ')', flags(options)); 251 | } 252 | /** 253 | * Create a path regexp from string input. 254 | */ 255 | function stringToRegexp(path, keys, options) { 256 | return tokensToRegexp(parse(path, options), keys, options); 257 | } 258 | /** 259 | * Expose a function for taking tokens and returning a RegExp. 260 | */ 261 | function tokensToRegexp(tokens, keys, options) { 262 | if (options === void 0) { 263 | options = {}; 264 | } 265 | var _a = options.strict, 266 | strict = _a === void 0 ? false : _a, 267 | _b = options.start, 268 | start = _b === void 0 ? true : _b, 269 | _c = options.end, 270 | end = _c === void 0 ? true : _c, 271 | _d = options.encode, 272 | encode = 273 | _d === void 0 274 | ? function (x) { 275 | return x; 276 | } 277 | : _d; 278 | var endsWith = '[' + escapeString(options.endsWith || '') + ']|$'; 279 | var delimiter = '[' + escapeString(options.delimiter || '/#?') + ']'; 280 | var route = start ? '^' : ''; 281 | // Iterate over the tokens and create our regexp string. 282 | for (var _i = 0, tokens_1 = tokens; _i < tokens_1.length; _i++) { 283 | var token = tokens_1[_i]; 284 | if (typeof token === 'string') { 285 | route += escapeString(encode(token)); 286 | } else { 287 | var prefix = escapeString(encode(token.prefix)); 288 | var suffix = escapeString(encode(token.suffix)); 289 | if (token.pattern) { 290 | if (keys) keys.push(token); 291 | if (prefix || suffix) { 292 | if (token.modifier === '+' || token.modifier === '*') { 293 | var mod = token.modifier === '*' ? '?' : ''; 294 | route += 295 | '(?:' + 296 | prefix + 297 | '((?:' + 298 | token.pattern + 299 | ')(?:' + 300 | suffix + 301 | prefix + 302 | '(?:' + 303 | token.pattern + 304 | '))*)' + 305 | suffix + 306 | ')' + 307 | mod; 308 | } else { 309 | route += 310 | '(?:' + 311 | prefix + 312 | '(' + 313 | token.pattern + 314 | ')' + 315 | suffix + 316 | ')' + 317 | token.modifier; 318 | } 319 | } else { 320 | route += '(' + token.pattern + ')' + token.modifier; 321 | } 322 | } else { 323 | route += '(?:' + prefix + suffix + ')' + token.modifier; 324 | } 325 | } 326 | } 327 | if (end) { 328 | if (!strict) route += delimiter + '?'; 329 | route += !options.endsWith ? '$' : '(?=' + endsWith + ')'; 330 | } else { 331 | var endToken = tokens[tokens.length - 1]; 332 | var isEndDelimited = 333 | typeof endToken === 'string' 334 | ? delimiter.indexOf(endToken[endToken.length - 1]) > -1 335 | : // tslint:disable-next-line 336 | endToken === undefined; 337 | if (!strict) { 338 | route += '(?:' + delimiter + '(?=' + endsWith + '))?'; 339 | } 340 | if (!isEndDelimited) { 341 | route += '(?=' + delimiter + '|' + endsWith + ')'; 342 | } 343 | } 344 | return new RegExp(route, flags(options)); 345 | } 346 | /** 347 | * Normalize the given path string, returning a regular expression. 348 | * 349 | * An empty array can be passed in for the keys, which will hold the 350 | * placeholder key descriptions. For example, using `/user/:id`, `keys` will 351 | * contain `[{ name: 'id', delimiter: '/', optional: false, repeat: false }]`. 352 | */ 353 | function pathToRegexp(path, keys, options) { 354 | if (path instanceof RegExp) return regexpToRegexp(path, keys); 355 | if (Array.isArray(path)) return arrayToRegexp(path, keys, options); 356 | return stringToRegexp(path, keys, options); 357 | } 358 | 359 | /** 360 | * Tiny request router. Allows overloading of handler type to be fully type safe. 361 | * 362 | * @example 363 | * import { Router, Method, Params } from 'tiny-request-router' 364 | * 365 | * // Let the router know that handlers are async functions returning a Response 366 | * type Handler = (params: Params) => Promise 367 | * 368 | * const router = new Router() 369 | */ 370 | var Router = /** @class */ (function () { 371 | function Router() { 372 | /** List of all registered routes. */ 373 | this.routes = []; 374 | } 375 | /** Add a route that matches any method. */ 376 | Router.prototype.all = function (path, handler, options) { 377 | if (options === void 0) { 378 | options = {}; 379 | } 380 | return this._push('ALL', path, handler, options); 381 | }; 382 | /** Add a route that matches the GET method. */ 383 | Router.prototype.get = function (path, handler, options) { 384 | if (options === void 0) { 385 | options = {}; 386 | } 387 | return this._push('GET', path, handler, options); 388 | }; 389 | /** Add a route that matches the POST method. */ 390 | Router.prototype.post = function (path, handler, options) { 391 | if (options === void 0) { 392 | options = {}; 393 | } 394 | return this._push('POST', path, handler, options); 395 | }; 396 | /** Add a route that matches the PUT method. */ 397 | Router.prototype.put = function (path, handler, options) { 398 | if (options === void 0) { 399 | options = {}; 400 | } 401 | return this._push('PUT', path, handler, options); 402 | }; 403 | /** Add a route that matches the PATCH method. */ 404 | Router.prototype.patch = function (path, handler, options) { 405 | if (options === void 0) { 406 | options = {}; 407 | } 408 | return this._push('PATCH', path, handler, options); 409 | }; 410 | /** Add a route that matches the DELETE method. */ 411 | Router.prototype['delete'] = function (path, handler, options) { 412 | if (options === void 0) { 413 | options = {}; 414 | } 415 | return this._push('DELETE', path, handler, options); 416 | }; 417 | /** Add a route that matches the HEAD method. */ 418 | Router.prototype.head = function (path, handler, options) { 419 | if (options === void 0) { 420 | options = {}; 421 | } 422 | return this._push('HEAD', path, handler, options); 423 | }; 424 | /** Add a route that matches the OPTIONS method. */ 425 | Router.prototype.options = function (path, handler, options) { 426 | if (options === void 0) { 427 | options = {}; 428 | } 429 | return this._push('OPTIONS', path, handler, options); 430 | }; 431 | /** 432 | * Match the provided method and path against the list of registered routes. 433 | * 434 | * @example 435 | * router.get('/foobar', async () => new Response('Hello')) 436 | * 437 | * const match = router.match('GET', '/foobar') 438 | * if (match) { 439 | * // Call the async function of that match 440 | * const response = await match.handler() 441 | * console.log(response) // => Response('Hello') 442 | * } 443 | */ 444 | Router.prototype.match = function (method, path) { 445 | for (var _i = 0, _a = this.routes; _i < _a.length; _i++) { 446 | var route = _a[_i]; 447 | // Skip immediately if method doesn't match 448 | if (route.method !== method && route.method !== 'ALL') continue; 449 | // Speed optimizations for catch all wildcard routes 450 | if (route.path === '(.*)') { 451 | return __assign(__assign({}, route), {params: {0: route.path}}); 452 | } 453 | if (route.path === '/' && route.options.end === false) { 454 | return __assign(__assign({}, route), {params: {}}); 455 | } 456 | // If method matches try to match path regexp 457 | var matches = route.regexp.exec(path); 458 | if (!matches || !matches.length) continue; 459 | return __assign(__assign({}, route), { 460 | matches: matches, 461 | params: keysToParams(matches, route.keys) 462 | }); 463 | } 464 | return null; 465 | }; 466 | Router.prototype._push = function (method, path, handler, options) { 467 | var keys = []; 468 | if (path === '*') { 469 | path = '(.*)'; 470 | } 471 | var regexp = pathToRegexp(path, keys, options); 472 | this.routes.push({ 473 | method: method, 474 | path: path, 475 | handler: handler, 476 | keys: keys, 477 | options: options, 478 | regexp: regexp 479 | }); 480 | return this; 481 | }; 482 | return Router; 483 | })(); 484 | // Convert an array of keys and matches to a params object 485 | var keysToParams = function (matches, keys) { 486 | var params = {}; 487 | for (var i = 1; i < matches.length; i++) { 488 | var key = keys[i - 1]; 489 | var prop = key.name; 490 | var val = matches[i]; 491 | if (val !== undefined) { 492 | params[prop] = val; 493 | } 494 | } 495 | return params; 496 | }; 497 | 498 | export {Router, pathToRegexp}; 499 | //# sourceMappingURL=router.browser.mjs.map 500 | -------------------------------------------------------------------------------- /public/htmx.min.js: -------------------------------------------------------------------------------- 1 | (function(e,t){if(typeof define==="function"&&define.amd){define([],t)}else if(typeof module==="object"&&module.exports){module.exports=t()}else{e.htmx=e.htmx||t()}})(typeof self!=="undefined"?self:this,function(){return function(){"use strict";var Q={onLoad:F,process:zt,on:de,off:ge,trigger:ce,ajax:Nr,find:C,findAll:f,closest:v,values:function(e,t){var r=dr(e,t||"post");return r.values},remove:_,addClass:z,removeClass:n,toggleClass:$,takeClass:W,defineExtension:Ur,removeExtension:Br,logAll:V,logNone:j,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:d,_:t,createEventSource:function(e){return new EventSource(e,{withCredentials:true})},createWebSocket:function(e){var t=new WebSocket(e,[]);t.binaryType=Q.config.wsBinaryType;return t},version:"1.9.10"};var r={addTriggerHandler:Lt,bodyContains:se,canAccessLocalStorage:U,findThisElement:xe,filterValues:yr,hasAttribute:o,getAttributeValue:te,getClosestAttributeValue:ne,getClosestMatch:c,getExpressionVars:Hr,getHeaders:xr,getInputValues:dr,getInternalData:ae,getSwapSpecification:wr,getTriggerSpecs:it,getTarget:ye,makeFragment:l,mergeObjects:le,makeSettleInfo:T,oobSwap:Ee,querySelectorExt:ue,selectAndSwap:je,settleImmediately:nr,shouldCancel:ut,triggerEvent:ce,triggerErrorEvent:fe,withExtensions:R};var w=["get","post","put","delete","patch"];var i=w.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");var S=e("head"),q=e("title"),H=e("svg",true);function e(e,t=false){return new RegExp(`<${e}(\\s[^>]*>|>)([\\s\\S]*?)<\\/${e}>`,t?"gim":"im")}function d(e){if(e==undefined){return undefined}let t=NaN;if(e.slice(-2)=="ms"){t=parseFloat(e.slice(0,-2))}else if(e.slice(-1)=="s"){t=parseFloat(e.slice(0,-1))*1e3}else if(e.slice(-1)=="m"){t=parseFloat(e.slice(0,-1))*1e3*60}else{t=parseFloat(e)}return isNaN(t)?undefined:t}function ee(e,t){return e.getAttribute&&e.getAttribute(t)}function o(e,t){return e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function te(e,t){return ee(e,t)||ee(e,"data-"+t)}function u(e){return e.parentElement}function re(){return document}function c(e,t){while(e&&!t(e)){e=u(e)}return e?e:null}function L(e,t,r){var n=te(t,r);var i=te(t,"hx-disinherit");if(e!==t&&i&&(i==="*"||i.split(" ").indexOf(r)>=0)){return"unset"}else{return n}}function ne(t,r){var n=null;c(t,function(e){return n=L(t,e,r)});if(n!=="unset"){return n}}function h(e,t){var r=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector;return r&&r.call(e,t)}function A(e){var t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;var r=t.exec(e);if(r){return r[1].toLowerCase()}else{return""}}function a(e,t){var r=new DOMParser;var n=r.parseFromString(e,"text/html");var i=n.body;while(t>0){t--;i=i.firstChild}if(i==null){i=re().createDocumentFragment()}return i}function N(e){return/",0);return i.querySelector("template").content}switch(r){case"thead":case"tbody":case"tfoot":case"colgroup":case"caption":return a(""+n+"
",1);case"col":return a(""+n+"
",2);case"tr":return a(""+n+"
",2);case"td":case"th":return a(""+n+"
",3);case"script":case"style":return a("
"+n+"
",1);default:return a(n,0)}}function ie(e){if(e){e()}}function I(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function k(e){return I(e,"Function")}function P(e){return I(e,"Object")}function ae(e){var t="htmx-internal-data";var r=e[t];if(!r){r=e[t]={}}return r}function M(e){var t=[];if(e){for(var r=0;r=0}function se(e){if(e.getRootNode&&e.getRootNode()instanceof window.ShadowRoot){return re().body.contains(e.getRootNode().host)}else{return re().body.contains(e)}}function D(e){return e.trim().split(/\s+/)}function le(e,t){for(var r in t){if(t.hasOwnProperty(r)){e[r]=t[r]}}return e}function E(e){try{return JSON.parse(e)}catch(e){b(e);return null}}function U(){var e="htmx:localStorageTest";try{localStorage.setItem(e,e);localStorage.removeItem(e);return true}catch(e){return false}}function B(t){try{var e=new URL(t);if(e){t=e.pathname+e.search}if(!/^\/$/.test(t)){t=t.replace(/\/+$/,"")}return t}catch(e){return t}}function t(e){return Tr(re().body,function(){return eval(e)})}function F(t){var e=Q.on("htmx:load",function(e){t(e.detail.elt)});return e}function V(){Q.logger=function(e,t,r){if(console){console.log(t,e,r)}}}function j(){Q.logger=null}function C(e,t){if(t){return e.querySelector(t)}else{return C(re(),e)}}function f(e,t){if(t){return e.querySelectorAll(t)}else{return f(re(),e)}}function _(e,t){e=g(e);if(t){setTimeout(function(){_(e);e=null},t)}else{e.parentElement.removeChild(e)}}function z(e,t,r){e=g(e);if(r){setTimeout(function(){z(e,t);e=null},r)}else{e.classList&&e.classList.add(t)}}function n(e,t,r){e=g(e);if(r){setTimeout(function(){n(e,t);e=null},r)}else{if(e.classList){e.classList.remove(t);if(e.classList.length===0){e.removeAttribute("class")}}}}function $(e,t){e=g(e);e.classList.toggle(t)}function W(e,t){e=g(e);oe(e.parentElement.children,function(e){n(e,t)});z(e,t)}function v(e,t){e=g(e);if(e.closest){return e.closest(t)}else{do{if(e==null||h(e,t)){return e}}while(e=e&&u(e));return null}}function s(e,t){return e.substring(0,t.length)===t}function G(e,t){return e.substring(e.length-t.length)===t}function J(e){var t=e.trim();if(s(t,"<")&&G(t,"/>")){return t.substring(1,t.length-2)}else{return t}}function Z(e,t){if(t.indexOf("closest ")===0){return[v(e,J(t.substr(8)))]}else if(t.indexOf("find ")===0){return[C(e,J(t.substr(5)))]}else if(t==="next"){return[e.nextElementSibling]}else if(t.indexOf("next ")===0){return[K(e,J(t.substr(5)))]}else if(t==="previous"){return[e.previousElementSibling]}else if(t.indexOf("previous ")===0){return[Y(e,J(t.substr(9)))]}else if(t==="document"){return[document]}else if(t==="window"){return[window]}else if(t==="body"){return[document.body]}else{return re().querySelectorAll(J(t))}}var K=function(e,t){var r=re().querySelectorAll(t);for(var n=0;n=0;n--){var i=r[n];if(i.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_FOLLOWING){return i}}};function ue(e,t){if(t){return Z(e,t)[0]}else{return Z(re().body,e)[0]}}function g(e){if(I(e,"String")){return C(e)}else{return e}}function ve(e,t,r){if(k(t)){return{target:re().body,event:e,listener:t}}else{return{target:g(e),event:t,listener:r}}}function de(t,r,n){jr(function(){var e=ve(t,r,n);e.target.addEventListener(e.event,e.listener)});var e=k(r);return e?r:n}function ge(t,r,n){jr(function(){var e=ve(t,r,n);e.target.removeEventListener(e.event,e.listener)});return k(r)?r:n}var me=re().createElement("output");function pe(e,t){var r=ne(e,t);if(r){if(r==="this"){return[xe(e,t)]}else{var n=Z(e,r);if(n.length===0){b('The selector "'+r+'" on '+t+" returned no matches!");return[me]}else{return n}}}}function xe(e,t){return c(e,function(e){return te(e,t)!=null})}function ye(e){var t=ne(e,"hx-target");if(t){if(t==="this"){return xe(e,"hx-target")}else{return ue(e,t)}}else{var r=ae(e);if(r.boosted){return re().body}else{return e}}}function be(e){var t=Q.config.attributesToSettle;for(var r=0;r0){o=e.substr(0,e.indexOf(":"));t=e.substr(e.indexOf(":")+1,e.length)}else{o=e}var r=re().querySelectorAll(t);if(r){oe(r,function(e){var t;var r=i.cloneNode(true);t=re().createDocumentFragment();t.appendChild(r);if(!Se(o,e)){t=r}var n={shouldSwap:true,target:e,fragment:t};if(!ce(e,"htmx:oobBeforeSwap",n))return;e=n.target;if(n["shouldSwap"]){Fe(o,e,e,t,a)}oe(a.elts,function(e){ce(e,"htmx:oobAfterSwap",n)})});i.parentNode.removeChild(i)}else{i.parentNode.removeChild(i);fe(re().body,"htmx:oobErrorNoTarget",{content:i})}return e}function Ce(e,t,r){var n=ne(e,"hx-select-oob");if(n){var i=n.split(",");for(var a=0;a0){var r=t.replace("'","\\'");var n=e.tagName.replace(":","\\:");var i=o.querySelector(n+"[id='"+r+"']");if(i&&i!==o){var a=e.cloneNode();we(e,i);s.tasks.push(function(){we(e,a)})}}})}function Oe(e){return function(){n(e,Q.config.addedClass);zt(e);Nt(e);qe(e);ce(e,"htmx:load")}}function qe(e){var t="[autofocus]";var r=h(e,t)?e:e.querySelector(t);if(r!=null){r.focus()}}function m(e,t,r,n){Te(e,r,n);while(r.childNodes.length>0){var i=r.firstChild;z(i,Q.config.addedClass);e.insertBefore(i,t);if(i.nodeType!==Node.TEXT_NODE&&i.nodeType!==Node.COMMENT_NODE){n.tasks.push(Oe(i))}}}function He(e,t){var r=0;while(r-1){var t=e.replace(H,"");var r=t.match(q);if(r){return r[2]}}}function je(e,t,r,n,i,a){i.title=Ve(n);var o=l(n);if(o){Ce(r,o,i);o=Be(r,o,a);Re(o);return Fe(e,r,t,o,i)}}function _e(e,t,r){var n=e.getResponseHeader(t);if(n.indexOf("{")===0){var i=E(n);for(var a in i){if(i.hasOwnProperty(a)){var o=i[a];if(!P(o)){o={value:o}}ce(r,a,o)}}}else{var s=n.split(",");for(var l=0;l0){var o=t[0];if(o==="]"){n--;if(n===0){if(a===null){i=i+"true"}t.shift();i+=")})";try{var s=Tr(e,function(){return Function(i)()},function(){return true});s.source=i;return s}catch(e){fe(re().body,"htmx:syntax:error",{error:e,source:i});return null}}}else if(o==="["){n++}if(Qe(o,a,r)){i+="(("+r+"."+o+") ? ("+r+"."+o+") : (window."+o+"))"}else{i=i+o}a=t.shift()}}}function y(e,t){var r="";while(e.length>0&&!t.test(e[0])){r+=e.shift()}return r}function tt(e){var t;if(e.length>0&&Ze.test(e[0])){e.shift();t=y(e,Ke).trim();e.shift()}else{t=y(e,x)}return t}var rt="input, textarea, select";function nt(e,t,r){var n=[];var i=Ye(t);do{y(i,Je);var a=i.length;var o=y(i,/[,\[\s]/);if(o!==""){if(o==="every"){var s={trigger:"every"};y(i,Je);s.pollInterval=d(y(i,/[,\[\s]/));y(i,Je);var l=et(e,i,"event");if(l){s.eventFilter=l}n.push(s)}else if(o.indexOf("sse:")===0){n.push({trigger:"sse",sseEvent:o.substr(4)})}else{var u={trigger:o};var l=et(e,i,"event");if(l){u.eventFilter=l}while(i.length>0&&i[0]!==","){y(i,Je);var f=i.shift();if(f==="changed"){u.changed=true}else if(f==="once"){u.once=true}else if(f==="consume"){u.consume=true}else if(f==="delay"&&i[0]===":"){i.shift();u.delay=d(y(i,x))}else if(f==="from"&&i[0]===":"){i.shift();if(Ze.test(i[0])){var c=tt(i)}else{var c=y(i,x);if(c==="closest"||c==="find"||c==="next"||c==="previous"){i.shift();var h=tt(i);if(h.length>0){c+=" "+h}}}u.from=c}else if(f==="target"&&i[0]===":"){i.shift();u.target=tt(i)}else if(f==="throttle"&&i[0]===":"){i.shift();u.throttle=d(y(i,x))}else if(f==="queue"&&i[0]===":"){i.shift();u.queue=y(i,x)}else if(f==="root"&&i[0]===":"){i.shift();u[f]=tt(i)}else if(f==="threshold"&&i[0]===":"){i.shift();u[f]=y(i,x)}else{fe(e,"htmx:syntax:error",{token:i.shift()})}}n.push(u)}}if(i.length===a){fe(e,"htmx:syntax:error",{token:i.shift()})}y(i,Je)}while(i[0]===","&&i.shift());if(r){r[t]=n}return n}function it(e){var t=te(e,"hx-trigger");var r=[];if(t){var n=Q.config.triggerSpecsCache;r=n&&n[t]||nt(e,t,n)}if(r.length>0){return r}else if(h(e,"form")){return[{trigger:"submit"}]}else if(h(e,'input[type="button"], input[type="submit"]')){return[{trigger:"click"}]}else if(h(e,rt)){return[{trigger:"change"}]}else{return[{trigger:"click"}]}}function at(e){ae(e).cancelled=true}function ot(e,t,r){var n=ae(e);n.timeout=setTimeout(function(){if(se(e)&&n.cancelled!==true){if(!ct(r,e,Wt("hx:poll:trigger",{triggerSpec:r,target:e}))){t(e)}ot(e,t,r)}},r.pollInterval)}function st(e){return location.hostname===e.hostname&&ee(e,"href")&&ee(e,"href").indexOf("#")!==0}function lt(t,r,e){if(t.tagName==="A"&&st(t)&&(t.target===""||t.target==="_self")||t.tagName==="FORM"){r.boosted=true;var n,i;if(t.tagName==="A"){n="get";i=ee(t,"href")}else{var a=ee(t,"method");n=a?a.toLowerCase():"get";if(n==="get"){}i=ee(t,"action")}e.forEach(function(e){ht(t,function(e,t){if(v(e,Q.config.disableSelector)){p(e);return}he(n,i,e,t)},r,e,true)})}}function ut(e,t){if(e.type==="submit"||e.type==="click"){if(t.tagName==="FORM"){return true}if(h(t,'input[type="submit"], button')&&v(t,"form")!==null){return true}if(t.tagName==="A"&&t.href&&(t.getAttribute("href")==="#"||t.getAttribute("href").indexOf("#")!==0)){return true}}return false}function ft(e,t){return ae(e).boosted&&e.tagName==="A"&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function ct(e,t,r){var n=e.eventFilter;if(n){try{return n.call(t,r)!==true}catch(e){fe(re().body,"htmx:eventFilter:error",{error:e,source:n.source});return true}}return false}function ht(a,o,e,s,l){var u=ae(a);var t;if(s.from){t=Z(a,s.from)}else{t=[a]}if(s.changed){t.forEach(function(e){var t=ae(e);t.lastValue=e.value})}oe(t,function(n){var i=function(e){if(!se(a)){n.removeEventListener(s.trigger,i);return}if(ft(a,e)){return}if(l||ut(e,a)){e.preventDefault()}if(ct(s,a,e)){return}var t=ae(e);t.triggerSpec=s;if(t.handledFor==null){t.handledFor=[]}if(t.handledFor.indexOf(a)<0){t.handledFor.push(a);if(s.consume){e.stopPropagation()}if(s.target&&e.target){if(!h(e.target,s.target)){return}}if(s.once){if(u.triggeredOnce){return}else{u.triggeredOnce=true}}if(s.changed){var r=ae(n);if(r.lastValue===n.value){return}r.lastValue=n.value}if(u.delayed){clearTimeout(u.delayed)}if(u.throttle){return}if(s.throttle>0){if(!u.throttle){o(a,e);u.throttle=setTimeout(function(){u.throttle=null},s.throttle)}}else if(s.delay>0){u.delayed=setTimeout(function(){o(a,e)},s.delay)}else{ce(a,"htmx:trigger");o(a,e)}}};if(e.listenerInfos==null){e.listenerInfos=[]}e.listenerInfos.push({trigger:s.trigger,listener:i,on:n});n.addEventListener(s.trigger,i)})}var vt=false;var dt=null;function gt(){if(!dt){dt=function(){vt=true};window.addEventListener("scroll",dt);setInterval(function(){if(vt){vt=false;oe(re().querySelectorAll("[hx-trigger='revealed'],[data-hx-trigger='revealed']"),function(e){mt(e)})}},200)}}function mt(t){if(!o(t,"data-hx-revealed")&&X(t)){t.setAttribute("data-hx-revealed","true");var e=ae(t);if(e.initHash){ce(t,"revealed")}else{t.addEventListener("htmx:afterProcessNode",function(e){ce(t,"revealed")},{once:true})}}}function pt(e,t,r){var n=D(r);for(var i=0;i=0){var t=wt(n);setTimeout(function(){xt(s,r,n+1)},t)}};t.onopen=function(e){n=0};ae(s).webSocket=t;t.addEventListener("message",function(e){if(yt(s)){return}var t=e.data;R(s,function(e){t=e.transformResponse(t,null,s)});var r=T(s);var n=l(t);var i=M(n.children);for(var a=0;a0){ce(u,"htmx:validation:halted",i);return}t.send(JSON.stringify(l));if(ut(e,u)){e.preventDefault()}})}else{fe(u,"htmx:noWebSocketSourceError")}}function wt(e){var t=Q.config.wsReconnectDelay;if(typeof t==="function"){return t(e)}if(t==="full-jitter"){var r=Math.min(e,6);var n=1e3*Math.pow(2,r);return n*Math.random()}b('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"')}function St(e,t,r){var n=D(r);for(var i=0;i0){setTimeout(i,n)}else{i()}}function Ht(t,i,e){var a=false;oe(w,function(r){if(o(t,"hx-"+r)){var n=te(t,"hx-"+r);a=true;i.path=n;i.verb=r;e.forEach(function(e){Lt(t,e,i,function(e,t){if(v(e,Q.config.disableSelector)){p(e);return}he(r,n,e,t)})})}});return a}function Lt(n,e,t,r){if(e.sseEvent){Rt(n,r,e.sseEvent)}else if(e.trigger==="revealed"){gt();ht(n,r,t,e);mt(n)}else if(e.trigger==="intersect"){var i={};if(e.root){i.root=ue(n,e.root)}if(e.threshold){i.threshold=parseFloat(e.threshold)}var a=new IntersectionObserver(function(e){for(var t=0;t0){t.polling=true;ot(n,r,e)}else{ht(n,r,t,e)}}function At(e){if(Q.config.allowScriptTags&&(e.type==="text/javascript"||e.type==="module"||e.type==="")){var t=re().createElement("script");oe(e.attributes,function(e){t.setAttribute(e.name,e.value)});t.textContent=e.textContent;t.async=false;if(Q.config.inlineScriptNonce){t.nonce=Q.config.inlineScriptNonce}var r=e.parentElement;try{r.insertBefore(t,e)}catch(e){b(e)}finally{if(e.parentElement){e.parentElement.removeChild(e)}}}}function Nt(e){if(h(e,"script")){At(e)}oe(f(e,"script"),function(e){At(e)})}function It(e){var t=e.attributes;for(var r=0;r0){var o=n.shift();var s=o.match(/^\s*([a-zA-Z:\-\.]+:)(.*)/);if(a===0&&s){o.split(":");i=s[1].slice(0,-1);r[i]=s[2]}else{r[i]+=o}a+=Bt(o)}for(var l in r){Ft(e,l,r[l])}}}function jt(e){Ae(e);for(var t=0;tQ.config.historyCacheSize){i.shift()}while(i.length>0){try{localStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(e){fe(re().body,"htmx:historyCacheError",{cause:e,cache:i});i.shift()}}}function Yt(e){if(!U()){return null}e=B(e);var t=E(localStorage.getItem("htmx-history-cache"))||[];for(var r=0;r=200&&this.status<400){ce(re().body,"htmx:historyCacheMissLoad",o);var e=l(this.response);e=e.querySelector("[hx-history-elt],[data-hx-history-elt]")||e;var t=Zt();var r=T(t);var n=Ve(this.response);if(n){var i=C("title");if(i){i.innerHTML=n}else{window.document.title=n}}Ue(t,e,r);nr(r.tasks);Jt=a;ce(re().body,"htmx:historyRestore",{path:a,cacheMiss:true,serverResponse:this.response})}else{fe(re().body,"htmx:historyCacheMissLoadError",o)}};e.send()}function ar(e){er();e=e||location.pathname+location.search;var t=Yt(e);if(t){var r=l(t.content);var n=Zt();var i=T(n);Ue(n,r,i);nr(i.tasks);document.title=t.title;setTimeout(function(){window.scrollTo(0,t.scroll)},0);Jt=e;ce(re().body,"htmx:historyRestore",{path:e,item:t})}else{if(Q.config.refreshOnHistoryMiss){window.location.reload(true)}else{ir(e)}}}function or(e){var t=pe(e,"hx-indicator");if(t==null){t=[e]}oe(t,function(e){var t=ae(e);t.requestCount=(t.requestCount||0)+1;e.classList["add"].call(e.classList,Q.config.requestClass)});return t}function sr(e){var t=pe(e,"hx-disabled-elt");if(t==null){t=[]}oe(t,function(e){var t=ae(e);t.requestCount=(t.requestCount||0)+1;e.setAttribute("disabled","")});return t}function lr(e,t){oe(e,function(e){var t=ae(e);t.requestCount=(t.requestCount||0)-1;if(t.requestCount===0){e.classList["remove"].call(e.classList,Q.config.requestClass)}});oe(t,function(e){var t=ae(e);t.requestCount=(t.requestCount||0)-1;if(t.requestCount===0){e.removeAttribute("disabled")}})}function ur(e,t){for(var r=0;r=0}function wr(e,t){var r=t?t:ne(e,"hx-swap");var n={swapStyle:ae(e).boosted?"innerHTML":Q.config.defaultSwapStyle,swapDelay:Q.config.defaultSwapDelay,settleDelay:Q.config.defaultSettleDelay};if(Q.config.scrollIntoViewOnBoost&&ae(e).boosted&&!br(e)){n["show"]="top"}if(r){var i=D(r);if(i.length>0){for(var a=0;a0?l.join(":"):null;n["scroll"]=u;n["scrollTarget"]=f}else if(o.indexOf("show:")===0){var c=o.substr(5);var l=c.split(":");var h=l.pop();var f=l.length>0?l.join(":"):null;n["show"]=h;n["showTarget"]=f}else if(o.indexOf("focus-scroll:")===0){var v=o.substr("focus-scroll:".length);n["focusScroll"]=v=="true"}else if(a==0){n["swapStyle"]=o}else{b("Unknown modifier in hx-swap: "+o)}}}}return n}function Sr(e){return ne(e,"hx-encoding")==="multipart/form-data"||h(e,"form")&&ee(e,"enctype")==="multipart/form-data"}function Er(t,r,n){var i=null;R(r,function(e){if(i==null){i=e.encodeParameters(t,n,r)}});if(i!=null){return i}else{if(Sr(r)){return pr(n)}else{return mr(n)}}}function T(e){return{tasks:[],elts:[e]}}function Cr(e,t){var r=e[0];var n=e[e.length-1];if(t.scroll){var i=null;if(t.scrollTarget){i=ue(r,t.scrollTarget)}if(t.scroll==="top"&&(r||i)){i=i||r;i.scrollTop=0}if(t.scroll==="bottom"&&(n||i)){i=i||n;i.scrollTop=i.scrollHeight}}if(t.show){var i=null;if(t.showTarget){var a=t.showTarget;if(t.showTarget==="window"){a="body"}i=ue(r,a)}if(t.show==="top"&&(r||i)){i=i||r;i.scrollIntoView({block:"start",behavior:Q.config.scrollBehavior})}if(t.show==="bottom"&&(n||i)){i=i||n;i.scrollIntoView({block:"end",behavior:Q.config.scrollBehavior})}}}function Rr(e,t,r,n){if(n==null){n={}}if(e==null){return n}var i=te(e,t);if(i){var a=i.trim();var o=r;if(a==="unset"){return null}if(a.indexOf("javascript:")===0){a=a.substr(11);o=true}else if(a.indexOf("js:")===0){a=a.substr(3);o=true}if(a.indexOf("{")!==0){a="{"+a+"}"}var s;if(o){s=Tr(e,function(){return Function("return ("+a+")")()},{})}else{s=E(a)}for(var l in s){if(s.hasOwnProperty(l)){if(n[l]==null){n[l]=s[l]}}}}return Rr(u(e),t,r,n)}function Tr(e,t,r){if(Q.config.allowEval){return t()}else{fe(e,"htmx:evalDisallowedError");return r}}function Or(e,t){return Rr(e,"hx-vars",true,t)}function qr(e,t){return Rr(e,"hx-vals",false,t)}function Hr(e){return le(Or(e),qr(e))}function Lr(t,r,n){if(n!==null){try{t.setRequestHeader(r,n)}catch(e){t.setRequestHeader(r,encodeURIComponent(n));t.setRequestHeader(r+"-URI-AutoEncoded","true")}}}function Ar(t){if(t.responseURL&&typeof URL!=="undefined"){try{var e=new URL(t.responseURL);return e.pathname+e.search}catch(e){fe(re().body,"htmx:badResponseUrl",{url:t.responseURL})}}}function O(e,t){return t.test(e.getAllResponseHeaders())}function Nr(e,t,r){e=e.toLowerCase();if(r){if(r instanceof Element||I(r,"String")){return he(e,t,null,null,{targetOverride:g(r),returnPromise:true})}else{return he(e,t,g(r.source),r.event,{handler:r.handler,headers:r.headers,values:r.values,targetOverride:g(r.target),swapOverride:r.swap,select:r.select,returnPromise:true})}}else{return he(e,t,null,null,{returnPromise:true})}}function Ir(e){var t=[];while(e){t.push(e);e=e.parentElement}return t}function kr(e,t,r){var n;var i;if(typeof URL==="function"){i=new URL(t,document.location.href);var a=document.location.origin;n=a===i.origin}else{i=t;n=s(t,document.location.origin)}if(Q.config.selfRequestsOnly){if(!n){return false}}return ce(e,"htmx:validateUrl",le({url:i,sameHost:n},r))}function he(t,r,n,i,a,e){var o=null;var s=null;a=a!=null?a:{};if(a.returnPromise&&typeof Promise!=="undefined"){var l=new Promise(function(e,t){o=e;s=t})}if(n==null){n=re().body}var M=a.handler||Mr;var X=a.select||null;if(!se(n)){ie(o);return l}var u=a.targetOverride||ye(n);if(u==null||u==me){fe(n,"htmx:targetError",{target:te(n,"hx-target")});ie(s);return l}var f=ae(n);var c=f.lastButtonClicked;if(c){var h=ee(c,"formaction");if(h!=null){r=h}var v=ee(c,"formmethod");if(v!=null){if(v.toLowerCase()!=="dialog"){t=v}}}var d=ne(n,"hx-confirm");if(e===undefined){var D=function(e){return he(t,r,n,i,a,!!e)};var U={target:u,elt:n,path:r,verb:t,triggeringEvent:i,etc:a,issueRequest:D,question:d};if(ce(n,"htmx:confirm",U)===false){ie(o);return l}}var g=n;var m=ne(n,"hx-sync");var p=null;var x=false;if(m){var B=m.split(":");var F=B[0].trim();if(F==="this"){g=xe(n,"hx-sync")}else{g=ue(n,F)}m=(B[1]||"drop").trim();f=ae(g);if(m==="drop"&&f.xhr&&f.abortable!==true){ie(o);return l}else if(m==="abort"){if(f.xhr){ie(o);return l}else{x=true}}else if(m==="replace"){ce(g,"htmx:abort")}else if(m.indexOf("queue")===0){var V=m.split(" ");p=(V[1]||"last").trim()}}if(f.xhr){if(f.abortable){ce(g,"htmx:abort")}else{if(p==null){if(i){var y=ae(i);if(y&&y.triggerSpec&&y.triggerSpec.queue){p=y.triggerSpec.queue}}if(p==null){p="last"}}if(f.queuedRequests==null){f.queuedRequests=[]}if(p==="first"&&f.queuedRequests.length===0){f.queuedRequests.push(function(){he(t,r,n,i,a)})}else if(p==="all"){f.queuedRequests.push(function(){he(t,r,n,i,a)})}else if(p==="last"){f.queuedRequests=[];f.queuedRequests.push(function(){he(t,r,n,i,a)})}ie(o);return l}}var b=new XMLHttpRequest;f.xhr=b;f.abortable=x;var w=function(){f.xhr=null;f.abortable=false;if(f.queuedRequests!=null&&f.queuedRequests.length>0){var e=f.queuedRequests.shift();e()}};var j=ne(n,"hx-prompt");if(j){var S=prompt(j);if(S===null||!ce(n,"htmx:prompt",{prompt:S,target:u})){ie(o);w();return l}}if(d&&!e){if(!confirm(d)){ie(o);w();return l}}var E=xr(n,u,S);if(t!=="get"&&!Sr(n)){E["Content-Type"]="application/x-www-form-urlencoded"}if(a.headers){E=le(E,a.headers)}var _=dr(n,t);var C=_.errors;var R=_.values;if(a.values){R=le(R,a.values)}var z=Hr(n);var $=le(R,z);var T=yr($,n);if(Q.config.getCacheBusterParam&&t==="get"){T["org.htmx.cache-buster"]=ee(u,"id")||"true"}if(r==null||r===""){r=re().location.href}var O=Rr(n,"hx-request");var W=ae(n).boosted;var q=Q.config.methodsThatUseUrlParams.indexOf(t)>=0;var H={boosted:W,useUrlParams:q,parameters:T,unfilteredParameters:$,headers:E,target:u,verb:t,errors:C,withCredentials:a.credentials||O.credentials||Q.config.withCredentials,timeout:a.timeout||O.timeout||Q.config.timeout,path:r,triggeringEvent:i};if(!ce(n,"htmx:configRequest",H)){ie(o);w();return l}r=H.path;t=H.verb;E=H.headers;T=H.parameters;C=H.errors;q=H.useUrlParams;if(C&&C.length>0){ce(n,"htmx:validation:halted",H);ie(o);w();return l}var G=r.split("#");var J=G[0];var L=G[1];var A=r;if(q){A=J;var Z=Object.keys(T).length!==0;if(Z){if(A.indexOf("?")<0){A+="?"}else{A+="&"}A+=mr(T);if(L){A+="#"+L}}}if(!kr(n,A,H)){fe(n,"htmx:invalidPath",H);ie(s);return l}b.open(t.toUpperCase(),A,true);b.overrideMimeType("text/html");b.withCredentials=H.withCredentials;b.timeout=H.timeout;if(O.noHeaders){}else{for(var N in E){if(E.hasOwnProperty(N)){var K=E[N];Lr(b,N,K)}}}var I={xhr:b,target:u,requestConfig:H,etc:a,boosted:W,select:X,pathInfo:{requestPath:r,finalRequestPath:A,anchor:L}};b.onload=function(){try{var e=Ir(n);I.pathInfo.responsePath=Ar(b);M(n,I);lr(k,P);ce(n,"htmx:afterRequest",I);ce(n,"htmx:afterOnLoad",I);if(!se(n)){var t=null;while(e.length>0&&t==null){var r=e.shift();if(se(r)){t=r}}if(t){ce(t,"htmx:afterRequest",I);ce(t,"htmx:afterOnLoad",I)}}ie(o);w()}catch(e){fe(n,"htmx:onLoadError",le({error:e},I));throw e}};b.onerror=function(){lr(k,P);fe(n,"htmx:afterRequest",I);fe(n,"htmx:sendError",I);ie(s);w()};b.onabort=function(){lr(k,P);fe(n,"htmx:afterRequest",I);fe(n,"htmx:sendAbort",I);ie(s);w()};b.ontimeout=function(){lr(k,P);fe(n,"htmx:afterRequest",I);fe(n,"htmx:timeout",I);ie(s);w()};if(!ce(n,"htmx:beforeRequest",I)){ie(o);w();return l}var k=or(n);var P=sr(n);oe(["loadstart","loadend","progress","abort"],function(t){oe([b,b.upload],function(e){e.addEventListener(t,function(e){ce(n,"htmx:xhr:"+t,{lengthComputable:e.lengthComputable,loaded:e.loaded,total:e.total})})})});ce(n,"htmx:beforeSend",I);var Y=q?null:Er(b,n,T);b.send(Y);return l}function Pr(e,t){var r=t.xhr;var n=null;var i=null;if(O(r,/HX-Push:/i)){n=r.getResponseHeader("HX-Push");i="push"}else if(O(r,/HX-Push-Url:/i)){n=r.getResponseHeader("HX-Push-Url");i="push"}else if(O(r,/HX-Replace-Url:/i)){n=r.getResponseHeader("HX-Replace-Url");i="replace"}if(n){if(n==="false"){return{}}else{return{type:i,path:n}}}var a=t.pathInfo.finalRequestPath;var o=t.pathInfo.responsePath;var s=ne(e,"hx-push-url");var l=ne(e,"hx-replace-url");var u=ae(e).boosted;var f=null;var c=null;if(s){f="push";c=s}else if(l){f="replace";c=l}else if(u){f="push";c=o||a}if(c){if(c==="false"){return{}}if(c==="true"){c=o||a}if(t.pathInfo.anchor&&c.indexOf("#")===-1){c=c+"#"+t.pathInfo.anchor}return{type:f,path:c}}else{return{}}}function Mr(l,u){var f=u.xhr;var c=u.target;var e=u.etc;var t=u.requestConfig;var h=u.select;if(!ce(l,"htmx:beforeOnLoad",u))return;if(O(f,/HX-Trigger:/i)){_e(f,"HX-Trigger",l)}if(O(f,/HX-Location:/i)){er();var r=f.getResponseHeader("HX-Location");var v;if(r.indexOf("{")===0){v=E(r);r=v["path"];delete v["path"]}Nr("GET",r,v).then(function(){tr(r)});return}var n=O(f,/HX-Refresh:/i)&&"true"===f.getResponseHeader("HX-Refresh");if(O(f,/HX-Redirect:/i)){location.href=f.getResponseHeader("HX-Redirect");n&&location.reload();return}if(n){location.reload();return}if(O(f,/HX-Retarget:/i)){if(f.getResponseHeader("HX-Retarget")==="this"){u.target=l}else{u.target=ue(l,f.getResponseHeader("HX-Retarget"))}}var d=Pr(l,u);var i=f.status>=200&&f.status<400&&f.status!==204;var g=f.response;var a=f.status>=400;var m=Q.config.ignoreTitle;var o=le({shouldSwap:i,serverResponse:g,isError:a,ignoreTitle:m},u);if(!ce(c,"htmx:beforeSwap",o))return;c=o.target;g=o.serverResponse;a=o.isError;m=o.ignoreTitle;u.target=c;u.failed=a;u.successful=!a;if(o.shouldSwap){if(f.status===286){at(l)}R(l,function(e){g=e.transformResponse(g,f,l)});if(d.type){er()}var s=e.swapOverride;if(O(f,/HX-Reswap:/i)){s=f.getResponseHeader("HX-Reswap")}var v=wr(l,s);if(v.hasOwnProperty("ignoreTitle")){m=v.ignoreTitle}c.classList.add(Q.config.swappingClass);var p=null;var x=null;var y=function(){try{var e=document.activeElement;var t={};try{t={elt:e,start:e?e.selectionStart:null,end:e?e.selectionEnd:null}}catch(e){}var r;if(h){r=h}if(O(f,/HX-Reselect:/i)){r=f.getResponseHeader("HX-Reselect")}if(d.type){ce(re().body,"htmx:beforeHistoryUpdate",le({history:d},u));if(d.type==="push"){tr(d.path);ce(re().body,"htmx:pushedIntoHistory",{path:d.path})}else{rr(d.path);ce(re().body,"htmx:replacedInHistory",{path:d.path})}}var n=T(c);je(v.swapStyle,c,l,g,n,r);if(t.elt&&!se(t.elt)&&ee(t.elt,"id")){var i=document.getElementById(ee(t.elt,"id"));var a={preventScroll:v.focusScroll!==undefined?!v.focusScroll:!Q.config.defaultFocusScroll};if(i){if(t.start&&i.setSelectionRange){try{i.setSelectionRange(t.start,t.end)}catch(e){}}i.focus(a)}}c.classList.remove(Q.config.swappingClass);oe(n.elts,function(e){if(e.classList){e.classList.add(Q.config.settlingClass)}ce(e,"htmx:afterSwap",u)});if(O(f,/HX-Trigger-After-Swap:/i)){var o=l;if(!se(l)){o=re().body}_e(f,"HX-Trigger-After-Swap",o)}var s=function(){oe(n.tasks,function(e){e.call()});oe(n.elts,function(e){if(e.classList){e.classList.remove(Q.config.settlingClass)}ce(e,"htmx:afterSettle",u)});if(u.pathInfo.anchor){var e=re().getElementById(u.pathInfo.anchor);if(e){e.scrollIntoView({block:"start",behavior:"auto"})}}if(n.title&&!m){var t=C("title");if(t){t.innerHTML=n.title}else{window.document.title=n.title}}Cr(n.elts,v);if(O(f,/HX-Trigger-After-Settle:/i)){var r=l;if(!se(l)){r=re().body}_e(f,"HX-Trigger-After-Settle",r)}ie(p)};if(v.settleDelay>0){setTimeout(s,v.settleDelay)}else{s()}}catch(e){fe(l,"htmx:swapError",u);ie(x);throw e}};var b=Q.config.globalViewTransitions;if(v.hasOwnProperty("transition")){b=v.transition}if(b&&ce(l,"htmx:beforeTransition",u)&&typeof Promise!=="undefined"&&document.startViewTransition){var w=new Promise(function(e,t){p=e;x=t});var S=y;y=function(){document.startViewTransition(function(){S();return w})}}if(v.swapDelay>0){setTimeout(y,v.swapDelay)}else{y()}}if(a){fe(l,"htmx:responseError",le({error:"Response Status Error Code "+f.status+" from "+u.pathInfo.requestPath},u))}}var Xr={};function Dr(){return{init:function(e){return null},onEvent:function(e,t){return true},transformResponse:function(e,t,r){return e},isInlineSwap:function(e){return false},handleSwap:function(e,t,r,n){return false},encodeParameters:function(e,t,r){return null}}}function Ur(e,t){if(t.init){t.init(r)}Xr[e]=le(Dr(),t)}function Br(e){delete Xr[e]}function Fr(e,r,n){if(e==undefined){return r}if(r==undefined){r=[]}if(n==undefined){n=[]}var t=te(e,"hx-ext");if(t){oe(t.split(","),function(e){e=e.replace(/ /g,"");if(e.slice(0,7)=="ignore:"){n.push(e.slice(7));return}if(n.indexOf(e)<0){var t=Xr[e];if(t&&r.indexOf(t)<0){r.push(t)}}})}return Fr(u(e),r,n)}var Vr=false;re().addEventListener("DOMContentLoaded",function(){Vr=true});function jr(e){if(Vr||re().readyState==="complete"){e()}else{re().addEventListener("DOMContentLoaded",e)}}function _r(){if(Q.config.includeIndicatorStyles!==false){re().head.insertAdjacentHTML("beforeend","")}}function zr(){var e=re().querySelector('meta[name="htmx-config"]');if(e){return E(e.content)}else{return null}}function $r(){var e=zr();if(e){Q.config=le(Q.config,e)}}jr(function(){$r();_r();var e=re().body;zt(e);var t=re().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(e){var t=e.target;var r=ae(t);if(r&&r.xhr){r.xhr.abort()}});const r=window.onpopstate?window.onpopstate.bind(window):null;window.onpopstate=function(e){if(e.state&&e.state.htmx){ar();oe(t,function(e){ce(e,"htmx:restored",{document:re(),triggerEvent:ce})})}else{if(r){r(e)}}};setTimeout(function(){ce(e,"htmx:load",{});e=null},0)});return Q}()}); --------------------------------------------------------------------------------