reactpy_django 5.0.0__py3-none-any.whl

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 (118) hide show
  1. js/node_modules/@pyscript/core/src/stdlib/pyscript/__init__.py +61 -0
  2. js/node_modules/@pyscript/core/src/stdlib/pyscript/display.py +177 -0
  3. js/node_modules/@pyscript/core/src/stdlib/pyscript/event_handling.py +76 -0
  4. js/node_modules/@pyscript/core/src/stdlib/pyscript/fetch.py +87 -0
  5. js/node_modules/@pyscript/core/src/stdlib/pyscript/ffi.py +18 -0
  6. js/node_modules/@pyscript/core/src/stdlib/pyscript/flatted.py +148 -0
  7. js/node_modules/@pyscript/core/src/stdlib/pyscript/magic_js.py +79 -0
  8. js/node_modules/@pyscript/core/src/stdlib/pyscript/storage.py +60 -0
  9. js/node_modules/@pyscript/core/src/stdlib/pyscript/util.py +33 -0
  10. js/node_modules/@pyscript/core/src/stdlib/pyscript/web.py +1176 -0
  11. js/node_modules/@pyscript/core/src/stdlib/pyscript/websocket.py +71 -0
  12. js/node_modules/@pyscript/core/src/stdlib/pyscript/workers.py +43 -0
  13. js/node_modules/flatted/python/flatted.py +149 -0
  14. js/node_modules/flatted/python/test.py +63 -0
  15. js/node_modules/polyscript/python/mip.py +288 -0
  16. reactpy_django/__init__.py +32 -0
  17. reactpy_django/apps.py +11 -0
  18. reactpy_django/checks.py +558 -0
  19. reactpy_django/clean.py +141 -0
  20. reactpy_django/components.py +284 -0
  21. reactpy_django/config.py +128 -0
  22. reactpy_django/database.py +31 -0
  23. reactpy_django/decorators.py +65 -0
  24. reactpy_django/exceptions.py +34 -0
  25. reactpy_django/hooks.py +469 -0
  26. reactpy_django/html.py +3 -0
  27. reactpy_django/http/__init__.py +0 -0
  28. reactpy_django/http/urls.py +18 -0
  29. reactpy_django/http/views.py +62 -0
  30. reactpy_django/management/__init__.py +0 -0
  31. reactpy_django/management/commands/__init__.py +0 -0
  32. reactpy_django/management/commands/clean_reactpy.py +37 -0
  33. reactpy_django/migrations/0001_initial.py +25 -0
  34. reactpy_django/migrations/0002_rename_created_at_componentparams_last_accessed.py +17 -0
  35. reactpy_django/migrations/0003_componentsession_delete_componentparams.py +28 -0
  36. reactpy_django/migrations/0004_config.py +27 -0
  37. reactpy_django/migrations/0005_alter_componentsession_last_accessed.py +17 -0
  38. reactpy_django/migrations/0006_userdatamodel.py +28 -0
  39. reactpy_django/migrations/__init__.py +0 -0
  40. reactpy_django/models.py +48 -0
  41. reactpy_django/py.typed +1 -0
  42. reactpy_django/pyscript/__init__.py +0 -0
  43. reactpy_django/pyscript/component_template.py +26 -0
  44. reactpy_django/pyscript/layout_handler.py +139 -0
  45. reactpy_django/router/__init__.py +5 -0
  46. reactpy_django/router/converters.py +8 -0
  47. reactpy_django/router/resolvers.py +22 -0
  48. reactpy_django/static/reactpy_django/client.js +1 -0
  49. reactpy_django/static/reactpy_django/morphdom/morphdom-esm.js +769 -0
  50. reactpy_django/static/reactpy_django/morphdom/morphdom-factory.js +705 -0
  51. reactpy_django/static/reactpy_django/morphdom/morphdom-umd.js +777 -0
  52. reactpy_django/static/reactpy_django/morphdom/morphdom-umd.min.js +1 -0
  53. reactpy_django/static/reactpy_django/morphdom/morphdom.js +771 -0
  54. reactpy_django/static/reactpy_django/pyscript/codemirror-BEtcgaoQ.js +2 -0
  55. reactpy_django/static/reactpy_django/pyscript/codemirror-BEtcgaoQ.js.map +1 -0
  56. reactpy_django/static/reactpy_django/pyscript/codemirror_commands-DDxffOmd.js +2 -0
  57. reactpy_django/static/reactpy_django/pyscript/codemirror_commands-DDxffOmd.js.map +1 -0
  58. reactpy_django/static/reactpy_django/pyscript/codemirror_lang-python-CnWnFqxD.js +2 -0
  59. reactpy_django/static/reactpy_django/pyscript/codemirror_lang-python-CnWnFqxD.js.map +1 -0
  60. reactpy_django/static/reactpy_django/pyscript/codemirror_language-CjmvX4ix.js +2 -0
  61. reactpy_django/static/reactpy_django/pyscript/codemirror_language-CjmvX4ix.js.map +1 -0
  62. reactpy_django/static/reactpy_django/pyscript/codemirror_state-D1qTXrff.js +2 -0
  63. reactpy_django/static/reactpy_django/pyscript/codemirror_state-D1qTXrff.js.map +1 -0
  64. reactpy_django/static/reactpy_django/pyscript/codemirror_view-DVb8uYMr.js +2 -0
  65. reactpy_django/static/reactpy_django/pyscript/codemirror_view-DVb8uYMr.js.map +1 -0
  66. reactpy_django/static/reactpy_django/pyscript/core-CjO3FOKB.js +2 -0
  67. reactpy_django/static/reactpy_django/pyscript/core-CjO3FOKB.js.map +1 -0
  68. reactpy_django/static/reactpy_django/pyscript/core.css +1 -0
  69. reactpy_django/static/reactpy_django/pyscript/core.js +2 -0
  70. reactpy_django/static/reactpy_django/pyscript/core.js.map +1 -0
  71. reactpy_django/static/reactpy_django/pyscript/deprecations-manager-pFtn19mE.js +2 -0
  72. reactpy_django/static/reactpy_django/pyscript/deprecations-manager-pFtn19mE.js.map +1 -0
  73. reactpy_django/static/reactpy_django/pyscript/error-tq-z48YI.js +2 -0
  74. reactpy_django/static/reactpy_django/pyscript/error-tq-z48YI.js.map +1 -0
  75. reactpy_django/static/reactpy_django/pyscript/index-S1Do43bx.js +2 -0
  76. reactpy_django/static/reactpy_django/pyscript/index-S1Do43bx.js.map +1 -0
  77. reactpy_django/static/reactpy_django/pyscript/mpy-DovD7Qjy.js +2 -0
  78. reactpy_django/static/reactpy_django/pyscript/mpy-DovD7Qjy.js.map +1 -0
  79. reactpy_django/static/reactpy_django/pyscript/py-BUsUWVJg.js +2 -0
  80. reactpy_django/static/reactpy_django/pyscript/py-BUsUWVJg.js.map +1 -0
  81. reactpy_django/static/reactpy_django/pyscript/py-editor-CeySmmer.js +2 -0
  82. reactpy_django/static/reactpy_django/pyscript/py-editor-CeySmmer.js.map +1 -0
  83. reactpy_django/static/reactpy_django/pyscript/py-terminal-CH_wV7wQ.js +2 -0
  84. reactpy_django/static/reactpy_django/pyscript/py-terminal-CH_wV7wQ.js.map +1 -0
  85. reactpy_django/static/reactpy_django/pyscript/storage.js +2 -0
  86. reactpy_django/static/reactpy_django/pyscript/storage.js.map +1 -0
  87. reactpy_django/static/reactpy_django/pyscript/toml-CvAfdf9_.js +3 -0
  88. reactpy_django/static/reactpy_django/pyscript/toml-CvAfdf9_.js.map +1 -0
  89. reactpy_django/static/reactpy_django/pyscript/toml-DiUM0_qs.js +3 -0
  90. reactpy_django/static/reactpy_django/pyscript/toml-DiUM0_qs.js.map +1 -0
  91. reactpy_django/static/reactpy_django/pyscript/xterm-BY7uk_OU.js +2 -0
  92. reactpy_django/static/reactpy_django/pyscript/xterm-BY7uk_OU.js.map +1 -0
  93. reactpy_django/static/reactpy_django/pyscript/xterm-readline-CZfBw7ic.js +2 -0
  94. reactpy_django/static/reactpy_django/pyscript/xterm-readline-CZfBw7ic.js.map +1 -0
  95. reactpy_django/static/reactpy_django/pyscript/xterm.css +7 -0
  96. reactpy_django/static/reactpy_django/pyscript/xterm_addon-fit--gyF3PcZ.js +2 -0
  97. reactpy_django/static/reactpy_django/pyscript/xterm_addon-fit--gyF3PcZ.js.map +1 -0
  98. reactpy_django/static/reactpy_django/pyscript/xterm_addon-web-links-Cnej-nJ6.js +2 -0
  99. reactpy_django/static/reactpy_django/pyscript/xterm_addon-web-links-Cnej-nJ6.js.map +1 -0
  100. reactpy_django/static/reactpy_django/pyscript/zip-DrwYHuF9.js +2 -0
  101. reactpy_django/static/reactpy_django/pyscript/zip-DrwYHuF9.js.map +1 -0
  102. reactpy_django/static/reactpy_django/pyscript-custom.css +3 -0
  103. reactpy_django/static/reactpy_django/pyscript-hide-debug.css +3 -0
  104. reactpy_django/templates/reactpy/component.html +27 -0
  105. reactpy_django/templates/reactpy/pyscript_component.html +2 -0
  106. reactpy_django/templates/reactpy/pyscript_setup.html +8 -0
  107. reactpy_django/templatetags/__init__.py +0 -0
  108. reactpy_django/templatetags/reactpy.py +250 -0
  109. reactpy_django/types.py +81 -0
  110. reactpy_django/utils.py +560 -0
  111. reactpy_django/websocket/__init__.py +0 -0
  112. reactpy_django/websocket/consumer.py +227 -0
  113. reactpy_django/websocket/paths.py +15 -0
  114. reactpy_django-5.0.0.dist-info/LICENSE.md +9 -0
  115. reactpy_django-5.0.0.dist-info/METADATA +152 -0
  116. reactpy_django-5.0.0.dist-info/RECORD +118 -0
  117. reactpy_django-5.0.0.dist-info/WHEEL +5 -0
  118. reactpy_django-5.0.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,1176 @@
1
+ """Lightweight interface to the DOM and HTML elements."""
2
+
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`).
5
+ from pyscript import document, when # NOQA
6
+
7
+
8
+ def wrap_dom_element(dom_element):
9
+ """Wrap an existing DOM element in an instance of a subclass of `Element`.
10
+
11
+ This is just a convenience function to avoid having to import the `Element` class
12
+ and use its class method.
13
+ """
14
+
15
+ return Element.wrap_dom_element(dom_element)
16
+
17
+
18
+ class Element:
19
+ # A lookup table to get an `Element` subclass by tag name. Used when wrapping an
20
+ # existing DOM element.
21
+ element_classes_by_tag_name = {}
22
+
23
+ @classmethod
24
+ def get_tag_name(cls):
25
+ """Return the HTML tag name for the class.
26
+
27
+ For classes that have a trailing underscore (because they clash with a Python
28
+ keyword or built-in), we remove it to get the tag name. e.g. for the `input_`
29
+ class, the tag name is `input`.
30
+
31
+ """
32
+ return cls.__name__.replace("_", "")
33
+
34
+ @classmethod
35
+ def register_element_classes(cls, element_classes):
36
+ """Register an iterable of element classes."""
37
+ for element_class in element_classes:
38
+ tag_name = element_class.get_tag_name()
39
+ cls.element_classes_by_tag_name[tag_name] = element_class
40
+
41
+ @classmethod
42
+ def unregister_element_classes(cls, element_classes):
43
+ """Unregister an iterable of element classes."""
44
+ for element_class in element_classes:
45
+ tag_name = element_class.get_tag_name()
46
+ cls.element_classes_by_tag_name.pop(tag_name, None)
47
+
48
+ @classmethod
49
+ def wrap_dom_element(cls, dom_element):
50
+ """Wrap an existing DOM element in an instance of a subclass of `Element`.
51
+
52
+ We look up the `Element` subclass by the DOM element's tag name. For any unknown
53
+ elements (custom tags etc.) use *this* class (`Element`).
54
+ """
55
+ element_cls = cls.element_classes_by_tag_name.get(
56
+ dom_element.tagName.lower(), cls
57
+ )
58
+
59
+ return element_cls(dom_element=dom_element)
60
+
61
+ def __init__(self, dom_element=None, classes=None, style=None, **kwargs):
62
+ """Create a new, or wrap an existing DOM element.
63
+
64
+ If `dom_element` is None we are being called to *create* a new element.
65
+ Otherwise, we are being called to *wrap* an existing DOM element.
66
+ """
67
+ self._dom_element = dom_element or document.createElement(
68
+ type(self).get_tag_name()
69
+ )
70
+
71
+ # A set-like interface to the element's `classList`.
72
+ self._classes = Classes(self)
73
+
74
+ # A dict-like interface to the element's `style` attribute.
75
+ self._style = Style(self)
76
+
77
+ # Set any specified classes, styles, and DOM properties.
78
+ self.update(classes=classes, style=style, **kwargs)
79
+
80
+ def __eq__(self, obj):
81
+ """Check for equality by comparing the underlying DOM element."""
82
+ return isinstance(obj, Element) and obj._dom_element == self._dom_element
83
+
84
+ def __getitem__(self, key):
85
+ """Get an item within the element's children.
86
+
87
+ If `key` is an integer or a slice we use it to index/slice the element's
88
+ children. Otherwise, we use `key` as a query selector.
89
+ """
90
+ if isinstance(key, int) or isinstance(key, slice):
91
+ return self.children[key]
92
+
93
+ return self.find(key)
94
+
95
+ def __getattr__(self, name):
96
+ # This allows us to get attributes on the underlying DOM element that clash
97
+ # with Python keywords or built-ins (e.g. the output element has an
98
+ # attribute `for` which is a Python keyword, so you can access it on the
99
+ # Element instance via `for_`).
100
+ if name.endswith("_"):
101
+ name = name[:-1]
102
+
103
+ return getattr(self._dom_element, name)
104
+
105
+ def __setattr__(self, name, value):
106
+ # This class overrides `__setattr__` to delegate "public" attributes to the
107
+ # underlying DOM element. BUT, we don't use the usual Python pattern where
108
+ # we set attributes on the element itself via `self.__dict__` as that is not
109
+ # yet supported in our build of MicroPython. Instead, we handle it here by
110
+ # using super for all "private" attributes (those starting with an underscore).
111
+ if name.startswith("_"):
112
+ super().__setattr__(name, value)
113
+
114
+ else:
115
+ # This allows us to set attributes on the underlying DOM element that clash
116
+ # with Python keywords or built-ins (e.g. the output element has an
117
+ # attribute `for` which is a Python keyword, so you can access it on the
118
+ # Element instance via `for_`).
119
+ if name.endswith("_"):
120
+ name = name[:-1]
121
+
122
+ setattr(self._dom_element, name, value)
123
+
124
+ @property
125
+ def children(self):
126
+ """Return the element's children as an `ElementCollection`."""
127
+ return ElementCollection.wrap_dom_elements(self._dom_element.children)
128
+
129
+ @property
130
+ def classes(self):
131
+ """Return the element's `classList` as a `Classes` instance."""
132
+ return self._classes
133
+
134
+ @property
135
+ def parent(self):
136
+ """Return the element's `parent `Element`."""
137
+ if self._dom_element.parentElement is None:
138
+ return None
139
+
140
+ return Element.wrap_dom_element(self._dom_element.parentElement)
141
+
142
+ @property
143
+ def style(self):
144
+ """Return the element's `style` attribute as a `Style` instance."""
145
+ return self._style
146
+
147
+ def append(self, *items):
148
+ """Append the specified items to the element."""
149
+ for item in items:
150
+ if isinstance(item, Element):
151
+ self._dom_element.appendChild(item._dom_element)
152
+
153
+ elif isinstance(item, ElementCollection):
154
+ for element in item:
155
+ self._dom_element.appendChild(element._dom_element)
156
+
157
+ # We check for list/tuple here and NOT for any iterable as it will match
158
+ # a JS Nodelist which is handled explicitly below.
159
+ # NodeList.
160
+ elif isinstance(item, list) or isinstance(item, tuple):
161
+ for child in item:
162
+ self.append(child)
163
+
164
+ else:
165
+ # In this case we know it's not an Element or an ElementCollection, so
166
+ # we guess that it's either a DOM element or NodeList returned via the
167
+ # ffi.
168
+ try:
169
+ # First, we try to see if it's an element by accessing the 'tagName'
170
+ # attribute.
171
+ item.tagName
172
+ self._dom_element.appendChild(item)
173
+
174
+ except AttributeError:
175
+ try:
176
+ # Ok, it's not an element, so let's see if it's a NodeList by
177
+ # accessing the 'length' attribute.
178
+ item.length
179
+ for element_ in item:
180
+ self._dom_element.appendChild(element_)
181
+
182
+ except AttributeError:
183
+ # Nope! This is not an element or a NodeList.
184
+ raise TypeError(
185
+ f'Element "{item}" is a proxy object, "'
186
+ f"but not a valid element or a NodeList."
187
+ )
188
+
189
+ def clone(self, clone_id=None):
190
+ """Make a clone of the element (clones the underlying DOM object too)."""
191
+ clone = Element.wrap_dom_element(self._dom_element.cloneNode(True))
192
+ clone.id = clone_id
193
+ return clone
194
+
195
+ def find(self, selector):
196
+ """Find all elements that match the specified selector.
197
+
198
+ Return the results as a (possibly empty) `ElementCollection`.
199
+ """
200
+ return ElementCollection.wrap_dom_elements(
201
+ self._dom_element.querySelectorAll(selector)
202
+ )
203
+
204
+ def show_me(self):
205
+ """Convenience method for 'element.scrollIntoView()'."""
206
+ self._dom_element.scrollIntoView()
207
+
208
+ def update(self, classes=None, style=None, **kwargs):
209
+ """Update the element with the specified classes, styles, and DOM properties."""
210
+
211
+ if classes:
212
+ self.classes.add(classes)
213
+
214
+ if style:
215
+ self.style.set(**style)
216
+
217
+ for name, value in kwargs.items():
218
+ setattr(self, name, value)
219
+
220
+
221
+ class Classes:
222
+ """A set-like interface to an element's `classList`."""
223
+
224
+ def __init__(self, element: Element):
225
+ self._element = element
226
+ self._class_list = self._element._dom_element.classList
227
+
228
+ def __contains__(self, item):
229
+ return item in self._class_list
230
+
231
+ def __eq__(self, other):
232
+ # We allow comparison with either another `Classes` instance...
233
+ if isinstance(other, Classes):
234
+ compare_with = list(other._class_list)
235
+
236
+ # ...or iterables of strings.
237
+ else:
238
+ # TODO: Check MP for existence of better iterable test.
239
+ try:
240
+ compare_with = iter(other)
241
+
242
+ except TypeError:
243
+ return False
244
+
245
+ return set(self._class_list) == set(compare_with)
246
+
247
+ def __iter__(self):
248
+ return iter(self._class_list)
249
+
250
+ def __len__(self):
251
+ return self._class_list.length
252
+
253
+ def __repr__(self):
254
+ return f"Classes({', '.join(self._class_list)})"
255
+
256
+ def __str__(self):
257
+ return " ".join(self._class_list)
258
+
259
+ def add(self, *class_names):
260
+ """Add one or more classes to the element."""
261
+ for class_name in class_names:
262
+ if isinstance(class_name, list):
263
+ for item in class_name:
264
+ self.add(item)
265
+
266
+ else:
267
+ self._class_list.add(class_name)
268
+
269
+ def contains(self, class_name):
270
+ """Check if the element has the specified class."""
271
+ return class_name in self
272
+
273
+ def remove(self, *class_names):
274
+ """Remove one or more classes from the element."""
275
+ for class_name in class_names:
276
+ if isinstance(class_name, list):
277
+ for item in class_name:
278
+ self.remove(item)
279
+
280
+ else:
281
+ self._class_list.remove(class_name)
282
+
283
+ def replace(self, old_class, new_class):
284
+ """Replace one of the element's classes with another."""
285
+ self.remove(old_class)
286
+ self.add(new_class)
287
+
288
+ def toggle(self, *class_names):
289
+ """Toggle one or more of the element's classes."""
290
+ for class_name in class_names:
291
+ if class_name in self:
292
+ self.remove(class_name)
293
+
294
+ else:
295
+ self.add(class_name)
296
+
297
+
298
+ class HasOptions:
299
+ """Mix-in for elements that have an options attribute.
300
+
301
+ The elements that support options are: <datalist>, <optgroup>, and <select>.
302
+ """
303
+
304
+ @property
305
+ def options(self):
306
+ """Return the element's options as an `Options"""
307
+ if not hasattr(self, "_options"):
308
+ self._options = Options(self)
309
+
310
+ return self._options
311
+
312
+
313
+ class Options:
314
+ """This class represents the <option>s of a <datalist>, <optgroup> or <select>.
315
+
316
+ It allows access to add and remove <option>s by using the `add`, `remove` and
317
+ `clear` methods.
318
+ """
319
+
320
+ def __init__(self, element):
321
+ self._element = element
322
+
323
+ def __getitem__(self, key):
324
+ return self.options[key]
325
+
326
+ def __iter__(self):
327
+ yield from self.options
328
+
329
+ def __len__(self):
330
+ return len(self.options)
331
+
332
+ def __repr__(self):
333
+ return f"{self.__class__.__name__} (length: {len(self)}) {self.options}"
334
+
335
+ @property
336
+ def options(self):
337
+ """Return the list of options."""
338
+ return [Element.wrap_dom_element(o) for o in self._element._dom_element.options]
339
+
340
+ @property
341
+ def selected(self):
342
+ """Return the selected option."""
343
+ return self.options[self._element._dom_element.selectedIndex]
344
+
345
+ def add(self, value=None, html=None, text=None, before=None, **kwargs):
346
+ """Add a new option to the element"""
347
+ if value is not None:
348
+ kwargs["value"] = value
349
+
350
+ if html is not None:
351
+ kwargs["innerHTML"] = html
352
+
353
+ if text is not None:
354
+ kwargs["text"] = text
355
+
356
+ new_option = option(**kwargs)
357
+
358
+ if before:
359
+ if isinstance(before, Element):
360
+ before = before._dom_element
361
+
362
+ self._element._dom_element.add(new_option._dom_element, before)
363
+
364
+ def clear(self):
365
+ """Remove all options."""
366
+ while len(self) > 0:
367
+ self.remove(0)
368
+
369
+ def remove(self, index):
370
+ """Remove the option at the specified index."""
371
+ self._element._dom_element.remove(index)
372
+
373
+
374
+ class Style:
375
+ """A dict-like interface to an element's `style` attribute."""
376
+
377
+ def __init__(self, element: Element):
378
+ self._element = element
379
+ self._style = self._element._dom_element.style
380
+
381
+ def __getitem__(self, key):
382
+ return self._style.getPropertyValue(key)
383
+
384
+ def __setitem__(self, key, value):
385
+ self._style.setProperty(key, value)
386
+
387
+ def remove(self, key):
388
+ """Remove a CSS property from the element."""
389
+ self._style.removeProperty(key)
390
+
391
+ def set(self, **kwargs):
392
+ """Set one or more CSS properties on the element."""
393
+ for key, value in kwargs.items():
394
+ self._element._dom_element.style.setProperty(key, value)
395
+
396
+ # CSS Properties
397
+ # Reference: https://github.com/microsoft/TypeScript/blob/main/src/lib/dom.generated.d.ts#L3799C1-L5005C2
398
+ # Following properties automatically generated from the above reference using
399
+ # tools/codegen_css_proxy.py
400
+ @property
401
+ def visible(self):
402
+ return self._element._dom_element.style.visibility
403
+
404
+ @visible.setter
405
+ def visible(self, value):
406
+ self._element._dom_element.style.visibility = value
407
+
408
+
409
+ class ContainerElement(Element):
410
+ """Base class for elements that can contain other elements."""
411
+
412
+ def __init__(
413
+ self, *args, children=None, dom_element=None, style=None, classes=None, **kwargs
414
+ ):
415
+ super().__init__(
416
+ dom_element=dom_element, style=style, classes=classes, **kwargs
417
+ )
418
+
419
+ for child in list(args) + (children or []):
420
+ if isinstance(child, Element) or isinstance(child, ElementCollection):
421
+ self.append(child)
422
+
423
+ else:
424
+ self._dom_element.insertAdjacentHTML("beforeend", child)
425
+
426
+ def __iter__(self):
427
+ yield from self.children
428
+
429
+
430
+ class ClassesCollection:
431
+ """A set-like interface to the classes of the elements in a collection."""
432
+
433
+ def __init__(self, collection):
434
+ self._collection = collection
435
+
436
+ def __contains__(self, class_name):
437
+ for element in self._collection:
438
+ if class_name in element.classes:
439
+ return True
440
+
441
+ return False
442
+
443
+ def __eq__(self, other):
444
+ return (
445
+ isinstance(other, ClassesCollection)
446
+ and self._collection == other._collection
447
+ )
448
+
449
+ def __iter__(self):
450
+ for class_name in self._all_class_names():
451
+ yield class_name
452
+
453
+ def __len__(self):
454
+ return len(self._all_class_names())
455
+
456
+ def __repr__(self):
457
+ return f"ClassesCollection({repr(self._collection)})"
458
+
459
+ def __str__(self):
460
+ return " ".join(self._all_class_names())
461
+
462
+ def add(self, *class_names):
463
+ """Add one or more classes to the elements in the collection."""
464
+ for element in self._collection:
465
+ element.classes.add(*class_names)
466
+
467
+ def contains(self, class_name):
468
+ """Check if any element in the collection has the specified class."""
469
+ return class_name in self
470
+
471
+ def remove(self, *class_names):
472
+ """Remove one or more classes from the elements in the collection."""
473
+
474
+ for element in self._collection:
475
+ element.classes.remove(*class_names)
476
+
477
+ def replace(self, old_class, new_class):
478
+ """Replace one of the classes in the elements in the collection with another."""
479
+ for element in self._collection:
480
+ element.classes.replace(old_class, new_class)
481
+
482
+ def toggle(self, *class_names):
483
+ """Toggle one or more classes on the elements in the collection."""
484
+ for element in self._collection:
485
+ element.classes.toggle(*class_names)
486
+
487
+ def _all_class_names(self):
488
+ all_class_names = set()
489
+ for element in self._collection:
490
+ for class_name in element.classes:
491
+ all_class_names.add(class_name)
492
+
493
+ return all_class_names
494
+
495
+
496
+ class StyleCollection:
497
+ """A dict-like interface to the styles of the elements in a collection."""
498
+
499
+ def __init__(self, collection):
500
+ self._collection = collection
501
+
502
+ def __getitem__(self, key):
503
+ return [element.style[key] for element in self._collection._elements]
504
+
505
+ def __setitem__(self, key, value):
506
+ for element in self._collection._elements:
507
+ element.style[key] = value
508
+
509
+ def __repr__(self):
510
+ return f"StyleCollection({repr(self._collection)})"
511
+
512
+ def remove(self, key):
513
+ """Remove a CSS property from the elements in the collection."""
514
+ for element in self._collection._elements:
515
+ element.style.remove(key)
516
+
517
+
518
+ class ElementCollection:
519
+ @classmethod
520
+ def wrap_dom_elements(cls, dom_elements):
521
+ """Wrap an iterable of dom_elements in an `ElementCollection`."""
522
+
523
+ return cls(
524
+ [Element.wrap_dom_element(dom_element) for dom_element in dom_elements]
525
+ )
526
+
527
+ def __init__(self, elements: [Element]):
528
+ self._elements = elements
529
+ self._classes = ClassesCollection(self)
530
+ self._style = StyleCollection(self)
531
+
532
+ def __eq__(self, obj):
533
+ """Check for equality by comparing the underlying DOM elements."""
534
+ return isinstance(obj, ElementCollection) and obj._elements == self._elements
535
+
536
+ def __getitem__(self, key):
537
+ """Get an item in the collection.
538
+
539
+ If `key` is an integer or a slice we use it to index/slice the collection.
540
+ Otherwise, we use `key` as a query selector.
541
+ """
542
+ if isinstance(key, int):
543
+ return self._elements[key]
544
+
545
+ elif isinstance(key, slice):
546
+ return ElementCollection(self._elements[key])
547
+
548
+ return self.find(key)
549
+
550
+ def __iter__(self):
551
+ yield from self._elements
552
+
553
+ def __len__(self):
554
+ return len(self._elements)
555
+
556
+ def __repr__(self):
557
+ return (
558
+ f"{self.__class__.__name__} (length: {len(self._elements)}) "
559
+ f"{self._elements}"
560
+ )
561
+
562
+ def __getattr__(self, name):
563
+ return [getattr(element, name) for element in self._elements]
564
+
565
+ def __setattr__(self, name, value):
566
+ # This class overrides `__setattr__` to delegate "public" attributes to the
567
+ # elements in the collection. BUT, we don't use the usual Python pattern where
568
+ # we set attributes on the collection itself via `self.__dict__` as that is not
569
+ # yet supported in our build of MicroPython. Instead, we handle it here by
570
+ # using super for all "private" attributes (those starting with an underscore).
571
+ if name.startswith("_"):
572
+ super().__setattr__(name, value)
573
+
574
+ else:
575
+ for element in self._elements:
576
+ setattr(element, name, value)
577
+
578
+ @property
579
+ def classes(self):
580
+ """Return the classes of the elements in the collection as a `ClassesCollection`."""
581
+ return self._classes
582
+
583
+ @property
584
+ def elements(self):
585
+ """Return the elements in the collection as a list."""
586
+ return self._elements
587
+
588
+ @property
589
+ def style(self):
590
+ """"""
591
+ return self._style
592
+
593
+ def find(self, selector):
594
+ """Find all elements that match the specified selector.
595
+
596
+ Return the results as a (possibly empty) `ElementCollection`.
597
+ """
598
+ elements = []
599
+ for element in self._elements:
600
+ elements.extend(element.find(selector))
601
+
602
+ return ElementCollection(elements)
603
+
604
+
605
+ # Classes for every HTML element. If the element tag name (e.g. "input") clashes with
606
+ # either a Python keyword or common symbol, then we suffix the class name with an "_"
607
+ # (e.g. the class for the "input" element is "input_").
608
+
609
+
610
+ class a(ContainerElement):
611
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a"""
612
+
613
+
614
+ class abbr(ContainerElement):
615
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/abbr"""
616
+
617
+
618
+ class address(ContainerElement):
619
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/address"""
620
+
621
+
622
+ class area(Element):
623
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/area"""
624
+
625
+
626
+ class article(ContainerElement):
627
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/article"""
628
+
629
+
630
+ class aside(ContainerElement):
631
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/aside"""
632
+
633
+
634
+ class audio(ContainerElement):
635
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/audio"""
636
+
637
+
638
+ class b(ContainerElement):
639
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/b"""
640
+
641
+
642
+ class base(Element):
643
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base"""
644
+
645
+
646
+ class blockquote(ContainerElement):
647
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/blockquote"""
648
+
649
+
650
+ class body(ContainerElement):
651
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/body"""
652
+
653
+
654
+ class br(Element):
655
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/br"""
656
+
657
+
658
+ class button(ContainerElement):
659
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button"""
660
+
661
+
662
+ class canvas(ContainerElement):
663
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas"""
664
+
665
+ def download(self, filename: str = "snapped.png"):
666
+ """Download the current element with the filename provided in input.
667
+
668
+ Inputs:
669
+ * filename (str): name of the file being downloaded
670
+
671
+ Output:
672
+ None
673
+ """
674
+ download_link = a(download=filename, href=self._dom_element.toDataURL())
675
+
676
+ # Adding the link to the DOM is recommended for browser compatibility to make
677
+ # sure that the click works.
678
+ self.append(download_link)
679
+
680
+ download_link._dom_element.click()
681
+
682
+ def draw(self, what, width=None, height=None):
683
+ """Draw `what` on the current element
684
+
685
+ Inputs:
686
+
687
+ * what (canvas image source): An element to draw into the context. The
688
+ specification permits any canvas image source, specifically, an
689
+ HTMLImageElement, an SVGImageElement, an HTMLVideoElement,
690
+ an HTMLCanvasElement, an ImageBitmap, an OffscreenCanvas, or a
691
+ VideoFrame.
692
+ """
693
+ if isinstance(what, Element):
694
+ what = what._dom_element
695
+
696
+ # https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage
697
+ ctx = self._dom_element.getContext("2d")
698
+ if width or height:
699
+ ctx.drawImage(what, 0, 0, width, height)
700
+
701
+ else:
702
+ ctx.drawImage(what, 0, 0)
703
+
704
+
705
+ class caption(ContainerElement):
706
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/caption"""
707
+
708
+
709
+ class cite(ContainerElement):
710
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/cite"""
711
+
712
+
713
+ class code(ContainerElement):
714
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/code"""
715
+
716
+
717
+ class col(Element):
718
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/col"""
719
+
720
+
721
+ class colgroup(ContainerElement):
722
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/colgroup"""
723
+
724
+
725
+ class data(ContainerElement):
726
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/data"""
727
+
728
+
729
+ class datalist(ContainerElement, HasOptions):
730
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/datalist"""
731
+
732
+
733
+ class dd(ContainerElement):
734
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dd"""
735
+
736
+
737
+ class del_(ContainerElement):
738
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/del"""
739
+
740
+
741
+ class details(ContainerElement):
742
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details"""
743
+
744
+
745
+ class dialog(ContainerElement):
746
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog"""
747
+
748
+
749
+ class div(ContainerElement):
750
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div"""
751
+
752
+
753
+ class dl(ContainerElement):
754
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dl"""
755
+
756
+
757
+ class dt(ContainerElement):
758
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dt"""
759
+
760
+
761
+ class em(ContainerElement):
762
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/em"""
763
+
764
+
765
+ class embed(Element):
766
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/embed"""
767
+
768
+
769
+ class fieldset(ContainerElement):
770
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/fieldset"""
771
+
772
+
773
+ class figcaption(ContainerElement):
774
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/figcaption"""
775
+
776
+
777
+ class figure(ContainerElement):
778
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/figure"""
779
+
780
+
781
+ class footer(ContainerElement):
782
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/footer"""
783
+
784
+
785
+ class form(ContainerElement):
786
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form"""
787
+
788
+
789
+ class h1(ContainerElement):
790
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h1"""
791
+
792
+
793
+ class h2(ContainerElement):
794
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h2"""
795
+
796
+
797
+ class h3(ContainerElement):
798
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h3"""
799
+
800
+
801
+ class h4(ContainerElement):
802
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h4"""
803
+
804
+
805
+ class h5(ContainerElement):
806
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h5"""
807
+
808
+
809
+ class h6(ContainerElement):
810
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h6"""
811
+
812
+
813
+ class head(ContainerElement):
814
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/head"""
815
+
816
+
817
+ class header(ContainerElement):
818
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/header"""
819
+
820
+
821
+ class hgroup(ContainerElement):
822
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/hgroup"""
823
+
824
+
825
+ class hr(Element):
826
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/hr"""
827
+
828
+
829
+ class html(ContainerElement):
830
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/html"""
831
+
832
+
833
+ class i(ContainerElement):
834
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/i"""
835
+
836
+
837
+ class iframe(ContainerElement):
838
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe"""
839
+
840
+
841
+ class img(Element):
842
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img"""
843
+
844
+
845
+ class input_(Element):
846
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input"""
847
+
848
+
849
+ class ins(ContainerElement):
850
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ins"""
851
+
852
+
853
+ class kbd(ContainerElement):
854
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/kbd"""
855
+
856
+
857
+ class label(ContainerElement):
858
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/label"""
859
+
860
+
861
+ class legend(ContainerElement):
862
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/legend"""
863
+
864
+
865
+ class li(ContainerElement):
866
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/li"""
867
+
868
+
869
+ class link(Element):
870
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link"""
871
+
872
+
873
+ class main(ContainerElement):
874
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/main"""
875
+
876
+
877
+ class map_(ContainerElement):
878
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/map"""
879
+
880
+
881
+ class mark(ContainerElement):
882
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/mark"""
883
+
884
+
885
+ class menu(ContainerElement):
886
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/menu"""
887
+
888
+
889
+ class meta(ContainerElement):
890
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta"""
891
+
892
+
893
+ class meter(ContainerElement):
894
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meter"""
895
+
896
+
897
+ class nav(ContainerElement):
898
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/nav"""
899
+
900
+
901
+ class object_(ContainerElement):
902
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/object"""
903
+
904
+
905
+ class ol(ContainerElement):
906
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ol"""
907
+
908
+
909
+ class optgroup(ContainerElement, HasOptions):
910
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/optgroup"""
911
+
912
+
913
+ class option(ContainerElement):
914
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/option"""
915
+
916
+
917
+ class output(ContainerElement):
918
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/output"""
919
+
920
+
921
+ class p(ContainerElement):
922
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/p"""
923
+
924
+
925
+ class param(ContainerElement):
926
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/param"""
927
+
928
+
929
+ class picture(ContainerElement):
930
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/picture"""
931
+
932
+
933
+ class pre(ContainerElement):
934
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/pre"""
935
+
936
+
937
+ class progress(ContainerElement):
938
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/progress"""
939
+
940
+
941
+ class q(ContainerElement):
942
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/q"""
943
+
944
+
945
+ class s(ContainerElement):
946
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/s"""
947
+
948
+
949
+ class script(ContainerElement):
950
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script"""
951
+
952
+
953
+ class section(ContainerElement):
954
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/section"""
955
+
956
+
957
+ class select(ContainerElement, HasOptions):
958
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select"""
959
+
960
+
961
+ class small(ContainerElement):
962
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/small"""
963
+
964
+
965
+ class source(Element):
966
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/source"""
967
+
968
+
969
+ class span(ContainerElement):
970
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/span"""
971
+
972
+
973
+ class strong(ContainerElement):
974
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/strong"""
975
+
976
+
977
+ class style(ContainerElement):
978
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/style"""
979
+
980
+
981
+ class sub(ContainerElement):
982
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/sub"""
983
+
984
+
985
+ class summary(ContainerElement):
986
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/summary"""
987
+
988
+
989
+ class sup(ContainerElement):
990
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/sup"""
991
+
992
+
993
+ class table(ContainerElement):
994
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/table"""
995
+
996
+
997
+ class tbody(ContainerElement):
998
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/tbody"""
999
+
1000
+
1001
+ class td(ContainerElement):
1002
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/td"""
1003
+
1004
+
1005
+ class template(ContainerElement):
1006
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template"""
1007
+
1008
+
1009
+ class textarea(ContainerElement):
1010
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea"""
1011
+
1012
+
1013
+ class tfoot(ContainerElement):
1014
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/tfoot"""
1015
+
1016
+
1017
+ class th(ContainerElement):
1018
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/th"""
1019
+
1020
+
1021
+ class thead(ContainerElement):
1022
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/thead"""
1023
+
1024
+
1025
+ class time(ContainerElement):
1026
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/time"""
1027
+
1028
+
1029
+ class title(ContainerElement):
1030
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/title"""
1031
+
1032
+
1033
+ class tr(ContainerElement):
1034
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/tr"""
1035
+
1036
+
1037
+ class track(Element):
1038
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/track"""
1039
+
1040
+
1041
+ class u(ContainerElement):
1042
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/u"""
1043
+
1044
+
1045
+ class ul(ContainerElement):
1046
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ul"""
1047
+
1048
+
1049
+ class var(ContainerElement):
1050
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/var"""
1051
+
1052
+
1053
+ class video(ContainerElement):
1054
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video"""
1055
+
1056
+ def snap(
1057
+ self,
1058
+ to: Element | str = None,
1059
+ width: int | None = None,
1060
+ height: int | None = None,
1061
+ ):
1062
+ """
1063
+ Capture a snapshot (i.e. a single frame) of a video to a canvas.
1064
+
1065
+ Inputs:
1066
+
1067
+ * to: the canvas to save the video frame to (if None, one is created).
1068
+ * width: width of the snapshot (defaults to the video width).
1069
+ * height: height of the snapshot (defaults to the video height).
1070
+
1071
+ Output:
1072
+ (Element) canvas element where the video frame snapshot was drawn into
1073
+ """
1074
+ width = width if width is not None else self.videoWidth
1075
+ height = height if height is not None else self.videoHeight
1076
+
1077
+ if to is None:
1078
+ to = canvas(width=width, height=height)
1079
+
1080
+ elif isinstance(to, Element):
1081
+ if to.tag != "canvas":
1082
+ raise TypeError("Element to snap to must be a canvas.")
1083
+
1084
+ elif getattr(to, "tagName", "") == "CANVAS":
1085
+ to = canvas(dom_element=to)
1086
+
1087
+ # If 'to' is a string, then assume it is a query selector.
1088
+ elif isinstance(to, str):
1089
+ nodelist = document.querySelectorAll(to) # NOQA
1090
+ if nodelist.length == 0:
1091
+ raise TypeError("No element with selector {to} to snap to.")
1092
+
1093
+ if nodelist[0].tagName != "CANVAS":
1094
+ raise TypeError("Element to snap to must be a canvas.")
1095
+
1096
+ to = canvas(dom_element=nodelist[0])
1097
+
1098
+ to.draw(self, width, height)
1099
+
1100
+ return to
1101
+
1102
+
1103
+ class wbr(Element):
1104
+ """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/wbr"""
1105
+
1106
+
1107
+ # fmt: off
1108
+ ELEMENT_CLASSES = [
1109
+ a, abbr, address, area, article, aside, audio,
1110
+ b, base, blockquote, body, br, button,
1111
+ canvas, caption, cite, code, col, colgroup,
1112
+ data, datalist, dd, del_, details, dialog, div, dl, dt,
1113
+ em, embed,
1114
+ fieldset, figcaption, figure, footer, form,
1115
+ h1, h2, h3, h4, h5, h6, head, header, hgroup, hr, html,
1116
+ i, iframe, img, input_, ins,
1117
+ kbd,
1118
+ label, legend, li, link,
1119
+ main, map_, mark, menu, meta, meter,
1120
+ nav,
1121
+ object_, ol, optgroup, option, output,
1122
+ p, param, picture, pre, progress,
1123
+ q,
1124
+ s, script, section, select, small, source, span, strong, style, sub, summary, sup,
1125
+ table, tbody, td, template, textarea, tfoot, th, thead, time, title, tr, track,
1126
+ u, ul,
1127
+ var, video,
1128
+ wbr,
1129
+ ]
1130
+ # fmt: on
1131
+
1132
+
1133
+ # Register all the default (aka "built-in") Element classes.
1134
+ Element.register_element_classes(ELEMENT_CLASSES)
1135
+
1136
+
1137
+ class Page:
1138
+ """Represents the whole page."""
1139
+
1140
+ def __init__(self):
1141
+ self.html = Element.wrap_dom_element(document.documentElement)
1142
+ self.body = Element.wrap_dom_element(document.body)
1143
+ self.head = Element.wrap_dom_element(document.head)
1144
+
1145
+ def __getitem__(self, selector):
1146
+ """Get an item on the page.
1147
+
1148
+ We don't index/slice the page like we do with `Element` and `ElementCollection`
1149
+ as it is a bit muddier what the ideal behavior should be. Instead, we simply
1150
+ use this as a convenience method to `find` elements on the page.
1151
+ """
1152
+ return self.find(selector)
1153
+
1154
+ @property
1155
+ def title(self):
1156
+ """Return the page title."""
1157
+ return document.title
1158
+
1159
+ @title.setter
1160
+ def title(self, value):
1161
+ """Set the page title."""
1162
+ document.title = value
1163
+
1164
+ def append(self, *items):
1165
+ """Shortcut for `page.body.append`."""
1166
+ self.body.append(*items)
1167
+
1168
+ def find(self, selector): # NOQA
1169
+ """Find all elements that match the specified selector.
1170
+
1171
+ Return the results as a (possibly empty) `ElementCollection`.
1172
+ """
1173
+ return ElementCollection.wrap_dom_elements(document.querySelectorAll(selector))
1174
+
1175
+
1176
+ page = Page()