Skip to content

Commit 4eae952

Browse files
authored
Revert v6 web component rewrite (#367)
Back to the roots, I carefully reverted the web component changes while keeping the updates to the Selenium suite and the package. Reverts 2706cb3
1 parent c1b8467 commit 4eae952

File tree

8 files changed

+728
-816
lines changed

8 files changed

+728
-816
lines changed

.pre-commit-config.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ repos:
2020
rev: 1.29.1
2121
hooks:
2222
- id: django-upgrade
23+
- repo: https://github.com/codingjoe/esupgrade
24+
rev: 2025.9.0
25+
hooks:
26+
- id: esupgrade
2327
- repo: https://github.com/hukkin/mdformat
2428
rev: 1.0.0
2529
hooks:

README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,51 @@ to your CORS policy.
153153
]
154154
```
155155

156+
### Progress Bar
157+
158+
S3File does emit progress signals that can be used to display some kind
159+
of progress bar. Signals named `progress` are emitted for both each
160+
individual file input as well as for the form as a whole.
161+
162+
The progress signal carries the following details:
163+
164+
```javascript
165+
console.debug(event.detail)
166+
167+
{
168+
progress: 0.4725307607171312 // total upload progress of either a form or single input
169+
loaded: 1048576 // total upload progress of either a form or single input
170+
total: 2219064 // total bytes to upload
171+
currentFile: File {…} // file object
172+
currentFileName: "text.txt" // file name of the file currently uploaded
173+
currentFileProgress: 0.47227834703299176 // upload progress of that file
174+
originalEvent: ProgressEvent {…} // the original XHR onprogress event
175+
}
176+
```
177+
178+
The following example implements a Bootstrap progress bar for upload
179+
progress of an entire form.
180+
181+
```html
182+
<div class="progress">
183+
<div class="progress-bar" role="progressbar" style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">0%</div>
184+
</div>
185+
```
186+
187+
```javascript
188+
const form = document.querySelector('form')
189+
const progressBar = document.querySelector('.progress-bar')
190+
191+
form.addEventListener('progress', (event) => {
192+
// event.detail.progress is a value between 0 and 1
193+
const percent = Math.round(event.detail.progress * 100)
194+
195+
progressBar.setAttribute('style', `width: ${percent}%`)
196+
progressBar.setAttribute('aria-valuenow', percent)
197+
progressBar.innerText = `${percent}%`
198+
})
199+
```
200+
156201
### Using S3File in development
157202

158203
Using S3File in development can be helpful especially if you want to use

s3file/forms.py

Lines changed: 47 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,122 +1,63 @@
11
import base64
2-
import html
32
import logging
43
import pathlib
54
import uuid
6-
from html.parser import HTMLParser
75

86
from django.conf import settings
9-
from django.templatetags.static import static
107
from django.utils.functional import cached_property
11-
from django.utils.html import format_html, html_safe
12-
from django.utils.safestring import mark_safe
138
from storages.utils import safe_join
149

15-
from s3file.middleware import S3FileMiddleware
16-
from s3file.storages import get_aws_location, storage
17-
18-
logger = logging.getLogger("s3file")
19-
20-
21-
class InputToS3FileRewriter(HTMLParser):
22-
"""HTML parser that rewrites <input type="file"> to <s3-file> custom elements."""
23-
24-
def __init__(self):
25-
super().__init__()
26-
self.output = []
27-
28-
def handle_starttag(self, tag, attrs):
29-
if tag == "input" and dict(attrs).get("type") == "file":
30-
self.output.append("<s3-file")
31-
for name, value in attrs:
32-
if name != "type":
33-
self.output.append(
34-
f' {name}="{html.escape(value, quote=True)}"'
35-
if value
36-
else f" {name}"
37-
)
38-
self.output.append(">")
39-
else:
40-
self.output.append(self.get_starttag_text())
41-
42-
def handle_endtag(self, tag):
43-
self.output.append(f"</{tag}>")
44-
45-
def handle_data(self, data):
46-
self.output.append(data)
47-
48-
def handle_startendtag(self, tag, attrs):
49-
if tag == "input" and dict(attrs).get("type") == "file":
50-
self.output.append("<s3-file")
51-
for name, value in attrs:
52-
if name != "type":
53-
self.output.append(
54-
f' {name}="{html.escape(value, quote=True)}"'
55-
if value
56-
else f" {name}"
57-
)
58-
self.output.append(">")
59-
else:
60-
self.output.append(self.get_starttag_text())
61-
62-
def handle_comment(self, data):
63-
# Preserve HTML comments in the output
64-
self.output.append(f"<!--{data}-->")
65-
66-
def handle_decl(self, decl):
67-
# Preserve declarations such as <!DOCTYPE ...> in the output
68-
self.output.append(f"<!{decl}>")
69-
70-
def handle_pi(self, data):
71-
# Preserve processing instructions such as <?xml ...?> in the output
72-
self.output.append(f"<?{data}>")
73-
74-
def handle_entityref(self, name):
75-
# Preserve HTML entities like &amp;, &lt;, &gt;
76-
self.output.append(f"&{name};")
77-
78-
def handle_charref(self, name):
79-
# Preserve character references like &#39;, &#x27;
80-
self.output.append(f"&#{name};")
81-
82-
def get_html(self):
83-
return "".join(self.output)
84-
85-
86-
@html_safe
87-
class Asset:
88-
"""A generic asset that can be included in a template."""
10+
try:
11+
from django.forms import Script
12+
except ImportError:
13+
from django.forms.utils import flatatt
14+
from django.templatetags.static import static
15+
from django.utils.html import format_html, html_safe
16+
17+
# Django < 6.0 backport
18+
@html_safe
19+
class MediaAsset:
20+
element_template = "{path}"
21+
22+
def __init__(self, path, **attributes):
23+
self._path = path
24+
self.attributes = attributes
25+
26+
def __eq__(self, other):
27+
return (self.__class__ is other.__class__ and self.path == other.path) or (
28+
isinstance(other, str) and self._path == other
29+
)
8930

90-
def __init__(self, path):
91-
self.path = path
31+
def __hash__(self):
32+
return hash(self._path)
9233

93-
def __eq__(self, other):
94-
return (self.__class__ is other.__class__ and self.path == other.path) or (
95-
other.__class__ is str and self.path == other
96-
)
34+
def __str__(self):
35+
return format_html(
36+
self.element_template,
37+
path=self.path,
38+
attributes=flatatt(self.attributes),
39+
)
9740

98-
def __hash__(self):
99-
return hash(self.path)
41+
def __repr__(self):
42+
return f"{type(self).__qualname__}({self._path!r})"
10043

101-
def __str__(self):
102-
return self.absolute_path(self.path)
44+
@property
45+
def path(self):
46+
if self._path.startswith(("http://", "https://", "/")):
47+
return self._path
48+
return static(self._path)
10349

104-
def absolute_path(self, path):
105-
if path.startswith(("http://", "https://", "/")):
106-
return path
107-
return static(path)
50+
class Script(MediaAsset):
51+
element_template = '<script src="{path}"{attributes}></script>'
10852

109-
def __repr__(self):
110-
return f"{type(self).__qualname__}: {self.path!r}"
53+
def __init__(self, src, **attributes):
54+
super().__init__(src, **attributes)
11155

11256

113-
class ESM(Asset):
114-
"""A JavaScript asset for ECMA Script Modules (ESM)."""
57+
from s3file.middleware import S3FileMiddleware
58+
from s3file.storages import get_aws_location, storage
11559

116-
def __str__(self):
117-
path = super().__str__()
118-
template = '<script src="{}" type="module"></script>'
119-
return format_html(template, self.absolute_path(path))
60+
logger = logging.getLogger("s3file")
12061

12162

12263
class S3FileInputMixin:
@@ -162,15 +103,12 @@ def build_attrs(self, *args, **kwargs):
162103
)
163104
defaults.update(attrs)
164105

106+
try:
107+
defaults["class"] += " s3file"
108+
except KeyError:
109+
defaults["class"] = "s3file"
165110
return defaults
166111

167-
def render(self, name, value, attrs=None, renderer=None):
168-
"""Render the widget as a custom element for Safari compatibility."""
169-
html_output = str(super().render(name, value, attrs=attrs, renderer=renderer))
170-
parser = InputToS3FileRewriter()
171-
parser.feed(html_output)
172-
return mark_safe(parser.get_html()) # noqa: S308
173-
174112
def get_conditions(self, accept):
175113
conditions = [
176114
{"bucket": self.bucket_name},
@@ -201,4 +139,4 @@ def upload_folder(self):
201139
) # S3 uses POSIX paths
202140

203141
class Media:
204-
js = [ESM("s3file/js/s3file.js")]
142+
js = [Script("s3file/js/s3file.js", type="module")]

0 commit comments

Comments
 (0)