This commit is contained in:
Waylon S. Walker 2024-12-11 09:17:38 -06:00
parent a70c24398a
commit e181f57a91
30 changed files with 2458 additions and 197 deletions

View file

@ -8,47 +8,46 @@
}
::-webkit-scrollbar {
height: 1rem;
width: 1rem;
height: .5rem;
width: .5rem;
}
::-webkit-scrollbar-track {
border-radius: 0.25rem;
border-radius: 9999px;
--tw-bg-opacity: 1;
background-color: #450a0a;
background-color: #002600;
}
body::-webkit-scrollbar-track {
border-radius: 0.25rem;
border-radius: 9999px;
--tw-bg-opacity: 1;
background-color: #450a0a;
background-color: #002600;
}
::-webkit-scrollbar-thumb {
border-radius: 0.25rem;
border-radius: 9999px;
--tw-bg-opacity: 1;
background-color: rgba(239,68,68,var(--tw-bg-opacity));
background-color: #1aff1a;
background-color: #00b300;
}
::-webkit-scrollbar-thumb:hover {
--tw-bg-opacity: 1;
background-color: #f87171;
background-color: #dc2626
background-color: #1aff1a;
}
body::-webkit-scrollbar-thumb {
border-radius: 0.25rem;
border-radius: 9999px;
--tw-bg-opacity: 1;
background-color: rgba(239,68,68,var(--tw-bg-opacity));
background-color: #00b300;
}
body::-webkit-scrollbar-thumb:hover {
--tw-bg-opacity: 1;
background-color: #f87171;
background-color: #dc2626
background-color: #1aff1a;
}

View file

@ -1,67 +1,155 @@
<!DOCTYPE html>
<html>
<head>
{% block head %}
<title>
{% block title %}HTMX Patterns{% endblock %}
</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="og:title" name="og:title" content="HTMX Patterns from the hypermedia.systems book" />
<meta name="twitter:title" name="twitter:title" content="HTMX Patterns" />
<meta name="twitter:card" content="summary_large_image">
<meta name="og:image" name="og:image"
content="https://shots.wayl.one/shot/?url={{ request.url }}&height=600&width=1200&scaled_width=1200&scaled_height=600&selectors=" />
<meta name="twitter:image" name="twitter:image"
content="https://shots.wayl.one/shot/?url={{ request.url }}&height=640&width=1280&scaled_width=1280&scaled_height=640&selectors=" />
<meta name="og:image:height" content="640" />
<meta name="og:image:width" content="1280" />
<meta name="og:url" name="og:url" content="{{ request.url }}" />
<meta name="description" name="description" content="HTMX Patterns from the hypermedia.systems book" />
<meta name="og:description" name="Check if my kids can play outside"
content="HTMX Patterns from the hypermedia.systems book" />
<meta name="twitter:description" name="twitter:description"
content="HTMX Patterns from the hypermedia.systems book" />
<head>
{% block head %}
<title>
{% block title %}HTMX Patterns{% endblock %}
</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="og:title" name="og:title" content="HTMX Patterns from the hypermedia.systems book" />
<meta name="twitter:title" name="twitter:title" content="HTMX Patterns" />
<meta name="twitter:card" content="summary_large_image">
<meta name="og:image" name="og:image"
content="https://shots.wayl.one/shot/?url={{ request.url }}&height=600&width=1200&scaled_width=1200&scaled_height=600&selectors=" />
<meta name="twitter:image" name="twitter:image"
content="https://shots.wayl.one/shot/?url={{ request.url }}&height=640&width=1280&scaled_width=1280&scaled_height=640&selectors=" />
<meta name="og:image:height" content="640" />
<meta name="og:image:width" content="1280" />
<meta name="og:url" name="og:url" content="{{ request.url }}" />
<meta name="description" name="description" content="HTMX Patterns from the hypermedia.systems book" />
<meta name="og:description" name="Check if my kids can play outside"
content="HTMX Patterns from the hypermedia.systems book" />
<meta name="twitter:description" name="twitter:description"
content="HTMX Patterns from the hypermedia.systems book" />
<link rel="icon" type="image/x-icon" href="{{ url_for('favicon') }}" />
<link href="{{ url_for('app_css') }}" rel="stylesheet" />
<script src="{{ url_for('htmx') }}"></script>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
{% if DEBUG %}
{{ hot_reload.script(url_for('hot-reload') ) | safe }}
{% endif %}
{% endblock %}
</head>
<link rel="icon" type="image/x-icon" href="{{ url_for('favicon') }}" />
<link href="{{ url_for('app_css') }}" rel="stylesheet" />
<script src="{{ url_for('htmx_js') }}"></script>
<script src="{{ url_for('ws_js') }}"></script>
<script src="{{ url_for('tailwind_js') }}"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
clifford: '#da373d',
//terminal
'terminal-100': '#e6ffe6',
'terminal-200': '#b3ffb3',
'terminal-300': '#80ff80',
'terminal-400': '#4dff4d',
'terminal-500': '#1aff1a',
'terminal-600': '#00e600',
'terminal-700': '#00b300',
'terminal-800': '#008000',
'terminal-900': '#004d00',
'terminal-950': '#002600',
{% set links = {
"HTMX-PATTERNS": url_for("index"),
"Boosted Links": url_for('boosted'),
"Infinite Scroll": url_for('infinite'),
} %}
//
// // aqua
// 'terminal-50': '#f0f9ff',
// 'terminal-100': '#e0f2fe',
// 'terminal-200': '#bae6fd',
// 'terminal-300': '#7dd3fc',
// 'terminal-400': '#38bdf8',
// 'terminal-500': '#0ea5e9',
// 'terminal-600': '#0284c7',
// 'terminal-700': '#0369a1',
// 'terminal-800': '#075985',
// 'terminal-900': '#0c4a6e',
// 'terminal-950': '#042c47',
//
// //rainbow
// 'terminal-50': '#ffecf6',
// 'terminal-100': '#fbb6ce',
// 'terminal-200': '#f687b3',
// 'terminal-300': '#ed64a6',
// 'terminal-400': '#d53f8c',
// 'terminal-500': '#b83280',
// 'terminal-600': '#97266d',
// 'terminal-700': '#702459',
// 'terminal-800': '#521b41',
// 'terminal-900': '#36162e',
// 'terminal-950': '#1c0e1f',
//
// // sherbert lemon
// 'terminal-50': '#fffdf1',
// 'terminal-100': '#fffbda',
// 'terminal-200': '#fff7b3',
// 'terminal-300': '#fff280',
// 'terminal-400': '#ffea4d',
// 'terminal-500': '#ffe01a',
// 'terminal-600': '#e6c900',
// 'terminal-700': '#b3a300',
// 'terminal-800': '#806c00',
// 'terminal-900': '#4d4700',
// 'terminal-950': '#262300',
//
// //cobalt
// 'terminal-50': '#f0f5f9',
// 'terminal-100': '#d9e5f2',
// 'terminal-200': '#a6c0e6',
// 'terminal-300': '#739bda',
// 'terminal-400': '#4b7fce',
// 'terminal-400': '#ffea4d',
// 'terminal-500': '#315fb6',
// 'terminal-600': '#e6c900',
// 'terminal-700': '#b3a300',
// 'terminal-800': '#1c3b67',
// 'terminal-900': '#4d4700',
// 'terminal-950': '#0e1d30',
//
boxShadow: {
xlc: "0 0 60px 15px rgba(0, 0, 0, 0.3)",
lgc: "0 0 20px 0px #80ff80",
},
}
<body
class="justify-center items-center min-h-screen bg-gray-900 bg-no-repeat bg-cover bg-gradient-to-b from-pink-950/50 min-w-screen text-shadow-xl text-shadow-zinc-950 w-screen h-screen">
<div id="grit"
class="absolute top-0 right-0 bottom-0 left-0 justify-center items-center min-w-full bg-repeat bg-cover"
style="background-image: url(https://fokais.com/grit.svg), url(https://fokais.com/grit-light.svg); animation: pulse 10s cubic-bezier(0.4, 0, 0.6, 1) infinite; pointer-events: none">
</div>
<div id="content" class="flex flex-col items-center min-h-screen min-w-screen text-white border-b">
<nav
class="flex flex-col sm:flex-row flex-wrap w-screen gap-x-8 gap-y-2 justify-center items-center w-full p-4 bg-black border-b-4 border-gray-800 mb-8">
}
}
}
</script>
{% if DEBUG %}
{{ hot_reload.script(url_for('hot-reload') ) | safe }}
{% endif %}
{% endblock %}
</head>
{% set links = {
"HTMX-PATTERNS": url_for("index"),
"Boosted Links": url_for('boosted'),
"Infinite Scroll": url_for('infinite'),
"Toast": url_for('get_toast'),
"WebSocket": url_for('websocket_index'),
} %}
<body
class="justify-center items-center min-h-screen bg-gray-900 bg-no-repeat bg-cover bg-gradient-to-b from-terminal-950/20 min-w-screen text-shadow-xl text-shadow-zinc-950 w-screen h-screen overflow-x-hidden">
<div id="grit"
class="absolute top-0 right-0 bottom-0 left-0 justify-center items-center min-w-full bg-repeat bg-cover"
style="background-image: url(https://fokais.com/grit.svg), url(https://fokais.com/grit-light.svg); animation: pulse 10s cubic-bezier(0.4, 0, 0.6, 1) infinite; pointer-events: none">
</div>
<div id="content" class="flex flex-col items-center min-h-screen min-w-screen text-white border-b">
<nav
class="flex flex-col sm:flex-row flex-wrap w-screen gap-x-8 gap-y-2 justify-center items-center w-full p-4 bg-black border-b-2 border-terminal-400 mb-8 font-mono">
<!-- <a href="/" class="text-3xl gap-4 font-bold">HTMX PATTERNS</a> -->
<!-- <a href="/infinite" class="text-3xl font-bold text-yellow-400">INFINITE</a> -->
{% for link, url in links.items() %}
<a href="{{ url }}"
class="text-xl sm:text-3xl font-bold uppercase {% if not loop.first %}text-yellow-400{% endif %}">{{
link
}}</a>
{% endfor %}
</nav>
{% block content %}
{{ body | safe }}
{% endblock %}
</div>
</body>
{% for link, url in links.items() %}
<a href="{{ url }}"
class="text-xl sm:text-xl font-thin uppercase {% if not loop.first %}text-terminal-600{% endif %}">
{{ link }}
</a>
{% endfor %}
</nav>
{% block content %}
{{ body | safe }}
{% endblock %}
</div>
<section id='toast' class="fixed bottom-4 right-4">
</section>
</body>
</html>

View file

@ -2,60 +2,62 @@
{% block title %}Contact - {{ person_id }} - {{ person.name }}{% endblock %}
{% block content %}
<h1 id="title"
class="inline-block px-4 pb-0 mx-auto mb-0 text-center text-6xl sm:text-8xl font-black text-transparent bg-clip-text bg-gradient-to-r from-red-600 via-pink-500 to-yellow-400 ring-red-500 text-shadow-xl text-shadow-zinc-950 ring-5 leading-none">
class="inline-block px-4 pb-0 mx-auto mb-0 text-center text-6xl sm:text-8xl font-thin text-transparent bg-clip-text bg-gradient-to-r from-terminal-600 via-terminal-500 to-terminal-900 ring-red-700 text-shadow-xl text-shadow-zinc-950 ring-5 leading-none">
HTMX PATTERNS - BOOSTED
</h1>
<p class='text-3xl font-bold mt-0 mb-16 max-w-xl text-center prose-xl'>
<p class='text-3xl px-2 mt-0 mb-16 max-w-xl text-center prose-xl text-terminal-500 font-extralight'>
Contact - {{ person_id }}
</p>
<p class='text-3xl font-bold mt-0 mb-16 max-w-2xl text-center prose-xl'>
{{ person.name }} -
{% if person is not none %}
<p
class='max-w-xl container text-xl font-light px-2 mt-0 mb-4 py-2 text-center text-terminal-500 bg-terminal-950 prose-xl ring-1 ring-terminal-500 rounded-xl shadow-lg shadow-terminal-300/20'>
<span class='font-normal'>{{ person.name.upper() }}</span> -
{{ person.phone_number }}
</p>
{% else %}
<p
class='max-w-xl container text-xl font-light px-2 mt-0 mb-4 py-2 text-center text-terminal-500 bg-terminal-950 prose-xl ring-1 ring-terminal-500 rounded-xl shadow-lg shadow-terminal-300/20'>
Person not found
</p>
{% endif %}
<h2 class='text-3xl font-bold mt-0 max-w-xl text-center prose-xl mt-8'>
{% macro link(id, text, boosted=false) -%}
<a
class="
{% if id is none %}
pointer-events-none bg-terminal-950 text-terminal-900 ring-terminal-900
{% else %}
bg-terminal-950 hover:bg-terminal-900 hover:text-terminal-400 text-terminal-500 shadow-lg shadow-terminal-300/20 hover:shadow-terminal-300/30 ring-terminal-300
{% endif %}
cursor-pointer block text-center font-bold py-2 px-4 rounded w-full ring-1
"
{% if id is not none %}
href="{{ url_for('boosted', id=id) }}"
{% endif %}
{% if boosted %}
hx-boost="true"
{% endif %}>
{{ text }}
</a>
{%- endmacro %}
<h2 class='text-3xl font-light mt-0 max-w-xl text-center prose-xl mt-8 text-terminal-500'>
Boosted Links
</h2>
<div class='flex flex-row gap-4'>
{% if prev_id is not none %}
<a href="{{ url_for('boosted', id=prev_id) }}"
class="cursor-pointer bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded" hx-boost='true'>
Previous
</a>
{% else %}
<a class="pointer-events-none bg-gray-500 text-white font-bold py-2 px-4 rounded">
Previous
</a>
{% endif %}
<a href="{{ url_for('boosted', id=next_id) }}"
class="cursor-pointer bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded" hx-boost='true'>
Next
</a>
{{ link(prev_id, 'Previous', boosted=True) }}
{{ link(next_id, 'Next', boosted=True) }}
</div>
<h2 class='text-3xl font-bold mt-0 max-w-xl text-center prose-xl mt-8'>
<h2 class='text-3xl font-light mt-0 max-w-xl text-center prose-xl mt-8 text-terminal-500'>
Normal Links
</h2>
<div class='flex flex-row gap-4'>
{% if prev_id is not none %}
<a href="{{ url_for('boosted', id=prev_id) }}"
class="cursor-pointer bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded">
Previous
</a>
{% else %}
<a class="pointer-events-none bg-gray-500 text-white font-bold py-2 px-4 rounded">
Previous
</a>
{% endif %}
<a href="{{ url_for('boosted', id=next_id) }}"
class="cursor-pointer bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded">
Next
</a>
{{ link(prev_id, 'Previous', boosted=False) }}
{{ link(next_id, 'Next', boosted=False) }}
</div>
{% endblock %}

View file

@ -1,19 +1,34 @@
{% extends "base.html" %}
{% block content %}
<h1 id="title"
class="inline-block px-4 pb-0 mx-auto mb-0 text-center text-6xl sm:text-8xl font-black text-transparent bg-clip-text bg-gradient-to-r from-red-600 via-pink-500 to-yellow-400 ring-red-500 text-shadow-xl text-shadow-zinc-950 ring-5 leading-none">
class="inline-block px-4 pb-0 mx-auto mb-0 text-center text-6xl sm:text-8xl font-thin text-transparent bg-clip-text bg-gradient-to-r from-terminal-600 via-terminal-500 to-terminal-900 ring-red-700 text-shadow-xl text-shadow-zinc-950 ring-5 leading-none">
HTMX PATTERNS
</h1>
<p class='text-3xl px-2 font-bold mt-0 mb-16 max-w-xl text-center prose-xl'>
<p class='text-3xl px-2 mt-0 mb-16 max-w-xl text-center prose-xl text-terminal-500 font-extralight'>
A collection of HTMX patterns
</p>
<p class='text-xl px-2 mt-0 mb-16 max-w-xl prose-xl text-terminal-500 font-extralight'>
These are patterns that I have written based on content from the <a
href="https://hypermedia.systems/htmx-in-action/">hypermedia.systems</a>
book. There is lots of code duplication as each pattern is meant to be standalone.
</p>
<p class='text-xl px-2 mt-0 mb-16 max-w-xl prose-xl text-terminal-500 font-extralight'>
I currently make use of htmx with fastapi, sqlmodel, sqlite, and tailwindcss
for many of my projects. These patterns are here to serve for reference to
myself implemented using this stack in the most pure way possible to remain
simple and understandable. Sometimes real projects get complicated and are
hard to bring in new features correctly. This is a playground with completely
separate routers, models, and templates for each project.
</p>
<ul id="patterns" class="flex flex-col sm:grid grid-cols-3 gap-4">
{% for link, url in links.items() %}
<li class='w-full'>
<a href="{{ url }}"
class="cursor-pointer block bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded w-full">
class="cursor-pointer block text-center bg-terminal-950 hover:bg-terminal-900 hover:text-terminal-400 text-terminal-500 font-bold py-2 px-4 rounded w-full ring-terminal-300 ring-1 shadow-lg shadow-terminal-300/20 hover:shadow-terminal-300/30">
{{ link }}
</a>
</li>

View file

@ -2,22 +2,22 @@
{% block title %}Contacts List{% endblock %}
{% block content %}
<h1 id="title"
class="inline-block px-4 pb-0 mx-auto mb-0 text-center text-6xl sm:text-8xl font-black text-transparent bg-clip-text bg-gradient-to-r from-red-600 via-pink-500 to-yellow-400 ring-red-500 text-shadow-xl text-shadow-zinc-950 ring-5 leading-none">
class="inline-block px-4 pb-0 mx-auto mb-0 text-center text-6xl sm:text-8xl font-thin text-transparent bg-clip-text bg-gradient-to-r from-terminal-600 via-terminal-500 to-terminal-900 ring-red-700 text-shadow-xl text-shadow-zinc-950 ring-5 leading-none">
HTMX PATTERNS - INFINITE
</h1>
<p class='text-3xl font-bold mt-0 mb-16 max-w-xl text-center prose-xl'>
<p class='text-3xl px-2 mt-0 mb-16 max-w-xl text-center prose-xl text-terminal-500 font-extralight'>
Contacts List
</p>
<ul id="persons" class="flex flex-col gap-4 mb-16 px-4 sm:px-0">
<ul id="persons" class="flex flex-col gap-4 mb-16 px-4 sm:px-0 container max-w-xl">
{% include "infinite/persons_partial.html" %}
</ul>
<div id='persons-loading' class='spinner mb-24 animate-bounce'>
<p class='text-xl prose-xl'>loading more contacts</p>
<p class='text-xl prose-xl text-terminal-700'>loading more contacts</p>
<p class='text-xl prose-xl'>
<em class='text-red-500'>with simulated slow down</em>
<em class='text-terminal-500'>with simulated slow down</em>
</p>
</div>

View file

@ -1,16 +1,14 @@
{% for person in persons %}
<li
{% if loop.last %}
<li hx-get="{{ url_for('infinite', page=next_page) }}" hx-trigger="intersect once" hx-target="#persons"
hx-swap='beforeend' hx-indicator="#persons-loading"
class="cursor-pointer bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded">
{{ person.name }} -
{{ person.phone_number }}
</li>
{% else %}
<li class="cursor-pointer bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded">
{{ person.name }} -
{{ person.phone_number }}
</li>
hx-get="{{ url_for('infinite', page=next_page) }}"
hx-trigger="intersect once"
hx-target="#persons"
hx-swap='beforeend'
hx-indicator="#persons-loading"
{% endif %}
class='max-w-xl container text-xl font-light px-2 mt-0 mb-4 py-2 text-center text-terminal-500 bg-terminal-950 prose-xl ring-1 ring-terminal-500 rounded-xl shadow-lg shadow-terminal-300/20'>
<span class='font-normal'>{{ person.name.upper() }}</span> -
{{ person.phone_number }}
</li>
{% endfor %}

62
templates/tailwind.js Normal file

File diff suppressed because one or more lines are too long

0
templates/tailwindcss.js Normal file
View file

View file

@ -0,0 +1,9 @@
<div
class='text-terminal-500 font-extralight text-center text-3xl my-2 bg-terminal-950 ring-5 ring-terminal-500 rounded-lg px-4 py-2'
hx-delete="{{ url_for('null') }}"
hx-trigger='load delay:5s'
hx-swap='outerHTML'
>
{{ message }}
</div>

View file

@ -0,0 +1,17 @@
{% extends "base.html" %}
{% block title %}Toast{% endblock %}
{% block content %}
<h1 id="title"
class="inline-block px-4 pb-0 mx-auto mb-0 text-center text-6xl sm:text-8xl font-thin text-transparent bg-clip-text bg-gradient-to-r from-terminal-600 via-terminal-500 to-terminal-900 ring-red-700 text-shadow-xl text-shadow-zinc-950 ring-5 leading-none">
HTMX PATTERNS - TOAST
</h1>
<button
class="text-3xl my-24 px-8 py-2 rounded rounded-xl bg-terminal-950 hover:bg-terminal-900 hover:text-terminal-400 text-terminal-500 shadow-lg shadow-terminal-300/20 hover:shadow-terminal-300/30 ring-terminal-300"
hx-post="{{ url_for('post_toast') }}"
hx-target="#toast"
hx-swap="beforeend"
>
Submit
</button>
{% endblock %}

View file

@ -0,0 +1,26 @@
{% extends "base.html" %}
{% block title %}WebSocket{% endblock %}
{% block content %}
<h1 id="title"
class="inline-block px-4 pb-0 mx-auto mb-0 text-center text-6xl sm:text-8xl font-thin text-transparent bg-clip-text bg-gradient-to-r from-terminal-600 via-terminal-500 to-terminal-900 ring-red-700 text-shadow-xl text-shadow-zinc-950 ring-5 leading-none">
HTMX PATTERNS - WEBSOCKET
</h1>
<button
class="text-3xl my-24 px-8 py-2 rounded rounded-xl bg-terminal-950 hover:bg-terminal-900 hover:text-terminal-400 text-terminal-500 shadow-lg shadow-terminal-300/20 hover:shadow-terminal-300/30 ring-terminal-300"
hx-post="{{ url_for('post_toast') }}"
hx-target="#toast"
hx-swap="beforeend"
>
Submit
</button>
<div hx-ext="ws" ws-connect="/websocket/">
<div id="notifications"></div>
<div id="chat_room">
...
</div>
<form id="form" ws-send>
<input name="chat_message" class='text-black'>
</form>
</div>
{% endblock %}

476
templates/ws.js Normal file
View file

@ -0,0 +1,476 @@
/*
WebSockets Extension
============================
This extension adds support for WebSockets to htmx. See /www/extensions/ws.md for usage instructions.
*/
(function () {
/** @type {import("../htmx").HtmxInternalApi} */
var api;
htmx.defineExtension("ws", {
/**
* init is called once, when this extension is first registered.
* @param {import("../htmx").HtmxInternalApi} apiRef
*/
init: function (apiRef) {
// Store reference to internal API
api = apiRef;
// Default function for creating new EventSource objects
if (!htmx.createWebSocket) {
htmx.createWebSocket = createWebSocket;
}
// Default setting for reconnect delay
if (!htmx.config.wsReconnectDelay) {
htmx.config.wsReconnectDelay = "full-jitter";
}
},
/**
* onEvent handles all events passed to this extension.
*
* @param {string} name
* @param {Event} evt
*/
onEvent: function (name, evt) {
var parent = evt.target || evt.detail.elt;
switch (name) {
// Try to close the socket when elements are removed
case "htmx:beforeCleanupElement":
var internalData = api.getInternalData(parent)
if (internalData.webSocket) {
internalData.webSocket.close();
}
return;
// Try to create websockets when elements are processed
case "htmx:beforeProcessNode":
forEach(queryAttributeOnThisOrChildren(parent, "ws-connect"), function (child) {
ensureWebSocket(child)
});
forEach(queryAttributeOnThisOrChildren(parent, "ws-send"), function (child) {
ensureWebSocketSend(child)
});
}
}
});
function splitOnWhitespace(trigger) {
return trigger.trim().split(/\s+/);
}
function getLegacyWebsocketURL(elt) {
var legacySSEValue = api.getAttributeValue(elt, "hx-ws");
if (legacySSEValue) {
var values = splitOnWhitespace(legacySSEValue);
for (var i = 0; i < values.length; i++) {
var value = values[i].split(/:(.+)/);
if (value[0] === "connect") {
return value[1];
}
}
}
}
/**
* ensureWebSocket creates a new WebSocket on the designated element, using
* the element's "ws-connect" attribute.
* @param {HTMLElement} socketElt
* @returns
*/
function ensureWebSocket(socketElt) {
// If the element containing the WebSocket connection no longer exists, then
// do not connect/reconnect the WebSocket.
if (!api.bodyContains(socketElt)) {
return;
}
// Get the source straight from the element's value
var wssSource = api.getAttributeValue(socketElt, "ws-connect")
if (wssSource == null || wssSource === "") {
var legacySource = getLegacyWebsocketURL(socketElt);
if (legacySource == null) {
return;
} else {
wssSource = legacySource;
}
}
// Guarantee that the wssSource value is a fully qualified URL
if (wssSource.indexOf("/") === 0) {
var base_part = location.hostname + (location.port ? ':' + location.port : '');
if (location.protocol === 'https:') {
wssSource = "wss://" + base_part + wssSource;
} else if (location.protocol === 'http:') {
wssSource = "ws://" + base_part + wssSource;
}
}
var socketWrapper = createWebsocketWrapper(socketElt, function () {
return htmx.createWebSocket(wssSource)
});
socketWrapper.addEventListener('message', function (event) {
if (maybeCloseWebSocketSource(socketElt)) {
return;
}
var response = event.data;
if (!api.triggerEvent(socketElt, "htmx:wsBeforeMessage", {
message: response,
socketWrapper: socketWrapper.publicInterface
})) {
return;
}
api.withExtensions(socketElt, function (extension) {
response = extension.transformResponse(response, null, socketElt);
});
var settleInfo = api.makeSettleInfo(socketElt);
var fragment = api.makeFragment(response);
if (fragment.children.length) {
var children = Array.from(fragment.children);
for (var i = 0; i < children.length; i++) {
api.oobSwap(api.getAttributeValue(children[i], "hx-swap-oob") || "true", children[i], settleInfo);
}
}
api.settleImmediately(settleInfo.tasks);
api.triggerEvent(socketElt, "htmx:wsAfterMessage", { message: response, socketWrapper: socketWrapper.publicInterface })
});
// Put the WebSocket into the HTML Element's custom data.
api.getInternalData(socketElt).webSocket = socketWrapper;
}
/**
* @typedef {Object} WebSocketWrapper
* @property {WebSocket} socket
* @property {Array<{message: string, sendElt: Element}>} messageQueue
* @property {number} retryCount
* @property {(message: string, sendElt: Element) => void} sendImmediately sendImmediately sends message regardless of websocket connection state
* @property {(message: string, sendElt: Element) => void} send
* @property {(event: string, handler: Function) => void} addEventListener
* @property {() => void} handleQueuedMessages
* @property {() => void} init
* @property {() => void} close
*/
/**
*
* @param socketElt
* @param socketFunc
* @returns {WebSocketWrapper}
*/
function createWebsocketWrapper(socketElt, socketFunc) {
var wrapper = {
socket: null,
messageQueue: [],
retryCount: 0,
/** @type {Object<string, Function[]>} */
events: {},
addEventListener: function (event, handler) {
if (this.socket) {
this.socket.addEventListener(event, handler);
}
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(handler);
},
sendImmediately: function (message, sendElt) {
if (!this.socket) {
api.triggerErrorEvent()
}
if (!sendElt || api.triggerEvent(sendElt, 'htmx:wsBeforeSend', {
message: message,
socketWrapper: this.publicInterface
})) {
this.socket.send(message);
sendElt && api.triggerEvent(sendElt, 'htmx:wsAfterSend', {
message: message,
socketWrapper: this.publicInterface
})
}
},
send: function (message, sendElt) {
if (this.socket.readyState !== this.socket.OPEN) {
this.messageQueue.push({ message: message, sendElt: sendElt });
} else {
this.sendImmediately(message, sendElt);
}
},
handleQueuedMessages: function () {
while (this.messageQueue.length > 0) {
var queuedItem = this.messageQueue[0]
if (this.socket.readyState === this.socket.OPEN) {
this.sendImmediately(queuedItem.message, queuedItem.sendElt);
this.messageQueue.shift();
} else {
break;
}
}
},
init: function () {
if (this.socket && this.socket.readyState === this.socket.OPEN) {
// Close discarded socket
this.socket.close()
}
// Create a new WebSocket and event handlers
/** @type {WebSocket} */
var socket = socketFunc();
// The event.type detail is added for interface conformance with the
// other two lifecycle events (open and close) so a single handler method
// can handle them polymorphically, if required.
api.triggerEvent(socketElt, "htmx:wsConnecting", { event: { type: 'connecting' } });
this.socket = socket;
socket.onopen = function (e) {
wrapper.retryCount = 0;
api.triggerEvent(socketElt, "htmx:wsOpen", { event: e, socketWrapper: wrapper.publicInterface });
wrapper.handleQueuedMessages();
}
socket.onclose = function (e) {
// If socket should not be connected, stop further attempts to establish connection
// If Abnormal Closure/Service Restart/Try Again Later, then set a timer to reconnect after a pause.
if (!maybeCloseWebSocketSource(socketElt) && [1006, 1012, 1013].indexOf(e.code) >= 0) {
var delay = getWebSocketReconnectDelay(wrapper.retryCount);
setTimeout(function () {
wrapper.retryCount += 1;
wrapper.init();
}, delay);
}
// Notify client code that connection has been closed. Client code can inspect `event` field
// to determine whether closure has been valid or abnormal
api.triggerEvent(socketElt, "htmx:wsClose", { event: e, socketWrapper: wrapper.publicInterface })
};
socket.onerror = function (e) {
api.triggerErrorEvent(socketElt, "htmx:wsError", { error: e, socketWrapper: wrapper });
maybeCloseWebSocketSource(socketElt);
};
var events = this.events;
Object.keys(events).forEach(function (k) {
events[k].forEach(function (e) {
socket.addEventListener(k, e);
})
});
},
close: function () {
this.socket.close()
}
}
wrapper.init();
wrapper.publicInterface = {
send: wrapper.send.bind(wrapper),
sendImmediately: wrapper.sendImmediately.bind(wrapper),
queue: wrapper.messageQueue
};
return wrapper;
}
/**
* ensureWebSocketSend attaches trigger handles to elements with
* "ws-send" attribute
* @param {HTMLElement} elt
*/
function ensureWebSocketSend(elt) {
var legacyAttribute = api.getAttributeValue(elt, "hx-ws");
if (legacyAttribute && legacyAttribute !== 'send') {
return;
}
var webSocketParent = api.getClosestMatch(elt, hasWebSocket)
processWebSocketSend(webSocketParent, elt);
}
/**
* hasWebSocket function checks if a node has webSocket instance attached
* @param {HTMLElement} node
* @returns {boolean}
*/
function hasWebSocket(node) {
return api.getInternalData(node).webSocket != null;
}
/**
* processWebSocketSend adds event listeners to the <form> element so that
* messages can be sent to the WebSocket server when the form is submitted.
* @param {HTMLElement} socketElt
* @param {HTMLElement} sendElt
*/
function processWebSocketSend(socketElt, sendElt) {
var nodeData = api.getInternalData(sendElt);
var triggerSpecs = api.getTriggerSpecs(sendElt);
triggerSpecs.forEach(function (ts) {
api.addTriggerHandler(sendElt, ts, nodeData, function (elt, evt) {
if (maybeCloseWebSocketSource(socketElt)) {
return;
}
/** @type {WebSocketWrapper} */
var socketWrapper = api.getInternalData(socketElt).webSocket;
var headers = api.getHeaders(sendElt, api.getTarget(sendElt));
var results = api.getInputValues(sendElt, 'post');
var errors = results.errors;
var rawParameters = results.values;
var expressionVars = api.getExpressionVars(sendElt);
var allParameters = api.mergeObjects(rawParameters, expressionVars);
var filteredParameters = api.filterValues(allParameters, sendElt);
var sendConfig = {
parameters: filteredParameters,
unfilteredParameters: allParameters,
headers: headers,
errors: errors,
triggeringEvent: evt,
messageBody: undefined,
socketWrapper: socketWrapper.publicInterface
};
if (!api.triggerEvent(elt, 'htmx:wsConfigSend', sendConfig)) {
return;
}
if (errors && errors.length > 0) {
api.triggerEvent(elt, 'htmx:validation:halted', errors);
return;
}
var body = sendConfig.messageBody;
if (body === undefined) {
var toSend = Object.assign({}, sendConfig.parameters);
if (sendConfig.headers)
toSend['HEADERS'] = headers;
body = JSON.stringify(toSend);
}
socketWrapper.send(body, elt);
if (evt && api.shouldCancel(evt, elt)) {
evt.preventDefault();
}
});
});
}
/**
* getWebSocketReconnectDelay is the default easing function for WebSocket reconnects.
* @param {number} retryCount // The number of retries that have already taken place
* @returns {number}
*/
function getWebSocketReconnectDelay(retryCount) {
/** @type {"full-jitter" | ((retryCount:number) => number)} */
var delay = htmx.config.wsReconnectDelay;
if (typeof delay === 'function') {
return delay(retryCount);
}
if (delay === 'full-jitter') {
var exp = Math.min(retryCount, 6);
var maxDelay = 1000 * Math.pow(2, exp);
return maxDelay * Math.random();
}
logError('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"');
}
/**
* maybeCloseWebSocketSource checks to the if the element that created the WebSocket
* still exists in the DOM. If NOT, then the WebSocket is closed and this function
* returns TRUE. If the element DOES EXIST, then no action is taken, and this function
* returns FALSE.
*
* @param {*} elt
* @returns
*/
function maybeCloseWebSocketSource(elt) {
if (!api.bodyContains(elt)) {
api.getInternalData(elt).webSocket.close();
return true;
}
return false;
}
/**
* createWebSocket is the default method for creating new WebSocket objects.
* it is hoisted into htmx.createWebSocket to be overridden by the user, if needed.
*
* @param {string} url
* @returns WebSocket
*/
function createWebSocket(url) {
var sock = new WebSocket(url, []);
sock.binaryType = htmx.config.wsBinaryType;
return sock;
}
/**
* queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT.
*
* @param {HTMLElement} elt
* @param {string} attributeName
*/
function queryAttributeOnThisOrChildren(elt, attributeName) {
var result = []
// If the parent element also contains the requested attribute, then add it to the results too.
if (api.hasAttribute(elt, attributeName) || api.hasAttribute(elt, "hx-ws")) {
result.push(elt);
}
// Search all child nodes that match the requested attribute
elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "], [data-hx-ws], [hx-ws]").forEach(function (node) {
result.push(node)
})
return result
}
/**
* @template T
* @param {T[]} arr
* @param {(T) => void} func
*/
function forEach(arr, func) {
if (arr) {
for (var i = 0; i < arr.length; i++) {
func(arr[i]);
}
}
}
})();