├── tests ├── __init__.py └── test_list.py ├── todo ├── tasks │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ └── 0001_initial.py │ ├── admin.py │ ├── forms.py │ ├── models.py │ ├── urls.py │ └── views.py ├── __init__.py ├── monkey.py ├── mypy_plugin.py ├── asgi.py ├── wsgi.py ├── urls.py └── settings.py ├── public └── favicon.ico ├── docker ├── backend │ ├── run-dev.sh │ └── Dockerfile └── postgres │ ├── dump.sh │ └── restore.sh ├── templates ├── tasks │ ├── done_state.html │ ├── partials │ │ ├── list-form.html │ │ ├── list-task.html │ │ └── list.html │ ├── form.html │ └── list.html └── base.html ├── requirements.in ├── manage.py ├── docker-compose.dev.yml.default ├── pyproject.toml ├── requirements.txt ├── README.rst └── static └── htmx-1.9.2.js /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /todo/tasks/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /todo/tasks/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /todo/__init__.py: -------------------------------------------------------------------------------- 1 | from . import monkey # noqa 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radiac/django-htmx-todo/main/public/favicon.ico -------------------------------------------------------------------------------- /todo/tasks/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib.admin import site 2 | 3 | from .models import Task 4 | 5 | 6 | site.register(Task) 7 | -------------------------------------------------------------------------------- /docker/backend/run-dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | cd /project 5 | 6 | ./manage.py migrate 7 | ./manage.py runserver 0:8000 8 | -------------------------------------------------------------------------------- /todo/tasks/forms.py: -------------------------------------------------------------------------------- 1 | from django.forms import ModelForm 2 | 3 | from .models import Task 4 | 5 | 6 | class TaskForm(ModelForm): 7 | class Meta: 8 | model = Task 9 | fields = ("label",) 10 | -------------------------------------------------------------------------------- /todo/monkey.py: -------------------------------------------------------------------------------- 1 | """ 2 | Global monkey patches 3 | 4 | Loaded by .__init__ 5 | """ 6 | import re 7 | 8 | from django.template import base as template_base 9 | 10 | 11 | # Add support for multi-line template tags 12 | template_base.tag_re = re.compile(template_base.tag_re.pattern, re.DOTALL) 13 | -------------------------------------------------------------------------------- /tests/test_list.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from todo.tasks.models import Task 4 | 5 | 6 | pytestmark = pytest.mark.django_db 7 | 8 | 9 | def test_task_list(client): 10 | task = Task.objects.create(label="test task 1", done=False) 11 | 12 | response = client.get("/") 13 | assert task in response.context["tasks"] 14 | assert task.label in str(response.content) 15 | -------------------------------------------------------------------------------- /templates/tasks/done_state.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Task is {% if not task.done %}not{% endif %} done{% endblock %} 4 | 5 | {% block content %} 6 | 7 |

The task is now marked as {% if not task.done %}not{% endif %} done:

8 | 9 |
10 | {{ task.label }} 11 |
12 | 13 | 16 | 17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /requirements.in: -------------------------------------------------------------------------------- 1 | # Dev 2 | black 3 | django-debug-toolbar 4 | ipdb 5 | ipython 6 | isort 7 | pip-tools 8 | ruff 9 | 10 | # Core 11 | django~=4.2.0 12 | pillow 13 | psycopg2-binary 14 | whitenoise 15 | 16 | # Third party 17 | django-configurations 18 | 19 | # Testing 20 | django-stubs[compatible-mypy] 21 | mypy~=1.3.0 # pinned for django-stubs 22 | model_bakery 23 | pytest 24 | pytest-cov 25 | pytest-django 26 | 27 | # Deployment 28 | gunicorn 29 | sentry-sdk 30 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "todo.settings") 10 | os.environ.setdefault("DJANGO_CONFIGURATION", "Dev") 11 | 12 | from configurations.management import execute_from_command_line 13 | 14 | execute_from_command_line(sys.argv) 15 | 16 | 17 | if __name__ == "__main__": 18 | main() 19 | -------------------------------------------------------------------------------- /todo/mypy_plugin.py: -------------------------------------------------------------------------------- 1 | """ 2 | Add support for django-configurations to django-stubs 3 | 4 | Based on https://github.com/typeddjango/django-stubs/pull/180#issuecomment-820062352 5 | """ 6 | from os import environ 7 | 8 | from configurations.importer import install 9 | from mypy_django_plugin import main 10 | 11 | 12 | def plugin(version): 13 | environ.setdefault("DJANGO_SETTINGS_MODULE", "todo.config") 14 | environ.setdefault("DJANGO_CONFIGURATION", "Test") 15 | install() 16 | return main.plugin(version) 17 | -------------------------------------------------------------------------------- /todo/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for todo project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | 13 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "todo.settings") 14 | os.environ.setdefault("DJANGO_CONFIGURATION", "Dev") 15 | 16 | from configurations.asgi import get_asgi_application # noqa 17 | 18 | 19 | application = get_asgi_application() 20 | -------------------------------------------------------------------------------- /todo/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for todo project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | 13 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "todo.settings") 14 | os.environ.setdefault("DJANGO_CONFIGURATION", "Dev") 15 | 16 | from configurations.wsgi import get_wsgi_application # noqa 17 | 18 | 19 | application = get_wsgi_application() 20 | -------------------------------------------------------------------------------- /todo/tasks/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class TaskQuerySet(models.QuerySet): 5 | def done(self): 6 | return self.filter(done=True) 7 | 8 | def not_done(self): 9 | return self.filter(done=False) 10 | 11 | 12 | class Task(models.Model): 13 | label = models.CharField(max_length=255) 14 | done = models.BooleanField(default=False) 15 | 16 | objects = TaskQuerySet.as_manager() 17 | 18 | class Meta: 19 | ordering = ("pk",) 20 | 21 | def __str__(self): 22 | return self.label 23 | -------------------------------------------------------------------------------- /docker/backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-slim 2 | 3 | RUN mkdir /build 4 | WORKDIR /build 5 | 6 | # Build process dependencies 7 | RUN apt-get update \ 8 | && apt-get install -y --no-install-recommends \ 9 | git \ 10 | gcc \ 11 | libc6-dev \ 12 | && rm -rf /var/lib/apt/lists/* 13 | 14 | # Expose runserver/gunicorn 15 | EXPOSE 8000/tcp 16 | 17 | # Full python requirements to support development 18 | COPY ./requirements.txt /build/requirements.txt 19 | RUN pip install -r /build/requirements.txt 20 | 21 | # Run Django 22 | WORKDIR /project 23 | -------------------------------------------------------------------------------- /todo/tasks/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import done_state_view, form_view, list_view 4 | 5 | 6 | app_name = "bookmarkman" 7 | urlpatterns = [ 8 | path("", list_view, name="list"), 9 | path("add/", form_view, name="add"), 10 | path("/", form_view, name="edit"), 11 | path( 12 | "done//", 13 | done_state_view, 14 | name="done", 15 | kwargs={"done_state": True}, 16 | ), 17 | path( 18 | "not_done//", 19 | done_state_view, 20 | name="not_done", 21 | kwargs={"done_state": False}, 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /templates/tasks/partials/list-form.html: -------------------------------------------------------------------------------- 1 | {% comment %} 2 | This is the only partial which isn't used by the non-js version of the site; technically 3 | we could have merged the HTML form, but the differences between them would have 4 | cluttered that template. 5 | {% endcomment %} 6 | 7 |
  • 8 |
    9 | {% csrf_token %} 10 | {{ form.label }} 11 | 18 |
    19 |
  • -------------------------------------------------------------------------------- /docker/postgres/dump.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Dump the database to the backup dir 3 | # 4 | # Usage: 5 | # docker-compose exec postgres /project/docker/postgres/dump.sh 6 | # 7 | 8 | # If no filename specified, use timestamp 9 | if [ -z "$1" ]; then 10 | # Timestamp 11 | FILENAME="$(date + "%Y-%m-%d_%H%M%S").dump" 12 | else 13 | FILENAME="$1" 14 | fi 15 | 16 | 17 | if [[ $FILENAME =~ \.dump$ ]]; then 18 | # If filename ends .dump, do a dump instead 19 | pg_dump --format=custom --username=$POSTGRES_DB $POSTGRES_DB > /backup/$FILENAME 20 | else 21 | pg_dump --clean --no-owner --username=$POSTGRES_DB > /backup/$FILENAME 22 | fi 23 | -------------------------------------------------------------------------------- /templates/tasks/form.html: -------------------------------------------------------------------------------- 1 | {% comment %} 2 | This form is only rendered for users who don't have htmx. In practice we should merge 3 | this and the partials/list-form - developers will normally just be using htmx, so as it 4 | stands there's a high risk this will get forgotten about when the partial list-form is 5 | made more complicated. 6 | {% endcomment %} 7 | 8 | {% extends "base.html" %} 9 | 10 | {% block title %}{% if task %}Edit{% else %}Add{% endif %} task{% endblock %} 11 | 12 | {% block content %} 13 | 14 |
    15 | {% csrf_token %} 16 | {{ form.as_p }} 17 | 18 |
    19 | 20 | {% endblock %} -------------------------------------------------------------------------------- /todo/tasks/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2023-06-22 16:47 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | initial = True 8 | 9 | dependencies = [] 10 | 11 | operations = [ 12 | migrations.CreateModel( 13 | name="Task", 14 | fields=[ 15 | ( 16 | "id", 17 | models.BigAutoField( 18 | auto_created=True, 19 | primary_key=True, 20 | serialize=False, 21 | verbose_name="ID", 22 | ), 23 | ), 24 | ("label", models.CharField(max_length=255)), 25 | ("done", models.BooleanField(default=False)), 26 | ], 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /docker/postgres/restore.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Restore the database from a dump 3 | # 4 | # Usage: 5 | # docker-compose exec postgres /project/docker/postgres/restore.sh 6 | # 7 | 8 | # If no filename specified, load from database.dump if found else database.sql 9 | if [ -z "$1" ]; then 10 | if [ -f /backup/database.dump ]; then 11 | FILENAME=database.dump 12 | else 13 | FILENAME=database.sql 14 | fi 15 | 16 | else 17 | FILENAME="$1" 18 | fi 19 | 20 | 21 | if [[ $FILENAME =~ \.dump$ ]]; then 22 | # For a PostgreSQL custom-format dump: 23 | pg_restore --clean --if-exists --no-acl --verbose --format=custom --no-owner \ 24 | --username=$POSTGRES_USER --dbname=$POSTGRES_DB /backup/$FILENAME.dump 25 | else 26 | # For an SQL dump created using dump.sh: 27 | psql --username=$POSTGRES_USER --dbname=$POSTGRES_DB --file=/backup/$FILENAME 28 | fi 29 | -------------------------------------------------------------------------------- /templates/tasks/partials/list-task.html: -------------------------------------------------------------------------------- 1 |
  • 2 | {# Task label doubles as edit; htmx swaps for a form #} 3 | {{ task.label }} 10 | 11 | {# Status toggles #} 12 | {% if not task.done %} 13 | Mark as done 20 | {% else %} 21 | Mark as not done 28 | {% endif %} 29 |
  • 30 | -------------------------------------------------------------------------------- /templates/tasks/partials/list.html: -------------------------------------------------------------------------------- 1 |
      2 | {% if filter_done is True or filter_done is False %} 3 |
    • 4 | Show all 9 |
    • 10 | {% endif %} 11 | 12 | {% if filter_done is not True %} 13 |
    • 14 | Only show done 19 |
    • 20 | {% endif %} 21 | 22 | {% if filter_done is not False %} 23 |
    • 24 | Only show not done 29 |
    • 30 | {% endif %} 31 |
    32 | 33 |
      34 | {% for task in tasks %} 35 | {% include "tasks/partials/list-task.html" %} 36 | {% endfor %} 37 |
    38 | -------------------------------------------------------------------------------- /docker-compose.dev.yml.default: -------------------------------------------------------------------------------- 1 | # Configuration for local development 2 | # 3 | version: '3' 4 | 5 | services: 6 | postgres: 7 | image: docker.io/postgres:12-alpine 8 | ports: 9 | - "5432:5432" 10 | volumes: 11 | - "../docker-store/db:/db" 12 | - "../docker-store/backup:/backup" 13 | - ".:/project" 14 | environment: 15 | PGDATA: /db 16 | POSTGRES_DB: postgres 17 | POSTGRES_USER: postgres 18 | POSTGRES_PASSWORD: postgres 19 | 20 | backend: 21 | build: 22 | context: ./ 23 | dockerfile: "./docker/backend/Dockerfile" 24 | command: [ "/project/docker/backend/run-dev.sh" ] 25 | environment: 26 | DJANGO_CONFIGURATION: Dev 27 | DJANGO_STORE_PATH: /store 28 | POSTGRES_HOST: postgres 29 | POSTGRES_DB: postgres 30 | POSTGRES_USER: postgres 31 | POSTGRES_PASSWORD: postgres 32 | PYTHONUNBUFFERED: 1 33 | ports: 34 | - "8000:8000" 35 | volumes: 36 | - "./:/project" 37 | - "../docker-store/store:/store" 38 | depends_on: 39 | - postgres 40 | -------------------------------------------------------------------------------- /todo/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | URL configuration for todo project. 3 | 4 | The `urlpatterns` list routes URLs to views. For more information please see: 5 | https://docs.djangoproject.com/en/4.2/topics/http/urls/ 6 | Examples: 7 | Function views 8 | 1. Add an import: from my_app import views 9 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 10 | Class-based views 11 | 1. Add an import: from other_app.views import Home 12 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 13 | Including another URLconf 14 | 1. Import the include() function: from django.urls import include, path 15 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 16 | """ 17 | from django.conf import settings 18 | from django.contrib import admin 19 | from django.urls import include, path 20 | 21 | 22 | urlpatterns = [ 23 | path("admin/", admin.site.urls), 24 | path("", include("todo.tasks.urls", namespace="tasks")), 25 | ] 26 | 27 | # Serve static files 28 | if settings.DEBUG: 29 | from django.conf.urls.static import static 30 | 31 | urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) # type: ignore 32 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) # type: ignore 33 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "todo-site" 3 | version = "0.1" 4 | 5 | [tool.pytest.ini_options] 6 | addopts = "--cov=todo --cov-report=term --cov-report=html" 7 | testpaths = [ 8 | "tests", 9 | ] 10 | DJANGO_SETTINGS_MODULE = "todo.settings" 11 | DJANGO_CONFIGURATION = "Test" 12 | 13 | [tool.coverage.run] 14 | source = ["todo"] 15 | 16 | [tool.black] 17 | line-length = 88 18 | target-version = ["py311"] 19 | include = "\\.pyi?$" 20 | 21 | [tool.isort] 22 | multi_line_output = 3 23 | line_length = 88 24 | known_django = "django" 25 | sections = ["FUTURE", "STDLIB", "DJANGO", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"] 26 | include_trailing_comma = true 27 | lines_after_imports = 2 28 | skip = [".git", "node_modules", ".tox"] 29 | 30 | [tool.mypy] 31 | follow_imports = "skip" 32 | ignore_missing_imports = true 33 | check_untyped_defs = true 34 | plugins = [ 35 | "./todo/mypy_plugin.py", 36 | ] 37 | 38 | [tool.django-stubs] 39 | django_settings_module = "todo.settings" 40 | 41 | [tool.ruff] 42 | line-length = 88 43 | select = ["E", "F"] 44 | ignore = [ 45 | "E501", # line length 46 | ] 47 | exclude = [ 48 | ".tox", 49 | ".git", 50 | "*/static/CACHE/*", 51 | "docs", 52 | "node_modules", 53 | "static_root", 54 | ] 55 | 56 | [tool.djlint] 57 | profile="django" 58 | -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | Tasks 6 | 43 | 44 | 45 | 46 |
    47 |

    {% block title %}{% endblock %}

    48 | 49 | {% block content %}{% endblock %} 50 |
    51 | 52 | 53 | -------------------------------------------------------------------------------- /templates/tasks/list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Tasks{% endblock %} 4 | 5 | {% block extra_style %} 6 | /* Basic task list styling */ 7 | ul.tasks { 8 | list-style:none; 9 | margin: 0; 10 | padding: 0; 11 | } 12 | ul.tasks li { 13 | display: flex; 14 | padding: 1rem 0.5rem; 15 | background: #f8f8f8; 16 | } 17 | ul.tasks li:nth-child(even) { 18 | background: #f0f0f0; 19 | } 20 | 21 | /* Done tasks have strikethrough */ 22 | ul.tasks li.done .task { 23 | text-decoration: line-through; 24 | } 25 | 26 | /* Link and button styling */ 27 | ul.tasks a { 28 | color: #333; 29 | text-decoration: none; 30 | } 31 | ul.tasks a:hover { 32 | text-decoration: underline; 33 | } 34 | ul.tasks a.button { 35 | margin-left: auto; 36 | color: #115599; 37 | } 38 | ul.tasks a.button:hover { 39 | color: #6699aa; 40 | } 41 | 42 | /* Style the task form when we're using HTMX */ 43 | ul.tasks li form { 44 | display: flex; 45 | flex: 1 1 auto; 46 | align-items: center; 47 | gap: 2rem; 48 | } 49 | ul.tasks li form [type="text"] { 50 | margin: 0; 51 | flex: 1 1 auto; 52 | } 53 | ul.tasks [type="submit"] { 54 | flex: 0 0 auto; 55 | margin-left: auto; 56 | } 57 | {% endblock %} 58 | 59 | {% block content %} 60 | 61 | {% include "tasks/partials/list.html" %} 62 | 63 | 73 | 74 | {% endblock %} -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.11 3 | # by the following command: 4 | # 5 | # pip-compile 6 | # 7 | asgiref==3.7.2 8 | # via django 9 | asttokens==2.2.1 10 | # via stack-data 11 | backcall==0.2.0 12 | # via ipython 13 | black==23.3.0 14 | # via -r requirements.in 15 | build==0.10.0 16 | # via pip-tools 17 | certifi==2023.5.7 18 | # via sentry-sdk 19 | click==8.1.3 20 | # via 21 | # black 22 | # pip-tools 23 | coverage[toml]==7.2.7 24 | # via pytest-cov 25 | decorator==5.1.1 26 | # via 27 | # ipdb 28 | # ipython 29 | django==4.2.2 30 | # via 31 | # -r requirements.in 32 | # django-configurations 33 | # django-debug-toolbar 34 | # django-stubs 35 | # django-stubs-ext 36 | # model-bakery 37 | django-configurations==2.4.1 38 | # via -r requirements.in 39 | django-debug-toolbar==4.1.0 40 | # via -r requirements.in 41 | django-stubs[compatible-mypy]==4.2.1 42 | # via -r requirements.in 43 | django-stubs-ext==4.2.1 44 | # via django-stubs 45 | executing==1.2.0 46 | # via stack-data 47 | gunicorn==20.1.0 48 | # via -r requirements.in 49 | iniconfig==2.0.0 50 | # via pytest 51 | ipdb==0.13.13 52 | # via -r requirements.in 53 | ipython==8.14.0 54 | # via 55 | # -r requirements.in 56 | # ipdb 57 | isort==5.12.0 58 | # via -r requirements.in 59 | jedi==0.18.2 60 | # via ipython 61 | matplotlib-inline==0.1.6 62 | # via ipython 63 | model-bakery==1.12.0 64 | # via -r requirements.in 65 | mypy==1.3.0 66 | # via 67 | # -r requirements.in 68 | # django-stubs 69 | mypy-extensions==1.0.0 70 | # via 71 | # black 72 | # mypy 73 | packaging==23.1 74 | # via 75 | # black 76 | # build 77 | # pytest 78 | parso==0.8.3 79 | # via jedi 80 | pathspec==0.11.1 81 | # via black 82 | pexpect==4.8.0 83 | # via ipython 84 | pickleshare==0.7.5 85 | # via ipython 86 | pillow==9.5.0 87 | # via -r requirements.in 88 | pip-tools==6.13.0 89 | # via -r requirements.in 90 | platformdirs==3.7.0 91 | # via black 92 | pluggy==1.2.0 93 | # via pytest 94 | prompt-toolkit==3.0.38 95 | # via ipython 96 | psycopg2-binary==2.9.6 97 | # via -r requirements.in 98 | ptyprocess==0.7.0 99 | # via pexpect 100 | pure-eval==0.2.2 101 | # via stack-data 102 | pygments==2.15.1 103 | # via ipython 104 | pyproject-hooks==1.0.0 105 | # via build 106 | pytest==7.3.2 107 | # via 108 | # -r requirements.in 109 | # pytest-cov 110 | # pytest-django 111 | pytest-cov==4.1.0 112 | # via -r requirements.in 113 | pytest-django==4.5.2 114 | # via -r requirements.in 115 | ruff==0.0.274 116 | # via -r requirements.in 117 | sentry-sdk==1.26.0 118 | # via -r requirements.in 119 | six==1.16.0 120 | # via asttokens 121 | sqlparse==0.4.4 122 | # via 123 | # django 124 | # django-debug-toolbar 125 | stack-data==0.6.2 126 | # via ipython 127 | traitlets==5.9.0 128 | # via 129 | # ipython 130 | # matplotlib-inline 131 | types-pytz==2023.3.0.0 132 | # via django-stubs 133 | types-pyyaml==6.0.12.10 134 | # via django-stubs 135 | typing-extensions==4.6.3 136 | # via 137 | # django-stubs 138 | # django-stubs-ext 139 | # mypy 140 | urllib3==2.0.3 141 | # via sentry-sdk 142 | wcwidth==0.2.6 143 | # via prompt-toolkit 144 | wheel==0.40.0 145 | # via pip-tools 146 | whitenoise==6.5.0 147 | # via -r requirements.in 148 | 149 | # The following packages are considered to be unsafe in a requirements file: 150 | # pip 151 | # setuptools 152 | -------------------------------------------------------------------------------- /todo/tasks/views.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from typing import Any 3 | 4 | from django.shortcuts import get_object_or_404, redirect, render 5 | 6 | from .forms import TaskForm 7 | from .models import Task 8 | 9 | 10 | def check_htmx(fn): 11 | """ 12 | Decorator to check if the request is from htmx, and add a flag to the request if it 13 | is. 14 | 15 | For function-based views like this, this would normally be done in middleware, 16 | probably using django-htmx. 17 | 18 | For class-based views you'd probably then want to add a check to get_template_name() 19 | and then pick up partial_template_name from a class attribute 20 | """ 21 | 22 | @wraps(fn) 23 | def is_htmx(request, *args, **kwargs): 24 | # htmx sets the HX-Request header to "true" when it's initiating the request 25 | if request.headers.get("HX-Request", "false") == "true": 26 | request.is_htmx = True 27 | else: 28 | request.is_htmx = False 29 | return fn(request, *args, **kwargs) 30 | 31 | return is_htmx 32 | 33 | 34 | @check_htmx 35 | def list_view(request): 36 | qs = Task.objects.all() 37 | 38 | filter_done: bool | None 39 | match request.GET.get("done"): 40 | case "true": 41 | filter_done = True 42 | qs = qs.done() 43 | case "false": 44 | filter_done = False 45 | qs = qs.not_done() 46 | case _: 47 | filter_done = None 48 | 49 | context: dict[str, Any] = { 50 | "tasks": qs, 51 | "filter_done": filter_done, 52 | } 53 | return render( 54 | request, 55 | template_name=( 56 | # If this came from htmx render the partial template, otherwise render the 57 | # full template - which inherits from base.html and includes the partial. 58 | "tasks/partials/list.html" 59 | if request.is_htmx 60 | else "tasks/list.html" 61 | ), 62 | context=context, 63 | ) 64 | 65 | 66 | @check_htmx 67 | def form_view(request, pk: int | None = None): 68 | task = None if pk is None else get_object_or_404(Task, pk=pk) 69 | context: dict[str, Any] = { 70 | "task": task, 71 | } 72 | 73 | form = TaskForm(request.POST or None, instance=task) 74 | if form.is_valid(): 75 | context["task"] = form.save() 76 | 77 | # Normally when you submit a form you want to send the user on to a new page 78 | # with a redirect to avoid refresh re-submissions. If the request comes from 79 | # htmx, we can just return a render of the new task 80 | if request.is_htmx: 81 | return render(request, "tasks/partials/list-task.html", context=context) 82 | else: 83 | return redirect("tasks:list") 84 | else: 85 | context["form"] = form 86 | 87 | return render( 88 | request, 89 | template_name=( 90 | "tasks/partials/list-form.html" 91 | if request.is_htmx 92 | else "tasks/form.html" 93 | ), 94 | context=context, 95 | ) 96 | 97 | 98 | @check_htmx 99 | def done_state_view(request, pk: int, done_state: bool): 100 | task = get_object_or_404(Task, pk=pk) 101 | if task.done != done_state: 102 | task.done = done_state 103 | task.save() 104 | 105 | return render( 106 | request, 107 | template_name=( 108 | "tasks/partials/list-task.html" 109 | if request.is_htmx 110 | else "tasks/done_state.html" 111 | ), 112 | context={ 113 | "task": task, 114 | }, 115 | ) 116 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Todo project 3 | ============ 4 | 5 | Example project using htmx 6 | 7 | 8 | Local development 9 | ================= 10 | 11 | 12 | Setup 13 | ----- 14 | 15 | Check out the repository into a new dir: 16 | 17 | .. code-block:: bash 18 | 19 | mkdir todo 20 | cd todo 21 | git clone ... repo 22 | cd repo 23 | cp docker-compose.dev.yml.default docker-compose.dev.yml 24 | 25 | 26 | This, along with the commands below, would normally be put in a shortcut file somewhere. 27 | I use `workdir `_, others use Makefile, Taskfile, 28 | local fabric commands etc. 29 | 30 | 31 | Running - tl;dr 32 | --------------- 33 | 34 | In different terminals: 35 | 36 | .. code-block:: bash 37 | 38 | docker compose -f docker-compose.dev.yml up postgres 39 | docker compose -f docker-compose.dev.yml up backend 40 | docker compose -f docker-compose.dev.yml exec backend /bin/bash 41 | 42 | 43 | Running - details 44 | ----------------- 45 | 46 | **Start postgres in docker:** 47 | 48 | .. code-block:: bash 49 | 50 | docker compose -f docker-compose.dev.yml up postgres 51 | 52 | 53 | Then there are three options to run Python: 54 | 55 | 56 | 1. **Either run Python outside docker:** 57 | 58 | Often easiest if you have no OS dependencies and you trust the code - easiest to use 59 | with IDEs. 60 | 61 | .. code-block:: bash 62 | 63 | python -m venv ../venv 64 | . ../venv/bin/activate 65 | pip install -r requirements.txt 66 | 67 | ./manage.py migrate 68 | ./manage.py runserver 0:8000 69 | 70 | If you start adding env vars without defaults, you'll also need an env file. 71 | 72 | 73 | 2. **Or use docker to ``runserver``** 74 | 75 | Best for a reasonably stable project where you don't need to keep stopping and 76 | starting the server and you want an environment consistent with prod and other devs. 77 | 78 | .. code-block:: bash 79 | 80 | docker compose -f docker-compose.dev.yml up backend 81 | 82 | You can then connect to the running container to run additional commands: 83 | 84 | .. code-block:: bash 85 | 86 | docker compose -f docker-compose.dev.yml exec backend /bin/bash 87 | 88 | ./manage.py createsuperuser 89 | 90 | 91 | 3. **Or use docker as a virtual machine:** 92 | 93 | Same as 2, but slightly less delay each time you stop and restart the server. 94 | 95 | #. Start the container:: 96 | 97 | docker compose -f docker-compose.dev.yml run --service-ports --entrypoint=/bin/bash backend 98 | 99 | #. Start Django as you would normally:: 100 | 101 | ./manage.py migrate 102 | ./manage.py runserver 0:8000 103 | 104 | #. In another shell, connect to the running container to run additional commands:: 105 | 106 | docker compose -f docker-compose.dev.yml exec backend /bin/bash 107 | 108 | ./manage.py createsuperuser 109 | 110 | The site is then available at http://localhost:8000/. I use a wildcard localhost DNS 111 | entry, eg http://todo.local.uzeweb.com:8000/. I can then have additional logins at 112 | http://todo-anon.local.uzeweb.com:8000/, http://todo-admin.local.uzeweb.com:8000/ 113 | etc without needing to shuffle browser profiles or cookies. 114 | 115 | 116 | Working with the database 117 | ------------------------- 118 | 119 | To dump from the database: 120 | 121 | .. code-block:: bash 122 | 123 | docker-compose -f docker-compose.dev.yml exec postgres /project/docker/postgres/dump.sh 124 | 125 | The dumped file is in ``../docker-store/backup`` 126 | 127 | To load the database from a dump (default ``database.dump``): 128 | 129 | .. code-block:: bash 130 | 131 | docker-compose -f docker-compose.dev.yml exec postgres /project/docker/postgres/restore.sh 132 | 133 | 134 | 135 | Changing requirements 136 | --------------------- 137 | 138 | This project uses ``pip-tools``: 139 | 140 | #. Modify ``requirements.in`` 141 | #. Run ``pip-compile`` 142 | #. Run ``pip-sync`` 143 | 144 | 145 | Linting 146 | ------- 147 | 148 | Install pre-commit:: 149 | 150 | pip install pipx 151 | pipx install pre-commit 152 | pre-commit install 153 | 154 | Run manually:: 155 | 156 | pre-commit run --all-files 157 | 158 | Skip checks during commit:: 159 | 160 | git commit --no-verify 161 | 162 | 163 | Testing 164 | ======= 165 | 166 | Use ``mypy`` to type check: 167 | 168 | .. code-block:: bash 169 | 170 | mypy 171 | 172 | 173 | Use ``pytest`` to run the tests on your current installation: 174 | 175 | .. code-block:: bash 176 | 177 | pytest 178 | -------------------------------------------------------------------------------- /todo/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for todo project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.2.2. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.2/ref/settings/ 11 | """ 12 | from os import environ 13 | from pathlib import Path 14 | 15 | from configurations import Configuration 16 | 17 | 18 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 19 | BASE_DIR = Path(__file__).resolve().parent.parent 20 | STORE_PATH = Path(environ.get("DJANGO_STORE_PATH", BASE_DIR.parent / "store")) 21 | 22 | 23 | class Common(Configuration): 24 | SECRET_KEY = environ.get("DJANGO_SECRET_KEY", "secret") 25 | ALLOWED_HOSTS = environ.get("DJANGO_ALLOWED_HOSTS", "*").split(",") 26 | 27 | # Application definition 28 | INSTALLED_APPS = [ 29 | "django.contrib.admin", 30 | "django.contrib.auth", 31 | "django.contrib.contenttypes", 32 | "django.contrib.sessions", 33 | "django.contrib.messages", 34 | "django.contrib.staticfiles", 35 | "django.contrib.postgres", 36 | # Third party 37 | "whitenoise.runserver_nostatic", 38 | # Project 39 | "todo.tasks", 40 | ] 41 | 42 | MIDDLEWARE = [ 43 | "django.middleware.security.SecurityMiddleware", 44 | "whitenoise.middleware.WhiteNoiseMiddleware", 45 | "django.contrib.sessions.middleware.SessionMiddleware", 46 | "django.middleware.common.CommonMiddleware", 47 | "django.middleware.csrf.CsrfViewMiddleware", 48 | "django.contrib.auth.middleware.AuthenticationMiddleware", 49 | "django.contrib.messages.middleware.MessageMiddleware", 50 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 51 | ] 52 | 53 | ROOT_URLCONF = "todo.urls" 54 | 55 | TEMPLATES = [ 56 | { 57 | "BACKEND": "django.template.backends.django.DjangoTemplates", 58 | "DIRS": [BASE_DIR / "templates"], 59 | "APP_DIRS": True, 60 | "OPTIONS": { 61 | "context_processors": [ 62 | "django.template.context_processors.debug", 63 | "django.template.context_processors.request", 64 | "django.contrib.auth.context_processors.auth", 65 | "django.contrib.messages.context_processors.messages", 66 | ], 67 | }, 68 | }, 69 | ] 70 | 71 | WSGI_APPLICATION = "todo.wsgi.application" 72 | 73 | # Database 74 | # https://docs.djangoproject.com/en/4.2/ref/settings/#databases 75 | DATABASES = { 76 | "default": { 77 | "ENGINE": "django.db.backends.postgresql_psycopg2", 78 | "NAME": environ.get("POSTGRES_NAME", "postgres"), 79 | "USER": environ.get("POSTGRES_USER", "postgres"), 80 | "PASSWORD": environ.get("POSTGRES_PASSWORD", "postgres"), 81 | "HOST": environ.get("POSTGRES_HOST", "localhost"), 82 | "CONN_MAX_AGE": 600, 83 | } 84 | } 85 | 86 | # Password validation 87 | # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators 88 | 89 | AUTH_PASSWORD_VALIDATORS = [ 90 | { 91 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 92 | }, 93 | { 94 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 95 | }, 96 | { 97 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 98 | }, 99 | { 100 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 101 | }, 102 | ] 103 | 104 | # Internationalization 105 | # https://docs.djangoproject.com/en/4.2/topics/i18n/ 106 | LANGUAGE_CODE = "en-us" 107 | TIME_ZONE = "UTC" 108 | USE_I18N = True 109 | USE_TZ = True 110 | 111 | # Static files (CSS, JavaScript, Images) 112 | # https://docs.djangoproject.com/en/4.2/howto/static-files/ 113 | 114 | STATIC_URL = "static/" 115 | STATIC_ROOT = STORE_PATH / "public" / "static" 116 | MEDIA_URL = "media/" 117 | MEDIA_ROOT = STORE_PATH / "public" / "media" 118 | STORAGES = { 119 | "default": { 120 | "BACKEND": "django.core.files.storage.FileSystemStorage", 121 | }, 122 | "staticfiles": { 123 | "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", 124 | }, 125 | } 126 | STATICFILES_DIRS = [BASE_DIR / "static"] 127 | WHITENOISE_ROOT = BASE_DIR / "public" 128 | 129 | # Default primary key field type 130 | # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field 131 | 132 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 133 | 134 | 135 | class Dev(Common): 136 | DEBUG = True 137 | AUTH_PASSWORD_VALIDATORS = [] 138 | 139 | # E-mail to file 140 | EMAIL_BACKEND = "django.core.mail.backends.filebased.EmailBackend" 141 | EMAIL_FILE_PATH = STORE_PATH / "tmp" / "emails" 142 | 143 | 144 | class Test(Dev): 145 | STORAGES = { 146 | "default": { 147 | "BACKEND": "django.core.files.storage.FileSystemStorage", 148 | }, 149 | "staticfiles": { 150 | "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", 151 | }, 152 | } 153 | 154 | 155 | class Deploy(Common): 156 | DEBUG = False 157 | 158 | @property 159 | def SECRET_KEY(self): 160 | return environ["SECRET_KEY"] 161 | 162 | @property 163 | def ALLOWED_HOSTS(self): 164 | return environ["ALLOWED_HOSTS"] 165 | 166 | EMAIL_HOST = environ.get("EMAIL_HOST", "localhost") 167 | -------------------------------------------------------------------------------- /static/htmx-1.9.2.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 z={onLoad:t,process:Tt,on:le,off:ue,trigger:ie,ajax:dr,find:b,findAll:f,closest:d,values:function(e,t){var r=Jt(e,t||"post");return r.values},remove:B,addClass:j,removeClass:n,toggleClass:U,takeClass:V,defineExtension:yr,removeExtension:br,logAll:F,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,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},parseInterval:v,_:e,createEventSource:function(e){return new EventSource(e,{withCredentials:true})},createWebSocket:function(e){var t=new WebSocket(e,[]);t.binaryType=z.config.wsBinaryType;return t},version:"1.9.2"};var C={addTriggerHandler:xt,bodyContains:ee,canAccessLocalStorage:D,filterValues:er,hasAttribute:q,getAttributeValue:G,getClosestMatch:c,getExpressionVars:fr,getHeaders:Qt,getInputValues:Jt,getInternalData:Y,getSwapSpecification:rr,getTriggerSpecs:ze,getTarget:de,makeFragment:l,mergeObjects:te,makeSettleInfo:S,oobSwap:me,selectAndSwap:Me,settleImmediately:Bt,shouldCancel:Ke,triggerEvent:ie,triggerErrorEvent:ne,withExtensions:w};var R=["get","post","put","delete","patch"];var O=R.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function v(e){if(e==undefined){return undefined}if(e.slice(-2)=="ms"){return parseFloat(e.slice(0,-2))||undefined}if(e.slice(-1)=="s"){return parseFloat(e.slice(0,-1))*1e3||undefined}if(e.slice(-1)=="m"){return parseFloat(e.slice(0,-1))*1e3*60||undefined}return parseFloat(e)||undefined}function $(e,t){return e.getAttribute&&e.getAttribute(t)}function q(e,t){return e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function G(e,t){return $(e,t)||$(e,"data-"+t)}function u(e){return e.parentElement}function J(){return document}function c(e,t){while(e&&!t(e)){e=u(e)}return e?e:null}function T(e,t,r){var n=G(t,r);var i=G(t,"hx-disinherit");if(e!==t&&i&&(i==="*"||i.split(" ").indexOf(r)>=0)){return"unset"}else{return n}}function Z(t,r){var n=null;c(t,function(e){return n=T(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 H(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 i(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=J().createDocumentFragment()}return i}function L(e){return e.match(/",0);return r.querySelector("template").content}else{var n=H(e);switch(n){case"thead":case"tbody":case"tfoot":case"colgroup":case"caption":return i(""+e+"
    ",1);case"col":return i(""+e+"
    ",2);case"tr":return i(""+e+"
    ",2);case"td":case"th":return i(""+e+"
    ",3);case"script":return i("
    "+e+"
    ",1);default:return i(e,0)}}}function K(e){if(e){e()}}function A(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function N(e){return A(e,"Function")}function I(e){return A(e,"Object")}function Y(e){var t="htmx-internal-data";var r=e[t];if(!r){r=e[t]={}}return r}function k(e){var t=[];if(e){for(var r=0;r=0}function ee(e){if(e.getRootNode&&e.getRootNode()instanceof ShadowRoot){return J().body.contains(e.getRootNode().host)}else{return J().body.contains(e)}}function M(e){return e.trim().split(/\s+/)}function te(e,t){for(var r in t){if(t.hasOwnProperty(r)){e[r]=t[r]}}return e}function y(e){try{return JSON.parse(e)}catch(e){x(e);return null}}function D(){var e="htmx:localStorageTest";try{localStorage.setItem(e,e);localStorage.removeItem(e);return true}catch(e){return false}}function X(t){try{var e=new URL(t);if(e){t=e.pathname+e.search}if(!t.match("^/$")){t=t.replace(/\/+$/,"")}return t}catch(e){return t}}function e(e){return sr(J().body,function(){return eval(e)})}function t(t){var e=z.on("htmx:load",function(e){t(e.detail.elt)});return e}function F(){z.logger=function(e,t,r){if(console){console.log(t,e,r)}}}function b(e,t){if(t){return e.querySelector(t)}else{return b(J(),e)}}function f(e,t){if(t){return e.querySelectorAll(t)}else{return f(J(),e)}}function B(e,t){e=s(e);if(t){setTimeout(function(){B(e);e=null},t)}else{e.parentElement.removeChild(e)}}function j(e,t,r){e=s(e);if(r){setTimeout(function(){j(e,t);e=null},r)}else{e.classList&&e.classList.add(t)}}function n(e,t,r){e=s(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 U(e,t){e=s(e);e.classList.toggle(t)}function V(e,t){e=s(e);Q(e.parentElement.children,function(e){n(e,t)});j(e,t)}function d(e,t){e=s(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 r(e){var t=e.trim();if(t.startsWith("<")&&t.endsWith("/>")){return t.substring(1,t.length-2)}else{return t}}function _(e,t){if(t.indexOf("closest ")===0){return[d(e,r(t.substr(8)))]}else if(t.indexOf("find ")===0){return[b(e,r(t.substr(5)))]}else if(t.indexOf("next ")===0){return[W(e,r(t.substr(5)))]}else if(t.indexOf("previous ")===0){return[oe(e,r(t.substr(9)))]}else if(t==="document"){return[document]}else if(t==="window"){return[window]}else{return J().querySelectorAll(r(t))}}var W=function(e,t){var r=J().querySelectorAll(t);for(var n=0;n=0;n--){var i=r[n];if(i.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_FOLLOWING){return i}}};function re(e,t){if(t){return _(e,t)[0]}else{return _(J().body,e)[0]}}function s(e){if(A(e,"String")){return b(e)}else{return e}}function se(e,t,r){if(N(t)){return{target:J().body,event:e,listener:t}}else{return{target:s(e),event:t,listener:r}}}function le(t,r,n){Sr(function(){var e=se(t,r,n);e.target.addEventListener(e.event,e.listener)});var e=N(r);return e?r:n}function ue(t,r,n){Sr(function(){var e=se(t,r,n);e.target.removeEventListener(e.event,e.listener)});return N(r)?r:n}var fe=J().createElement("output");function ce(e,t){var r=Z(e,t);if(r){if(r==="this"){return[he(e,t)]}else{var n=_(e,r);if(n.length===0){x('The selector "'+r+'" on '+t+" returned no matches!");return[fe]}else{return n}}}}function he(e,t){return c(e,function(e){return G(e,t)!=null})}function de(e){var t=Z(e,"hx-target");if(t){if(t==="this"){return he(e,"hx-target")}else{return re(e,t)}}else{var r=Y(e);if(r.boosted){return J().body}else{return e}}}function ve(e){var t=z.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=J().querySelectorAll(t);if(r){Q(r,function(e){var t;var r=i.cloneNode(true);t=J().createDocumentFragment();t.appendChild(r);if(!pe(o,e)){t=r}var n={shouldSwap:true,target:e,fragment:t};if(!ie(e,"htmx:oobBeforeSwap",n))return;e=n.target;if(n["shouldSwap"]){ke(o,e,e,t,a)}Q(a.elts,function(e){ie(e,"htmx:oobAfterSwap",n)})});i.parentNode.removeChild(i)}else{i.parentNode.removeChild(i);ne(J().body,"htmx:oobErrorNoTarget",{content:i})}return e}function xe(e,t,r){var n=Z(e,"hx-select-oob");if(n){var i=n.split(",");for(let e=0;e0){var t=e.id.replace("'","\\'");var r=e.tagName.replace(":","\\:");var n=a.querySelector(r+"[id='"+t+"']");if(n&&n!==a){var i=e.cloneNode();ge(e,n);o.tasks.push(function(){ge(e,i)})}}})}function we(e){return function(){n(e,z.config.addedClass);Tt(e);bt(e);Se(e);ie(e,"htmx:load")}}function Se(e){var t="[autofocus]";var r=h(e,t)?e:e.querySelector(t);if(r!=null){r.focus()}}function a(e,t,r,n){be(e,r,n);while(r.childNodes.length>0){var i=r.firstChild;j(i,z.config.addedClass);e.insertBefore(i,t);if(i.nodeType!==Node.TEXT_NODE&&i.nodeType!==Node.COMMENT_NODE){n.tasks.push(we(i))}}}function Ee(e,t){var r=0;while(r-1){var t=e.replace(/]*>|>)([\s\S]*?)<\/svg>/gim,"");var r=t.match(/]*>|>)([\s\S]*?)<\/title>/im);if(r){return r[2]}}}function Me(e,t,r,n,i){i.title=Pe(n);var a=l(n);if(a){xe(r,a,i);a=Ie(r,a);ye(a);return ke(e,r,t,a,i)}}function De(e,t,r){var n=e.getResponseHeader(t);if(n.indexOf("{")===0){var i=y(n);for(var a in i){if(i.hasOwnProperty(a)){var o=i[a];if(!I(o)){o={value:o}}ie(r,a,o)}}}else{ie(r,n,[])}}var Xe=/\s/;var g=/[\s,]/;var Fe=/[_$a-zA-Z]/;var Be=/[_$a-zA-Z0-9]/;var je=['"',"'","/"];var p=/[^\s]/;function Ue(e){var t=[];var r=0;while(r0){var o=t[0];if(o==="]"){n--;if(n===0){if(a===null){i=i+"true"}t.shift();i+=")})";try{var s=sr(e,function(){return Function(i)()},function(){return true});s.source=i;return s}catch(e){ne(J().body,"htmx:syntax:error",{error:e,source:i});return null}}}else if(o==="["){n++}if(Ve(o,a,r)){i+="(("+r+"."+o+") ? ("+r+"."+o+") : (window."+o+"))"}else{i=i+o}a=t.shift()}}}function m(e,t){var r="";while(e.length>0&&!e[0].match(t)){r+=e.shift()}return r}var We="input, textarea, select";function ze(e){var t=G(e,"hx-trigger");var r=[];if(t){var n=Ue(t);do{m(n,p);var i=n.length;var a=m(n,/[,\[\s]/);if(a!==""){if(a==="every"){var o={trigger:"every"};m(n,p);o.pollInterval=v(m(n,/[,\[\s]/));m(n,p);var s=_e(e,n,"event");if(s){o.eventFilter=s}r.push(o)}else if(a.indexOf("sse:")===0){r.push({trigger:"sse",sseEvent:a.substr(4)})}else{var l={trigger:a};var s=_e(e,n,"event");if(s){l.eventFilter=s}while(n.length>0&&n[0]!==","){m(n,p);var u=n.shift();if(u==="changed"){l.changed=true}else if(u==="once"){l.once=true}else if(u==="consume"){l.consume=true}else if(u==="delay"&&n[0]===":"){n.shift();l.delay=v(m(n,g))}else if(u==="from"&&n[0]===":"){n.shift();var f=m(n,g);if(f==="closest"||f==="find"||f==="next"||f==="previous"){n.shift();f+=" "+m(n,g)}l.from=f}else if(u==="target"&&n[0]===":"){n.shift();l.target=m(n,g)}else if(u==="throttle"&&n[0]===":"){n.shift();l.throttle=v(m(n,g))}else if(u==="queue"&&n[0]===":"){n.shift();l.queue=m(n,g)}else if((u==="root"||u==="threshold")&&n[0]===":"){n.shift();l[u]=m(n,g)}else{ne(e,"htmx:syntax:error",{token:n.shift()})}}r.push(l)}}if(n.length===i){ne(e,"htmx:syntax:error",{token:n.shift()})}m(n,p)}while(n[0]===","&&n.shift())}if(r.length>0){return r}else if(h(e,"form")){return[{trigger:"submit"}]}else if(h(e,'input[type="button"]')){return[{trigger:"click"}]}else if(h(e,We)){return[{trigger:"change"}]}else{return[{trigger:"click"}]}}function $e(e){Y(e).cancelled=true}function Ge(e,t,r){var n=Y(e);n.timeout=setTimeout(function(){if(ee(e)&&n.cancelled!==true){if(!Qe(r,Lt("hx:poll:trigger",{triggerSpec:r,target:e}))){t(e)}Ge(e,t,r)}},r.pollInterval)}function Je(e){return location.hostname===e.hostname&&$(e,"href")&&$(e,"href").indexOf("#")!==0}function Ze(t,r,e){if(t.tagName==="A"&&Je(t)&&(t.target===""||t.target==="_self")||t.tagName==="FORM"){r.boosted=true;var n,i;if(t.tagName==="A"){n="get";i=t.href}else{var a=$(t,"method");n=a?a.toLowerCase():"get";if(n==="get"){}i=$(t,"action")}e.forEach(function(e){et(t,function(e,t){ae(n,i,e,t)},r,e,true)})}}function Ke(e,t){if(e.type==="submit"||e.type==="click"){if(t.tagName==="FORM"){return true}if(h(t,'input[type="submit"], button')&&d(t,"form")!==null){return true}if(t.tagName==="A"&&t.href&&(t.getAttribute("href")==="#"||t.getAttribute("href").indexOf("#")!==0)){return true}}return false}function Ye(e,t){return Y(e).boosted&&e.tagName==="A"&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function Qe(e,t){var r=e.eventFilter;if(r){try{return r(t)!==true}catch(e){ne(J().body,"htmx:eventFilter:error",{error:e,source:r.source});return true}}return false}function et(i,a,e,o,s){var l=Y(i);var t;if(o.from){t=_(i,o.from)}else{t=[i]}if(o.changed){l.lastValue=i.value}Q(t,function(r){var n=function(e){if(!ee(i)){r.removeEventListener(o.trigger,n);return}if(Ye(i,e)){return}if(s||Ke(e,i)){e.preventDefault()}if(Qe(o,e)){return}var t=Y(e);t.triggerSpec=o;if(t.handledFor==null){t.handledFor=[]}if(t.handledFor.indexOf(i)<0){t.handledFor.push(i);if(o.consume){e.stopPropagation()}if(o.target&&e.target){if(!h(e.target,o.target)){return}}if(o.once){if(l.triggeredOnce){return}else{l.triggeredOnce=true}}if(o.changed){if(l.lastValue===i.value){return}else{l.lastValue=i.value}}if(l.delayed){clearTimeout(l.delayed)}if(l.throttle){return}if(o.throttle){if(!l.throttle){a(i,e);l.throttle=setTimeout(function(){l.throttle=null},o.throttle)}}else if(o.delay){l.delayed=setTimeout(function(){a(i,e)},o.delay)}else{ie(i,"htmx:trigger");a(i,e)}}};if(e.listenerInfos==null){e.listenerInfos=[]}e.listenerInfos.push({trigger:o.trigger,listener:n,on:r});r.addEventListener(o.trigger,n)})}var tt=false;var rt=null;function nt(){if(!rt){rt=function(){tt=true};window.addEventListener("scroll",rt);setInterval(function(){if(tt){tt=false;Q(J().querySelectorAll("[hx-trigger='revealed'],[data-hx-trigger='revealed']"),function(e){it(e)})}},200)}}function it(t){if(!q(t,"data-hx-revealed")&&P(t)){t.setAttribute("data-hx-revealed","true");var e=Y(t);if(e.initHash){ie(t,"revealed")}else{t.addEventListener("htmx:afterProcessNode",function(e){ie(t,"revealed")},{once:true})}}}function at(e,t,r){var n=M(r);for(var i=0;i=0){var t=ut(n);setTimeout(function(){ot(s,r,n+1)},t)}};t.onopen=function(e){n=0};Y(s).webSocket=t;t.addEventListener("message",function(e){if(st(s)){return}var t=e.data;w(s,function(e){t=e.transformResponse(t,null,s)});var r=S(s);var n=l(t);var i=k(n.children);for(var a=0;a0){ie(u,"htmx:validation:halted",i);return}t.send(JSON.stringify(l));if(Ke(e,u)){e.preventDefault()}})}else{ne(u,"htmx:noWebSocketSourceError")}}function ut(e){var t=z.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()}x('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"')}function ft(e,t,r){var n=M(r);for(var i=0;i0){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+=Ct(o)}for(var l in r){Rt(e,l,r[l])}}}function qt(t){if(t.closest&&t.closest(z.config.disableSelector)){return}var r=Y(t);if(r.initHash!==Ce(t)){r.initHash=Ce(t);Re(t);Ot(t);ie(t,"htmx:beforeProcessNode");if(t.value){r.lastValue=t.value}var e=ze(t);var n=mt(t,r,e);if(!n){if(Z(t,"hx-boost")==="true"){Ze(t,r,e)}else if(q(t,"hx-trigger")){e.forEach(function(e){xt(t,e,r,function(){})})}}if(t.tagName==="FORM"){Et(t)}var i=G(t,"hx-sse");if(i){ft(t,r,i)}var a=G(t,"hx-ws");if(a){at(t,r,a)}ie(t,"htmx:afterProcessNode")}}function Tt(e){e=s(e);qt(e);Q(St(e),function(e){qt(e)})}function Ht(e){return e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase()}function Lt(e,t){var r;if(window.CustomEvent&&typeof window.CustomEvent==="function"){r=new CustomEvent(e,{bubbles:true,cancelable:true,detail:t})}else{r=J().createEvent("CustomEvent");r.initCustomEvent(e,true,true,t)}return r}function ne(e,t,r){ie(e,t,te({error:t},r))}function At(e){return e==="htmx:afterProcessNode"}function w(e,t){Q(wr(e),function(e){try{t(e)}catch(e){x(e)}})}function x(e){if(console.error){console.error(e)}else if(console.log){console.log("ERROR: ",e)}}function ie(e,t,r){e=s(e);if(r==null){r={}}r["elt"]=e;var n=Lt(t,r);if(z.logger&&!At(t)){z.logger(e,t,r)}if(r.error){x(r.error);ie(e,"htmx:error",{errorInfo:r})}var i=e.dispatchEvent(n);var a=Ht(t);if(i&&a!==t){var o=Lt(a,n.detail);i=i&&e.dispatchEvent(o)}w(e,function(e){i=i&&e.onEvent(t,n)!==false});return i}var Nt=location.pathname+location.search;function It(){var e=J().querySelector("[hx-history-elt],[data-hx-history-elt]");return e||J().body}function kt(e,t,r,n){if(!D()){return}e=X(e);var i=y(localStorage.getItem("htmx-history-cache"))||[];for(var a=0;az.config.historyCacheSize){i.shift()}while(i.length>0){try{localStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(e){ne(J().body,"htmx:historyCacheError",{cause:e,cache:i});i.shift()}}}function Pt(e){if(!D()){return null}e=X(e);var t=y(localStorage.getItem("htmx-history-cache"))||[];for(var r=0;r=200&&this.status<400){ie(J().body,"htmx:historyCacheMissLoad",o);var e=l(this.response);e=e.querySelector("[hx-history-elt],[data-hx-history-elt]")||e;var t=It();var r=S(t);var n=Pe(this.response);if(n){var i=b("title");if(i){i.innerHTML=n}else{window.document.title=n}}Ne(t,e,r);Bt(r.tasks);Nt=a;ie(J().body,"htmx:historyRestore",{path:a,cacheMiss:true,serverResponse:this.response})}else{ne(J().body,"htmx:historyCacheMissLoadError",o)}};e.send()}function Ut(e){Dt();e=e||location.pathname+location.search;var t=Pt(e);if(t){var r=l(t.content);var n=It();var i=S(n);Ne(n,r,i);Bt(i.tasks);document.title=t.title;window.scrollTo(0,t.scroll);Nt=e;ie(J().body,"htmx:historyRestore",{path:e,item:t})}else{if(z.config.refreshOnHistoryMiss){window.location.reload(true)}else{jt(e)}}}function Vt(e){var t=ce(e,"hx-indicator");if(t==null){t=[e]}Q(t,function(e){var t=Y(e);t.requestCount=(t.requestCount||0)+1;e.classList["add"].call(e.classList,z.config.requestClass)});return t}function _t(e){Q(e,function(e){var t=Y(e);t.requestCount=(t.requestCount||0)-1;if(t.requestCount===0){e.classList["remove"].call(e.classList,z.config.requestClass)}})}function Wt(e,t){for(var r=0;r=0}function rr(e,t){var r=t?t:Z(e,"hx-swap");var n={swapStyle:Y(e).boosted?"innerHTML":z.config.defaultSwapStyle,swapDelay:z.config.defaultSwapDelay,settleDelay:z.config.defaultSettleDelay};if(Y(e).boosted&&!tr(e)){n["show"]="top"}if(r){var i=M(r);if(i.length>0){n["swapStyle"]=i[0];for(var a=1;a0?l.join(":"):null;n["scroll"]=u;n["scrollTarget"]=f}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}if(o.indexOf("focus-scroll:")===0){var d=o.substr("focus-scroll:".length);n["focusScroll"]=d=="true"}}}}return n}function nr(e){return Z(e,"hx-encoding")==="multipart/form-data"||h(e,"form")&&$(e,"enctype")==="multipart/form-data"}function ir(t,r,n){var i=null;w(r,function(e){if(i==null){i=e.encodeParameters(t,n,r)}});if(i!=null){return i}else{if(nr(r)){return Yt(n)}else{return Kt(n)}}}function S(e){return{tasks:[],elts:[e]}}function ar(e,t){var r=e[0];var n=e[e.length-1];if(t.scroll){var i=null;if(t.scrollTarget){i=re(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=re(r,a)}if(t.show==="top"&&(r||i)){i=i||r;i.scrollIntoView({block:"start",behavior:z.config.scrollBehavior})}if(t.show==="bottom"&&(n||i)){i=i||n;i.scrollIntoView({block:"end",behavior:z.config.scrollBehavior})}}}function or(e,t,r,n){if(n==null){n={}}if(e==null){return n}var i=G(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=sr(e,function(){return Function("return ("+a+")")()},{})}else{s=y(a)}for(var l in s){if(s.hasOwnProperty(l)){if(n[l]==null){n[l]=s[l]}}}}return or(u(e),t,r,n)}function sr(e,t,r){if(z.config.allowEval){return t()}else{ne(e,"htmx:evalDisallowedError");return r}}function lr(e,t){return or(e,"hx-vars",true,t)}function ur(e,t){return or(e,"hx-vals",false,t)}function fr(e){return te(lr(e),ur(e))}function cr(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 hr(t){if(t.responseURL&&typeof URL!=="undefined"){try{var e=new URL(t.responseURL);return e.pathname+e.search}catch(e){ne(J().body,"htmx:badResponseUrl",{url:t.responseURL})}}}function E(e,t){return e.getAllResponseHeaders().match(t)}function dr(e,t,r){e=e.toLowerCase();if(r){if(r instanceof Element||A(r,"String")){return ae(e,t,null,null,{targetOverride:s(r),returnPromise:true})}else{return ae(e,t,s(r.source),r.event,{handler:r.handler,headers:r.headers,values:r.values,targetOverride:s(r.target),swapOverride:r.swap,returnPromise:true})}}else{return ae(e,t,null,null,{returnPromise:true})}}function vr(e){var t=[];while(e){t.push(e);e=e.parentElement}return t}function ae(e,t,n,r,i,M){var a=null;var o=null;i=i!=null?i:{};if(i.returnPromise&&typeof Promise!=="undefined"){var s=new Promise(function(e,t){a=e;o=t})}if(n==null){n=J().body}var D=i.handler||pr;if(!ee(n)){return}var l=i.targetOverride||de(n);if(l==null||l==fe){ne(n,"htmx:targetError",{target:G(n,"hx-target")});return}if(!M){var X=function(){return ae(e,t,n,r,i,true)};var F={target:l,elt:n,path:t,verb:e,triggeringEvent:r,etc:i,issueRequest:X};if(ie(n,"htmx:confirm",F)===false){return}}var u=n;var f=Y(n);var c=Z(n,"hx-sync");var h=null;var d=false;if(c){var v=c.split(":");var g=v[0].trim();if(g==="this"){u=he(n,"hx-sync")}else{u=re(n,g)}c=(v[1]||"drop").trim();f=Y(u);if(c==="drop"&&f.xhr&&f.abortable!==true){return}else if(c==="abort"){if(f.xhr){return}else{d=true}}else if(c==="replace"){ie(u,"htmx:abort")}else if(c.indexOf("queue")===0){var B=c.split(" ");h=(B[1]||"last").trim()}}if(f.xhr){if(f.abortable){ie(u,"htmx:abort")}else{if(h==null){if(r){var p=Y(r);if(p&&p.triggerSpec&&p.triggerSpec.queue){h=p.triggerSpec.queue}}if(h==null){h="last"}}if(f.queuedRequests==null){f.queuedRequests=[]}if(h==="first"&&f.queuedRequests.length===0){f.queuedRequests.push(function(){ae(e,t,n,r,i)})}else if(h==="all"){f.queuedRequests.push(function(){ae(e,t,n,r,i)})}else if(h==="last"){f.queuedRequests=[];f.queuedRequests.push(function(){ae(e,t,n,r,i)})}return}}var m=new XMLHttpRequest;f.xhr=m;f.abortable=d;var x=function(){f.xhr=null;f.abortable=false;if(f.queuedRequests!=null&&f.queuedRequests.length>0){var e=f.queuedRequests.shift();e()}};var y=Z(n,"hx-prompt");if(y){var b=prompt(y);if(b===null||!ie(n,"htmx:prompt",{prompt:b,target:l})){K(a);x();return s}}var w=Z(n,"hx-confirm");if(w){if(!confirm(w)){K(a);x();return s}}var S=Qt(n,l,b);if(i.headers){S=te(S,i.headers)}var E=Jt(n,e);var C=E.errors;var R=E.values;if(i.values){R=te(R,i.values)}var j=fr(n);var O=te(R,j);var q=er(O,n);if(e!=="get"&&!nr(n)){S["Content-Type"]="application/x-www-form-urlencoded"}if(z.config.getCacheBusterParam&&e==="get"){q["org.htmx.cache-buster"]=$(l,"id")||"true"}if(t==null||t===""){t=J().location.href}var T=or(n,"hx-request");var H=Y(n).boosted;var L={boosted:H,parameters:q,unfilteredParameters:O,headers:S,target:l,verb:e,errors:C,withCredentials:i.credentials||T.credentials||z.config.withCredentials,timeout:i.timeout||T.timeout||z.config.timeout,path:t,triggeringEvent:r};if(!ie(n,"htmx:configRequest",L)){K(a);x();return s}t=L.path;e=L.verb;S=L.headers;q=L.parameters;C=L.errors;if(C&&C.length>0){ie(n,"htmx:validation:halted",L);K(a);x();return s}var U=t.split("#");var V=U[0];var A=U[1];var N=null;if(e==="get"){N=V;var _=Object.keys(q).length!==0;if(_){if(N.indexOf("?")<0){N+="?"}else{N+="&"}N+=Kt(q);if(A){N+="#"+A}}m.open("GET",N,true)}else{m.open(e.toUpperCase(),t,true)}m.overrideMimeType("text/html");m.withCredentials=L.withCredentials;m.timeout=L.timeout;if(T.noHeaders){}else{for(var I in S){if(S.hasOwnProperty(I)){var W=S[I];cr(m,I,W)}}}var k={xhr:m,target:l,requestConfig:L,etc:i,boosted:H,pathInfo:{requestPath:t,finalRequestPath:N||t,anchor:A}};m.onload=function(){try{var e=vr(n);k.pathInfo.responsePath=hr(m);D(n,k);_t(P);ie(n,"htmx:afterRequest",k);ie(n,"htmx:afterOnLoad",k);if(!ee(n)){var t=null;while(e.length>0&&t==null){var r=e.shift();if(ee(r)){t=r}}if(t){ie(t,"htmx:afterRequest",k);ie(t,"htmx:afterOnLoad",k)}}K(a);x()}catch(e){ne(n,"htmx:onLoadError",te({error:e},k));throw e}};m.onerror=function(){_t(P);ne(n,"htmx:afterRequest",k);ne(n,"htmx:sendError",k);K(o);x()};m.onabort=function(){_t(P);ne(n,"htmx:afterRequest",k);ne(n,"htmx:sendAbort",k);K(o);x()};m.ontimeout=function(){_t(P);ne(n,"htmx:afterRequest",k);ne(n,"htmx:timeout",k);K(o);x()};if(!ie(n,"htmx:beforeRequest",k)){K(a);x();return s}var P=Vt(n);Q(["loadstart","loadend","progress","abort"],function(t){Q([m,m.upload],function(e){e.addEventListener(t,function(e){ie(n,"htmx:xhr:"+t,{lengthComputable:e.lengthComputable,loaded:e.loaded,total:e.total})})})});ie(n,"htmx:beforeSend",k);m.send(e==="get"?null:ir(m,n,q));return s}function gr(e,t){var r=t.xhr;var n=null;var i=null;if(E(r,/HX-Push:/i)){n=r.getResponseHeader("HX-Push");i="push"}else if(E(r,/HX-Push-Url:/i)){n=r.getResponseHeader("HX-Push-Url");i="push"}else if(E(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=Z(e,"hx-push-url");var l=Z(e,"hx-replace-url");var u=Y(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 pr(s,l){var u=l.xhr;var f=l.target;var e=l.etc;if(!ie(s,"htmx:beforeOnLoad",l))return;if(E(u,/HX-Trigger:/i)){De(u,"HX-Trigger",s)}if(E(u,/HX-Location:/i)){Dt();var t=u.getResponseHeader("HX-Location");var c;if(t.indexOf("{")===0){c=y(t);t=c["path"];delete c["path"]}dr("GET",t,c).then(function(){Xt(t)});return}if(E(u,/HX-Redirect:/i)){location.href=u.getResponseHeader("HX-Redirect");return}if(E(u,/HX-Refresh:/i)){if("true"===u.getResponseHeader("HX-Refresh")){location.reload();return}}if(E(u,/HX-Retarget:/i)){l.target=J().querySelector(u.getResponseHeader("HX-Retarget"))}var h=gr(s,l);var r=u.status>=200&&u.status<400&&u.status!==204;var d=u.response;var n=u.status>=400;var i=te({shouldSwap:r,serverResponse:d,isError:n},l);if(!ie(f,"htmx:beforeSwap",i))return;f=i.target;d=i.serverResponse;n=i.isError;l.target=f;l.failed=n;l.successful=!n;if(i.shouldSwap){if(u.status===286){$e(s)}w(s,function(e){d=e.transformResponse(d,u,s)});if(h.type){Dt()}var a=e.swapOverride;if(E(u,/HX-Reswap:/i)){a=u.getResponseHeader("HX-Reswap")}var c=rr(s,a);f.classList.add(z.config.swappingClass);var v=null;var g=null;var o=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 n=S(f);Me(c.swapStyle,f,s,d,n);if(t.elt&&!ee(t.elt)&&t.elt.id){var r=document.getElementById(t.elt.id);var i={preventScroll:c.focusScroll!==undefined?!c.focusScroll:!z.config.defaultFocusScroll};if(r){if(t.start&&r.setSelectionRange){try{r.setSelectionRange(t.start,t.end)}catch(e){}}r.focus(i)}}f.classList.remove(z.config.swappingClass);Q(n.elts,function(e){if(e.classList){e.classList.add(z.config.settlingClass)}ie(e,"htmx:afterSwap",l)});if(E(u,/HX-Trigger-After-Swap:/i)){var a=s;if(!ee(s)){a=J().body}De(u,"HX-Trigger-After-Swap",a)}var o=function(){Q(n.tasks,function(e){e.call()});Q(n.elts,function(e){if(e.classList){e.classList.remove(z.config.settlingClass)}ie(e,"htmx:afterSettle",l)});if(h.type){if(h.type==="push"){Xt(h.path);ie(J().body,"htmx:pushedIntoHistory",{path:h.path})}else{Ft(h.path);ie(J().body,"htmx:replacedInHistory",{path:h.path})}}if(l.pathInfo.anchor){var e=b("#"+l.pathInfo.anchor);if(e){e.scrollIntoView({block:"start",behavior:"auto"})}}if(n.title){var t=b("title");if(t){t.innerHTML=n.title}else{window.document.title=n.title}}ar(n.elts,c);if(E(u,/HX-Trigger-After-Settle:/i)){var r=s;if(!ee(s)){r=J().body}De(u,"HX-Trigger-After-Settle",r)}K(v)};if(c.settleDelay>0){setTimeout(o,c.settleDelay)}else{o()}}catch(e){ne(s,"htmx:swapError",l);K(g);throw e}};var p=z.config.globalViewTransitions;if(c.hasOwnProperty("transition")){p=c.transition}if(p&&ie(s,"htmx:beforeTransition",l)&&typeof Promise!=="undefined"&&document.startViewTransition){var m=new Promise(function(e,t){v=e;g=t});var x=o;o=function(){document.startViewTransition(function(){x();return m})}}if(c.swapDelay>0){setTimeout(o,c.swapDelay)}else{o()}}if(n){ne(s,"htmx:responseError",te({error:"Response Status Error Code "+u.status+" from "+l.pathInfo.requestPath},l))}}var mr={};function xr(){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 yr(e,t){if(t.init){t.init(C)}mr[e]=te(xr(),t)}function br(e){delete mr[e]}function wr(e,r,n){if(e==undefined){return r}if(r==undefined){r=[]}if(n==undefined){n=[]}var t=G(e,"hx-ext");if(t){Q(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=mr[e];if(t&&r.indexOf(t)<0){r.push(t)}}})}return wr(u(e),r,n)}function Sr(e){if(J().readyState!=="loading"){e()}else{J().addEventListener("DOMContentLoaded",e)}}function Er(){if(z.config.includeIndicatorStyles!==false){J().head.insertAdjacentHTML("beforeend","")}}function Cr(){var e=J().querySelector('meta[name="htmx-config"]');if(e){return y(e.content)}else{return null}}function Rr(){var e=Cr();if(e){z.config=te(z.config,e)}}Sr(function(){Rr();Er();var e=J().body;Tt(e);var t=J().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(e){var t=e.target;var r=Y(t);if(r&&r.xhr){r.xhr.abort()}});var r=window.onpopstate;window.onpopstate=function(e){if(e.state&&e.state.htmx){Ut();Q(t,function(e){ie(e,"htmx:restored",{document:J(),triggerEvent:ie})})}else{if(r){r(e)}}};setTimeout(function(){ie(e,"htmx:load",{});e=null},0)});return z}()}); --------------------------------------------------------------------------------