webtimeline 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- webtimeline-0.0.1/LICENSE +24 -0
- webtimeline-0.0.1/PKG-INFO +162 -0
- webtimeline-0.0.1/README.md +143 -0
- webtimeline-0.0.1/pyproject.toml +23 -0
- webtimeline-0.0.1/webtimeline/__init__.py +55 -0
- webtimeline-0.0.1/webtimeline/__main__.py +42 -0
- webtimeline-0.0.1/webtimeline/web/__init__.py +0 -0
- webtimeline-0.0.1/webtimeline/web/app.py +143 -0
- webtimeline-0.0.1/webtimeline/web/static/htmx.min.js +1 -0
- webtimeline-0.0.1/webtimeline/web/static/sse.js +290 -0
- webtimeline-0.0.1/webtimeline/web/templates/chartpage.html +27 -0
- webtimeline-0.0.1/webtimeline/web/templates/notfound.html +18 -0
- webtimeline-0.0.1/webtimeline/wtl.py +171 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
This is free and unencumbered software released into the public domain.
|
|
2
|
+
|
|
3
|
+
Anyone is free to copy, modify, publish, use, compile, sell, or
|
|
4
|
+
distribute this software, either in source code form or as a compiled
|
|
5
|
+
binary, for any purpose, commercial or non-commercial, and by any
|
|
6
|
+
means.
|
|
7
|
+
|
|
8
|
+
In jurisdictions that recognize copyright laws, the author or authors
|
|
9
|
+
of this software dedicate any and all copyright interest in the
|
|
10
|
+
software to the public domain. We make this dedication for the benefit
|
|
11
|
+
of the public at large and to the detriment of our heirs and
|
|
12
|
+
successors. We intend this dedication to be an overt act of
|
|
13
|
+
relinquishment in perpetuity of all present and future rights to this
|
|
14
|
+
software under copyright law.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
19
|
+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
20
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
21
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
|
23
|
+
|
|
24
|
+
For more information, please refer to <https://unlicense.org>
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: webtimeline
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Python Web served time graph
|
|
5
|
+
Keywords: chart,line,plot,graph,web,served,time
|
|
6
|
+
Author-email: Bernard Czenkusz <bernie@skipole.co.uk>
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-Expression: Unlicense
|
|
10
|
+
Classifier: Topic :: Scientific/Engineering
|
|
11
|
+
Classifier: Topic :: Office/Business
|
|
12
|
+
Classifier: Topic :: Education
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: litestar[standard]>=2.19.0
|
|
15
|
+
Requires-Dist: Mako>=1.3.10
|
|
16
|
+
Requires-Dist: minilineplot>=0.0.8
|
|
17
|
+
Project-URL: Source, https://github.com/bernie-skipole/webtimeline
|
|
18
|
+
|
|
19
|
+
# webtimeline
|
|
20
|
+
|
|
21
|
+
Python Web served time graph
|
|
22
|
+
|
|
23
|
+
This provides a class 'WebTimeLine' which generates a web server, serving a page with a lineplot.
|
|
24
|
+
|
|
25
|
+
This is an example, using HTMX, MAKO and LITESTAR and is primarily intended as a record for myself.
|
|
26
|
+
|
|
27
|
+
A coroutine method of the class can be used to add points, in the form of (time.time(), value)
|
|
28
|
+
|
|
29
|
+
These are dynamically added to the plot, and the web page will be updated as points are added.
|
|
30
|
+
|
|
31
|
+
An example browser image is:
|
|
32
|
+
|
|
33
|
+

|
|
34
|
+
|
|
35
|
+
The program can be installed from Pypi into a virtual environment, which will automaticall pull in dependencies.
|
|
36
|
+
|
|
37
|
+
Running "python3 -m webtimeline" will then run the following script and will serve the above chart:
|
|
38
|
+
|
|
39
|
+
import asyncio, time, random
|
|
40
|
+
|
|
41
|
+
from webtimeline import WebTimeLine
|
|
42
|
+
|
|
43
|
+
## This creates an example web service, with random measurements every ten seconds
|
|
44
|
+
|
|
45
|
+
tline = WebTimeLine(host='localhost', port=8000, basepath=None, hours=1, title="My Title", description="Data display")
|
|
46
|
+
|
|
47
|
+
# set a y axis, lowest value 0.0
|
|
48
|
+
# highest 80.0
|
|
49
|
+
# with four intervals up the axis (five values shown at grid lines)
|
|
50
|
+
# and axis numbers printed with one decimal point
|
|
51
|
+
|
|
52
|
+
tline.set_y_axis(ymin=0.0, ymax=80.0, yintervals=4, yformat=".1f")
|
|
53
|
+
|
|
54
|
+
async def my_function(tline):
|
|
55
|
+
"Create data and send it using tline.putpoint()"
|
|
56
|
+
while True:
|
|
57
|
+
value = random.uniform(30, 70) # random value used here
|
|
58
|
+
await tline.putpoint(time.time(), value)
|
|
59
|
+
await asyncio.sleep(10) # pause 10 seconds between readings
|
|
60
|
+
|
|
61
|
+
## create two tasks, one runs the web server, one gathers data
|
|
62
|
+
|
|
63
|
+
async def runchart():
|
|
64
|
+
async with asyncio.TaskGroup() as tg:
|
|
65
|
+
tg.create_task( tline.serve(tg) )
|
|
66
|
+
tg.create_task( my_function(tline) )
|
|
67
|
+
print("Now serving at localhost:8000")
|
|
68
|
+
|
|
69
|
+
asyncio.run(runchart())
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
The web page is purposely minimal, without any CSS. The code could be obtained from github and the Mako templates altered for a more pleasant view.
|
|
73
|
+
|
|
74
|
+
Dependencies are:
|
|
75
|
+
|
|
76
|
+
litestar[standard]
|
|
77
|
+
mako
|
|
78
|
+
minilineplot
|
|
79
|
+
|
|
80
|
+
Details of the WebTimeLine class are:
|
|
81
|
+
|
|
82
|
+
**WebTimeLine(host, port, basepath, hours, height, width, title, description)**
|
|
83
|
+
|
|
84
|
+
host is a string, default 'localhost'
|
|
85
|
+
|
|
86
|
+
port is an integer, default 8000
|
|
87
|
+
|
|
88
|
+
basepath is either None, or a string such as '/graph/' which will set a path segment which will be prepended to the URL path.
|
|
89
|
+
|
|
90
|
+
hours is the hours, between 1 and 48, displayed along the x axis.
|
|
91
|
+
|
|
92
|
+
height is the height of the image, default 600.
|
|
93
|
+
|
|
94
|
+
width is the width of the image, default 800.
|
|
95
|
+
|
|
96
|
+
title, if given is a string shown at the top of the graph.
|
|
97
|
+
|
|
98
|
+
description, if given, is a string shown at the bottom of the graph.
|
|
99
|
+
|
|
100
|
+
**Methods**
|
|
101
|
+
|
|
102
|
+
**serve(tg)**
|
|
103
|
+
|
|
104
|
+
(async method) Set this as a task to serve the web page, tg should be a taskgroup
|
|
105
|
+
|
|
106
|
+
**putpoint(t, v)**
|
|
107
|
+
|
|
108
|
+
(async method) await this to add a point to the graph.
|
|
109
|
+
|
|
110
|
+
t must be a time.time() point.
|
|
111
|
+
|
|
112
|
+
v is the value to be plotted.
|
|
113
|
+
|
|
114
|
+
**set_colors(backcol, gridcol, axiscol, chartbackcol, linecol)**
|
|
115
|
+
|
|
116
|
+
If called sets chart colours.
|
|
117
|
+
|
|
118
|
+
backcol is default "white", The background colour of the whole image
|
|
119
|
+
|
|
120
|
+
gridcol is default "grey", The colour of the chart grid
|
|
121
|
+
|
|
122
|
+
axiscol is default "black", The colour of axis, title and description
|
|
123
|
+
|
|
124
|
+
chartbackcol is default "white", The background colour of the chart
|
|
125
|
+
|
|
126
|
+
linecol is default "blue", The colour of the line being plotted
|
|
127
|
+
|
|
128
|
+
All these colour names are SVG names and can be set as:
|
|
129
|
+
|
|
130
|
+
Color Names: "red", "blue" etc.
|
|
131
|
+
|
|
132
|
+
Hex Codes: "#FF0000" for red.
|
|
133
|
+
|
|
134
|
+
RGB/RGBA: "rgb(255,0,0)" or "rgba(255,0,0,0.5)" (with opacity).
|
|
135
|
+
|
|
136
|
+
HSL/HSLA: "hsl(0,100%,50%)" or "hsla(0,100%,50%,0.5)" (hue, saturation, lightness, alpha)
|
|
137
|
+
|
|
138
|
+
**set_title(title)**
|
|
139
|
+
|
|
140
|
+
Sets the title of the graph, and updates the graph with the new title.
|
|
141
|
+
|
|
142
|
+
**set_description(description)**
|
|
143
|
+
|
|
144
|
+
Sets the description of the graph, and updates the graph with the new description.
|
|
145
|
+
|
|
146
|
+
**set_y_axis(ymin, ymax, yintervals, yformat)**
|
|
147
|
+
|
|
148
|
+
If this is not called, an automatic y scaling will be used.
|
|
149
|
+
|
|
150
|
+
If it is called, then these values will be set, however if any y point exceeds these values, then the chart will revert to auto-scaling.
|
|
151
|
+
|
|
152
|
+
If you wish to revert to autoscaling, call this with None values.
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# webtimeline
|
|
2
|
+
|
|
3
|
+
Python Web served time graph
|
|
4
|
+
|
|
5
|
+
This provides a class 'WebTimeLine' which generates a web server, serving a page with a lineplot.
|
|
6
|
+
|
|
7
|
+
This is an example, using HTMX, MAKO and LITESTAR and is primarily intended as a record for myself.
|
|
8
|
+
|
|
9
|
+
A coroutine method of the class can be used to add points, in the form of (time.time(), value)
|
|
10
|
+
|
|
11
|
+
These are dynamically added to the plot, and the web page will be updated as points are added.
|
|
12
|
+
|
|
13
|
+
An example browser image is:
|
|
14
|
+
|
|
15
|
+

|
|
16
|
+
|
|
17
|
+
The program can be installed from Pypi into a virtual environment, which will automaticall pull in dependencies.
|
|
18
|
+
|
|
19
|
+
Running "python3 -m webtimeline" will then run the following script and will serve the above chart:
|
|
20
|
+
|
|
21
|
+
import asyncio, time, random
|
|
22
|
+
|
|
23
|
+
from webtimeline import WebTimeLine
|
|
24
|
+
|
|
25
|
+
## This creates an example web service, with random measurements every ten seconds
|
|
26
|
+
|
|
27
|
+
tline = WebTimeLine(host='localhost', port=8000, basepath=None, hours=1, title="My Title", description="Data display")
|
|
28
|
+
|
|
29
|
+
# set a y axis, lowest value 0.0
|
|
30
|
+
# highest 80.0
|
|
31
|
+
# with four intervals up the axis (five values shown at grid lines)
|
|
32
|
+
# and axis numbers printed with one decimal point
|
|
33
|
+
|
|
34
|
+
tline.set_y_axis(ymin=0.0, ymax=80.0, yintervals=4, yformat=".1f")
|
|
35
|
+
|
|
36
|
+
async def my_function(tline):
|
|
37
|
+
"Create data and send it using tline.putpoint()"
|
|
38
|
+
while True:
|
|
39
|
+
value = random.uniform(30, 70) # random value used here
|
|
40
|
+
await tline.putpoint(time.time(), value)
|
|
41
|
+
await asyncio.sleep(10) # pause 10 seconds between readings
|
|
42
|
+
|
|
43
|
+
## create two tasks, one runs the web server, one gathers data
|
|
44
|
+
|
|
45
|
+
async def runchart():
|
|
46
|
+
async with asyncio.TaskGroup() as tg:
|
|
47
|
+
tg.create_task( tline.serve(tg) )
|
|
48
|
+
tg.create_task( my_function(tline) )
|
|
49
|
+
print("Now serving at localhost:8000")
|
|
50
|
+
|
|
51
|
+
asyncio.run(runchart())
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
The web page is purposely minimal, without any CSS. The code could be obtained from github and the Mako templates altered for a more pleasant view.
|
|
55
|
+
|
|
56
|
+
Dependencies are:
|
|
57
|
+
|
|
58
|
+
litestar[standard]
|
|
59
|
+
mako
|
|
60
|
+
minilineplot
|
|
61
|
+
|
|
62
|
+
Details of the WebTimeLine class are:
|
|
63
|
+
|
|
64
|
+
**WebTimeLine(host, port, basepath, hours, height, width, title, description)**
|
|
65
|
+
|
|
66
|
+
host is a string, default 'localhost'
|
|
67
|
+
|
|
68
|
+
port is an integer, default 8000
|
|
69
|
+
|
|
70
|
+
basepath is either None, or a string such as '/graph/' which will set a path segment which will be prepended to the URL path.
|
|
71
|
+
|
|
72
|
+
hours is the hours, between 1 and 48, displayed along the x axis.
|
|
73
|
+
|
|
74
|
+
height is the height of the image, default 600.
|
|
75
|
+
|
|
76
|
+
width is the width of the image, default 800.
|
|
77
|
+
|
|
78
|
+
title, if given is a string shown at the top of the graph.
|
|
79
|
+
|
|
80
|
+
description, if given, is a string shown at the bottom of the graph.
|
|
81
|
+
|
|
82
|
+
**Methods**
|
|
83
|
+
|
|
84
|
+
**serve(tg)**
|
|
85
|
+
|
|
86
|
+
(async method) Set this as a task to serve the web page, tg should be a taskgroup
|
|
87
|
+
|
|
88
|
+
**putpoint(t, v)**
|
|
89
|
+
|
|
90
|
+
(async method) await this to add a point to the graph.
|
|
91
|
+
|
|
92
|
+
t must be a time.time() point.
|
|
93
|
+
|
|
94
|
+
v is the value to be plotted.
|
|
95
|
+
|
|
96
|
+
**set_colors(backcol, gridcol, axiscol, chartbackcol, linecol)**
|
|
97
|
+
|
|
98
|
+
If called sets chart colours.
|
|
99
|
+
|
|
100
|
+
backcol is default "white", The background colour of the whole image
|
|
101
|
+
|
|
102
|
+
gridcol is default "grey", The colour of the chart grid
|
|
103
|
+
|
|
104
|
+
axiscol is default "black", The colour of axis, title and description
|
|
105
|
+
|
|
106
|
+
chartbackcol is default "white", The background colour of the chart
|
|
107
|
+
|
|
108
|
+
linecol is default "blue", The colour of the line being plotted
|
|
109
|
+
|
|
110
|
+
All these colour names are SVG names and can be set as:
|
|
111
|
+
|
|
112
|
+
Color Names: "red", "blue" etc.
|
|
113
|
+
|
|
114
|
+
Hex Codes: "#FF0000" for red.
|
|
115
|
+
|
|
116
|
+
RGB/RGBA: "rgb(255,0,0)" or "rgba(255,0,0,0.5)" (with opacity).
|
|
117
|
+
|
|
118
|
+
HSL/HSLA: "hsl(0,100%,50%)" or "hsla(0,100%,50%,0.5)" (hue, saturation, lightness, alpha)
|
|
119
|
+
|
|
120
|
+
**set_title(title)**
|
|
121
|
+
|
|
122
|
+
Sets the title of the graph, and updates the graph with the new title.
|
|
123
|
+
|
|
124
|
+
**set_description(description)**
|
|
125
|
+
|
|
126
|
+
Sets the description of the graph, and updates the graph with the new description.
|
|
127
|
+
|
|
128
|
+
**set_y_axis(ymin, ymax, yintervals, yformat)**
|
|
129
|
+
|
|
130
|
+
If this is not called, an automatic y scaling will be used.
|
|
131
|
+
|
|
132
|
+
If it is called, then these values will be set, however if any y point exceeds these values, then the chart will revert to auto-scaling.
|
|
133
|
+
|
|
134
|
+
If you wish to revert to autoscaling, call this with None values.
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["flit_core >=3.2,<4"]
|
|
3
|
+
build-backend = "flit_core.buildapi"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "webtimeline"
|
|
7
|
+
authors = [{name = "Bernard Czenkusz", email = "bernie@skipole.co.uk"}]
|
|
8
|
+
license = "Unlicense"
|
|
9
|
+
classifiers = ["Topic :: Scientific/Engineering", "Topic :: Office/Business", "Topic :: Education"]
|
|
10
|
+
version = "0.0.1"
|
|
11
|
+
description="Python Web served time graph"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
|
+
keywords=['chart', 'line', 'plot', 'graph', 'web', 'served', 'time']
|
|
15
|
+
dependencies = ["litestar[standard]>=2.19.0", "Mako>=1.3.10", "minilineplot>=0.0.8"]
|
|
16
|
+
|
|
17
|
+
[project.urls]
|
|
18
|
+
Source = "https://github.com/bernie-skipole/webtimeline"
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
webtimeline = "webtimeline.__main__:main"
|
|
22
|
+
|
|
23
|
+
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
from .wtl import WebTimeLine
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# Usage
|
|
7
|
+
|
|
8
|
+
# Create instance of WebTimeLine class
|
|
9
|
+
|
|
10
|
+
# tline = WebTimeLine(host='localhost', port=8000, basepath=None, hours=4, title="My Title")
|
|
11
|
+
|
|
12
|
+
# hours should be between 1 and 48
|
|
13
|
+
# a task tline.serve(tg) should be created to run the web server, tg being a taskgroup
|
|
14
|
+
# tline.putpoint(t, v) should be awaited to insert points
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
####### Example
|
|
18
|
+
|
|
19
|
+
# install dependencies
|
|
20
|
+
# pip install litestar[standard]
|
|
21
|
+
# pip install mako
|
|
22
|
+
# pip install minilineplot
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
######### and the script would be something like :
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# import asyncio, time
|
|
29
|
+
#
|
|
30
|
+
# from webtimeline import WebTimeLine
|
|
31
|
+
#
|
|
32
|
+
## Create WebTimeLine object
|
|
33
|
+
# tline = WebTimeLine(host='localhost', port=8000, basepath=None, hours=4, title="My Title")
|
|
34
|
+
#
|
|
35
|
+
# async def my_function(tline):
|
|
36
|
+
# while True:
|
|
37
|
+
# # Get value from somewhere ....
|
|
38
|
+
# # and puts the measurement into the plot
|
|
39
|
+
# await tline.putpoint(time.time(), v)
|
|
40
|
+
# await asyncio.sleep(10) # pause between readings, and allow webserver to work
|
|
41
|
+
#
|
|
42
|
+
#
|
|
43
|
+
## create two tasks, one runs the web server, one runs my_function(tline) gathering data
|
|
44
|
+
#
|
|
45
|
+
# async def runchart():
|
|
46
|
+
# async with asyncio.TaskGroup() as tg:
|
|
47
|
+
# tg.create_task( tline.serve(tg) )
|
|
48
|
+
# tg.create_task( my_function(tline) )
|
|
49
|
+
#
|
|
50
|
+
## And run the loop
|
|
51
|
+
# asyncio.run(runchart())
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import asyncio, time, random
|
|
2
|
+
|
|
3
|
+
from . import WebTimeLine
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### This creates an example web service, with random measurements every ten seconds
|
|
7
|
+
|
|
8
|
+
## Create WebTimeLine object
|
|
9
|
+
tline = WebTimeLine(host='localhost', port=8000, basepath=None, hours=1, title="My Title", description="Data display")
|
|
10
|
+
|
|
11
|
+
# set a y axis, lowest value 0.0
|
|
12
|
+
# highest 80.0
|
|
13
|
+
# with four intervals up the axis (five values shown at grid lines)
|
|
14
|
+
# and axis numbers printed with one decimal point
|
|
15
|
+
|
|
16
|
+
tline.set_y_axis(ymin=0.0, ymax=80.0, yintervals=4, yformat=".1f")
|
|
17
|
+
|
|
18
|
+
async def my_function(tline):
|
|
19
|
+
"Create data and send it using tline.putpoint()"
|
|
20
|
+
while True:
|
|
21
|
+
value = random.uniform(30, 70) # random value used here
|
|
22
|
+
await tline.putpoint(time.time(), value)
|
|
23
|
+
await asyncio.sleep(10) # pause 10 seconds between readings
|
|
24
|
+
|
|
25
|
+
## create two tasks, one runs the web server, one gathers data
|
|
26
|
+
|
|
27
|
+
async def runchart():
|
|
28
|
+
async with asyncio.TaskGroup() as tg:
|
|
29
|
+
tg.create_task( tline.serve(tg) )
|
|
30
|
+
tg.create_task( my_function(tline) )
|
|
31
|
+
print("Now serving at localhost:8000")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def main():
|
|
36
|
+
"Run the program"
|
|
37
|
+
asyncio.run(runchart())
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
if __name__ == "__main__":
|
|
41
|
+
# And run main
|
|
42
|
+
main()
|
|
File without changes
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
from os import listdir, remove
|
|
6
|
+
from os.path import isfile, join
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from collections.abc import AsyncGenerator
|
|
11
|
+
|
|
12
|
+
from asyncio.exceptions import TimeoutError
|
|
13
|
+
|
|
14
|
+
from litestar import Litestar, get, post, Request
|
|
15
|
+
from litestar.plugins.htmx import HTMXPlugin, HTMXTemplate, ClientRedirect, ClientRefresh
|
|
16
|
+
from litestar.contrib.mako import MakoTemplateEngine
|
|
17
|
+
from litestar.template.config import TemplateConfig
|
|
18
|
+
from litestar.response import Template, Redirect, File
|
|
19
|
+
from litestar.static_files import create_static_files_router
|
|
20
|
+
from litestar.datastructures import Cookie, State
|
|
21
|
+
|
|
22
|
+
from litestar.connection import ASGIConnection
|
|
23
|
+
from litestar.exceptions import NotFoundException
|
|
24
|
+
|
|
25
|
+
from litestar.response import ServerSentEvent, ServerSentEventMessage
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# location of static files, for CSS and javascript
|
|
29
|
+
STATICFILES = Path(__file__).parent.resolve() / "static"
|
|
30
|
+
|
|
31
|
+
# location of template files
|
|
32
|
+
TEMPLATEFILES = Path(__file__).parent.resolve() / "templates"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Dictionary of Global variables
|
|
36
|
+
PARAMETERS = {}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class CheckChange:
|
|
40
|
+
"""Iterate whenever a chart change occurs."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def __aiter__(self):
|
|
44
|
+
return self
|
|
45
|
+
|
|
46
|
+
async def __anext__(self):
|
|
47
|
+
"Whenever there is a change, return a ServerSentEventMessage"
|
|
48
|
+
global PARAMETERS
|
|
49
|
+
mkchart = PARAMETERS['mkchart']
|
|
50
|
+
while True:
|
|
51
|
+
try:
|
|
52
|
+
await asyncio.wait_for(mkchart.chart_event.wait(), timeout=5.0)
|
|
53
|
+
except TimeoutError:
|
|
54
|
+
# perhaps check for a stop flage here
|
|
55
|
+
continue
|
|
56
|
+
# a chart_event has occurred
|
|
57
|
+
return ServerSentEventMessage(event="newchart")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# SSE Handler
|
|
62
|
+
@get(path="/check", sync_to_thread=False)
|
|
63
|
+
def check() -> ServerSentEvent:
|
|
64
|
+
return ServerSentEvent(CheckChange())
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def gotonotfound_error_handler(request: Request, exc: Exception) -> ClientRedirect|Redirect:
|
|
68
|
+
"""If a NotFoundException is raised, this handles it, and redirects
|
|
69
|
+
the caller to the not found page"""
|
|
70
|
+
global PARAMETERS
|
|
71
|
+
basepath = PARAMETERS["basepath"]
|
|
72
|
+
if basepath:
|
|
73
|
+
redirectpath = basepath + "notfound"
|
|
74
|
+
else:
|
|
75
|
+
redirectpath = "/notfound"
|
|
76
|
+
if request.htmx:
|
|
77
|
+
return ClientRedirect(redirectpath)
|
|
78
|
+
return Redirect(redirectpath)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@get("/notfound", sync_to_thread=False )
|
|
82
|
+
def notfound(request: Request) -> Template:
|
|
83
|
+
"This is the not found page of your site"
|
|
84
|
+
return Template("notfound.html")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@get("/")
|
|
88
|
+
async def publicroot(request: Request) -> ClientRedirect|Redirect:
|
|
89
|
+
"This is the public root folder of your site"
|
|
90
|
+
global PARAMETERS
|
|
91
|
+
basepath = PARAMETERS["basepath"]
|
|
92
|
+
if basepath:
|
|
93
|
+
redirectpath = basepath + "chartpage.html"
|
|
94
|
+
else:
|
|
95
|
+
redirectpath = "/chartpage.html"
|
|
96
|
+
if request.htmx:
|
|
97
|
+
return ClientRedirect(redirectpath)
|
|
98
|
+
return Redirect(redirectpath)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@get("/chartpage.html" )
|
|
102
|
+
async def chartpage(request: Request) -> Template:
|
|
103
|
+
"This is the chart page of your site"
|
|
104
|
+
global PARAMETERS
|
|
105
|
+
mkchart = PARAMETERS['mkchart']
|
|
106
|
+
if mkchart.chart is None:
|
|
107
|
+
return Template("chartpage.html", context={"chart":None})
|
|
108
|
+
return Template("chartpage.html", context={"chart":mkchart.chart.to_string()})
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@get("/getchart" )
|
|
112
|
+
async def getchart(request: Request) -> Template:
|
|
113
|
+
"This is just the chart"
|
|
114
|
+
global PARAMETERS
|
|
115
|
+
mkchart = PARAMETERS['mkchart']
|
|
116
|
+
if mkchart.chart is None:
|
|
117
|
+
return HTMXTemplate(None, template_str="<p>No chart generated</p>")
|
|
118
|
+
return HTMXTemplate(None, template_str=mkchart.chart.to_string())
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def make_app(basepath, mkchart):
|
|
123
|
+
# Initialize the Litestar app with a Mako template engine and register the routes
|
|
124
|
+
global PARAMETERS, STATICFILES, TEMPLATEFILES
|
|
125
|
+
PARAMETERS['basepath'] = basepath
|
|
126
|
+
PARAMETERS['mkchart'] = mkchart
|
|
127
|
+
|
|
128
|
+
app = Litestar( path = basepath,
|
|
129
|
+
route_handlers=[publicroot,
|
|
130
|
+
notfound,
|
|
131
|
+
chartpage,
|
|
132
|
+
check,
|
|
133
|
+
getchart,
|
|
134
|
+
create_static_files_router(path="/static", directories=[STATICFILES]),
|
|
135
|
+
],
|
|
136
|
+
exception_handlers={ NotFoundException: gotonotfound_error_handler},
|
|
137
|
+
plugins=[HTMXPlugin()],
|
|
138
|
+
template_config=TemplateConfig(directory=TEMPLATEFILES,
|
|
139
|
+
engine=MakoTemplateEngine
|
|
140
|
+
),
|
|
141
|
+
openapi_config=None
|
|
142
|
+
)
|
|
143
|
+
return app
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var htmx=function(){"use strict";const Q={onLoad:null,process:null,on:null,off:null,trigger:null,ajax:null,find:null,findAll:null,closest:null,values:function(e,t){const n=dn(e,t||"post");return n.values},remove:null,addClass:null,removeClass:null,toggleClass:null,takeClass:null,swap:null,defineExtension:null,removeExtension:null,logAll:null,logNone:null,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,allowScriptTags:true,inlineScriptNonce:"",inlineStyleNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",scrollBehavior:"instant",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get","delete"],selfRequestsOnly:true,ignoreTitle:false,scrollIntoViewOnBoost:true,triggerSpecsCache:null,disableInheritance:false,responseHandling:[{code:"204",swap:false},{code:"[23]..",swap:true},{code:"[45]..",swap:false,error:true}],allowNestedOobSwaps:true,historyRestoreAsHxRequest:true},parseInterval:null,location:location,_:null,version:"2.0.6"};Q.onLoad=j;Q.process=Ft;Q.on=xe;Q.off=be;Q.trigger=ae;Q.ajax=Ln;Q.find=f;Q.findAll=x;Q.closest=g;Q.remove=z;Q.addClass=K;Q.removeClass=G;Q.toggleClass=W;Q.takeClass=Z;Q.swap=$e;Q.defineExtension=zn;Q.removeExtension=$n;Q.logAll=V;Q.logNone=_;Q.parseInterval=d;Q._=e;const n={addTriggerHandler:St,bodyContains:se,canAccessLocalStorage:B,findThisElement:Se,filterValues:yn,swap:$e,hasAttribute:s,getAttributeValue:a,getClosestAttributeValue:ne,getClosestMatch:q,getExpressionVars:Tn,getHeaders:mn,getInputValues:dn,getInternalData:oe,getSwapSpecification:bn,getTriggerSpecs:st,getTarget:Ee,makeFragment:P,mergeObjects:le,makeSettleInfo:Sn,oobSwap:He,querySelectorExt:ue,settleImmediately:Yt,shouldCancel:ht,triggerEvent:ae,triggerErrorEvent:fe,withExtensions:jt};const de=["get","post","put","delete","patch"];const T=de.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function d(e){if(e==undefined){return undefined}let t=NaN;if(e.slice(-2)=="ms"){t=parseFloat(e.slice(0,-2))}else if(e.slice(-1)=="s"){t=parseFloat(e.slice(0,-1))*1e3}else if(e.slice(-1)=="m"){t=parseFloat(e.slice(0,-1))*1e3*60}else{t=parseFloat(e)}return isNaN(t)?undefined:t}function ee(e,t){return e instanceof Element&&e.getAttribute(t)}function s(e,t){return!!e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function a(e,t){return ee(e,t)||ee(e,"data-"+t)}function u(e){const t=e.parentElement;if(!t&&e.parentNode instanceof ShadowRoot)return e.parentNode;return t}function te(){return document}function y(e,t){return e.getRootNode?e.getRootNode({composed:t}):te()}function q(e,t){while(e&&!t(e)){e=u(e)}return e||null}function o(e,t,n){const r=a(t,n);const o=a(t,"hx-disinherit");var i=a(t,"hx-inherit");if(e!==t){if(Q.config.disableInheritance){if(i&&(i==="*"||i.split(" ").indexOf(n)>=0)){return r}else{return null}}if(o&&(o==="*"||o.split(" ").indexOf(n)>=0)){return"unset"}}return r}function ne(t,n){let r=null;q(t,function(e){return!!(r=o(t,ce(e),n))});if(r!=="unset"){return r}}function h(e,t){return e instanceof Element&&e.matches(t)}function A(e){const t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;const n=t.exec(e);if(n){return n[1].toLowerCase()}else{return""}}function L(e){const t=new DOMParser;return t.parseFromString(e,"text/html")}function N(e,t){while(t.childNodes.length>0){e.append(t.childNodes[0])}}function r(e){const t=te().createElement("script");ie(e.attributes,function(e){t.setAttribute(e.name,e.value)});t.textContent=e.textContent;t.async=false;if(Q.config.inlineScriptNonce){t.nonce=Q.config.inlineScriptNonce}return t}function i(e){return e.matches("script")&&(e.type==="text/javascript"||e.type==="module"||e.type==="")}function I(e){Array.from(e.querySelectorAll("script")).forEach(e=>{if(i(e)){const t=r(e);const n=e.parentNode;try{n.insertBefore(t,e)}catch(e){R(e)}finally{e.remove()}}})}function P(e){const t=e.replace(/<head(\s[^>]*)?>[\s\S]*?<\/head>/i,"");const n=A(t);let r;if(n==="html"){r=new DocumentFragment;const i=L(e);N(r,i.body);r.title=i.title}else if(n==="body"){r=new DocumentFragment;const i=L(t);N(r,i.body);r.title=i.title}else{const i=L('<body><template class="internal-htmx-wrapper">'+t+"</template></body>");r=i.querySelector("template").content;r.title=i.title;var o=r.querySelector("title");if(o&&o.parentNode===r){o.remove();r.title=o.innerText}}if(r){if(Q.config.allowScriptTags){I(r)}else{r.querySelectorAll("script").forEach(e=>e.remove())}}return r}function re(e){if(e){e()}}function t(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function D(e){return typeof e==="function"}function k(e){return t(e,"Object")}function oe(e){const t="htmx-internal-data";let n=e[t];if(!n){n=e[t]={}}return n}function M(t){const n=[];if(t){for(let e=0;e<t.length;e++){n.push(t[e])}}return n}function ie(t,n){if(t){for(let e=0;e<t.length;e++){n(t[e])}}}function F(e){const t=e.getBoundingClientRect();const n=t.top;const r=t.bottom;return n<window.innerHeight&&r>=0}function se(e){return e.getRootNode({composed:true})===document}function X(e){return e.trim().split(/\s+/)}function le(e,t){for(const n in t){if(t.hasOwnProperty(n)){e[n]=t[n]}}return e}function v(e){try{return JSON.parse(e)}catch(e){R(e);return null}}function B(){const e="htmx:sessionStorageTest";try{sessionStorage.setItem(e,e);sessionStorage.removeItem(e);return true}catch(e){return false}}function U(e){const t=new URL(e,"http://x");if(t){e=t.pathname+t.search}if(e!="/"){e=e.replace(/\/+$/,"")}return e}function e(e){return On(te().body,function(){return eval(e)})}function j(t){const e=Q.on("htmx:load",function(e){t(e.detail.elt)});return e}function V(){Q.logger=function(e,t,n){if(console){console.log(t,e,n)}}}function _(){Q.logger=null}function f(e,t){if(typeof e!=="string"){return e.querySelector(t)}else{return f(te(),e)}}function x(e,t){if(typeof e!=="string"){return e.querySelectorAll(t)}else{return x(te(),e)}}function b(){return window}function z(e,t){e=w(e);if(t){b().setTimeout(function(){z(e);e=null},t)}else{u(e).removeChild(e)}}function ce(e){return e instanceof Element?e:null}function $(e){return e instanceof HTMLElement?e:null}function J(e){return typeof e==="string"?e:null}function p(e){return e instanceof Element||e instanceof Document||e instanceof DocumentFragment?e:null}function K(e,t,n){e=ce(w(e));if(!e){return}if(n){b().setTimeout(function(){K(e,t);e=null},n)}else{e.classList&&e.classList.add(t)}}function G(e,t,n){let r=ce(w(e));if(!r){return}if(n){b().setTimeout(function(){G(r,t);r=null},n)}else{if(r.classList){r.classList.remove(t);if(r.classList.length===0){r.removeAttribute("class")}}}}function W(e,t){e=w(e);e.classList.toggle(t)}function Z(e,t){e=w(e);ie(e.parentElement.children,function(e){G(e,t)});K(ce(e),t)}function g(e,t){e=ce(w(e));if(e){return e.closest(t)}return null}function l(e,t){return e.substring(0,t.length)===t}function Y(e,t){return e.substring(e.length-t.length)===t}function pe(e){const t=e.trim();if(l(t,"<")&&Y(t,"/>")){return t.substring(1,t.length-2)}else{return t}}function m(t,r,n){if(r.indexOf("global ")===0){return m(t,r.slice(7),true)}t=w(t);const o=[];{let t=0;let n=0;for(let e=0;e<r.length;e++){const l=r[e];if(l===","&&t===0){o.push(r.substring(n,e));n=e+1;continue}if(l==="<"){t++}else if(l==="/"&&e<r.length-1&&r[e+1]===">"){t--}}if(n<r.length){o.push(r.substring(n))}}const i=[];const s=[];while(o.length>0){const r=pe(o.shift());let e;if(r.indexOf("closest ")===0){e=g(ce(t),pe(r.slice(8)))}else if(r.indexOf("find ")===0){e=f(p(t),pe(r.slice(5)))}else if(r==="next"||r==="nextElementSibling"){e=ce(t).nextElementSibling}else if(r.indexOf("next ")===0){e=ge(t,pe(r.slice(5)),!!n)}else if(r==="previous"||r==="previousElementSibling"){e=ce(t).previousElementSibling}else if(r.indexOf("previous ")===0){e=me(t,pe(r.slice(9)),!!n)}else if(r==="document"){e=document}else if(r==="window"){e=window}else if(r==="body"){e=document.body}else if(r==="root"){e=y(t,!!n)}else if(r==="host"){e=t.getRootNode().host}else{s.push(r)}if(e){i.push(e)}}if(s.length>0){const e=s.join(",");const c=p(y(t,!!n));i.push(...M(c.querySelectorAll(e)))}return i}var ge=function(t,e,n){const r=p(y(t,n)).querySelectorAll(e);for(let e=0;e<r.length;e++){const o=r[e];if(o.compareDocumentPosition(t)===Node.DOCUMENT_POSITION_PRECEDING){return o}}};var me=function(t,e,n){const r=p(y(t,n)).querySelectorAll(e);for(let e=r.length-1;e>=0;e--){const o=r[e];if(o.compareDocumentPosition(t)===Node.DOCUMENT_POSITION_FOLLOWING){return o}}};function ue(e,t){if(typeof e!=="string"){return m(e,t)[0]}else{return m(te().body,e)[0]}}function w(e,t){if(typeof e==="string"){return f(p(t)||document,e)}else{return e}}function ye(e,t,n,r){if(D(t)){return{target:te().body,event:J(e),listener:t,options:n}}else{return{target:w(e),event:J(t),listener:n,options:r}}}function xe(t,n,r,o){Gn(function(){const e=ye(t,n,r,o);e.target.addEventListener(e.event,e.listener,e.options)});const e=D(n);return e?n:r}function be(t,n,r){Gn(function(){const e=ye(t,n,r);e.target.removeEventListener(e.event,e.listener)});return D(n)?n:r}const ve=te().createElement("output");function we(t,n){const e=ne(t,n);if(e){if(e==="this"){return[Se(t,n)]}else{const r=m(t,e);const o=/(^|,)(\s*)inherit(\s*)($|,)/.test(e);if(o){const i=ce(q(t,function(e){return e!==t&&s(ce(e),n)}));if(i){r.push(...we(i,n))}}if(r.length===0){R('The selector "'+e+'" on '+n+" returned no matches!");return[ve]}else{return r}}}}function Se(e,t){return ce(q(e,function(e){return a(ce(e),t)!=null}))}function Ee(e){const t=ne(e,"hx-target");if(t){if(t==="this"){return Se(e,"hx-target")}else{return ue(e,t)}}else{const n=oe(e);if(n.boosted){return te().body}else{return e}}}function Ce(e){return Q.config.attributesToSettle.includes(e)}function Oe(t,n){ie(t.attributes,function(e){if(!n.hasAttribute(e.name)&&Ce(e.name)){t.removeAttribute(e.name)}});ie(n.attributes,function(e){if(Ce(e.name)){t.setAttribute(e.name,e.value)}})}function Re(t,e){const n=Jn(e);for(let e=0;e<n.length;e++){const r=n[e];try{if(r.isInlineSwap(t)){return true}}catch(e){R(e)}}return t==="outerHTML"}function He(e,o,i,t){t=t||te();let n="#"+CSS.escape(ee(o,"id"));let s="outerHTML";if(e==="true"){}else if(e.indexOf(":")>0){s=e.substring(0,e.indexOf(":"));n=e.substring(e.indexOf(":")+1)}else{s=e}o.removeAttribute("hx-swap-oob");o.removeAttribute("data-hx-swap-oob");const r=m(t,n,false);if(r.length){ie(r,function(e){let t;const n=o.cloneNode(true);t=te().createDocumentFragment();t.appendChild(n);if(!Re(s,e)){t=p(n)}const r={shouldSwap:true,target:e,fragment:t};if(!ae(e,"htmx:oobBeforeSwap",r))return;e=r.target;if(r.shouldSwap){qe(t);_e(s,e,e,t,i);Te()}ie(i.elts,function(e){ae(e,"htmx:oobAfterSwap",r)})});o.parentNode.removeChild(o)}else{o.parentNode.removeChild(o);fe(te().body,"htmx:oobErrorNoTarget",{content:o})}return e}function Te(){const e=f("#--htmx-preserve-pantry--");if(e){for(const t of[...e.children]){const n=f("#"+t.id);n.parentNode.moveBefore(t,n);n.remove()}e.remove()}}function qe(e){ie(x(e,"[hx-preserve], [data-hx-preserve]"),function(e){const t=a(e,"id");const n=te().getElementById(t);if(n!=null){if(e.moveBefore){let e=f("#--htmx-preserve-pantry--");if(e==null){te().body.insertAdjacentHTML("afterend","<div id='--htmx-preserve-pantry--'></div>");e=f("#--htmx-preserve-pantry--")}e.moveBefore(n,null)}else{e.parentNode.replaceChild(n,e)}}})}function Ae(l,e,c){ie(e.querySelectorAll("[id]"),function(t){const n=ee(t,"id");if(n&&n.length>0){const r=n.replace("'","\\'");const o=t.tagName.replace(":","\\:");const e=p(l);const i=e&&e.querySelector(o+"[id='"+r+"']");if(i&&i!==e){const s=t.cloneNode();Oe(t,i);c.tasks.push(function(){Oe(t,s)})}}})}function Le(e){return function(){G(e,Q.config.addedClass);Ft(ce(e));Ne(p(e));ae(e,"htmx:load")}}function Ne(e){const t="[autofocus]";const n=$(h(e,t)?e:e.querySelector(t));if(n!=null){n.focus()}}function c(e,t,n,r){Ae(e,n,r);while(n.childNodes.length>0){const o=n.firstChild;K(ce(o),Q.config.addedClass);e.insertBefore(o,t);if(o.nodeType!==Node.TEXT_NODE&&o.nodeType!==Node.COMMENT_NODE){r.tasks.push(Le(o))}}}function Ie(e,t){let n=0;while(n<e.length){t=(t<<5)-t+e.charCodeAt(n++)|0}return t}function Pe(t){let n=0;for(let e=0;e<t.attributes.length;e++){const r=t.attributes[e];if(r.value){n=Ie(r.name,n);n=Ie(r.value,n)}}return n}function De(t){const n=oe(t);if(n.onHandlers){for(let e=0;e<n.onHandlers.length;e++){const r=n.onHandlers[e];be(t,r.event,r.listener)}delete n.onHandlers}}function ke(e){const t=oe(e);if(t.timeout){clearTimeout(t.timeout)}if(t.listenerInfos){ie(t.listenerInfos,function(e){if(e.on){be(e.on,e.trigger,e.listener)}})}De(e);ie(Object.keys(t),function(e){if(e!=="firstInitCompleted")delete t[e]})}function S(e){ae(e,"htmx:beforeCleanupElement");ke(e);ie(e.children,function(e){S(e)})}function Me(t,e,n){if(t.tagName==="BODY"){return Ve(t,e,n)}let r;const o=t.previousSibling;const i=u(t);if(!i){return}c(i,t,e,n);if(o==null){r=i.firstChild}else{r=o.nextSibling}n.elts=n.elts.filter(function(e){return e!==t});while(r&&r!==t){if(r instanceof Element){n.elts.push(r)}r=r.nextSibling}S(t);t.remove()}function Fe(e,t,n){return c(e,e.firstChild,t,n)}function Xe(e,t,n){return c(u(e),e,t,n)}function Be(e,t,n){return c(e,null,t,n)}function Ue(e,t,n){return c(u(e),e.nextSibling,t,n)}function je(e){S(e);const t=u(e);if(t){return t.removeChild(e)}}function Ve(e,t,n){const r=e.firstChild;c(e,r,t,n);if(r){while(r.nextSibling){S(r.nextSibling);e.removeChild(r.nextSibling)}S(r);e.removeChild(r)}}function _e(t,e,n,r,o){switch(t){case"none":return;case"outerHTML":Me(n,r,o);return;case"afterbegin":Fe(n,r,o);return;case"beforebegin":Xe(n,r,o);return;case"beforeend":Be(n,r,o);return;case"afterend":Ue(n,r,o);return;case"delete":je(n);return;default:var i=Jn(e);for(let e=0;e<i.length;e++){const s=i[e];try{const l=s.handleSwap(t,n,r,o);if(l){if(Array.isArray(l)){for(let e=0;e<l.length;e++){const c=l[e];if(c.nodeType!==Node.TEXT_NODE&&c.nodeType!==Node.COMMENT_NODE){o.tasks.push(Le(c))}}}return}}catch(e){R(e)}}if(t==="innerHTML"){Ve(n,r,o)}else{_e(Q.config.defaultSwapStyle,e,n,r,o)}}}function ze(e,n,r){var t=x(e,"[hx-swap-oob], [data-hx-swap-oob]");ie(t,function(e){if(Q.config.allowNestedOobSwaps||e.parentElement===null){const t=a(e,"hx-swap-oob");if(t!=null){He(t,e,n,r)}}else{e.removeAttribute("hx-swap-oob");e.removeAttribute("data-hx-swap-oob")}});return t.length>0}function $e(h,d,p,g){if(!g){g={}}let m=null;let n=null;let e=function(){re(g.beforeSwapCallback);h=w(h);const r=g.contextElement?y(g.contextElement,false):te();const e=document.activeElement;let t={};t={elt:e,start:e?e.selectionStart:null,end:e?e.selectionEnd:null};const o=Sn(h);if(p.swapStyle==="textContent"){h.textContent=d}else{let n=P(d);o.title=g.title||n.title;if(g.historyRequest){n=n.querySelector("[hx-history-elt],[data-hx-history-elt]")||n}if(g.selectOOB){const i=g.selectOOB.split(",");for(let t=0;t<i.length;t++){const s=i[t].split(":",2);let e=s[0].trim();if(e.indexOf("#")===0){e=e.substring(1)}const l=s[1]||"true";const c=n.querySelector("#"+e);if(c){He(l,c,o,r)}}}ze(n,o,r);ie(x(n,"template"),function(e){if(e.content&&ze(e.content,o,r)){e.remove()}});if(g.select){const u=te().createDocumentFragment();ie(n.querySelectorAll(g.select),function(e){u.appendChild(e)});n=u}qe(n);_e(p.swapStyle,g.contextElement,h,n,o);Te()}if(t.elt&&!se(t.elt)&&ee(t.elt,"id")){const f=document.getElementById(ee(t.elt,"id"));const a={preventScroll:p.focusScroll!==undefined?!p.focusScroll:!Q.config.defaultFocusScroll};if(f){if(t.start&&f.setSelectionRange){try{f.setSelectionRange(t.start,t.end)}catch(e){}}f.focus(a)}}h.classList.remove(Q.config.swappingClass);ie(o.elts,function(e){if(e.classList){e.classList.add(Q.config.settlingClass)}ae(e,"htmx:afterSwap",g.eventInfo)});re(g.afterSwapCallback);if(!p.ignoreTitle){Bn(o.title)}const n=function(){ie(o.tasks,function(e){e.call()});ie(o.elts,function(e){if(e.classList){e.classList.remove(Q.config.settlingClass)}ae(e,"htmx:afterSettle",g.eventInfo)});if(g.anchor){const e=ce(w("#"+g.anchor));if(e){e.scrollIntoView({block:"start",behavior:"auto"})}}En(o.elts,p);re(g.afterSettleCallback);re(m)};if(p.settleDelay>0){b().setTimeout(n,p.settleDelay)}else{n()}};let t=Q.config.globalViewTransitions;if(p.hasOwnProperty("transition")){t=p.transition}const r=g.contextElement||te();if(t&&ae(r,"htmx:beforeTransition",g.eventInfo)&&typeof Promise!=="undefined"&&document.startViewTransition){const o=new Promise(function(e,t){m=e;n=t});const i=e;e=function(){document.startViewTransition(function(){i();return o})}}try{if(p?.swapDelay&&p.swapDelay>0){b().setTimeout(e,p.swapDelay)}else{e()}}catch(e){fe(r,"htmx:swapError",g.eventInfo);re(n);throw e}}function Je(e,t,n){const r=e.getResponseHeader(t);if(r.indexOf("{")===0){const o=v(r);for(const i in o){if(o.hasOwnProperty(i)){let e=o[i];if(k(e)){n=e.target!==undefined?e.target:n}else{e={value:e}}ae(n,i,e)}}}else{const s=r.split(",");for(let e=0;e<s.length;e++){ae(n,s[e].trim(),[])}}}const Ke=/\s/;const E=/[\s,]/;const Ge=/[_$a-zA-Z]/;const We=/[_$a-zA-Z0-9]/;const Ze=['"',"'","/"];const C=/[^\s]/;const Ye=/[{(]/;const Qe=/[})]/;function et(e){const t=[];let n=0;while(n<e.length){if(Ge.exec(e.charAt(n))){var r=n;while(We.exec(e.charAt(n+1))){n++}t.push(e.substring(r,n+1))}else if(Ze.indexOf(e.charAt(n))!==-1){const o=e.charAt(n);var r=n;n++;while(n<e.length&&e.charAt(n)!==o){if(e.charAt(n)==="\\"){n++}n++}t.push(e.substring(r,n+1))}else{const i=e.charAt(n);t.push(i)}n++}return t}function tt(e,t,n){return Ge.exec(e.charAt(0))&&e!=="true"&&e!=="false"&&e!=="this"&&e!==n&&t!=="."}function nt(r,o,i){if(o[0]==="["){o.shift();let e=1;let t=" return (function("+i+"){ return (";let n=null;while(o.length>0){const s=o[0];if(s==="]"){e--;if(e===0){if(n===null){t=t+"true"}o.shift();t+=")})";try{const l=On(r,function(){return Function(t)()},function(){return true});l.source=t;return l}catch(e){fe(te().body,"htmx:syntax:error",{error:e,source:t});return null}}}else if(s==="["){e++}if(tt(s,n,i)){t+="(("+i+"."+s+") ? ("+i+"."+s+") : (window."+s+"))"}else{t=t+s}n=o.shift()}}}function O(e,t){let n="";while(e.length>0&&!t.test(e[0])){n+=e.shift()}return n}function rt(e){let t;if(e.length>0&&Ye.test(e[0])){e.shift();t=O(e,Qe).trim();e.shift()}else{t=O(e,E)}return t}const ot="input, textarea, select";function it(e,t,n){const r=[];const o=et(t);do{O(o,C);const l=o.length;const c=O(o,/[,\[\s]/);if(c!==""){if(c==="every"){const u={trigger:"every"};O(o,C);u.pollInterval=d(O(o,/[,\[\s]/));O(o,C);var i=nt(e,o,"event");if(i){u.eventFilter=i}r.push(u)}else{const f={trigger:c};var i=nt(e,o,"event");if(i){f.eventFilter=i}O(o,C);while(o.length>0&&o[0]!==","){const a=o.shift();if(a==="changed"){f.changed=true}else if(a==="once"){f.once=true}else if(a==="consume"){f.consume=true}else if(a==="delay"&&o[0]===":"){o.shift();f.delay=d(O(o,E))}else if(a==="from"&&o[0]===":"){o.shift();if(Ye.test(o[0])){var s=rt(o)}else{var s=O(o,E);if(s==="closest"||s==="find"||s==="next"||s==="previous"){o.shift();const h=rt(o);if(h.length>0){s+=" "+h}}}f.from=s}else if(a==="target"&&o[0]===":"){o.shift();f.target=rt(o)}else if(a==="throttle"&&o[0]===":"){o.shift();f.throttle=d(O(o,E))}else if(a==="queue"&&o[0]===":"){o.shift();f.queue=O(o,E)}else if(a==="root"&&o[0]===":"){o.shift();f[a]=rt(o)}else if(a==="threshold"&&o[0]===":"){o.shift();f[a]=O(o,E)}else{fe(e,"htmx:syntax:error",{token:o.shift()})}O(o,C)}r.push(f)}}if(o.length===l){fe(e,"htmx:syntax:error",{token:o.shift()})}O(o,C)}while(o[0]===","&&o.shift());if(n){n[t]=r}return r}function st(e){const t=a(e,"hx-trigger");let n=[];if(t){const r=Q.config.triggerSpecsCache;n=r&&r[t]||it(e,t,r)}if(n.length>0){return n}else if(h(e,"form")){return[{trigger:"submit"}]}else if(h(e,'input[type="button"], input[type="submit"]')){return[{trigger:"click"}]}else if(h(e,ot)){return[{trigger:"change"}]}else{return[{trigger:"click"}]}}function lt(e){oe(e).cancelled=true}function ct(e,t,n){const r=oe(e);r.timeout=b().setTimeout(function(){if(se(e)&&r.cancelled!==true){if(!pt(n,e,Bt("hx:poll:trigger",{triggerSpec:n,target:e}))){t(e)}ct(e,t,n)}},n.pollInterval)}function ut(e){return location.hostname===e.hostname&&ee(e,"href")&&ee(e,"href").indexOf("#")!==0}function ft(e){return g(e,Q.config.disableSelector)}function at(t,n,e){if(t instanceof HTMLAnchorElement&&ut(t)&&(t.target===""||t.target==="_self")||t.tagName==="FORM"&&String(ee(t,"method")).toLowerCase()!=="dialog"){n.boosted=true;let r,o;if(t.tagName==="A"){r="get";o=ee(t,"href")}else{const i=ee(t,"method");r=i?i.toLowerCase():"get";o=ee(t,"action");if(o==null||o===""){o=location.href}if(r==="get"&&o.includes("?")){o=o.replace(/\?[^#]+/,"")}}e.forEach(function(e){gt(t,function(e,t){const n=ce(e);if(ft(n)){S(n);return}he(r,o,n,t)},n,e,true)})}}function ht(e,t){if(e.type==="submit"||e.type==="click"){t=ce(e.target)||t;if(t.tagName==="FORM"){return true}if(t.form&&t.type==="submit"){return true}t=t.closest("a");if(t&&t.href&&(t.getAttribute("href")==="#"||t.getAttribute("href").indexOf("#")!==0)){return true}}return false}function dt(e,t){return oe(e).boosted&&e instanceof HTMLAnchorElement&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function pt(e,t,n){const r=e.eventFilter;if(r){try{return r.call(t,n)!==true}catch(e){const o=r.source;fe(te().body,"htmx:eventFilter:error",{error:e,source:o});return true}}return false}function gt(l,c,e,u,f){const a=oe(l);let t;if(u.from){t=m(l,u.from)}else{t=[l]}if(u.changed){if(!("lastValue"in a)){a.lastValue=new WeakMap}t.forEach(function(e){if(!a.lastValue.has(u)){a.lastValue.set(u,new WeakMap)}a.lastValue.get(u).set(e,e.value)})}ie(t,function(i){const s=function(e){if(!se(l)){i.removeEventListener(u.trigger,s);return}if(dt(l,e)){return}if(f||ht(e,l)){e.preventDefault()}if(pt(u,l,e)){return}const t=oe(e);t.triggerSpec=u;if(t.handledFor==null){t.handledFor=[]}if(t.handledFor.indexOf(l)<0){t.handledFor.push(l);if(u.consume){e.stopPropagation()}if(u.target&&e.target){if(!h(ce(e.target),u.target)){return}}if(u.once){if(a.triggeredOnce){return}else{a.triggeredOnce=true}}if(u.changed){const n=e.target;const r=n.value;const o=a.lastValue.get(u);if(o.has(n)&&o.get(n)===r){return}o.set(n,r)}if(a.delayed){clearTimeout(a.delayed)}if(a.throttle){return}if(u.throttle>0){if(!a.throttle){ae(l,"htmx:trigger");c(l,e);a.throttle=b().setTimeout(function(){a.throttle=null},u.throttle)}}else if(u.delay>0){a.delayed=b().setTimeout(function(){ae(l,"htmx:trigger");c(l,e)},u.delay)}else{ae(l,"htmx:trigger");c(l,e)}}};if(e.listenerInfos==null){e.listenerInfos=[]}e.listenerInfos.push({trigger:u.trigger,listener:s,on:i});i.addEventListener(u.trigger,s)})}let mt=false;let yt=null;function xt(){if(!yt){yt=function(){mt=true};window.addEventListener("scroll",yt);window.addEventListener("resize",yt);setInterval(function(){if(mt){mt=false;ie(te().querySelectorAll("[hx-trigger*='revealed'],[data-hx-trigger*='revealed']"),function(e){bt(e)})}},200)}}function bt(e){if(!s(e,"data-hx-revealed")&&F(e)){e.setAttribute("data-hx-revealed","true");const t=oe(e);if(t.initHash){ae(e,"revealed")}else{e.addEventListener("htmx:afterProcessNode",function(){ae(e,"revealed")},{once:true})}}}function vt(e,t,n,r){const o=function(){if(!n.loaded){n.loaded=true;ae(e,"htmx:trigger");t(e)}};if(r>0){b().setTimeout(o,r)}else{o()}}function wt(t,n,e){let i=false;ie(de,function(r){if(s(t,"hx-"+r)){const o=a(t,"hx-"+r);i=true;n.path=o;n.verb=r;e.forEach(function(e){St(t,e,n,function(e,t){const n=ce(e);if(ft(n)){S(n);return}he(r,o,n,t)})})}});return i}function St(r,e,t,n){if(e.trigger==="revealed"){xt();gt(r,n,t,e);bt(ce(r))}else if(e.trigger==="intersect"){const o={};if(e.root){o.root=ue(r,e.root)}if(e.threshold){o.threshold=parseFloat(e.threshold)}const i=new IntersectionObserver(function(t){for(let e=0;e<t.length;e++){const n=t[e];if(n.isIntersecting){ae(r,"intersect");break}}},o);i.observe(ce(r));gt(ce(r),n,t,e)}else if(!t.firstInitCompleted&&e.trigger==="load"){if(!pt(e,r,Bt("load",{elt:r}))){vt(ce(r),n,t,e.delay)}}else if(e.pollInterval>0){t.polling=true;ct(ce(r),n,e)}else{gt(r,n,t,e)}}function Et(e){const t=ce(e);if(!t){return false}const n=t.attributes;for(let e=0;e<n.length;e++){const r=n[e].name;if(l(r,"hx-on:")||l(r,"data-hx-on:")||l(r,"hx-on-")||l(r,"data-hx-on-")){return true}}return false}const Ct=(new XPathEvaluator).createExpression('.//*[@*[ starts-with(name(), "hx-on:") or starts-with(name(), "data-hx-on:") or'+' starts-with(name(), "hx-on-") or starts-with(name(), "data-hx-on-") ]]');function Ot(e,t){if(Et(e)){t.push(ce(e))}const n=Ct.evaluate(e);let r=null;while(r=n.iterateNext())t.push(ce(r))}function Rt(e){const t=[];if(e instanceof DocumentFragment){for(const n of e.childNodes){Ot(n,t)}}else{Ot(e,t)}return t}function Ht(e){if(e.querySelectorAll){const n=", [hx-boost] a, [data-hx-boost] a, a[hx-boost], a[data-hx-boost]";const r=[];for(const i in Vn){const s=Vn[i];if(s.getSelectors){var t=s.getSelectors();if(t){r.push(t)}}}const o=e.querySelectorAll(T+n+", form, [type='submit'],"+" [hx-ext], [data-hx-ext], [hx-trigger], [data-hx-trigger]"+r.flat().map(e=>", "+e).join(""));return o}else{return[]}}function Tt(e){const t=At(e.target);const n=Nt(e);if(n){n.lastButtonClicked=t}}function qt(e){const t=Nt(e);if(t){t.lastButtonClicked=null}}function At(e){return g(ce(e),"button, input[type='submit']")}function Lt(e){return e.form||g(e,"form")}function Nt(e){const t=At(e.target);if(!t){return}const n=Lt(t);return oe(n)}function It(e){e.addEventListener("click",Tt);e.addEventListener("focusin",Tt);e.addEventListener("focusout",qt)}function Pt(t,e,n){const r=oe(t);if(!Array.isArray(r.onHandlers)){r.onHandlers=[]}let o;const i=function(e){On(t,function(){if(ft(t)){return}if(!o){o=new Function("event",n)}o.call(t,e)})};t.addEventListener(e,i);r.onHandlers.push({event:e,listener:i})}function Dt(t){De(t);for(let e=0;e<t.attributes.length;e++){const n=t.attributes[e].name;const r=t.attributes[e].value;if(l(n,"hx-on")||l(n,"data-hx-on")){const o=n.indexOf("-on")+3;const i=n.slice(o,o+1);if(i==="-"||i===":"){let e=n.slice(o+1);if(l(e,":")){e="htmx"+e}else if(l(e,"-")){e="htmx:"+e.slice(1)}else if(l(e,"htmx-")){e="htmx:"+e.slice(5)}Pt(t,e,r)}}}}function kt(t){ae(t,"htmx:beforeProcessNode");const n=oe(t);const e=st(t);const r=wt(t,n,e);if(!r){if(ne(t,"hx-boost")==="true"){at(t,n,e)}else if(s(t,"hx-trigger")){e.forEach(function(e){St(t,e,n,function(){})})}}if(t.tagName==="FORM"||ee(t,"type")==="submit"&&s(t,"form")){It(t)}n.firstInitCompleted=true;ae(t,"htmx:afterProcessNode")}function Mt(e){if(!(e instanceof Element)){return false}const t=oe(e);const n=Pe(e);if(t.initHash!==n){ke(e);t.initHash=n;return true}return false}function Ft(e){e=w(e);if(ft(e)){S(e);return}const t=[];if(Mt(e)){t.push(e)}ie(Ht(e),function(e){if(ft(e)){S(e);return}if(Mt(e)){t.push(e)}});ie(Rt(e),Dt);ie(t,kt)}function Xt(e){return e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase()}function Bt(e,t){return new CustomEvent(e,{bubbles:true,cancelable:true,composed:true,detail:t})}function fe(e,t,n){ae(e,t,le({error:t},n))}function Ut(e){return e==="htmx:afterProcessNode"}function jt(e,t,n){ie(Jn(e,[],n),function(e){try{t(e)}catch(e){R(e)}})}function R(e){console.error(e)}function ae(e,t,n){e=w(e);if(n==null){n={}}n.elt=e;const r=Bt(t,n);if(Q.logger&&!Ut(t)){Q.logger(e,t,n)}if(n.error){R(n.error);ae(e,"htmx:error",{errorInfo:n})}let o=e.dispatchEvent(r);const i=Xt(t);if(o&&i!==t){const s=Bt(i,r.detail);o=o&&e.dispatchEvent(s)}jt(ce(e),function(e){o=o&&(e.onEvent(t,r)!==false&&!r.defaultPrevented)});return o}let Vt=location.pathname+location.search;function _t(e){Vt=e;if(B()){sessionStorage.setItem("htmx-current-path-for-history",e)}}function zt(){const e=te().querySelector("[hx-history-elt],[data-hx-history-elt]");return e||te().body}function $t(t,e){if(!B()){return}const n=Kt(e);const r=te().title;const o=window.scrollY;if(Q.config.historyCacheSize<=0){sessionStorage.removeItem("htmx-history-cache");return}t=U(t);const i=v(sessionStorage.getItem("htmx-history-cache"))||[];for(let e=0;e<i.length;e++){if(i[e].url===t){i.splice(e,1);break}}const s={url:t,content:n,title:r,scroll:o};ae(te().body,"htmx:historyItemCreated",{item:s,cache:i});i.push(s);while(i.length>Q.config.historyCacheSize){i.shift()}while(i.length>0){try{sessionStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(e){fe(te().body,"htmx:historyCacheError",{cause:e,cache:i});i.shift()}}}function Jt(t){if(!B()){return null}t=U(t);const n=v(sessionStorage.getItem("htmx-history-cache"))||[];for(let e=0;e<n.length;e++){if(n[e].url===t){return n[e]}}return null}function Kt(e){const t=Q.config.requestClass;const n=e.cloneNode(true);ie(x(n,"."+t),function(e){G(e,t)});ie(x(n,"[data-disabled-by-htmx]"),function(e){e.removeAttribute("disabled")});return n.innerHTML}function Gt(){const e=zt();let t=Vt;if(B()){t=sessionStorage.getItem("htmx-current-path-for-history")}t=t||location.pathname+location.search;const n=te().querySelector('[hx-history="false" i],[data-hx-history="false" i]');if(!n){ae(te().body,"htmx:beforeHistorySave",{path:t,historyElt:e});$t(t,e)}if(Q.config.historyEnabled)history.replaceState({htmx:true},te().title,location.href)}function Wt(e){if(Q.config.getCacheBusterParam){e=e.replace(/org\.htmx\.cache-buster=[^&]*&?/,"");if(Y(e,"&")||Y(e,"?")){e=e.slice(0,-1)}}if(Q.config.historyEnabled){history.pushState({htmx:true},"",e)}_t(e)}function Zt(e){if(Q.config.historyEnabled)history.replaceState({htmx:true},"",e);_t(e)}function Yt(e){ie(e,function(e){e.call(undefined)})}function Qt(e){const t=new XMLHttpRequest;const n={swapStyle:"innerHTML",swapDelay:0,settleDelay:0};const r={path:e,xhr:t,historyElt:zt(),swapSpec:n};t.open("GET",e,true);if(Q.config.historyRestoreAsHxRequest){t.setRequestHeader("HX-Request","true")}t.setRequestHeader("HX-History-Restore-Request","true");t.setRequestHeader("HX-Current-URL",location.href);t.onload=function(){if(this.status>=200&&this.status<400){r.response=this.response;ae(te().body,"htmx:historyCacheMissLoad",r);$e(r.historyElt,r.response,n,{contextElement:r.historyElt,historyRequest:true});_t(r.path);ae(te().body,"htmx:historyRestore",{path:e,cacheMiss:true,serverResponse:r.response})}else{fe(te().body,"htmx:historyCacheMissLoadError",r)}};if(ae(te().body,"htmx:historyCacheMiss",r)){t.send()}}function en(e){Gt();e=e||location.pathname+location.search;const t=Jt(e);if(t){const n={swapStyle:"innerHTML",swapDelay:0,settleDelay:0,scroll:t.scroll};const r={path:e,item:t,historyElt:zt(),swapSpec:n};if(ae(te().body,"htmx:historyCacheHit",r)){$e(r.historyElt,t.content,n,{contextElement:r.historyElt,title:t.title});_t(r.path);ae(te().body,"htmx:historyRestore",r)}}else{if(Q.config.refreshOnHistoryMiss){Q.location.reload(true)}else{Qt(e)}}}function tn(e){let t=we(e,"hx-indicator");if(t==null){t=[e]}ie(t,function(e){const t=oe(e);t.requestCount=(t.requestCount||0)+1;e.classList.add.call(e.classList,Q.config.requestClass)});return t}function nn(e){let t=we(e,"hx-disabled-elt");if(t==null){t=[]}ie(t,function(e){const t=oe(e);t.requestCount=(t.requestCount||0)+1;e.setAttribute("disabled","");e.setAttribute("data-disabled-by-htmx","")});return t}function rn(e,t){ie(e.concat(t),function(e){const t=oe(e);t.requestCount=(t.requestCount||1)-1});ie(e,function(e){const t=oe(e);if(t.requestCount===0){e.classList.remove.call(e.classList,Q.config.requestClass)}});ie(t,function(e){const t=oe(e);if(t.requestCount===0){e.removeAttribute("disabled");e.removeAttribute("data-disabled-by-htmx")}})}function on(t,n){for(let e=0;e<t.length;e++){const r=t[e];if(r.isSameNode(n)){return true}}return false}function sn(e){const t=e;if(t.name===""||t.name==null||t.disabled||g(t,"fieldset[disabled]")){return false}if(t.type==="button"||t.type==="submit"||t.tagName==="image"||t.tagName==="reset"||t.tagName==="file"){return false}if(t.type==="checkbox"||t.type==="radio"){return t.checked}return true}function ln(t,e,n){if(t!=null&&e!=null){if(Array.isArray(e)){e.forEach(function(e){n.append(t,e)})}else{n.append(t,e)}}}function cn(t,n,r){if(t!=null&&n!=null){let e=r.getAll(t);if(Array.isArray(n)){e=e.filter(e=>n.indexOf(e)<0)}else{e=e.filter(e=>e!==n)}r.delete(t);ie(e,e=>r.append(t,e))}}function un(e){if(e instanceof HTMLSelectElement&&e.multiple){return M(e.querySelectorAll("option:checked")).map(function(e){return e.value})}if(e instanceof HTMLInputElement&&e.files){return M(e.files)}return e.value}function fn(t,n,r,e,o){if(e==null||on(t,e)){return}else{t.push(e)}if(sn(e)){const i=ee(e,"name");ln(i,un(e),n);if(o){an(e,r)}}if(e instanceof HTMLFormElement){ie(e.elements,function(e){if(t.indexOf(e)>=0){cn(e.name,un(e),n)}else{t.push(e)}if(o){an(e,r)}});new FormData(e).forEach(function(e,t){if(e instanceof File&&e.name===""){return}ln(t,e,n)})}}function an(e,t){const n=e;if(n.willValidate){ae(n,"htmx:validation:validate");if(!n.checkValidity()){t.push({elt:n,message:n.validationMessage,validity:n.validity});ae(n,"htmx:validation:failed",{message:n.validationMessage,validity:n.validity})}}}function hn(n,e){for(const t of e.keys()){n.delete(t)}e.forEach(function(e,t){n.append(t,e)});return n}function dn(e,t){const n=[];const r=new FormData;const o=new FormData;const i=[];const s=oe(e);if(s.lastButtonClicked&&!se(s.lastButtonClicked)){s.lastButtonClicked=null}let l=e instanceof HTMLFormElement&&e.noValidate!==true||a(e,"hx-validate")==="true";if(s.lastButtonClicked){l=l&&s.lastButtonClicked.formNoValidate!==true}if(t!=="get"){fn(n,o,i,Lt(e),l)}fn(n,r,i,e,l);if(s.lastButtonClicked||e.tagName==="BUTTON"||e.tagName==="INPUT"&&ee(e,"type")==="submit"){const u=s.lastButtonClicked||e;const f=ee(u,"name");ln(f,u.value,o)}const c=we(e,"hx-include");ie(c,function(e){fn(n,r,i,ce(e),l);if(!h(e,"form")){ie(p(e).querySelectorAll(ot),function(e){fn(n,r,i,e,l)})}});hn(r,o);return{errors:i,formData:r,values:kn(r)}}function pn(e,t,n){if(e!==""){e+="&"}if(String(n)==="[object Object]"){n=JSON.stringify(n)}const r=encodeURIComponent(n);e+=encodeURIComponent(t)+"="+r;return e}function gn(e){e=Pn(e);let n="";e.forEach(function(e,t){n=pn(n,t,e)});return n}function mn(e,t,n){const r={"HX-Request":"true","HX-Trigger":ee(e,"id"),"HX-Trigger-Name":ee(e,"name"),"HX-Target":a(t,"id"),"HX-Current-URL":location.href};Cn(e,"hx-headers",false,r);if(n!==undefined){r["HX-Prompt"]=n}if(oe(e).boosted){r["HX-Boosted"]="true"}return r}function yn(n,e){const t=ne(e,"hx-params");if(t){if(t==="none"){return new FormData}else if(t==="*"){return n}else if(t.indexOf("not ")===0){ie(t.slice(4).split(","),function(e){e=e.trim();n.delete(e)});return n}else{const r=new FormData;ie(t.split(","),function(t){t=t.trim();if(n.has(t)){n.getAll(t).forEach(function(e){r.append(t,e)})}});return r}}else{return n}}function xn(e){return!!ee(e,"href")&&ee(e,"href").indexOf("#")>=0}function bn(e,t){const n=t||ne(e,"hx-swap");const r={swapStyle:oe(e).boosted?"innerHTML":Q.config.defaultSwapStyle,swapDelay:Q.config.defaultSwapDelay,settleDelay:Q.config.defaultSettleDelay};if(Q.config.scrollIntoViewOnBoost&&oe(e).boosted&&!xn(e)){r.show="top"}if(n){const s=X(n);if(s.length>0){for(let e=0;e<s.length;e++){const l=s[e];if(l.indexOf("swap:")===0){r.swapDelay=d(l.slice(5))}else if(l.indexOf("settle:")===0){r.settleDelay=d(l.slice(7))}else if(l.indexOf("transition:")===0){r.transition=l.slice(11)==="true"}else if(l.indexOf("ignoreTitle:")===0){r.ignoreTitle=l.slice(12)==="true"}else if(l.indexOf("scroll:")===0){const c=l.slice(7);var o=c.split(":");const u=o.pop();var i=o.length>0?o.join(":"):null;r.scroll=u;r.scrollTarget=i}else if(l.indexOf("show:")===0){const f=l.slice(5);var o=f.split(":");const a=o.pop();var i=o.length>0?o.join(":"):null;r.show=a;r.showTarget=i}else if(l.indexOf("focus-scroll:")===0){const h=l.slice("focus-scroll:".length);r.focusScroll=h=="true"}else if(e==0){r.swapStyle=l}else{R("Unknown modifier in hx-swap: "+l)}}}}return r}function vn(e){return ne(e,"hx-encoding")==="multipart/form-data"||h(e,"form")&&ee(e,"enctype")==="multipart/form-data"}function wn(t,n,r){let o=null;jt(n,function(e){if(o==null){o=e.encodeParameters(t,r,n)}});if(o!=null){return o}else{if(vn(n)){return hn(new FormData,Pn(r))}else{return gn(r)}}}function Sn(e){return{tasks:[],elts:[e]}}function En(e,t){const n=e[0];const r=e[e.length-1];if(t.scroll){var o=null;if(t.scrollTarget){o=ce(ue(n,t.scrollTarget))}if(t.scroll==="top"&&(n||o)){o=o||n;o.scrollTop=0}if(t.scroll==="bottom"&&(r||o)){o=o||r;o.scrollTop=o.scrollHeight}if(typeof t.scroll==="number"){b().setTimeout(function(){window.scrollTo(0,t.scroll)},0)}}if(t.show){var o=null;if(t.showTarget){let e=t.showTarget;if(t.showTarget==="window"){e="body"}o=ce(ue(n,e))}if(t.show==="top"&&(n||o)){o=o||n;o.scrollIntoView({block:"start",behavior:Q.config.scrollBehavior})}if(t.show==="bottom"&&(r||o)){o=o||r;o.scrollIntoView({block:"end",behavior:Q.config.scrollBehavior})}}}function Cn(r,e,o,i,s){if(i==null){i={}}if(r==null){return i}const l=a(r,e);if(l){let e=l.trim();let t=o;if(e==="unset"){return null}if(e.indexOf("javascript:")===0){e=e.slice(11);t=true}else if(e.indexOf("js:")===0){e=e.slice(3);t=true}if(e.indexOf("{")!==0){e="{"+e+"}"}let n;if(t){n=On(r,function(){if(s){return Function("event","return ("+e+")").call(r,s)}else{return Function("return ("+e+")").call(r)}},{})}else{n=v(e)}for(const c in n){if(n.hasOwnProperty(c)){if(i[c]==null){i[c]=n[c]}}}}return Cn(ce(u(r)),e,o,i,s)}function On(e,t,n){if(Q.config.allowEval){return t()}else{fe(e,"htmx:evalDisallowedError");return n}}function Rn(e,t,n){return Cn(e,"hx-vars",true,n,t)}function Hn(e,t,n){return Cn(e,"hx-vals",false,n,t)}function Tn(e,t){return le(Rn(e,t),Hn(e,t))}function qn(t,n,r){if(r!==null){try{t.setRequestHeader(n,r)}catch(e){t.setRequestHeader(n,encodeURIComponent(r));t.setRequestHeader(n+"-URI-AutoEncoded","true")}}}function An(t){if(t.responseURL){try{const e=new URL(t.responseURL);return e.pathname+e.search}catch(e){fe(te().body,"htmx:badResponseUrl",{url:t.responseURL})}}}function H(e,t){return t.test(e.getAllResponseHeaders())}function Ln(t,n,r){t=t.toLowerCase();if(r){if(r instanceof Element||typeof r==="string"){return he(t,n,null,null,{targetOverride:w(r)||ve,returnPromise:true})}else{let e=w(r.target);if(r.target&&!e||r.source&&!e&&!w(r.source)){e=ve}return he(t,n,w(r.source),r.event,{handler:r.handler,headers:r.headers,values:r.values,targetOverride:e,swapOverride:r.swap,select:r.select,returnPromise:true})}}else{return he(t,n,null,null,{returnPromise:true})}}function Nn(e){const t=[];while(e){t.push(e);e=e.parentElement}return t}function In(e,t,n){const r=new URL(t,location.protocol!=="about:"?location.href:window.origin);const o=location.protocol!=="about:"?location.origin:window.origin;const i=o===r.origin;if(Q.config.selfRequestsOnly){if(!i){return false}}return ae(e,"htmx:validateUrl",le({url:r,sameHost:i},n))}function Pn(e){if(e instanceof FormData)return e;const t=new FormData;for(const n in e){if(e.hasOwnProperty(n)){if(e[n]&&typeof e[n].forEach==="function"){e[n].forEach(function(e){t.append(n,e)})}else if(typeof e[n]==="object"&&!(e[n]instanceof Blob)){t.append(n,JSON.stringify(e[n]))}else{t.append(n,e[n])}}}return t}function Dn(r,o,e){return new Proxy(e,{get:function(t,e){if(typeof e==="number")return t[e];if(e==="length")return t.length;if(e==="push"){return function(e){t.push(e);r.append(o,e)}}if(typeof t[e]==="function"){return function(){t[e].apply(t,arguments);r.delete(o);t.forEach(function(e){r.append(o,e)})}}if(t[e]&&t[e].length===1){return t[e][0]}else{return t[e]}},set:function(e,t,n){e[t]=n;r.delete(o);e.forEach(function(e){r.append(o,e)});return true}})}function kn(o){return new Proxy(o,{get:function(e,t){if(typeof t==="symbol"){const r=Reflect.get(e,t);if(typeof r==="function"){return function(){return r.apply(o,arguments)}}else{return r}}if(t==="toJSON"){return()=>Object.fromEntries(o)}if(t in e){if(typeof e[t]==="function"){return function(){return o[t].apply(o,arguments)}}}const n=o.getAll(t);if(n.length===0){return undefined}else if(n.length===1){return n[0]}else{return Dn(e,t,n)}},set:function(t,n,e){if(typeof n!=="string"){return false}t.delete(n);if(e&&typeof e.forEach==="function"){e.forEach(function(e){t.append(n,e)})}else if(typeof e==="object"&&!(e instanceof Blob)){t.append(n,JSON.stringify(e))}else{t.append(n,e)}return true},deleteProperty:function(e,t){if(typeof t==="string"){e.delete(t)}return true},ownKeys:function(e){return Reflect.ownKeys(Object.fromEntries(e))},getOwnPropertyDescriptor:function(e,t){return Reflect.getOwnPropertyDescriptor(Object.fromEntries(e),t)}})}function he(t,n,r,o,i,k){let s=null;let l=null;i=i!=null?i:{};if(i.returnPromise&&typeof Promise!=="undefined"){var e=new Promise(function(e,t){s=e;l=t})}if(r==null){r=te().body}const M=i.handler||jn;const F=i.select||null;if(!se(r)){re(s);return e}const c=i.targetOverride||ce(Ee(r));if(c==null||c==ve){fe(r,"htmx:targetError",{target:ne(r,"hx-target")});re(l);return e}let u=oe(r);const f=u.lastButtonClicked;if(f){const A=ee(f,"formaction");if(A!=null){n=A}const L=ee(f,"formmethod");if(L!=null){if(de.includes(L.toLowerCase())){t=L}else{re(s);return e}}}const a=ne(r,"hx-confirm");if(k===undefined){const K=function(e){return he(t,n,r,o,i,!!e)};const G={target:c,elt:r,path:n,verb:t,triggeringEvent:o,etc:i,issueRequest:K,question:a};if(ae(r,"htmx:confirm",G)===false){re(s);return e}}let h=r;let d=ne(r,"hx-sync");let p=null;let X=false;if(d){const N=d.split(":");const I=N[0].trim();if(I==="this"){h=Se(r,"hx-sync")}else{h=ce(ue(r,I))}d=(N[1]||"drop").trim();u=oe(h);if(d==="drop"&&u.xhr&&u.abortable!==true){re(s);return e}else if(d==="abort"){if(u.xhr){re(s);return e}else{X=true}}else if(d==="replace"){ae(h,"htmx:abort")}else if(d.indexOf("queue")===0){const W=d.split(" ");p=(W[1]||"last").trim()}}if(u.xhr){if(u.abortable){ae(h,"htmx:abort")}else{if(p==null){if(o){const P=oe(o);if(P&&P.triggerSpec&&P.triggerSpec.queue){p=P.triggerSpec.queue}}if(p==null){p="last"}}if(u.queuedRequests==null){u.queuedRequests=[]}if(p==="first"&&u.queuedRequests.length===0){u.queuedRequests.push(function(){he(t,n,r,o,i)})}else if(p==="all"){u.queuedRequests.push(function(){he(t,n,r,o,i)})}else if(p==="last"){u.queuedRequests=[];u.queuedRequests.push(function(){he(t,n,r,o,i)})}re(s);return e}}const g=new XMLHttpRequest;u.xhr=g;u.abortable=X;const m=function(){u.xhr=null;u.abortable=false;if(u.queuedRequests!=null&&u.queuedRequests.length>0){const e=u.queuedRequests.shift();e()}};const B=ne(r,"hx-prompt");if(B){var y=prompt(B);if(y===null||!ae(r,"htmx:prompt",{prompt:y,target:c})){re(s);m();return e}}if(a&&!k){if(!confirm(a)){re(s);m();return e}}let x=mn(r,c,y);if(t!=="get"&&!vn(r)){x["Content-Type"]="application/x-www-form-urlencoded"}if(i.headers){x=le(x,i.headers)}const U=dn(r,t);let b=U.errors;const j=U.formData;if(i.values){hn(j,Pn(i.values))}const V=Pn(Tn(r,o));const v=hn(j,V);let w=yn(v,r);if(Q.config.getCacheBusterParam&&t==="get"){w.set("org.htmx.cache-buster",ee(c,"id")||"true")}if(n==null||n===""){n=location.href}const S=Cn(r,"hx-request");const _=oe(r).boosted;let E=Q.config.methodsThatUseUrlParams.indexOf(t)>=0;const C={boosted:_,useUrlParams:E,formData:w,parameters:kn(w),unfilteredFormData:v,unfilteredParameters:kn(v),headers:x,elt:r,target:c,verb:t,errors:b,withCredentials:i.credentials||S.credentials||Q.config.withCredentials,timeout:i.timeout||S.timeout||Q.config.timeout,path:n,triggeringEvent:o};if(!ae(r,"htmx:configRequest",C)){re(s);m();return e}n=C.path;t=C.verb;x=C.headers;w=Pn(C.parameters);b=C.errors;E=C.useUrlParams;if(b&&b.length>0){ae(r,"htmx:validation:halted",C);re(s);m();return e}const z=n.split("#");const $=z[0];const O=z[1];let R=n;if(E){R=$;const Z=!w.keys().next().done;if(Z){if(R.indexOf("?")<0){R+="?"}else{R+="&"}R+=gn(w);if(O){R+="#"+O}}}if(!In(r,R,C)){fe(r,"htmx:invalidPath",C);re(l);m();return e}g.open(t.toUpperCase(),R,true);g.overrideMimeType("text/html");g.withCredentials=C.withCredentials;g.timeout=C.timeout;if(S.noHeaders){}else{for(const D in x){if(x.hasOwnProperty(D)){const Y=x[D];qn(g,D,Y)}}}const H={xhr:g,target:c,requestConfig:C,etc:i,boosted:_,select:F,pathInfo:{requestPath:n,finalRequestPath:R,responsePath:null,anchor:O}};g.onload=function(){try{const t=Nn(r);H.pathInfo.responsePath=An(g);M(r,H);if(H.keepIndicators!==true){rn(T,q)}ae(r,"htmx:afterRequest",H);ae(r,"htmx:afterOnLoad",H);if(!se(r)){let e=null;while(t.length>0&&e==null){const n=t.shift();if(se(n)){e=n}}if(e){ae(e,"htmx:afterRequest",H);ae(e,"htmx:afterOnLoad",H)}}re(s)}catch(e){fe(r,"htmx:onLoadError",le({error:e},H));throw e}finally{m()}};g.onerror=function(){rn(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:sendError",H);re(l);m()};g.onabort=function(){rn(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:sendAbort",H);re(l);m()};g.ontimeout=function(){rn(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:timeout",H);re(l);m()};if(!ae(r,"htmx:beforeRequest",H)){re(s);m();return e}var T=tn(r);var q=nn(r);ie(["loadstart","loadend","progress","abort"],function(t){ie([g,g.upload],function(e){e.addEventListener(t,function(e){ae(r,"htmx:xhr:"+t,{lengthComputable:e.lengthComputable,loaded:e.loaded,total:e.total})})})});ae(r,"htmx:beforeSend",H);const J=E?null:wn(g,r,w);g.send(J);return e}function Mn(e,t){const n=t.xhr;let r=null;let o=null;if(H(n,/HX-Push:/i)){r=n.getResponseHeader("HX-Push");o="push"}else if(H(n,/HX-Push-Url:/i)){r=n.getResponseHeader("HX-Push-Url");o="push"}else if(H(n,/HX-Replace-Url:/i)){r=n.getResponseHeader("HX-Replace-Url");o="replace"}if(r){if(r==="false"){return{}}else{return{type:o,path:r}}}const i=t.pathInfo.finalRequestPath;const s=t.pathInfo.responsePath;const l=ne(e,"hx-push-url");const c=ne(e,"hx-replace-url");const u=oe(e).boosted;let f=null;let a=null;if(l){f="push";a=l}else if(c){f="replace";a=c}else if(u){f="push";a=s||i}if(a){if(a==="false"){return{}}if(a==="true"){a=s||i}if(t.pathInfo.anchor&&a.indexOf("#")===-1){a=a+"#"+t.pathInfo.anchor}return{type:f,path:a}}else{return{}}}function Fn(e,t){var n=new RegExp(e.code);return n.test(t.toString(10))}function Xn(e){for(var t=0;t<Q.config.responseHandling.length;t++){var n=Q.config.responseHandling[t];if(Fn(n,e.status)){return n}}return{swap:false}}function Bn(e){if(e){const t=f("title");if(t){t.textContent=e}else{window.document.title=e}}}function Un(e,t){if(t==="this"){return e}const n=ce(ue(e,t));if(n==null){fe(e,"htmx:targetError",{target:t});throw new Error(`Invalid re-target ${t}`)}return n}function jn(t,e){const n=e.xhr;let r=e.target;const o=e.etc;const i=e.select;if(!ae(t,"htmx:beforeOnLoad",e))return;if(H(n,/HX-Trigger:/i)){Je(n,"HX-Trigger",t)}if(H(n,/HX-Location:/i)){Gt();let e=n.getResponseHeader("HX-Location");var s;if(e.indexOf("{")===0){s=v(e);e=s.path;delete s.path}Ln("get",e,s).then(function(){Wt(e)});return}const l=H(n,/HX-Refresh:/i)&&n.getResponseHeader("HX-Refresh")==="true";if(H(n,/HX-Redirect:/i)){e.keepIndicators=true;Q.location.href=n.getResponseHeader("HX-Redirect");l&&Q.location.reload();return}if(l){e.keepIndicators=true;Q.location.reload();return}const c=Mn(t,e);const u=Xn(n);const f=u.swap;let a=!!u.error;let h=Q.config.ignoreTitle||u.ignoreTitle;let d=u.select;if(u.target){e.target=Un(t,u.target)}var p=o.swapOverride;if(p==null&&u.swapOverride){p=u.swapOverride}if(H(n,/HX-Retarget:/i)){e.target=Un(t,n.getResponseHeader("HX-Retarget"))}if(H(n,/HX-Reswap:/i)){p=n.getResponseHeader("HX-Reswap")}var g=n.response;var m=le({shouldSwap:f,serverResponse:g,isError:a,ignoreTitle:h,selectOverride:d,swapOverride:p},e);if(u.event&&!ae(r,u.event,m))return;if(!ae(r,"htmx:beforeSwap",m))return;r=m.target;g=m.serverResponse;a=m.isError;h=m.ignoreTitle;d=m.selectOverride;p=m.swapOverride;e.target=r;e.failed=a;e.successful=!a;if(m.shouldSwap){if(n.status===286){lt(t)}jt(t,function(e){g=e.transformResponse(g,n,t)});if(c.type){Gt()}var y=bn(t,p);if(!y.hasOwnProperty("ignoreTitle")){y.ignoreTitle=h}r.classList.add(Q.config.swappingClass);if(i){d=i}if(H(n,/HX-Reselect:/i)){d=n.getResponseHeader("HX-Reselect")}const x=ne(t,"hx-select-oob");const b=ne(t,"hx-select");$e(r,g,y,{select:d==="unset"?null:d||b,selectOOB:x,eventInfo:e,anchor:e.pathInfo.anchor,contextElement:t,afterSwapCallback:function(){if(H(n,/HX-Trigger-After-Swap:/i)){let e=t;if(!se(t)){e=te().body}Je(n,"HX-Trigger-After-Swap",e)}},afterSettleCallback:function(){if(H(n,/HX-Trigger-After-Settle:/i)){let e=t;if(!se(t)){e=te().body}Je(n,"HX-Trigger-After-Settle",e)}},beforeSwapCallback:function(){if(c.type){ae(te().body,"htmx:beforeHistoryUpdate",le({history:c},e));if(c.type==="push"){Wt(c.path);ae(te().body,"htmx:pushedIntoHistory",{path:c.path})}else{Zt(c.path);ae(te().body,"htmx:replacedInHistory",{path:c.path})}}}})}if(a){fe(t,"htmx:responseError",le({error:"Response Status Error Code "+n.status+" from "+e.pathInfo.requestPath},e))}}const Vn={};function _n(){return{init:function(e){return null},getSelectors:function(){return null},onEvent:function(e,t){return true},transformResponse:function(e,t,n){return e},isInlineSwap:function(e){return false},handleSwap:function(e,t,n,r){return false},encodeParameters:function(e,t,n){return null}}}function zn(e,t){if(t.init){t.init(n)}Vn[e]=le(_n(),t)}function $n(e){delete Vn[e]}function Jn(e,n,r){if(n==undefined){n=[]}if(e==undefined){return n}if(r==undefined){r=[]}const t=a(e,"hx-ext");if(t){ie(t.split(","),function(e){e=e.replace(/ /g,"");if(e.slice(0,7)=="ignore:"){r.push(e.slice(7));return}if(r.indexOf(e)<0){const t=Vn[e];if(t&&n.indexOf(t)<0){n.push(t)}}})}return Jn(ce(u(e)),n,r)}var Kn=false;te().addEventListener("DOMContentLoaded",function(){Kn=true});function Gn(e){if(Kn||te().readyState==="complete"){e()}else{te().addEventListener("DOMContentLoaded",e)}}function Wn(){if(Q.config.includeIndicatorStyles!==false){const e=Q.config.inlineStyleNonce?` nonce="${Q.config.inlineStyleNonce}"`:"";te().head.insertAdjacentHTML("beforeend","<style"+e+"> ."+Q.config.indicatorClass+"{opacity:0} ."+Q.config.requestClass+" ."+Q.config.indicatorClass+"{opacity:1; transition: opacity 200ms ease-in;} ."+Q.config.requestClass+"."+Q.config.indicatorClass+"{opacity:1; transition: opacity 200ms ease-in;} </style>")}}function Zn(){const e=te().querySelector('meta[name="htmx-config"]');if(e){return v(e.content)}else{return null}}function Yn(){const e=Zn();if(e){Q.config=le(Q.config,e)}}Gn(function(){Yn();Wn();let e=te().body;Ft(e);const t=te().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(e){const t=e.target;const n=oe(t);if(n&&n.xhr){n.xhr.abort()}});const n=window.onpopstate?window.onpopstate.bind(window):null;window.onpopstate=function(e){if(e.state&&e.state.htmx){en();ie(t,function(e){ae(e,"htmx:restored",{document:te(),triggerEvent:ae})})}else{if(n){n(e)}}};b().setTimeout(function(){ae(e,"htmx:load",{});e=null},0)});return Q}();
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Server Sent Events Extension
|
|
3
|
+
============================
|
|
4
|
+
This extension adds support for Server Sent Events to htmx. See /www/extensions/sse.md for usage instructions.
|
|
5
|
+
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
(function() {
|
|
9
|
+
/** @type {import("../htmx").HtmxInternalApi} */
|
|
10
|
+
var api
|
|
11
|
+
|
|
12
|
+
htmx.defineExtension('sse', {
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Init saves the provided reference to the internal HTMX API.
|
|
16
|
+
*
|
|
17
|
+
* @param {import("../htmx").HtmxInternalApi} api
|
|
18
|
+
* @returns void
|
|
19
|
+
*/
|
|
20
|
+
init: function(apiRef) {
|
|
21
|
+
// store a reference to the internal API.
|
|
22
|
+
api = apiRef
|
|
23
|
+
|
|
24
|
+
// set a function in the public API for creating new EventSource objects
|
|
25
|
+
if (htmx.createEventSource == undefined) {
|
|
26
|
+
htmx.createEventSource = createEventSource
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
getSelectors: function() {
|
|
31
|
+
return ['[sse-connect]', '[data-sse-connect]', '[sse-swap]', '[data-sse-swap]']
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* onEvent handles all events passed to this extension.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} name
|
|
38
|
+
* @param {Event} evt
|
|
39
|
+
* @returns void
|
|
40
|
+
*/
|
|
41
|
+
onEvent: function(name, evt) {
|
|
42
|
+
var parent = evt.target || evt.detail.elt
|
|
43
|
+
switch (name) {
|
|
44
|
+
case 'htmx:beforeCleanupElement':
|
|
45
|
+
var internalData = api.getInternalData(parent)
|
|
46
|
+
// Try to remove remove an EventSource when elements are removed
|
|
47
|
+
var source = internalData.sseEventSource
|
|
48
|
+
if (source) {
|
|
49
|
+
api.triggerEvent(parent, 'htmx:sseClose', {
|
|
50
|
+
source,
|
|
51
|
+
type: 'nodeReplaced',
|
|
52
|
+
})
|
|
53
|
+
internalData.sseEventSource.close()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
// Try to create EventSources when elements are processed
|
|
59
|
+
case 'htmx:afterProcessNode':
|
|
60
|
+
ensureEventSourceOnElement(parent)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
/// ////////////////////////////////////////////
|
|
66
|
+
// HELPER FUNCTIONS
|
|
67
|
+
/// ////////////////////////////////////////////
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* createEventSource is the default method for creating new EventSource objects.
|
|
71
|
+
* it is hoisted into htmx.config.createEventSource to be overridden by the user, if needed.
|
|
72
|
+
*
|
|
73
|
+
* @param {string} url
|
|
74
|
+
* @returns EventSource
|
|
75
|
+
*/
|
|
76
|
+
function createEventSource(url) {
|
|
77
|
+
return new EventSource(url, { withCredentials: true })
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* registerSSE looks for attributes that can contain sse events, right
|
|
82
|
+
* now hx-trigger and sse-swap and adds listeners based on these attributes too
|
|
83
|
+
* the closest event source
|
|
84
|
+
*
|
|
85
|
+
* @param {HTMLElement} elt
|
|
86
|
+
*/
|
|
87
|
+
function registerSSE(elt) {
|
|
88
|
+
// Add message handlers for every `sse-swap` attribute
|
|
89
|
+
if (api.getAttributeValue(elt, 'sse-swap')) {
|
|
90
|
+
// Find closest existing event source
|
|
91
|
+
var sourceElement = api.getClosestMatch(elt, hasEventSource)
|
|
92
|
+
if (sourceElement == null) {
|
|
93
|
+
// api.triggerErrorEvent(elt, "htmx:noSSESourceError")
|
|
94
|
+
return null // no eventsource in parentage, orphaned element
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Set internalData and source
|
|
98
|
+
var internalData = api.getInternalData(sourceElement)
|
|
99
|
+
var source = internalData.sseEventSource
|
|
100
|
+
|
|
101
|
+
var sseSwapAttr = api.getAttributeValue(elt, 'sse-swap')
|
|
102
|
+
var sseEventNames = sseSwapAttr.split(',')
|
|
103
|
+
|
|
104
|
+
for (var i = 0; i < sseEventNames.length; i++) {
|
|
105
|
+
const sseEventName = sseEventNames[i].trim()
|
|
106
|
+
const listener = function(event) {
|
|
107
|
+
// If the source is missing then close SSE
|
|
108
|
+
if (maybeCloseSSESource(sourceElement)) {
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// If the body no longer contains the element, remove the listener
|
|
113
|
+
if (!api.bodyContains(elt)) {
|
|
114
|
+
source.removeEventListener(sseEventName, listener)
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// swap the response into the DOM and trigger a notification
|
|
119
|
+
if (!api.triggerEvent(elt, 'htmx:sseBeforeMessage', event)) {
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
swap(elt, event.data)
|
|
123
|
+
api.triggerEvent(elt, 'htmx:sseMessage', event)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Register the new listener
|
|
127
|
+
api.getInternalData(elt).sseEventListener = listener
|
|
128
|
+
source.addEventListener(sseEventName, listener)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Add message handlers for every `hx-trigger="sse:*"` attribute
|
|
133
|
+
if (api.getAttributeValue(elt, 'hx-trigger')) {
|
|
134
|
+
// Find closest existing event source
|
|
135
|
+
var sourceElement = api.getClosestMatch(elt, hasEventSource)
|
|
136
|
+
if (sourceElement == null) {
|
|
137
|
+
// api.triggerErrorEvent(elt, "htmx:noSSESourceError")
|
|
138
|
+
return null // no eventsource in parentage, orphaned element
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Set internalData and source
|
|
142
|
+
var internalData = api.getInternalData(sourceElement)
|
|
143
|
+
var source = internalData.sseEventSource
|
|
144
|
+
|
|
145
|
+
var triggerSpecs = api.getTriggerSpecs(elt)
|
|
146
|
+
triggerSpecs.forEach(function(ts) {
|
|
147
|
+
if (ts.trigger.slice(0, 4) !== 'sse:') {
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
var listener = function (event) {
|
|
152
|
+
if (maybeCloseSSESource(sourceElement)) {
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
if (!api.bodyContains(elt)) {
|
|
156
|
+
source.removeEventListener(ts.trigger.slice(4), listener)
|
|
157
|
+
}
|
|
158
|
+
// Trigger events to be handled by the rest of htmx
|
|
159
|
+
htmx.trigger(elt, ts.trigger, event)
|
|
160
|
+
htmx.trigger(elt, 'htmx:sseMessage', event)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Register the new listener
|
|
164
|
+
api.getInternalData(elt).sseEventListener = listener
|
|
165
|
+
source.addEventListener(ts.trigger.slice(4), listener)
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* ensureEventSourceOnElement creates a new EventSource connection on the provided element.
|
|
172
|
+
* If a usable EventSource already exists, then it is returned. If not, then a new EventSource
|
|
173
|
+
* is created and stored in the element's internalData.
|
|
174
|
+
* @param {HTMLElement} elt
|
|
175
|
+
* @param {number} retryCount
|
|
176
|
+
* @returns {EventSource | null}
|
|
177
|
+
*/
|
|
178
|
+
function ensureEventSourceOnElement(elt, retryCount) {
|
|
179
|
+
if (elt == null) {
|
|
180
|
+
return null
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// handle extension source creation attribute
|
|
184
|
+
if (api.getAttributeValue(elt, 'sse-connect')) {
|
|
185
|
+
var sseURL = api.getAttributeValue(elt, 'sse-connect')
|
|
186
|
+
if (sseURL == null) {
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
ensureEventSource(elt, sseURL, retryCount)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
registerSSE(elt)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function ensureEventSource(elt, url, retryCount) {
|
|
197
|
+
var source = htmx.createEventSource(url)
|
|
198
|
+
|
|
199
|
+
source.onerror = function(err) {
|
|
200
|
+
// Log an error event
|
|
201
|
+
api.triggerErrorEvent(elt, 'htmx:sseError', { error: err, source })
|
|
202
|
+
|
|
203
|
+
// If parent no longer exists in the document, then clean up this EventSource
|
|
204
|
+
if (maybeCloseSSESource(elt)) {
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Otherwise, try to reconnect the EventSource
|
|
209
|
+
if (source.readyState === EventSource.CLOSED) {
|
|
210
|
+
retryCount = retryCount || 0
|
|
211
|
+
retryCount = Math.max(Math.min(retryCount * 2, 128), 1)
|
|
212
|
+
var timeout = retryCount * 500
|
|
213
|
+
window.setTimeout(function() {
|
|
214
|
+
ensureEventSourceOnElement(elt, retryCount)
|
|
215
|
+
}, timeout)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
source.onopen = function(evt) {
|
|
220
|
+
api.triggerEvent(elt, 'htmx:sseOpen', { source })
|
|
221
|
+
|
|
222
|
+
if (retryCount && retryCount > 0) {
|
|
223
|
+
const childrenToFix = elt.querySelectorAll("[sse-swap], [data-sse-swap], [hx-trigger], [data-hx-trigger]")
|
|
224
|
+
for (let i = 0; i < childrenToFix.length; i++) {
|
|
225
|
+
registerSSE(childrenToFix[i])
|
|
226
|
+
}
|
|
227
|
+
// We want to increase the reconnection delay for consecutive failed attempts only
|
|
228
|
+
retryCount = 0
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
api.getInternalData(elt).sseEventSource = source
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
var closeAttribute = api.getAttributeValue(elt, "sse-close");
|
|
236
|
+
if (closeAttribute) {
|
|
237
|
+
// close eventsource when this message is received
|
|
238
|
+
source.addEventListener(closeAttribute, function() {
|
|
239
|
+
api.triggerEvent(elt, 'htmx:sseClose', {
|
|
240
|
+
source,
|
|
241
|
+
type: 'message',
|
|
242
|
+
})
|
|
243
|
+
source.close()
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* maybeCloseSSESource confirms that the parent element still exists.
|
|
250
|
+
* If not, then any associated SSE source is closed and the function returns true.
|
|
251
|
+
*
|
|
252
|
+
* @param {HTMLElement} elt
|
|
253
|
+
* @returns boolean
|
|
254
|
+
*/
|
|
255
|
+
function maybeCloseSSESource(elt) {
|
|
256
|
+
if (!api.bodyContains(elt)) {
|
|
257
|
+
var source = api.getInternalData(elt).sseEventSource
|
|
258
|
+
if (source != undefined) {
|
|
259
|
+
api.triggerEvent(elt, 'htmx:sseClose', {
|
|
260
|
+
source,
|
|
261
|
+
type: 'nodeMissing',
|
|
262
|
+
})
|
|
263
|
+
source.close()
|
|
264
|
+
// source = null
|
|
265
|
+
return true
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return false
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* @param {HTMLElement} elt
|
|
274
|
+
* @param {string} content
|
|
275
|
+
*/
|
|
276
|
+
function swap(elt, content) {
|
|
277
|
+
api.withExtensions(elt, function(extension) {
|
|
278
|
+
content = extension.transformResponse(content, null, elt)
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
var swapSpec = api.getSwapSpecification(elt)
|
|
282
|
+
var target = api.getTarget(elt)
|
|
283
|
+
api.swap(target, content, swapSpec)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
function hasEventSource(node) {
|
|
288
|
+
return api.getInternalData(node).sseEventSource != null
|
|
289
|
+
}
|
|
290
|
+
})()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<meta charset="UTF-8">
|
|
4
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
5
|
+
<title>Chart</title>
|
|
6
|
+
<script src="static/htmx.min.js"></script>
|
|
7
|
+
<script src="static/sse.js"></script>
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
<body>
|
|
11
|
+
|
|
12
|
+
<div hx-ext="sse" sse-connect="check">
|
|
13
|
+
|
|
14
|
+
<div style="margin-top:8vh" hx-get="getchart" hx-trigger="sse:newchart" hx-target=this>
|
|
15
|
+
<div>
|
|
16
|
+
% if chart:
|
|
17
|
+
<div>${chart|n}</div>
|
|
18
|
+
% else:
|
|
19
|
+
<p>No data yet: Waiting..</p>
|
|
20
|
+
% endif
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
</body>
|
|
27
|
+
</html>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
## notfound.html - The page showing a not found message
|
|
5
|
+
|
|
6
|
+
<meta charset="UTF-8">
|
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
8
|
+
<title>Not Found</title>
|
|
9
|
+
|
|
10
|
+
<body>
|
|
11
|
+
|
|
12
|
+
<div style="margin-top:8vh">
|
|
13
|
+
<h3>Sorry, the page you requested has not been found</h3>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
</body>
|
|
18
|
+
</html>
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
|
|
2
|
+
import asyncio
|
|
3
|
+
|
|
4
|
+
import uvicorn
|
|
5
|
+
|
|
6
|
+
import minilineplot
|
|
7
|
+
|
|
8
|
+
from .web.app import make_app
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class WebTimeLine:
|
|
12
|
+
|
|
13
|
+
def __init__(self, host='localhost', port=8000, basepath=None, hours=4, height=600, width=800, title="", description=""):
|
|
14
|
+
self._host = host
|
|
15
|
+
self._port = port
|
|
16
|
+
self._queue = asyncio.Queue()
|
|
17
|
+
self._mkchart = MakeChart(hours=hours, height=height, width=width, title=title, description=description, queue = self._queue)
|
|
18
|
+
self._server = None
|
|
19
|
+
|
|
20
|
+
# ensure basepath is either None, or a string with leading and tailing '/' characters
|
|
21
|
+
if basepath:
|
|
22
|
+
basepath = basepath.strip("/. ")
|
|
23
|
+
if basepath:
|
|
24
|
+
self._basepath = f"/{basepath}/"
|
|
25
|
+
else:
|
|
26
|
+
self._basepath = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def serve(self, tg):
|
|
31
|
+
app = make_app(self._basepath, self._mkchart)
|
|
32
|
+
config = uvicorn.Config(app=app, host=self._host, port=self._port, log_level="error")
|
|
33
|
+
self._server = uvicorn.Server(config)
|
|
34
|
+
tg.create_task( self._server.serve() )
|
|
35
|
+
tg.create_task( self._mkchart.run() )
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
async def putpoint(self, t, v):
|
|
39
|
+
"Put a point timestamp, value"
|
|
40
|
+
item = (t,v)
|
|
41
|
+
await self._queue.put(item)
|
|
42
|
+
|
|
43
|
+
def set_colors(self,
|
|
44
|
+
backcol = "white", # The background colour of the whole image
|
|
45
|
+
gridcol = "grey", # Color of the chart grid
|
|
46
|
+
axiscol = "black", # Color of axis, title and description
|
|
47
|
+
chartbackcol = "white", # Background colour of the chart
|
|
48
|
+
linecol = "blue" # Color of the line being plotted
|
|
49
|
+
):
|
|
50
|
+
self._mkchart.set_colors(backcol, gridcol, axiscol, chartbackcol, linecol)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def set_title(self, title):
|
|
54
|
+
self._mkchart.set_title(title)
|
|
55
|
+
|
|
56
|
+
def set_description(self, description):
|
|
57
|
+
self._mkchart.set_description(description)
|
|
58
|
+
|
|
59
|
+
def set_y_axis(self, ymin, ymax, yintervals, yformat):
|
|
60
|
+
"""If this is not called, an automatic y scaling will be used.
|
|
61
|
+
If it is called, then these values will be set, however if any y point
|
|
62
|
+
exceeds the values, then the chart will revert to auto-scaling.
|
|
63
|
+
If you wish to revert to autoscaling, call this with None values."""
|
|
64
|
+
self._mkchart.set_y_axis(ymin, ymax, yintervals, yformat)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class MakeChart:
|
|
68
|
+
|
|
69
|
+
def __init__(self, hours, height, width, title, description, queue):
|
|
70
|
+
self.hours = hours
|
|
71
|
+
self.height = height
|
|
72
|
+
self.width = width
|
|
73
|
+
self.title = title
|
|
74
|
+
self.description=description
|
|
75
|
+
self.queue = queue
|
|
76
|
+
self.backcol = "white"
|
|
77
|
+
self.gridcol = "grey"
|
|
78
|
+
self.axiscol = "black"
|
|
79
|
+
self.chartbackcol = "white"
|
|
80
|
+
self.linecol = "blue"
|
|
81
|
+
self.points = []
|
|
82
|
+
self.chart = None
|
|
83
|
+
|
|
84
|
+
self.ymin = None
|
|
85
|
+
self.ymax = None
|
|
86
|
+
self.yintervals = None
|
|
87
|
+
self.yformat = None
|
|
88
|
+
|
|
89
|
+
# this event is triggered when a chart event occurs
|
|
90
|
+
self.chart_event = asyncio.Event()
|
|
91
|
+
|
|
92
|
+
async def run(self):
|
|
93
|
+
"Creates the chart when a new point added"
|
|
94
|
+
while True:
|
|
95
|
+
item = await self.queue.get()
|
|
96
|
+
self.points.append(item)
|
|
97
|
+
last_t = item[0]
|
|
98
|
+
first_t = self.points[0][0]
|
|
99
|
+
tspan = last_t - first_t
|
|
100
|
+
plotted_span = self.hours * 3600
|
|
101
|
+
if first_t < last_t - plotted_span:
|
|
102
|
+
self.points.pop(0)
|
|
103
|
+
self.make_chart()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def make_chart(self):
|
|
107
|
+
if len(self.points)<2:
|
|
108
|
+
return
|
|
109
|
+
line = minilineplot.Line(values=self.points,
|
|
110
|
+
color = self.linecol)
|
|
111
|
+
self.chart = minilineplot.Axis(lines=[line],
|
|
112
|
+
imagewidth=self.width,
|
|
113
|
+
imageheight=self.height,
|
|
114
|
+
title = self.title,
|
|
115
|
+
description = self.description,
|
|
116
|
+
gridcol = self.gridcol,
|
|
117
|
+
axiscol = self.axiscol,
|
|
118
|
+
chartbackcol = self.chartbackcol,
|
|
119
|
+
backcol = self.backcol)
|
|
120
|
+
ymax = max(p[1] for p in self.points)
|
|
121
|
+
ymin = min(p[1] for p in self.points)
|
|
122
|
+
if self.ymin is None or self.ymax is None or self.yformat is None or self.yintervals is None :
|
|
123
|
+
self.chart.auto_y()
|
|
124
|
+
elif ymin<self.ymin or ymax>self.ymax:
|
|
125
|
+
self.chart.auto_y()
|
|
126
|
+
else:
|
|
127
|
+
self.chart.ymax = self.ymax
|
|
128
|
+
self.chart.ymin = self.ymin
|
|
129
|
+
self.chart.yintervals = self.yintervals
|
|
130
|
+
self.chart.yformat = self.yformat
|
|
131
|
+
self.chart.auto_time_x(hourspan = self.hours)
|
|
132
|
+
# flag a chart event
|
|
133
|
+
self.chart_event.set()
|
|
134
|
+
self.chart_event.clear()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def set_colors(self, backcol, gridcol, axiscol, chartbackcol, linecol):
|
|
138
|
+
self.backcol = backcol
|
|
139
|
+
self.gridcol = gridcol
|
|
140
|
+
self.axiscol = axiscol
|
|
141
|
+
self.chartbackcol = chartbackcol
|
|
142
|
+
self.linecol = linecol
|
|
143
|
+
self.make_chart()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def set_title(self, title):
|
|
147
|
+
self.title = title
|
|
148
|
+
self.make_chart()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def set_description(self, description):
|
|
152
|
+
self.description = description
|
|
153
|
+
self.make_chart()
|
|
154
|
+
|
|
155
|
+
def set_y_axis(self, ymin, ymax, yintervals, yformat):
|
|
156
|
+
"""If this is not called, an automatic y scaling will be used.
|
|
157
|
+
If it is called, then these values will be set, however if any y point
|
|
158
|
+
exceeds the values, then the chart will revert to auto-scaling.
|
|
159
|
+
If you wish to revert to autoscaling, call this with None values."""
|
|
160
|
+
self.ymin = ymin
|
|
161
|
+
self.ymax = ymax
|
|
162
|
+
self.yintervals = yintervals
|
|
163
|
+
self.yformat = yformat
|
|
164
|
+
self.make_chart()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
|