@pyscript/core 0.7.10 → 0.7.12

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.
Files changed (80) hide show
  1. package/dist/{codemirror-7vXPINKi.js → codemirror-WbVPJuAs.js} +2 -2
  2. package/dist/codemirror-WbVPJuAs.js.map +1 -0
  3. package/dist/{codemirror_commands-CN4gxvZk.js → codemirror_commands-BRu2f-2p.js} +2 -2
  4. package/dist/codemirror_commands-BRu2f-2p.js.map +1 -0
  5. package/dist/codemirror_lang-python-q-wuh0nL.js +2 -0
  6. package/dist/codemirror_lang-python-q-wuh0nL.js.map +1 -0
  7. package/dist/codemirror_language-DqPeLcFN.js +2 -0
  8. package/dist/codemirror_language-DqPeLcFN.js.map +1 -0
  9. package/dist/{codemirror_state-BIAL8JKm.js → codemirror_state-DWQh5Ruf.js} +2 -2
  10. package/dist/codemirror_state-DWQh5Ruf.js.map +1 -0
  11. package/dist/codemirror_view-CMXZSWgf.js +2 -0
  12. package/dist/codemirror_view-CMXZSWgf.js.map +1 -0
  13. package/dist/core-DmpFMpAn.js +4 -0
  14. package/dist/core-DmpFMpAn.js.map +1 -0
  15. package/dist/core.js +1 -1
  16. package/dist/{deprecations-manager-qW023Rjf.js → deprecations-manager-HQLYNCYn.js} +2 -2
  17. package/dist/{deprecations-manager-qW023Rjf.js.map → deprecations-manager-HQLYNCYn.js.map} +1 -1
  18. package/dist/{donkey-7fH6-q0I.js → donkey-B3F8VdSV.js} +2 -2
  19. package/dist/{donkey-7fH6-q0I.js.map → donkey-B3F8VdSV.js.map} +1 -1
  20. package/dist/{error-CO7OuWCh.js → error-DS_5_C5_.js} +2 -2
  21. package/dist/{error-CO7OuWCh.js.map → error-DS_5_C5_.js.map} +1 -1
  22. package/dist/index-DaYI1YXo.js +2 -0
  23. package/dist/index-DaYI1YXo.js.map +1 -0
  24. package/dist/{mpy-BZxQ23WL.js → mpy-Bo2uW6nt.js} +2 -2
  25. package/dist/{mpy-BZxQ23WL.js.map → mpy-Bo2uW6nt.js.map} +1 -1
  26. package/dist/{py-DI_TP8Id.js → py-BEf8j7L5.js} +2 -2
  27. package/dist/{py-DI_TP8Id.js.map → py-BEf8j7L5.js.map} +1 -1
  28. package/dist/{py-editor-BJBbMtNv.js → py-editor-C0XF2rwE.js} +2 -2
  29. package/dist/{py-editor-BJBbMtNv.js.map → py-editor-C0XF2rwE.js.map} +1 -1
  30. package/dist/{py-game-C7N5JK0D.js → py-game-eWNz96mt.js} +2 -2
  31. package/dist/{py-game-C7N5JK0D.js.map → py-game-eWNz96mt.js.map} +1 -1
  32. package/dist/{py-terminal-Cy8siD6F.js → py-terminal-VtavPj1S.js} +2 -2
  33. package/dist/{py-terminal-Cy8siD6F.js.map → py-terminal-VtavPj1S.js.map} +1 -1
  34. package/dist/xterm_addon-fit-DxKdSnof.js +14 -0
  35. package/dist/xterm_addon-fit-DxKdSnof.js.map +1 -0
  36. package/dist/xterm_addon-web-links-B6rWzrcs.js +14 -0
  37. package/dist/xterm_addon-web-links-B6rWzrcs.js.map +1 -0
  38. package/dist/zip-CgZGjqjF.js +2 -0
  39. package/dist/zip-CgZGjqjF.js.map +1 -0
  40. package/package.json +16 -15
  41. package/src/3rd-party/xterm_addon-fit.js +14 -2
  42. package/src/3rd-party/xterm_addon-web-links.js +14 -2
  43. package/src/core.js +13 -2
  44. package/src/stdlib/pyscript/__init__.py +100 -31
  45. package/src/stdlib/pyscript/context.py +198 -0
  46. package/src/stdlib/pyscript/display.py +211 -127
  47. package/src/stdlib/pyscript/events.py +191 -88
  48. package/src/stdlib/pyscript/fetch.py +156 -25
  49. package/src/stdlib/pyscript/ffi.py +132 -16
  50. package/src/stdlib/pyscript/flatted.py +78 -1
  51. package/src/stdlib/pyscript/fs.py +207 -50
  52. package/src/stdlib/pyscript/media.py +210 -50
  53. package/src/stdlib/pyscript/storage.py +214 -27
  54. package/src/stdlib/pyscript/util.py +28 -7
  55. package/src/stdlib/pyscript/web.py +1079 -881
  56. package/src/stdlib/pyscript/websocket.py +252 -45
  57. package/src/stdlib/pyscript/workers.py +176 -27
  58. package/src/stdlib/pyscript.js +13 -13
  59. package/src/sync.js +1 -1
  60. package/types/stdlib/pyscript.d.ts +1 -1
  61. package/dist/codemirror-7vXPINKi.js.map +0 -1
  62. package/dist/codemirror_commands-CN4gxvZk.js.map +0 -1
  63. package/dist/codemirror_lang-python-CkOVBHci.js +0 -2
  64. package/dist/codemirror_lang-python-CkOVBHci.js.map +0 -1
  65. package/dist/codemirror_language-DOkvasqm.js +0 -2
  66. package/dist/codemirror_language-DOkvasqm.js.map +0 -1
  67. package/dist/codemirror_state-BIAL8JKm.js.map +0 -1
  68. package/dist/codemirror_view-Bt4sLgyA.js +0 -2
  69. package/dist/codemirror_view-Bt4sLgyA.js.map +0 -1
  70. package/dist/core-5ORB_Mcj.js +0 -4
  71. package/dist/core-5ORB_Mcj.js.map +0 -1
  72. package/dist/index-jZ1aOVVJ.js +0 -2
  73. package/dist/index-jZ1aOVVJ.js.map +0 -1
  74. package/dist/xterm_addon-fit--gyF3PcZ.js +0 -2
  75. package/dist/xterm_addon-fit--gyF3PcZ.js.map +0 -1
  76. package/dist/xterm_addon-web-links-D95xh2la.js +0 -2
  77. package/dist/xterm_addon-web-links-D95xh2la.js.map +0 -1
  78. package/dist/zip-CakRHzZu.js +0 -2
  79. package/dist/zip-CakRHzZu.js.map +0 -1
  80. package/src/stdlib/pyscript/magic_js.py +0 -84
@@ -1,184 +1,585 @@
1
- """Lightweight interface to the DOM and HTML elements."""
1
+ """
2
+ A lightweight Pythonic interface to the DOM and HTML elements that helps you
3
+ interact with web pages, making it easy to find, create, manipulate, and
4
+ compose HTML elements from Python.
5
+
6
+ Highlights include:
2
7
 
3
- # `when` is not used in this module. It is imported here save the user an additional
4
- # import (i.e. they can get what they need from `pyscript.web`).
8
+ Use the `page` object to find elements on the current page:
9
+
10
+ ```python
11
+ from pyscript import web
5
12
 
6
- # from __future__ import annotations # CAUTION: This is not supported in MicroPython.
13
+
14
+ # Find by CSS selector (returns an ElementCollection).
15
+ divs = web.page.find("div")
16
+ buttons = web.page.find(".button-class")
17
+
18
+ # Get element by ID (returns single Element or None).
19
+ header = web.page["header-id"]
20
+ header = web.page["#header-id"] # the "#" prefix is optional.
21
+
22
+ # Access page structure.
23
+ web.page.body.append(some_element)
24
+ web.page.title = "New Page Title"
25
+ ```
26
+
27
+ Create new elements and compose them together:
28
+
29
+ ```python
30
+ # Create simple elements.
31
+ div = web.div("Hello, World!")
32
+ paragraph = web.p("Some text", id="my-para", className="text-content")
33
+
34
+ # Compose elements together.
35
+ container = web.div(
36
+ web.h1("Title"),
37
+ web.p("First paragraph"),
38
+ web.p("Second paragraph"),
39
+ id="container"
40
+ )
41
+
42
+ # Add to the page.
43
+ web.page.body.append(container)
44
+
45
+ # Create with initial attributes.
46
+ link = web.a(
47
+ "Click me",
48
+ href="https://example.com",
49
+ target="_blank",
50
+ classes=["link", "external"]
51
+ )
52
+ ```
53
+
54
+ Modify element content and attributes:
55
+
56
+ ```python
57
+ # Update content.
58
+ element.innerHTML = "<b>Bold text</b>"
59
+ element.textContent = "Plain text"
60
+
61
+ # Update attributes.
62
+ element.id = "new-id"
63
+ element.title = "Tooltip text"
64
+
65
+ # Bulk update with convenience method.
66
+ element.update(
67
+ classes=["active", "highlighted"],
68
+ style={"color": "red", "font-size": "16px"},
69
+ title="Updated tooltip"
70
+ )
71
+ ```
72
+
73
+ An element's CSS classes behave like a Python `set`:
74
+
75
+ ```python
76
+ # Add and remove classes
77
+ element.classes.add("active")
78
+ element.classes.add("highlighted")
79
+ element.classes.remove("hidden")
80
+
81
+ # Check membership.
82
+ if "active" in element.classes:
83
+ print("Element is active")
84
+
85
+ # Clear all classes.
86
+ element.classes.clear()
87
+
88
+ # Discard (no error if missing).
89
+ element.classes.discard("maybe-not-there")
90
+ ```
91
+
92
+ An element's styles behave like a Python `dict`:
93
+
94
+ ```python
95
+ # Set individual styles.
96
+ element.style["color"] = "red"
97
+ element.style["background-color"] = "#f0f0f0"
98
+ element.style["font-size"] = "16px"
99
+
100
+ # Remove a style.
101
+ del element.style["margin"]
102
+
103
+ # Check if style is set.
104
+ if "color" in element.style:
105
+ print(f"Color is {element.style['color']}")
106
+ ```
107
+
108
+ Update multiple elements at once via an `ElementCollection`:
109
+
110
+ ```python
111
+ # Find multiple elements (returns an ElementCollection).
112
+ items = web.page.find(".list-item")
113
+
114
+ # Iterate over collection.
115
+ for item in items:
116
+ item.innerHTML = "Updated"
117
+ item.classes.add("processed")
118
+
119
+ # Bulk update all elements.
120
+ items.update_all(
121
+ innerHTML="Hello",
122
+ className="updated-item"
123
+ )
124
+
125
+ # Index and slice collections.
126
+ first = items[0]
127
+ subset = items[1:3]
128
+
129
+ # Get an element by ID within the collection.
130
+ special = items["special-id"]
131
+
132
+ # Find descendants within the collection.
133
+ subitems = items.find(".sub-item")
134
+ ```
135
+
136
+ Manage `select` element options (also for `datalist` and `optgroup`):
137
+
138
+ ```python
139
+ # Get existing select.
140
+ select = web.page["my-select"]
141
+
142
+ # Add options.
143
+ select.options.add(value="1", html="Option 1")
144
+ select.options.add(value="2", html="Option 2", selected=True)
145
+
146
+ # Get selected option.
147
+ selected = select.options.selected
148
+ print(f"Selected: {selected.value}")
149
+
150
+ # Iterate over options.
151
+ for option in select.options:
152
+ print(f"{option.value}: {option.innerHTML}")
153
+
154
+ # Clear all options.
155
+ select.options.clear()
156
+
157
+ # Remove specific option by index.
158
+ select.options.remove(0)
159
+ ```
160
+
161
+ Attach event handlers to elements:
162
+
163
+ ```python
164
+ from pyscript import when
165
+
166
+ button = web.button("Click me", id="my-button")
167
+
168
+ # Use the when decorator.
169
+ @when("click", button)
170
+ def handle_click(event):
171
+ print("Button clicked!")
172
+
173
+ # Or add directly to the event.
174
+ def another_handler(event):
175
+ print("Another handler")
176
+
177
+ button.on_click.add_listener(another_handler)
178
+
179
+ # Pass handler during creation.
180
+ button = web.button("Click", on_click=handle_click)
181
+ ```
182
+
183
+ All `Element` instances provide direct access to the underlying DOM element
184
+ via attribute delegation:
185
+
186
+ ```python
187
+ # Most DOM methods are accessible directly.
188
+ element.scrollIntoView()
189
+ element.focus()
190
+ element.blur()
191
+
192
+ # But we do have a historic convenience method for scrolling into view.
193
+ element.show_me() # Calls scrollIntoView()
194
+
195
+ # Access the raw DOM element when needed for special cases.
196
+ dom_element = element._dom_element
197
+ ```
198
+
199
+ The main entry point is the `page` object, which represents the current
200
+ document and provides access to common elements like `page.body` and methods
201
+ like `page.find()` for querying the DOM.
202
+ """
7
203
 
8
204
  from pyscript import document, when, Event # noqa: F401
9
205
  from pyscript.ffi import create_proxy, is_none
10
206
 
11
207
 
12
- def wrap_dom_element(dom_element):
13
- """Wrap an existing DOM element in an instance of a subclass of `Element`.
208
+ # Utility functions for finding and wrapping DOM elements.
209
+
210
+
211
+ def _wrap_if_not_none(dom_element):
212
+ """
213
+ Wrap a `dom_element`, returning `None` if the element is `None`/`null`.
214
+ """
215
+ return Element.wrap_dom_element(dom_element) if not is_none(dom_element) else None
216
+
217
+
218
+ def _find_by_id(dom_node, target_id):
219
+ """
220
+ Find an element by `id` within a `dom_node`.
221
+
222
+ The `target_id` can optionally start with '#'. Returns a wrapped `Element`
223
+ or `None` if not found.
224
+ """
225
+ element_id = target_id[1:] if target_id.startswith("#") else target_id
226
+ result = dom_node.querySelector(f"#{element_id}")
227
+ return _wrap_if_not_none(result)
228
+
14
229
 
15
- This is just a convenience function to avoid having to import the `Element` class
16
- and use its class method.
230
+ def _find_and_wrap(dom_node, selector):
17
231
  """
232
+ Find all descendants of `dom_node` matching the CSS `selector`.
18
233
 
19
- return Element.wrap_dom_element(dom_element)
234
+ Returns an `ElementCollection` of wrapped elements.
235
+ """
236
+ return ElementCollection.wrap_dom_elements(dom_node.querySelectorAll(selector))
20
237
 
21
238
 
22
239
  class Element:
23
- # A lookup table to get an `Element` subclass by tag name. Used when wrapping an
24
- # existing DOM element.
240
+ """
241
+ The base class for all [HTML elements](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements).
242
+
243
+ Provides a Pythonic interface to DOM elements with support for attributes,
244
+ events, styles, classes, and DOM manipulation. It can create new elements
245
+ or wrap existing DOM elements.
246
+
247
+ Elements are typically created using the tag-specific classes found
248
+ within this namespace (e.g. `web.div`, `web.span`, `web.button`):
249
+
250
+ ```python
251
+ from pyscript import web
252
+
253
+
254
+ # Create a simple div.
255
+ div = web.div("Hello, World!")
256
+
257
+ # Create with attributes.
258
+ link = web.a("Click me", href="https://example.com", target="_blank")
259
+
260
+ # Create with classes and styles.
261
+ button = web.button(
262
+ "Submit",
263
+ classes=["primary", "large"],
264
+ style={"background-color": "blue", "color": "white"},
265
+ id="submit-btn"
266
+ )
267
+ ```
268
+
269
+ !!! info
270
+
271
+ Some elements have an underscore suffix in their class names (e.g.
272
+ `select_`, `input_`).
273
+
274
+ This is to avoid clashes with Python keywords. The underscore is removed
275
+ when determining the actual HTML tag name.
276
+
277
+ Wrap existing DOM elements found on the page:
278
+
279
+ ```python
280
+ # Find and wrap an element by CSS selector.
281
+ existing = web.page.find(".my_class")[0]
282
+
283
+ # Or, better, just use direct ID lookup (with or without the
284
+ # leading '#').
285
+ existing = web.page["my-element"]
286
+ ```
287
+
288
+ Element attributes are accessible as Python properties:
289
+
290
+ ```python
291
+ # Get attributes.
292
+ element_id = div.id
293
+ element_title = div.title
294
+ element_href = link.href
295
+
296
+ # Set attributes.
297
+ div.id = "new-id"
298
+ div.title = "Tooltip text"
299
+ link.href = "https://new-url.com"
300
+
301
+ # HTML content.
302
+ div.innerHTML = "<b>Bold text</b>"
303
+ div.textContent = "Plain text"
304
+ ```
305
+
306
+ CSS classes are managed through a `set`-like interface:
307
+
308
+ ```python
309
+ # Add classes.
310
+ element.classes.add("active")
311
+ element.classes.add("highlighted")
312
+
313
+ # Remove classes.
314
+ element.classes.remove("inactive")
315
+ element.classes.discard("maybe-missing") # No error if absent
316
+
317
+ # Check membership.
318
+ if "active" in element.classes:
319
+ print("Element is active")
320
+
321
+ # Iterate over classes.
322
+ for cls in element.classes:
323
+ print(cls)
324
+ ```
325
+
326
+ Explicit CSS styles are managed through a `dict`-like interface:
327
+
328
+ ```python
329
+ # Set styles using CSS property names (hyphenated).
330
+ element.style["color"] = "red"
331
+ element.style["background-color"] = "#f0f0f0"
332
+ element.style["font-size"] = "16px"
333
+
334
+ # Get styles.
335
+ color = element.style["color"]
336
+
337
+ # Remove styles.
338
+ del element.style["margin"]
339
+ ```
340
+
341
+ Add, find, and navigate elements:
342
+
343
+ ```python
344
+ # Append children.
345
+ parent.append(child_element)
346
+ parent.append(child1, child2, child3) # Multiple at once
347
+
348
+ # Find descendants using CSS selectors.
349
+ buttons = parent.find("button")
350
+ items = parent.find(".item-class")
351
+
352
+ # Navigate the tree.
353
+ child = parent.children[0]
354
+ parent_elem = child.parent
355
+
356
+ # Access children by index or slice.
357
+ first_child = parent[0]
358
+ first_three = parent[0:3]
359
+
360
+ # Get a child explicitly by ID. Returns None if not found.
361
+ specific = parent["child-id"]
362
+ ```
363
+
364
+ Attach event listeners to elements:
365
+
366
+ ```python
367
+ button = web.button("Click me")
368
+
369
+ # Use the @when decorator with event name.
370
+ from pyscript import when
371
+
372
+ @when("click", button)
373
+ def handle_click(event):
374
+ print("Clicked!")
375
+
376
+ # Or use the on_* event directly with @when.
377
+ @when(button.on_click)
378
+ def handle_click(event):
379
+ print("Also works!")
380
+
381
+ # Pass handlers during element creation.
382
+ button = web.button("Click", on_click=handle_click)
383
+ ```
384
+
385
+ Update multiple properties at once:
386
+
387
+ ```python
388
+ element.update(
389
+ classes=["active", "highlighted"],
390
+ style={"color": "red", "font-size": "20px"},
391
+ id="updated-id",
392
+ title="New tooltip"
393
+ )
394
+ ```
395
+
396
+ !!! warning
397
+ **Some HTML attributes clash with Python keywords and use trailing
398
+ underscores**.
399
+
400
+ Use `for_` instead of `for`, and `class_` instead of `class`.
401
+
402
+ ```python
403
+ # The 'for' attribute (on labels)
404
+ label = web.label("Username", for_="username-input")
405
+
406
+ # The 'class' attribute (although 'classes' is preferred)
407
+ div.class_ = "my-class"
408
+ ```
409
+
410
+ Create copies of elements:
411
+
412
+ ```python
413
+ original = web.div("Original content", id="original")
414
+ clone = original.clone(clone_id="cloned")
415
+ ```
416
+
417
+ Access the underlying DOM element when needed:
418
+
419
+ ```python
420
+ # Most DOM properties and methods are accessible directly.
421
+ element.focus()
422
+ element.scrollIntoView()
423
+ bounding_rect = element.getBoundingClientRect()
424
+
425
+ # Or access the raw DOM element.
426
+ dom_element = element._dom_element
427
+ ```
428
+ """
429
+
430
+ # Lookup table: tag name -> Element subclass.
25
431
  element_classes_by_tag_name = {}
26
432
 
27
433
  @classmethod
28
434
  def get_tag_name(cls):
29
- """Return the HTML tag name for the class.
30
-
31
- For classes that have a trailing underscore (because they clash with a Python
32
- keyword or built-in), we remove it to get the tag name. e.g. for the `input_`
33
- class, the tag name is `input`.
435
+ """
436
+ Get the HTML tag name for this class.
34
437
 
438
+ Classes ending with underscore (e.g. `input_`) have it removed to get
439
+ the actual HTML tag name.
35
440
  """
36
441
  return cls.__name__.replace("_", "")
37
442
 
38
443
  @classmethod
39
444
  def register_element_classes(cls, element_classes):
40
- """Register an iterable of element classes."""
445
+ """
446
+ Register `Element` subclasses for tag-based lookup.
447
+ """
41
448
  for element_class in element_classes:
42
449
  tag_name = element_class.get_tag_name()
43
450
  cls.element_classes_by_tag_name[tag_name] = element_class
44
451
 
45
452
  @classmethod
46
453
  def unregister_element_classes(cls, element_classes):
47
- """Unregister an iterable of element classes."""
454
+ """
455
+ Unregister `Element` subclasses from tag-based lookup.
456
+ """
48
457
  for element_class in element_classes:
49
458
  tag_name = element_class.get_tag_name()
50
459
  cls.element_classes_by_tag_name.pop(tag_name, None)
51
460
 
52
461
  @classmethod
53
462
  def wrap_dom_element(cls, dom_element):
54
- """Wrap an existing DOM element in an instance of a subclass of `Element`.
463
+ """
464
+ Wrap a DOM element in the appropriate `Element` subclass.
55
465
 
56
- We look up the `Element` subclass by the DOM element's tag name. For any unknown
57
- elements (custom tags etc.) use *this* class (`Element`).
466
+ Looks up the subclass by tag name. Unknown tags use the base `Element`
467
+ class.
58
468
  """
59
469
  element_cls = cls.element_classes_by_tag_name.get(
60
470
  dom_element.tagName.lower(), cls
61
471
  )
62
-
63
472
  return element_cls(dom_element=dom_element)
64
473
 
65
474
  def __init__(self, dom_element=None, classes=None, style=None, **kwargs):
66
- """Create a new, or wrap an existing DOM element.
67
-
68
- If `dom_element` is None we are being called to *create* a new element.
69
- Otherwise, we are being called to *wrap* an existing DOM element.
70
475
  """
71
- self._dom_element = (
72
- document.createElement(type(self).get_tag_name())
73
- if is_none(dom_element)
74
- else dom_element
75
- )
476
+ Create or wrap a DOM element.
76
477
 
77
- # HTML on_events attached to the element become pyscript.Event instances.
478
+ If `dom_element` is `None`, this creates a new element. Otherwise wraps
479
+ the provided DOM element. The `**kwargs` can include HTML attributes
480
+ and event handlers (names starting with `on_`).
481
+ """
482
+ # Create or wrap the DOM element.
483
+ if is_none(dom_element):
484
+ self._dom_element = document.createElement(type(self).get_tag_name())
485
+ else:
486
+ self._dom_element = dom_element
487
+ # Event handling.
78
488
  self._on_events = {}
79
-
80
- # Handle kwargs for handling named events with a default handler function.
81
- properties = {}
82
- for name, handler in kwargs.items():
83
- if name.startswith("on_"):
84
- ev = self.get_event(name) # Create the default Event instance.
85
- ev.add_listener(handler)
86
- else:
87
- properties[name] = handler
88
-
89
- # A set-like interface to the element's `classList`.
90
- self._classes = Classes(self)
91
-
92
- # A dict-like interface to the element's `style` attribute.
93
- self._style = Style(self)
94
-
95
- # Set any specified classes, styles, and DOM properties.
96
- self.update(classes=classes, style=style, **properties)
489
+ self.update(classes=classes, style=style, **kwargs)
97
490
 
98
491
  def __eq__(self, obj):
99
- """Check for equality by comparing the underlying DOM element."""
492
+ """
493
+ Check equality by comparing underlying DOM elements.
494
+ """
100
495
  return isinstance(obj, Element) and obj._dom_element == self._dom_element
101
496
 
102
497
  def __getitem__(self, key):
103
- """Get an item within the element's children.
498
+ """
499
+ Get an item within this element.
104
500
 
105
- If `key` is an integer or a slice we use it to index/slice the element's
106
- children. Otherwise, we use `key` as a query selector.
501
+ Behaviour depends on the key type:
502
+
503
+ - Integer: returns the child at that index.
504
+ - Slice: returns a collection of children in that slice.
505
+ - String: looks up an element by id (with or without '#' prefix).
506
+
507
+ ```python
508
+ element[0] # First child.
509
+ element[1:3] # Second and third children.
510
+ element["my-id"] # Element with id="my-id" (or None).
511
+ element["#my-id"] # Same as above (# is optional).
512
+ ```
107
513
  """
514
+
108
515
  if isinstance(key, (int, slice)):
109
516
  return self.children[key]
110
-
111
- return self.find(key)
517
+ if isinstance(key, str):
518
+ return _find_by_id(self._dom_element, key)
519
+ raise TypeError(
520
+ f"Element indices must be integers, slices, or strings, "
521
+ f"not {type(key).__name__}."
522
+ )
112
523
 
113
524
  def __getattr__(self, name):
114
525
  """
115
526
  Get an attribute from the element.
116
527
 
117
- If the attribute is an event (e.g. "on_click"), we wrap it in an `Event`
118
- instance and return that. Otherwise, we return the attribute from the
119
- underlying DOM element.
528
+ Attributes starting with `on_` return `Event` instances. Other
529
+ attributes are retrieved from the underlying DOM element.
120
530
  """
121
531
  if name.startswith("on_"):
122
532
  return self.get_event(name)
123
- # This allows us to get attributes on the underlying DOM element that clash
124
- # with Python keywords or built-ins (e.g. the output element has an
125
- # attribute `for` which is a Python keyword, so you can access it on the
126
- # Element instance via `for_`).
127
- if name.endswith("_"):
128
- name = name[:-1] # noqa: FURB188 No str.removesuffix() in MicroPython.
129
- if name == "for":
130
- # The `for` attribute is a special case as it is a keyword in both
131
- # Python and JavaScript.
132
- # We need to get it from the underlying DOM element as `htmlFor`.
133
- name = "htmlFor"
134
- return getattr(self._dom_element, name)
533
+ dom_name = self._normalize_attribute_name(name)
534
+ return getattr(self._dom_element, dom_name)
135
535
 
136
536
  def __setattr__(self, name, value):
137
- # This class overrides `__setattr__` to delegate "public" attributes to the
138
- # underlying DOM element. BUT, we don't use the usual Python pattern where
139
- # we set attributes on the element itself via `self.__dict__` as that is not
140
- # yet supported in our build of MicroPython. Instead, we handle it here by
141
- # using super for all "private" attributes (those starting with an underscore).
537
+ """
538
+ Set an attribute on the element.
539
+
540
+ Private attributes (starting with `_`) are set on the Python object.
541
+ Public attributes are set on the underlying DOM element. Attributes
542
+ starting with `on_` are treated as events.
543
+ """
142
544
  if name.startswith("_"):
143
545
  super().__setattr__(name, value)
144
-
546
+ elif name.startswith("on_"):
547
+ # Separate events...
548
+ self.get_event(name).add_listener(value)
145
549
  else:
146
- # This allows us to set attributes on the underlying DOM element that clash
147
- # with Python keywords or built-ins (e.g. the output element has an
148
- # attribute `for` which is a Python keyword, so you can access it on the
149
- # Element instance via `for_`).
150
- if name.endswith("_"):
151
- name = name[:-1] # noqa: FURB188 No str.removesuffix() in MicroPython.
152
- if name == "for":
153
- # The `for` attribute is a special case as it is a keyword in both
154
- # Python and JavaScript.
155
- # We need to set it on the underlying DOM element as `htmlFor`.
156
- name = "htmlFor"
157
-
158
- if name.startswith("on_"):
159
- # Ensure on-events are cached in the _on_events dict if the
160
- # user is setting them directly.
161
- self._on_events[name] = value
162
-
163
- setattr(self._dom_element, name, value)
550
+ # ...from regular attributes.
551
+ dom_name = self._normalize_attribute_name(name)
552
+ setattr(self._dom_element, dom_name, value)
553
+
554
+ def _normalize_attribute_name(self, name):
555
+ """
556
+ Normalize Python attribute names to DOM attribute names.
557
+
558
+ Removes trailing underscores and maps special cases.
559
+ """
560
+ if name.endswith("_"):
561
+ name = name[:-1]
562
+ if name == "for":
563
+ return "htmlFor"
564
+ if name == "class":
565
+ return "className"
566
+ return name
164
567
 
165
568
  def get_event(self, name):
166
569
  """
167
570
  Get an `Event` instance for the specified event name.
571
+
572
+ Event names must start with `on_` (e.g. `on_click`). Creates and
573
+ caches `Event` instances that are triggered when the DOM event fires.
168
574
  """
169
575
  if not name.startswith("on_"):
170
- msg = "Event names must start with 'on_'."
171
- raise ValueError(msg)
172
- event_name = name[3:] # Remove the "on_" prefix.
576
+ raise ValueError("Event names must start with 'on_'.")
577
+ event_name = name[3:] # Remove 'on_' prefix.
173
578
  if not hasattr(self._dom_element, event_name):
174
- msg = f"Element has no '{event_name}' event."
175
- raise ValueError(msg)
579
+ raise ValueError(f"Element has no '{event_name}' event.")
176
580
  if name in self._on_events:
177
581
  return self._on_events[name]
178
- # Such an on-event exists in the DOM element, but we haven't yet
179
- # wrapped it in an Event instance. Let's do that now. When the
180
- # underlying DOM element's event is triggered, the Event instance
181
- # will be triggered too.
582
+ # Create Event instance and wire it to the DOM event.
182
583
  ev = Event()
183
584
  self._on_events[name] = ev
184
585
  self._dom_element.addEventListener(event_name, create_proxy(ev.trigger))
@@ -186,199 +587,307 @@ class Element:
186
587
 
187
588
  @property
188
589
  def children(self):
189
- """Return the element's children as an `ElementCollection`."""
590
+ """
591
+ Return this element's children as an `ElementCollection`.
592
+ """
190
593
  return ElementCollection.wrap_dom_elements(self._dom_element.children)
191
594
 
192
595
  @property
193
596
  def classes(self):
194
- """Return the element's `classList` as a `Classes` instance."""
597
+ """
598
+ Return the element's CSS classes as a `set`-like `Classes` object.
599
+
600
+ Supports set operations: `add`, `remove`, `discard`, `clear`.
601
+ Check membership with `in`, iterate with `for`, get length with `len()`.
602
+
603
+ ```python
604
+ element.classes.add("active")
605
+ if "disabled" in element.classes:
606
+ ...
607
+ ```
608
+ """
609
+ if not hasattr(self, "_classes"):
610
+ self._classes = Classes(self)
195
611
  return self._classes
196
612
 
613
+ @property
614
+ def style(self):
615
+ """
616
+ Return the element's CSS styles as a `dict`-like `Style` object.
617
+
618
+ Access using `dict`-style syntax with standard
619
+ [CSS property names (hyphenated)](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference).
620
+
621
+ ```python
622
+ element.style["background-color"] = "red"
623
+ element.style["font-size"] = "16px"
624
+ del element.style["margin"]
625
+ ```
626
+ """
627
+ if not hasattr(self, "_style"):
628
+ self._style = Style(self)
629
+ return self._style
630
+
197
631
  @property
198
632
  def parent(self):
199
- """Return the element's `parent `Element`."""
633
+ """
634
+ Return this element's parent `Element`, or `None`.
635
+ """
200
636
  if is_none(self._dom_element.parentElement):
201
637
  return None
202
-
203
638
  return Element.wrap_dom_element(self._dom_element.parentElement)
204
639
 
205
- @property
206
- def style(self):
207
- """Return the element's `style` attribute as a `Style` instance."""
208
- return self._style
209
-
210
640
  def append(self, *items):
211
- """Append the specified items to the element."""
641
+ """
642
+ Append items to this element's `children`.
643
+
644
+ Accepts `Element` instances, `ElementCollection` instances, lists,
645
+ tuples, raw DOM elements, and NodeLists.
646
+ """
212
647
  for item in items:
213
648
  if isinstance(item, Element):
214
649
  self._dom_element.appendChild(item._dom_element)
215
-
216
650
  elif isinstance(item, ElementCollection):
217
651
  for element in item:
218
652
  self._dom_element.appendChild(element._dom_element)
219
-
220
- # We check for list/tuple here and NOT for any iterable as it will match
221
- # a JS Nodelist which is handled explicitly below.
222
- # NodeList.
223
653
  elif isinstance(item, (list, tuple)):
224
654
  for child in item:
225
655
  self.append(child)
226
-
656
+ elif hasattr(item, "tagName"):
657
+ # Raw DOM element.
658
+ self._dom_element.appendChild(item)
659
+ elif hasattr(item, "length"):
660
+ # NodeList or similar iterable.
661
+ for element in item:
662
+ self._dom_element.appendChild(element)
227
663
  else:
228
- # In this case we know it's not an Element or an ElementCollection, so
229
- # we guess that it's either a DOM element or NodeList returned via the
230
- # ffi.
231
- try:
232
- # First, we try to see if it's an element by accessing the 'tagName'
233
- # attribute.
234
- item.tagName
235
- self._dom_element.appendChild(item)
236
-
237
- except AttributeError:
238
- try:
239
- # Ok, it's not an element, so let's see if it's a NodeList by
240
- # accessing the 'length' attribute.
241
- item.length
242
- for element_ in item:
243
- self._dom_element.appendChild(element_)
244
-
245
- except AttributeError:
246
- # Nope! This is not an element or a NodeList.
247
- msg = (
248
- f'Element "{item}" is a proxy object, "'
249
- f"but not a valid element or a NodeList."
250
- )
251
- raise TypeError(msg)
664
+ raise TypeError(f"Cannot append {type(item).__name__} to element.")
252
665
 
253
666
  def clone(self, clone_id=None):
254
- """Make a clone of the element (clones the underlying DOM object too)."""
667
+ """
668
+ Clone this element and its underlying DOM element.
669
+
670
+ Optionally assign a new `id` to the clone.
671
+ """
255
672
  clone = Element.wrap_dom_element(self._dom_element.cloneNode(True))
256
673
  clone.id = clone_id
257
674
  return clone
258
675
 
259
676
  def find(self, selector):
260
- """Find all elements that match the specified selector.
677
+ """
678
+ Find all descendant elements matching the
679
+ [CSS `selector`](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Selectors).
261
680
 
262
- Return the results as a (possibly empty) `ElementCollection`.
681
+ Returns an `ElementCollection` (possibly empty).
682
+
683
+ ```python
684
+ element.find("div") # All div descendants.
685
+ element.find(".my-class") # All elements with class.
686
+ element.find("#my-id") # Element with id (as collection).
687
+ element.find("div.my-class") # All divs with class.
688
+ ```
263
689
  """
264
- return ElementCollection.wrap_dom_elements(
265
- self._dom_element.querySelectorAll(selector)
266
- )
690
+ return _find_and_wrap(self._dom_element, selector)
267
691
 
268
692
  def show_me(self):
269
- """Convenience method for 'element.scrollIntoView()'."""
693
+ """
694
+ Scroll this element into view.
695
+ """
270
696
  self._dom_element.scrollIntoView()
271
697
 
272
698
  def update(self, classes=None, style=None, **kwargs):
273
- """Update the element with the specified classes, styles, and DOM properties."""
699
+ """
700
+ Update this element's `classes`, `style`, and `attributes`
701
+ (via arbitrary `**kwargs`).
274
702
 
703
+ Convenience method for bulk updates.
704
+ """
275
705
  if classes:
276
- self.classes.add(classes)
277
-
706
+ if isinstance(classes, str):
707
+ self.classes.add(classes)
708
+ else:
709
+ for class_name in classes:
710
+ self.classes.add(class_name)
278
711
  if style:
279
- self.style.set(**style)
280
-
712
+ for key, value in style.items():
713
+ self.style[key] = value
281
714
  for name, value in kwargs.items():
282
715
  setattr(self, name, value)
283
716
 
284
717
 
285
- class Classes:
286
- """A set-like interface to an element's `classList`."""
718
+ class Classes(set):
719
+ """
720
+ Behaves like a Python `set` with changes automatically reflected in the
721
+ element's `classList`.
722
+
723
+ ```python
724
+ # Add and remove classes.
725
+ element.classes.add("active")
726
+ element.classes.remove("inactive")
727
+ element.classes.discard("maybe-missing") # No error if absent.
728
+
729
+ # Check membership.
730
+ if "active" in element.classes:
731
+ print("Element is active")
732
+
733
+ # Clear all classes.
734
+ element.classes.clear()
735
+
736
+ # Iterate over classes.
737
+ for cls in element.classes:
738
+ print(cls)
739
+ ```
740
+ """
287
741
 
288
- def __init__(self, element: Element):
289
- self._element = element
290
- self._class_list = self._element._dom_element.classList
742
+ def __init__(self, element):
743
+ """Initialise the Classes set for the given element."""
744
+ self._class_list = element._dom_element.classList
745
+ super().__init__(self._class_list)
291
746
 
292
- def __contains__(self, item):
293
- return item in self._class_list
747
+ def _extract_class_names(self, class_name):
748
+ """
749
+ If the class_name contains several class names separated by spaces,
750
+ split them and return as a list. Otherwise, return the class_name as is
751
+ in a list.
752
+ """
753
+ return (
754
+ [name for name in class_name.split() if name]
755
+ if " " in class_name
756
+ else [class_name]
757
+ )
294
758
 
295
- def __eq__(self, other):
296
- # We allow comparison with either another `Classes` instance...
297
- if isinstance(other, Classes):
298
- compare_with = list(other._class_list)
759
+ def add(self, class_name):
760
+ """Add a class."""
761
+ for name in self._extract_class_names(class_name):
762
+ super().add(name)
763
+ self._class_list.add(name)
764
+
765
+ def remove(self, class_name):
766
+ """Remove a class."""
767
+ for name in self._extract_class_names(class_name):
768
+ super().remove(name)
769
+ self._class_list.remove(name)
770
+
771
+ def discard(self, class_name):
772
+ """Remove a class if present."""
773
+ for name in self._extract_class_names(class_name):
774
+ super().discard(name)
775
+ if name in self._class_list:
776
+ self._class_list.remove(name)
299
777
 
300
- # ...or iterables of strings.
301
- else:
302
- # TODO: Check MP for existence of better iterable test.
303
- try:
304
- compare_with = iter(other)
778
+ def clear(self):
779
+ """Remove all classes."""
780
+ super().clear()
781
+ while self._class_list.length > 0:
782
+ self._class_list.remove(self._class_list.item(0))
305
783
 
306
- except TypeError:
307
- return False
308
784
 
309
- return set(self._class_list) == set(compare_with)
785
+ class Style(dict):
786
+ """
787
+ Behaves like a Python `dict` with changes automatically reflected in the
788
+ element's `style` attribute.
310
789
 
311
- def __iter__(self):
312
- return iter(self._class_list)
790
+ ```python
791
+ # Set and get styles using CSS property names (hyphenated).
792
+ element.style["color"] = "red"
793
+ element.style["background-color"] = "#f0f0f0"
794
+ element.style["font-size"] = "16px"
313
795
 
314
- def __len__(self):
315
- return self._class_list.length
796
+ # Get a style value.
797
+ color = element.style["color"]
316
798
 
317
- def __repr__(self):
318
- return f"Classes({', '.join(self._class_list)})"
799
+ # Remove a style.
800
+ del element.style["margin"]
319
801
 
320
- def __str__(self):
321
- return " ".join(self._class_list)
802
+ # Check if a style is set.
803
+ if "color" in element.style:
804
+ print(f"Color is {element.style['color']}")
805
+ ```
806
+ """
322
807
 
323
- def add(self, *class_names):
324
- """Add one or more classes to the element."""
325
- for class_name in class_names:
326
- if isinstance(class_name, list):
327
- for item in class_name:
328
- self.add(item)
808
+ def __init__(self, element):
809
+ """Initialise the Style dict for the given element."""
810
+ self._style = element._dom_element.style
811
+ super().__init__()
329
812
 
330
- else:
331
- self._class_list.add(class_name)
813
+ def __setitem__(self, key, value):
814
+ """Set a style property."""
815
+ super().__setitem__(key, value)
816
+ self._style.setProperty(key, str(value))
332
817
 
333
- def contains(self, class_name):
334
- """Check if the element has the specified class."""
335
- return class_name in self
818
+ def __delitem__(self, key):
819
+ """Remove a style property."""
820
+ super().__delitem__(key)
821
+ self._style.removeProperty(key)
336
822
 
337
- def remove(self, *class_names):
338
- """Remove one or more classes from the element."""
339
- for class_name in class_names:
340
- if isinstance(class_name, list):
341
- for item in class_name:
342
- self.remove(item)
343
823
 
344
- else:
345
- self._class_list.remove(class_name)
824
+ class HasOptions:
825
+ """
826
+ Mixin for elements with options (`datalist`, `optgroup`, `select`).
346
827
 
347
- def replace(self, old_class, new_class):
348
- """Replace one of the element's classes with another."""
349
- self.remove(old_class)
350
- self.add(new_class)
828
+ Provides an `options` property that returns an `Options` instance. Used
829
+ in conjunction with the `Options` class.
351
830
 
352
- def toggle(self, *class_names):
353
- """Toggle one or more of the element's classes."""
354
- for class_name in class_names:
355
- if class_name in self:
356
- self.remove(class_name)
831
+ ```python
832
+ # Get a select element and work with its options.
833
+ select = web.page["my-select"]
357
834
 
358
- else:
359
- self.add(class_name)
835
+ # Add options.
836
+ select.options.add(value="1", html="Option 1")
837
+ select.options.add(value="2", html="Option 2", selected=True)
360
838
 
839
+ # Get the selected option.
840
+ selected = select.options.selected
361
841
 
362
- class HasOptions:
363
- """Mix-in for elements that have an options attribute.
842
+ # Iterate over options.
843
+ for option in select.options:
844
+ print(f"{option.value}: {option.innerHTML}")
364
845
 
365
- The elements that support options are: <datalist>, <optgroup>, and <select>.
846
+ # Clear all options.
847
+ select.options.clear()
848
+ ```
366
849
  """
367
850
 
368
851
  @property
369
852
  def options(self):
370
- """Return the element's options as an `Options"""
853
+ """Return this element's options as an `Options` instance."""
371
854
  if not hasattr(self, "_options"):
372
855
  self._options = Options(self)
373
-
374
856
  return self._options
375
857
 
376
858
 
377
859
  class Options:
378
- """This class represents the <option>s of a <datalist>, <optgroup> or <select>.
860
+ """
861
+ Interface to the options of a `datalist`, `optgroup`, or `select` element.
862
+
863
+ Supports adding, removing, and accessing option elements. Used in
864
+ conjunction with the `HasOptions` mixin.
865
+
866
+ ```python
867
+ # Add options to a select element.
868
+ select.options.add(value="1", html="Option 1")
869
+ select.options.add(value="2", html="Option 2", selected=True)
870
+
871
+ # Insert option at specific position.
872
+ select.options.add(value="1.5", html="Option 1.5", before=1)
873
+
874
+ # Access options by index.
875
+ first_option = select.options[0]
876
+
877
+ # Get the selected option.
878
+ selected = select.options.selected
879
+ print(f"Selected: {selected.value}")
880
+
881
+ # Iterate over all options.
882
+ for option in select.options:
883
+ print(option.innerHTML)
884
+
885
+ # Remove option by index.
886
+ select.options.remove(0)
379
887
 
380
- It allows access to add and remove <option>s by using the `add`, `remove` and
381
- `clear` methods.
888
+ # Clear all options.
889
+ select.options.clear()
890
+ ```
382
891
  """
383
892
 
384
893
  def __init__(self, element):
@@ -394,30 +903,39 @@ class Options:
394
903
  return len(self.options)
395
904
 
396
905
  def __repr__(self):
397
- return f"{self.__class__.__name__} (length: {len(self)}) {self.options}"
906
+ return f"{self.__class__.__name__} (length: {len(self)}) " f"{self.options}"
398
907
 
399
908
  @property
400
909
  def options(self):
401
- """Return the list of options."""
910
+ """
911
+ Return the list of option elements.
912
+ """
402
913
  return [Element.wrap_dom_element(o) for o in self._element._dom_element.options]
403
914
 
404
915
  @property
405
916
  def selected(self):
406
- """Return the selected option."""
917
+ """
918
+ Return the currently selected option.
919
+ """
407
920
  return self.options[self._element._dom_element.selectedIndex]
408
921
 
409
922
  def add(self, value=None, html=None, text=None, before=None, **kwargs):
410
- """Add a new option to the element"""
411
- if value is not None:
412
- kwargs["value"] = value
923
+ """
924
+ Add a new option to the element.
413
925
 
414
- if html is not None:
926
+ Can specify `value`, `html` content, and `text`. The `before` parameter can
927
+ be an `Element` or index to insert before. The `**kwargs` are additional
928
+ arbitrary attributes for the new option element.
929
+ """
930
+ if value:
931
+ kwargs["value"] = value
932
+ if html:
415
933
  kwargs["innerHTML"] = html
416
-
417
- if text is not None:
934
+ if text:
418
935
  kwargs["text"] = text
419
936
 
420
- new_option = option(**kwargs)
937
+ # The `option` element class is dynamically created below.
938
+ new_option = option(**kwargs) # noqa: F821
421
939
 
422
940
  if before and isinstance(before, Element):
423
941
  before = before._dom_element
@@ -425,56 +943,62 @@ class Options:
425
943
  self._element._dom_element.add(new_option._dom_element, before)
426
944
 
427
945
  def clear(self):
428
- """Remove all options."""
429
- while len(self) > 0:
430
- self.remove(0)
946
+ """
947
+ Remove all options.
948
+ """
949
+ self._element._dom_element.length = 0
431
950
 
432
951
  def remove(self, index):
433
- """Remove the option at the specified index."""
952
+ """
953
+ Remove the option at the specified `index`.
954
+ """
434
955
  self._element._dom_element.remove(index)
435
956
 
436
957
 
437
- class Style:
438
- """A dict-like interface to an element's `style` attribute."""
439
-
440
- def __init__(self, element: Element):
441
- self._element = element
442
- self._style = self._element._dom_element.style
443
-
444
- def __getitem__(self, key):
445
- return self._style.getPropertyValue(key)
958
+ class ContainerElement(Element):
959
+ """
960
+ Base class for elements that can contain other elements.
446
961
 
447
- def __setitem__(self, key, value):
448
- self._style.setProperty(key, value)
962
+ Extends `Element` with convenient child handling during initialization.
449
963
 
450
- def remove(self, key):
451
- """Remove a CSS property from the element."""
452
- self._style.removeProperty(key)
964
+ ```python
965
+ from pyscript import web
453
966
 
454
- def set(self, **kwargs):
455
- """Set one or more CSS properties on the element."""
456
- for key, value in kwargs.items():
457
- self._element._dom_element.style.setProperty(key, value)
458
967
 
459
- # CSS Properties
460
- # Reference: https://github.com/microsoft/TypeScript/blob/main/src/lib/dom.generated.d.ts#L3799C1-L5005C2
461
- # Following properties automatically generated from the above reference using
462
- # tools/codegen_css_proxy.py
463
- @property
464
- def visible(self):
465
- return self._element._dom_element.style.visibility
968
+ # Create with child elements as arguments.
969
+ div = web.div(
970
+ web.h1("Title"),
971
+ web.p("Paragraph 1"),
972
+ web.p("Paragraph 2")
973
+ )
466
974
 
467
- @visible.setter
468
- def visible(self, value):
469
- self._element._dom_element.style.visibility = value
975
+ # Or use the children keyword argument.
976
+ div = web.div(children=[web.p("Child 1"), web.p("Child 2")])
470
977
 
978
+ # Mix elements and HTML strings.
979
+ div = web.div(
980
+ web.h1("Title"),
981
+ "<p>HTML content</p>",
982
+ web.button("Click me")
983
+ )
471
984
 
472
- class ContainerElement(Element):
473
- """Base class for elements that can contain other elements."""
985
+ # Iterate over children.
986
+ for child in div:
987
+ print(child.innerHTML)
988
+ ```
989
+ """
474
990
 
475
991
  def __init__(
476
992
  self, *args, children=None, dom_element=None, style=None, classes=None, **kwargs
477
993
  ):
994
+ """
995
+ Create a container element with optional `children`.
996
+
997
+ Children can be passed as positional `*args` or via the `children`
998
+ keyword argument. String children are inserted as unescaped HTML. The
999
+ `style`, `classes`, and `**kwargs` are passed to the base `Element`
1000
+ initializer.
1001
+ """
478
1002
  super().__init__(
479
1003
  dom_element=dom_element, style=style, classes=classes, **kwargs
480
1004
  )
@@ -482,7 +1006,6 @@ class ContainerElement(Element):
482
1006
  for child in list(args) + (children or []):
483
1007
  if isinstance(child, (Element, ElementCollection)):
484
1008
  self.append(child)
485
-
486
1009
  else:
487
1010
  self._dom_element.insertAdjacentHTML("beforeend", child)
488
1011
 
@@ -490,124 +1013,93 @@ class ContainerElement(Element):
490
1013
  yield from self.children
491
1014
 
492
1015
 
493
- class ClassesCollection:
494
- """A set-like interface to the classes of the elements in a collection."""
495
-
496
- def __init__(self, collection):
497
- self._collection = collection
498
-
499
- def __contains__(self, class_name):
500
- for element in self._collection:
501
- if class_name in element.classes:
502
- return True
503
-
504
- return False
505
-
506
- def __eq__(self, other):
507
- return (
508
- isinstance(other, ClassesCollection)
509
- and self._collection == other._collection
510
- )
1016
+ class ElementCollection:
1017
+ """
1018
+ A collection of Element instances with `list`-like operations.
511
1019
 
512
- def __iter__(self):
513
- yield from self._all_class_names()
1020
+ Supports iteration, indexing, slicing, and finding descendants.
1021
+ For bulk operations, iterate over the collection explicitly or use
1022
+ the `update_all` method.
514
1023
 
515
- def __len__(self):
516
- return len(self._all_class_names())
1024
+ ```python
1025
+ # Get a collection of elements.
1026
+ items = web.page.find(".item")
517
1027
 
518
- def __repr__(self):
519
- return f"ClassesCollection({self._collection!r})"
1028
+ # Access by index.
1029
+ first = items[0]
1030
+ last = items[-1]
520
1031
 
521
- def __str__(self):
522
- return " ".join(self._all_class_names())
1032
+ # Slice the collection.
1033
+ subset = items[1:3]
523
1034
 
524
- def add(self, *class_names):
525
- """Add one or more classes to the elements in the collection."""
526
- for element in self._collection:
527
- element.classes.add(*class_names)
1035
+ # Look up a specific element by id (returns None if not found).
1036
+ specific = items["item-id"]
528
1037
 
529
- def contains(self, class_name):
530
- """Check if any element in the collection has the specified class."""
531
- return class_name in self
1038
+ # Iterate over elements.
1039
+ for item in items:
1040
+ item.innerHTML = "Updated"
1041
+ item.classes.add("processed")
532
1042
 
533
- def remove(self, *class_names):
534
- """Remove one or more classes from the elements in the collection."""
1043
+ # Bulk update all contained elements.
1044
+ items.update_all(innerHTML="Hello", className="updated")
535
1045
 
536
- for element in self._collection:
537
- element.classes.remove(*class_names)
1046
+ # Find matches within the collection.
1047
+ buttons = items.find("button")
538
1048
 
539
- def replace(self, old_class, new_class):
540
- """Replace one of the classes in the elements in the collection with another."""
541
- for element in self._collection:
542
- element.classes.replace(old_class, new_class)
1049
+ # Get the count.
1050
+ count = len(items)
1051
+ ```
1052
+ """
543
1053
 
544
- def toggle(self, *class_names):
545
- """Toggle one or more classes on the elements in the collection."""
546
- for element in self._collection:
547
- element.classes.toggle(*class_names)
548
-
549
- def _all_class_names(self):
550
- all_class_names = set()
551
- for element in self._collection:
552
- for class_name in element.classes:
553
- all_class_names.add(class_name)
554
-
555
- return all_class_names
556
-
557
-
558
- class StyleCollection:
559
- """A dict-like interface to the styles of the elements in a collection."""
560
-
561
- def __init__(self, collection):
562
- self._collection = collection
563
-
564
- def __getitem__(self, key):
565
- return [element.style[key] for element in self._collection._elements]
566
-
567
- def __setitem__(self, key, value):
568
- for element in self._collection._elements:
569
- element.style[key] = value
570
-
571
- def __repr__(self):
572
- return f"StyleCollection({self._collection!r})"
573
-
574
- def remove(self, key):
575
- """Remove a CSS property from the elements in the collection."""
576
- for element in self._collection._elements:
577
- element.style.remove(key)
578
-
579
-
580
- class ElementCollection:
581
1054
  @classmethod
582
1055
  def wrap_dom_elements(cls, dom_elements):
583
- """Wrap an iterable of dom_elements in an `ElementCollection`."""
584
-
1056
+ """
1057
+ Wrap an iterable of DOM elements in an `ElementCollection`.
1058
+ """
585
1059
  return cls(
586
1060
  [Element.wrap_dom_element(dom_element) for dom_element in dom_elements]
587
1061
  )
588
1062
 
589
- def __init__(self, elements: [Element]):
1063
+ def __init__(self, elements):
590
1064
  self._elements = elements
591
- self._classes = ClassesCollection(self)
592
- self._style = StyleCollection(self)
593
1065
 
594
1066
  def __eq__(self, obj):
595
- """Check for equality by comparing the underlying DOM elements."""
1067
+ """
1068
+ Check equality by comparing elements.
1069
+ """
596
1070
  return isinstance(obj, ElementCollection) and obj._elements == self._elements
597
1071
 
598
1072
  def __getitem__(self, key):
599
- """Get an item in the collection.
1073
+ """
1074
+ Get items from the collection.
600
1075
 
601
- If `key` is an integer or a slice we use it to index/slice the collection.
602
- Otherwise, we use `key` as a query selector.
1076
+ Behaviour depends on the key type:
1077
+
1078
+ - Integer: returns the element at that index.
1079
+ - Slice: returns a new collection with elements in that slice.
1080
+ - String: looks up an element by id (with or without '#' prefix).
1081
+
1082
+ ```python
1083
+ collection[0] # First element.
1084
+ collection[1:3] # New collection with 2nd and 3rd elements.
1085
+ collection["my-id"] # Element with id="my-id" (or None).
1086
+ collection["#my-id"] # Same as above (# is optional).
1087
+ ```
603
1088
  """
604
1089
  if isinstance(key, int):
605
1090
  return self._elements[key]
606
-
607
1091
  if isinstance(key, slice):
608
1092
  return ElementCollection(self._elements[key])
609
-
610
- return self.find(key)
1093
+ if isinstance(key, str):
1094
+ for element in self._elements:
1095
+ result = _find_by_id(element._dom_element, key)
1096
+ if result:
1097
+ return result
1098
+ return None
1099
+ raise TypeError(
1100
+ f"Collection indices must be integers, slices, or strings, "
1101
+ f"not {type(key).__name__}"
1102
+ )
611
1103
 
612
1104
  def __iter__(self):
613
1105
  yield from self._elements
@@ -621,621 +1113,327 @@ class ElementCollection:
621
1113
  f"{self._elements}"
622
1114
  )
623
1115
 
624
- def __getattr__(self, name):
625
- return [getattr(element, name) for element in self._elements]
626
-
627
- def __setattr__(self, name, value):
628
- # This class overrides `__setattr__` to delegate "public" attributes to the
629
- # elements in the collection. BUT, we don't use the usual Python pattern where
630
- # we set attributes on the collection itself via `self.__dict__` as that is not
631
- # yet supported in our build of MicroPython. Instead, we handle it here by
632
- # using super for all "private" attributes (those starting with an underscore).
633
- if name.startswith("_"):
634
- super().__setattr__(name, value)
635
-
636
- else:
637
- for element in self._elements:
638
- setattr(element, name, value)
639
-
640
- @property
641
- def classes(self):
642
- """Return the classes of the elements in the collection as a `ClassesCollection`."""
643
- return self._classes
644
-
645
1116
  @property
646
1117
  def elements(self):
647
- """Return the elements in the collection as a list."""
1118
+ """
1119
+ Return the underlying `list` of elements.
1120
+ """
648
1121
  return self._elements
649
1122
 
650
- @property
651
- def style(self):
652
- """"""
653
- return self._style
654
-
655
1123
  def find(self, selector):
656
- """Find all elements that match the specified selector.
1124
+ """
1125
+ Find all descendants matching the
1126
+ [CSS `selector`](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Selectors).
657
1127
 
658
- Return the results as a (possibly empty) `ElementCollection`.
1128
+ Searches within all elements in the collection.
1129
+
1130
+ ```python
1131
+ collection.find("div") # All div descendants.
1132
+ collection.find(".my-class") # All elements with class.
1133
+ collection.find("#my-id") # Element with id (as collection).
1134
+ ```
659
1135
  """
660
1136
  elements = []
661
1137
  for element in self._elements:
662
- elements.extend(element.find(selector))
663
-
1138
+ elements.extend(_find_and_wrap(element._dom_element, selector))
664
1139
  return ElementCollection(elements)
665
1140
 
1141
+ def update_all(self, **kwargs):
1142
+ """
1143
+ Explicitly update all elements with the given attributes.
666
1144
 
667
- # Classes for every HTML element. If the element tag name (e.g. "input") clashes with
668
- # either a Python keyword or common symbol, then we suffix the class name with an "_"
669
- # (e.g. the class for the "input" element is "input_").
670
-
671
-
672
- class a(ContainerElement):
673
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a"""
674
-
675
-
676
- class abbr(ContainerElement):
677
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/abbr"""
678
-
679
-
680
- class address(ContainerElement):
681
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/address"""
682
-
683
-
684
- class area(Element):
685
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/area"""
686
-
687
-
688
- class article(ContainerElement):
689
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/article"""
690
-
691
-
692
- class aside(ContainerElement):
693
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/aside"""
694
-
695
-
696
- class audio(ContainerElement):
697
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/audio"""
698
-
699
-
700
- class b(ContainerElement):
701
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/b"""
702
-
703
-
704
- class base(Element):
705
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base"""
706
-
707
-
708
- class blockquote(ContainerElement):
709
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/blockquote"""
710
-
711
-
712
- class body(ContainerElement):
713
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/body"""
714
-
715
-
716
- class br(Element):
717
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/br"""
1145
+ ```python
1146
+ collection.update_all(innerHTML="Hello")
1147
+ collection.update_all(className="active", title="Updated")
1148
+ ```
1149
+ """
1150
+ for element in self._elements:
1151
+ for name, value in kwargs.items():
1152
+ setattr(element, name, value)
718
1153
 
719
1154
 
720
- class button(ContainerElement):
721
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button"""
1155
+ # Special elements with custom methods and mixins.
722
1156
 
723
1157
 
724
1158
  class canvas(ContainerElement):
725
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas"""
726
-
727
- def download(self, filename: str = "snapped.png"):
728
- """Download the current element with the filename provided in input.
729
-
730
- Inputs:
731
- * filename (str): name of the file being downloaded
1159
+ """
1160
+ A bespoke
1161
+ [HTML canvas element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas)
1162
+ with Pythonic drawing and download capabilities.
1163
+ """
732
1164
 
733
- Output:
734
- None
1165
+ def download(self, filename="snapped.png"):
735
1166
  """
736
- download_link = a(download=filename, href=self._dom_element.toDataURL())
1167
+ Download the canvas content as an image file.
737
1168
 
738
- # Adding the link to the DOM is recommended for browser compatibility to make
739
- # sure that the click works.
1169
+ Creates a temporary download link and triggers it.
1170
+ """
1171
+ # The `a` element class is dynamically created below.
1172
+ download_link = a( # noqa: F821
1173
+ download=filename, href=self._dom_element.toDataURL()
1174
+ )
740
1175
  self.append(download_link)
741
-
742
1176
  download_link._dom_element.click()
743
1177
 
744
1178
  def draw(self, what, width=None, height=None):
745
- """Draw `what` on the current element
746
-
747
- Inputs:
1179
+ """
1180
+ Draw a 2d image source (`what`) onto the canvas. Optionally scale to
1181
+ `width` and `height`.
748
1182
 
749
- * what (canvas image source): An element to draw into the context. The
750
- specification permits any canvas image source, specifically, an
751
- HTMLImageElement, an SVGImageElement, an HTMLVideoElement,
752
- an HTMLCanvasElement, an ImageBitmap, an OffscreenCanvas, or a
753
- VideoFrame.
1183
+ Accepts canvas image sources: `HTMLImageElement`, `SVGImageElement`,
1184
+ `HTMLVideoElement`, `HTMLCanvasElement`, `ImageBitmap`,
1185
+ `OffscreenCanvas`, or `VideoFrame`.
754
1186
  """
755
1187
  if isinstance(what, Element):
756
1188
  what = what._dom_element
757
1189
 
758
- # https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage
759
1190
  ctx = self._dom_element.getContext("2d")
760
1191
  if width or height:
761
1192
  ctx.drawImage(what, 0, 0, width, height)
762
-
763
1193
  else:
764
1194
  ctx.drawImage(what, 0, 0)
765
1195
 
766
1196
 
767
- class caption(ContainerElement):
768
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/caption"""
769
-
770
-
771
- class cite(ContainerElement):
772
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/cite"""
773
-
774
-
775
- class code(ContainerElement):
776
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/code"""
777
-
778
-
779
- class col(Element):
780
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/col"""
781
-
782
-
783
- class colgroup(ContainerElement):
784
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/colgroup"""
1197
+ class video(ContainerElement):
1198
+ """
1199
+ A bespoke
1200
+ [HTML video element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video)
1201
+ with Pythonic snapshot capability (to render an image to a canvas).
1202
+ """
785
1203
 
1204
+ def snap(self, to=None, width=None, height=None):
1205
+ """
1206
+ Capture a video frame `to` a canvas element. Optionally scale to
1207
+ `width` and `height`.
786
1208
 
787
- class data(ContainerElement):
788
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/data"""
1209
+ If no canvas is provided, this will create one. The to parameter
1210
+ can be a canvas Element, raw DOM canvas, or CSS selector string.
1211
+ """
1212
+ width = width if width else self.videoWidth
1213
+ height = height if height else self.videoHeight
1214
+ if is_none(to):
1215
+ to = canvas(width=width, height=height)
1216
+ elif isinstance(to, Element):
1217
+ if to.tag != "canvas":
1218
+ raise TypeError("Element to snap to must be a canvas.")
1219
+ elif getattr(to, "tagName", "") == "CANVAS":
1220
+ to = canvas(dom_element=to)
1221
+ elif isinstance(to, str):
1222
+ nodelist = document.querySelectorAll(to)
1223
+ if nodelist.length == 0:
1224
+ raise TypeError(f"No element with selector {to} to snap to.")
1225
+ if nodelist[0].tagName != "CANVAS":
1226
+ raise TypeError("Element to snap to must be a canvas.")
1227
+ to = canvas(dom_element=nodelist[0])
1228
+ to.draw(self, width, height)
1229
+ return to
789
1230
 
790
1231
 
791
1232
  class datalist(ContainerElement, HasOptions):
792
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/datalist"""
793
-
794
-
795
- class dd(ContainerElement):
796
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dd"""
797
-
798
-
799
- class del_(ContainerElement):
800
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/del"""
801
-
802
-
803
- class details(ContainerElement):
804
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details"""
805
-
806
-
807
- class dialog(ContainerElement):
808
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog"""
809
-
810
-
811
- class div(ContainerElement):
812
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div"""
813
-
814
-
815
- class dl(ContainerElement):
816
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dl"""
817
-
818
-
819
- class dt(ContainerElement):
820
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dt"""
821
-
822
-
823
- class em(ContainerElement):
824
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/em"""
825
-
826
-
827
- class embed(Element):
828
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/embed"""
829
-
830
-
831
- class fieldset(ContainerElement):
832
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/fieldset"""
833
-
834
-
835
- class figcaption(ContainerElement):
836
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/figcaption"""
837
-
838
-
839
- class figure(ContainerElement):
840
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/figure"""
841
-
842
-
843
- class footer(ContainerElement):
844
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/footer"""
845
-
846
-
847
- class form(ContainerElement):
848
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form"""
849
-
850
-
851
- class h1(ContainerElement):
852
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h1"""
853
-
854
-
855
- class h2(ContainerElement):
856
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h2"""
857
-
858
-
859
- class h3(ContainerElement):
860
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h3"""
861
-
862
-
863
- class h4(ContainerElement):
864
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h4"""
865
-
866
-
867
- class h5(ContainerElement):
868
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h5"""
869
-
870
-
871
- class h6(ContainerElement):
872
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h6"""
873
-
874
-
875
- class head(ContainerElement):
876
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/head"""
877
-
878
-
879
- class header(ContainerElement):
880
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/header"""
881
-
882
-
883
- class hgroup(ContainerElement):
884
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/hgroup"""
885
-
886
-
887
- class hr(Element):
888
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/hr"""
889
-
890
-
891
- class html(ContainerElement):
892
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/html"""
893
-
894
-
895
- class i(ContainerElement):
896
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/i"""
897
-
898
-
899
- class iframe(ContainerElement):
900
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe"""
901
-
902
-
903
- class img(Element):
904
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img"""
905
-
906
-
907
- class input_(Element):
908
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input"""
909
-
910
-
911
- class ins(ContainerElement):
912
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ins"""
913
-
914
-
915
- class kbd(ContainerElement):
916
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/kbd"""
917
-
918
-
919
- class label(ContainerElement):
920
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/label"""
921
-
922
-
923
- class legend(ContainerElement):
924
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/legend"""
925
-
926
-
927
- class li(ContainerElement):
928
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/li"""
929
-
930
-
931
- class link(Element):
932
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link"""
933
-
934
-
935
- class main(ContainerElement):
936
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/main"""
937
-
938
-
939
- class map_(ContainerElement):
940
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/map"""
941
-
942
-
943
- class mark(ContainerElement):
944
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/mark"""
945
-
946
-
947
- class menu(ContainerElement):
948
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/menu"""
949
-
950
-
951
- class meta(ContainerElement):
952
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta"""
953
-
954
-
955
- class meter(ContainerElement):
956
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meter"""
957
-
958
-
959
- class nav(ContainerElement):
960
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/nav"""
961
-
962
-
963
- class object_(ContainerElement):
964
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/object"""
965
-
1233
+ """
1234
+ HTML datalist element with options support.
966
1235
 
967
- class ol(ContainerElement):
968
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ol"""
1236
+ Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/datalist
1237
+ """
969
1238
 
970
1239
 
971
1240
  class optgroup(ContainerElement, HasOptions):
972
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/optgroup"""
973
-
974
-
975
- class option(ContainerElement):
976
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/option"""
977
-
978
-
979
- class output(ContainerElement):
980
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/output"""
981
-
982
-
983
- class p(ContainerElement):
984
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/p"""
985
-
986
-
987
- class param(ContainerElement):
988
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/param"""
989
-
990
-
991
- class picture(ContainerElement):
992
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/picture"""
993
-
994
-
995
- class pre(ContainerElement):
996
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/pre"""
997
-
998
-
999
- class progress(ContainerElement):
1000
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/progress"""
1001
-
1002
-
1003
- class q(ContainerElement):
1004
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/q"""
1005
-
1006
-
1007
- class s(ContainerElement):
1008
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/s"""
1009
-
1010
-
1011
- class script(ContainerElement):
1012
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script"""
1013
-
1241
+ """
1242
+ HTML optgroup element with options support.
1014
1243
 
1015
- class section(ContainerElement):
1016
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/section"""
1244
+ Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/optgroup
1245
+ """
1017
1246
 
1018
1247
 
1019
1248
  class select(ContainerElement, HasOptions):
1020
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select"""
1021
-
1022
-
1023
- class small(ContainerElement):
1024
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/small"""
1025
-
1026
-
1027
- class source(Element):
1028
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/source"""
1029
-
1030
-
1031
- class span(ContainerElement):
1032
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/span"""
1033
-
1034
-
1035
- class strong(ContainerElement):
1036
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/strong"""
1037
-
1038
-
1039
- class style(ContainerElement):
1040
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/style"""
1041
-
1042
-
1043
- class sub(ContainerElement):
1044
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/sub"""
1045
-
1046
-
1047
- class summary(ContainerElement):
1048
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/summary"""
1049
-
1050
-
1051
- class sup(ContainerElement):
1052
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/sup"""
1053
-
1054
-
1055
- class table(ContainerElement):
1056
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/table"""
1057
-
1058
-
1059
- class tbody(ContainerElement):
1060
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/tbody"""
1061
-
1062
-
1063
- class td(ContainerElement):
1064
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/td"""
1065
-
1066
-
1067
- class template(ContainerElement):
1068
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template"""
1069
-
1070
-
1071
- class textarea(ContainerElement):
1072
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea"""
1073
-
1074
-
1075
- class tfoot(ContainerElement):
1076
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/tfoot"""
1077
-
1078
-
1079
- class th(ContainerElement):
1080
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/th"""
1081
-
1082
-
1083
- class thead(ContainerElement):
1084
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/thead"""
1085
-
1086
-
1087
- class time(ContainerElement):
1088
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/time"""
1089
-
1090
-
1091
- class title(ContainerElement):
1092
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/title"""
1093
-
1094
-
1095
- class tr(ContainerElement):
1096
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/tr"""
1097
-
1098
-
1099
- class track(Element):
1100
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/track"""
1101
-
1102
-
1103
- class u(ContainerElement):
1104
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/u"""
1105
-
1106
-
1107
- class ul(ContainerElement):
1108
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ul"""
1109
-
1110
-
1111
- class var(ContainerElement):
1112
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/var"""
1113
-
1114
-
1115
- class video(ContainerElement):
1116
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video"""
1117
-
1118
- def snap(
1119
- self,
1120
- to: Element | str = None,
1121
- width: int | None = None,
1122
- height: int | None = None,
1123
- ):
1124
- """
1125
- Capture a snapshot (i.e. a single frame) of a video to a canvas.
1249
+ """
1250
+ HTML select element with options support.
1126
1251
 
1127
- Inputs:
1252
+ Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select
1253
+ """
1128
1254
 
1129
- * to: the canvas to save the video frame to (if None, one is created).
1130
- * width: width of the snapshot (defaults to the video width).
1131
- * height: height of the snapshot (defaults to the video height).
1132
1255
 
1133
- Output:
1134
- (Element) canvas element where the video frame snapshot was drawn into
1135
- """
1136
- width = width if width is not None else self.videoWidth
1137
- height = height if height is not None else self.videoHeight
1256
+ # Container elements that can have children.
1257
+ # Note: canvas, video, datalist, optgroup, and select are defined above
1258
+ # with special implementations due to the HasOptions mixin.
1259
+ # fmt: off
1260
+ CONTAINER_TAGS = [
1261
+ "a", "abbr", "address", "article", "aside", "audio",
1262
+ "b", "blockquote", "body", "button",
1263
+ "caption", "cite", "code", "colgroup",
1264
+ "data", "dd", "del", "details", "dialog", "div", "dl", "dt",
1265
+ "em",
1266
+ "fieldset", "figcaption", "figure", "footer", "form",
1267
+ "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hgroup", "html",
1268
+ "i", "iframe", "ins",
1269
+ "kbd",
1270
+ "label", "legend", "li",
1271
+ "main", "map", "mark", "menu", "meta", "meter",
1272
+ "nav",
1273
+ "object", "ol", "option", "output",
1274
+ "p", "param", "picture", "pre", "progress",
1275
+ "q",
1276
+ "s", "script", "section", "small", "span", "strong", "style",
1277
+ "sub", "summary", "sup",
1278
+ "table", "tbody", "td", "template", "textarea", "tfoot", "th", "thead",
1279
+ "time", "title", "tr",
1280
+ "u", "ul",
1281
+ "var",
1282
+ "wbr",
1283
+ ]
1284
+ """
1285
+ Container elements that can have children. Each becomes a class in the
1286
+ `pyscript.web` namespace and corresponds to an HTML tag.
1287
+ """
1288
+ # fmt: on
1138
1289
 
1139
- if is_none(to):
1140
- to = canvas(width=width, height=height)
1290
+ # Void elements that cannot have children.
1291
+ VOID_TAGS = [
1292
+ "area",
1293
+ "base",
1294
+ "br",
1295
+ "col",
1296
+ "embed",
1297
+ "hr",
1298
+ "img",
1299
+ "input",
1300
+ "link",
1301
+ "source",
1302
+ "track",
1303
+ ]
1304
+ """
1305
+ Void elements that cannot have children. Each becomes a class in the
1306
+ `pyscript.web` namespace and corresponds to an HTML tag.
1307
+ """
1141
1308
 
1142
- elif isinstance(to, Element):
1143
- if to.tag != "canvas":
1144
- msg = "Element to snap to must be a canvas."
1145
- raise TypeError(msg)
1146
1309
 
1147
- elif getattr(to, "tagName", "") == "CANVAS":
1148
- to = canvas(dom_element=to)
1310
+ def _create_element_classes():
1311
+ """
1312
+ Create element classes dynamically and register them.
1149
1313
 
1150
- # If 'to' is a string, then assume it is a query selector.
1151
- elif isinstance(to, str):
1152
- nodelist = document.querySelectorAll(to) # NOQA
1153
- if nodelist.length == 0:
1154
- msg = "No element with selector {to} to snap to."
1155
- raise TypeError(msg)
1314
+ Generates classes for all standard HTML elements, using the appropriate
1315
+ base class (ContainerElement or Element) for each tag.
1316
+ """
1317
+ # The existing special element classes defined above.
1318
+ classes = [canvas, video, datalist, optgroup, select]
1319
+ for tag in CONTAINER_TAGS:
1320
+ # Tags that clash with Python keywords get a trailing underscore.
1321
+ class_name = f"{tag}_" if tag in ("del", "map", "object") else tag
1322
+ doc = (
1323
+ f"HTML <{tag}> element. "
1324
+ f"Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/"
1325
+ f"Element/{tag}"
1326
+ )
1327
+ cls = type(class_name, (ContainerElement,), {"__doc__": doc})
1328
+ globals()[class_name] = cls
1329
+ classes.append(cls)
1330
+ for tag in VOID_TAGS:
1331
+ class_name = f"{tag}_" if tag == "input" else tag
1332
+ doc = (
1333
+ f"HTML <{tag}> element. "
1334
+ f"Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/"
1335
+ f"Element/{tag}"
1336
+ )
1337
+ cls = type(class_name, (Element,), {"__doc__": doc})
1338
+ globals()[class_name] = cls
1339
+ classes.append(cls)
1340
+ Element.register_element_classes(classes)
1156
1341
 
1157
- if nodelist[0].tagName != "CANVAS":
1158
- msg = "Element to snap to must be a canvas."
1159
- raise TypeError(msg)
1160
1342
 
1161
- to = canvas(dom_element=nodelist[0])
1343
+ # Initialize element classes at module load time. :-)
1344
+ _create_element_classes()
1162
1345
 
1163
- to.draw(self, width, height)
1164
1346
 
1165
- return to
1347
+ class Page:
1348
+ """
1349
+ Represents the current web page.
1166
1350
 
1351
+ Provides access to the document's `html`, `head`, and `body` elements,
1352
+ plus convenience methods for finding elements and appending to the body.
1167
1353
 
1168
- class wbr(Element):
1169
- """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/wbr"""
1354
+ ```python
1355
+ from pyscript import web
1170
1356
 
1171
1357
 
1172
- # fmt: off
1173
- ELEMENT_CLASSES = [
1174
- a, abbr, address, area, article, aside, audio,
1175
- b, base, blockquote, body, br, button,
1176
- canvas, caption, cite, code, col, colgroup,
1177
- data, datalist, dd, del_, details, dialog, div, dl, dt,
1178
- em, embed,
1179
- fieldset, figcaption, figure, footer, form,
1180
- h1, h2, h3, h4, h5, h6, head, header, hgroup, hr, html,
1181
- i, iframe, img, input_, ins,
1182
- kbd,
1183
- label, legend, li, link,
1184
- main, map_, mark, menu, meta, meter,
1185
- nav,
1186
- object_, ol, optgroup, option, output,
1187
- p, param, picture, pre, progress,
1188
- q,
1189
- s, script, section, select, small, source, span, strong, style, sub, summary, sup,
1190
- table, tbody, td, template, textarea, tfoot, th, thead, time, title, tr, track,
1191
- u, ul,
1192
- var, video,
1193
- wbr,
1194
- ]
1195
- # fmt: on
1358
+ # Access page structure.
1359
+ web.page.html # The <html> element.
1360
+ web.page.head # The <head> element.
1361
+ web.page.body # The <body> element.
1196
1362
 
1363
+ # Get and set page title.
1364
+ web.page.title = "New Title"
1365
+ print(web.page.title)
1197
1366
 
1198
- # Register all the default (aka "built-in") Element classes.
1199
- Element.register_element_classes(ELEMENT_CLASSES)
1367
+ # Find elements by CSS selector.
1368
+ divs = web.page.find("div")
1369
+ items = web.page.find(".item-class")
1200
1370
 
1371
+ # Look up element by id.
1372
+ element = web.page["my-id"]
1373
+ element = web.page["#my-id"] # The "#" prefix is optional.
1201
1374
 
1202
- class Page:
1203
- """Represents the whole page."""
1375
+ # Append to the body (shortcut for page.body.append).
1376
+ web.page.append(web.div("Hello"))
1377
+ ```
1378
+ """
1204
1379
 
1205
1380
  def __init__(self):
1206
1381
  self.html = Element.wrap_dom_element(document.documentElement)
1207
1382
  self.body = Element.wrap_dom_element(document.body)
1208
1383
  self.head = Element.wrap_dom_element(document.head)
1209
1384
 
1210
- def __getitem__(self, selector):
1211
- """Get an item on the page.
1385
+ def __getitem__(self, key):
1386
+ """
1387
+ Look up an element by id.
1388
+
1389
+ The '#' prefix is optional and will be stripped if present.
1390
+ Returns None if no element with that id exists.
1212
1391
 
1213
- We don't index/slice the page like we do with `Element` and `ElementCollection`
1214
- as it is a bit muddier what the ideal behavior should be. Instead, we simply
1215
- use this as a convenience method to `find` elements on the page.
1392
+ ```python
1393
+ page["my-id"] # Element with id="my-id" (or None)
1394
+ page["#my-id"] # Same as above (# is optional)
1395
+ ```
1216
1396
  """
1217
- return self.find(selector)
1397
+ return _find_by_id(document, key)
1218
1398
 
1219
1399
  @property
1220
1400
  def title(self):
1221
- """Return the page title."""
1401
+ """
1402
+ Get the page `title`.
1403
+ """
1222
1404
  return document.title
1223
1405
 
1224
1406
  @title.setter
1225
1407
  def title(self, value):
1226
- """Set the page title."""
1408
+ """
1409
+ Set the page `title`.
1410
+ """
1227
1411
  document.title = value
1228
1412
 
1229
1413
  def append(self, *items):
1230
- """Shortcut for `page.body.append`."""
1414
+ """
1415
+ Append items to the page `body`.
1416
+
1417
+ Shortcut for `page.body.append(*items)`.
1418
+ """
1231
1419
  self.body.append(*items)
1232
1420
 
1233
- def find(self, selector): # NOQA
1234
- """Find all elements that match the specified selector.
1421
+ def find(self, selector):
1422
+ """
1423
+ Find all elements matching the
1424
+ [CSS `selector`](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Selectors).
1425
+
1426
+ Returns an `ElementCollection` of matching elements.
1235
1427
 
1236
- Return the results as a (possibly empty) `ElementCollection`.
1428
+ ```python
1429
+ page.find("div") # All divs on the page
1430
+ page.find(".my-class") # All elements with class
1431
+ page.find("#my-id") # Element with id (as collection)
1432
+ page.find("div.my-class") # All divs with class
1433
+ ```
1237
1434
  """
1238
- return ElementCollection.wrap_dom_elements(document.querySelectorAll(selector))
1435
+ return _find_and_wrap(document, selector)
1239
1436
 
1240
1437
 
1241
1438
  page = Page()
1439
+ """A reference to the current web page. An instance of the `Page` class."""