@pyscript/core 0.5.2-rc2 → 0.5.3-rc1

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 (39) hide show
  1. package/dist/{codemirror-D5H78LwF.js → codemirror-DP2LKuBT.js} +2 -2
  2. package/dist/{codemirror-D5H78LwF.js.map → codemirror-DP2LKuBT.js.map} +1 -1
  3. package/dist/{codemirror_commands-BS7VlXdv.js → codemirror_commands-Dtn37S2-.js} +2 -2
  4. package/dist/{codemirror_commands-BS7VlXdv.js.map → codemirror_commands-Dtn37S2-.js.map} +1 -1
  5. package/dist/{codemirror_lang-python-BNKMM3aS.js → codemirror_lang-python-CD-6L-Uh.js} +2 -2
  6. package/dist/{codemirror_lang-python-BNKMM3aS.js.map → codemirror_lang-python-CD-6L-Uh.js.map} +1 -1
  7. package/dist/{codemirror_language-CBQniB_I.js → codemirror_language-D6ce_yTr.js} +2 -2
  8. package/dist/{codemirror_language-CBQniB_I.js.map → codemirror_language-D6ce_yTr.js.map} +1 -1
  9. package/dist/codemirror_view-byykwGDe.js +2 -0
  10. package/dist/codemirror_view-byykwGDe.js.map +1 -0
  11. package/dist/core-CAHVPjK9.js +2 -0
  12. package/dist/core-CAHVPjK9.js.map +1 -0
  13. package/dist/core.js +1 -1
  14. package/dist/{deprecations-manager-NZiilPwd.js → deprecations-manager-CeicOp-C.js} +2 -2
  15. package/dist/{deprecations-manager-NZiilPwd.js.map → deprecations-manager-CeicOp-C.js.map} +1 -1
  16. package/dist/{error-C8ancGMn.js → error-bhRxUJ0H.js} +2 -2
  17. package/dist/{error-C8ancGMn.js.map → error-bhRxUJ0H.js.map} +1 -1
  18. package/dist/{index-CKVCnmMK.js → index-CdWlITxy.js} +2 -2
  19. package/dist/{index-CKVCnmMK.js.map → index-CdWlITxy.js.map} +1 -1
  20. package/dist/{mpy-BMuA4LtC.js → mpy-Cgg31iaC.js} +2 -2
  21. package/dist/{mpy-BMuA4LtC.js.map → mpy-Cgg31iaC.js.map} +1 -1
  22. package/dist/{py-CEZskUpM.js → py-D5PpdbCy.js} +2 -2
  23. package/dist/{py-CEZskUpM.js.map → py-D5PpdbCy.js.map} +1 -1
  24. package/dist/{py-editor-wBsF6NjT.js → py-editor-BDuee0vY.js} +2 -2
  25. package/dist/{py-editor-wBsF6NjT.js.map → py-editor-BDuee0vY.js.map} +1 -1
  26. package/dist/{py-terminal-rg1cOOgx.js → py-terminal-BBpf7LFW.js} +2 -2
  27. package/dist/{py-terminal-rg1cOOgx.js.map → py-terminal-BBpf7LFW.js.map} +1 -1
  28. package/dist/toml-DiUM0_qs.js.map +1 -1
  29. package/dist/zip-BUaoNci7.js.map +1 -1
  30. package/package.json +4 -4
  31. package/src/stdlib/pyscript/event_handling.py +1 -1
  32. package/src/stdlib/pyscript/{web/elements.py → web.py} +400 -322
  33. package/src/stdlib/pyscript.js +2 -5
  34. package/types/stdlib/pyscript.d.ts +1 -4
  35. package/dist/codemirror_view-BNvLVbLs.js +0 -2
  36. package/dist/codemirror_view-BNvLVbLs.js.map +0 -1
  37. package/dist/core-DghpsAMR.js +0 -2
  38. package/dist/core-DghpsAMR.js.map +0 -1
  39. package/src/stdlib/pyscript/web/__init__.py +0 -22
@@ -1,36 +1,60 @@
1
- try:
2
- from typing import Any
1
+ """Lightweight interface to the DOM and HTML elements."""
3
2
 
4
- except ImportError:
5
- Any = "Any"
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
6
 
7
- try:
8
- import warnings
9
7
 
10
- except ImportError:
11
- # TODO: For now it probably means we are in MicroPython. We should figure
12
- # out the "right" way to handle this. For now we just ignore the warning
13
- # and logging to console
14
- class warnings:
15
- @staticmethod
16
- def warn(*args, **kwargs):
17
- print("WARNING: ", *args, **kwargs)
8
+ def wrap_dom_element(dom_element):
9
+ """Wrap an existing DOM element in an instance of a subclass of `Element`.
18
10
 
11
+ This is just a convenience function to avoid having to import the `Element` class
12
+ and use its class method.
13
+ """
19
14
 
20
- from pyscript import document
15
+ return Element.wrap_dom_element(dom_element)
21
16
 
22
17
 
23
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
+
24
23
  @classmethod
25
- def from_dom_element(cls, dom_element):
26
- """Create an instance of a subclass of `Element` for a DOM element."""
24
+ def get_tag_name(cls):
25
+ """Return the HTML tag name for the class.
27
26
 
28
- element_cls = ELEMENT_CLASSES_BY_TAG_NAME.get(dom_element.tagName.lower())
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("_", "")
29
33
 
30
- # For any unknown elements (custom tags etc.) create an instance of this
31
- # class ('Element').
32
- if not element_cls:
33
- element_cls = cls
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
+ )
34
58
 
35
59
  return element_cls(dom_element=dom_element)
36
60
 
@@ -41,16 +65,33 @@ class Element:
41
65
  Otherwise, we are being called to *wrap* an existing DOM element.
42
66
  """
43
67
  self._dom_element = dom_element or document.createElement(
44
- type(self).__name__.replace("_", "")
68
+ type(self).get_tag_name()
45
69
  )
46
70
 
47
- self._parent = None
71
+ # A set-like interface to the element's `classList`.
48
72
  self._classes = Classes(self)
73
+
74
+ # A dict-like interface to the element's `style` attribute.
49
75
  self._style = Style(self)
50
76
 
51
77
  # Set any specified classes, styles, and DOM properties.
52
78
  self.update(classes=classes, style=style, **kwargs)
53
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
+
54
95
  def __getattr__(self, name):
55
96
  # This allows us to get attributes on the underlying DOM element that clash
56
97
  # with Python keywords or built-ins (e.g. the output element has an
@@ -80,119 +121,102 @@ class Element:
80
121
 
81
122
  setattr(self._dom_element, name, value)
82
123
 
83
- def update(self, classes=None, style=None, **kwargs):
84
- """Update the element with the specified classes, styles, and DOM properties."""
85
-
86
- if classes:
87
- self.classes.add(classes)
88
-
89
- if isinstance(style, dict):
90
- self.style.set(**style)
91
-
92
- elif style is not None:
93
- raise ValueError(
94
- f"Style should be a dictionary, received {style} "
95
- f"(type {type(style)}) instead."
96
- )
97
-
98
- self._set_dom_properties(**kwargs)
99
-
100
- def _set_dom_properties(self, **kwargs):
101
- """Set the specified DOM properties.
102
-
103
- Args:
104
- **kwargs: The properties to set
105
- """
106
- for name, value in kwargs.items():
107
- setattr(self, name, value)
108
-
109
- def __eq__(self, obj):
110
- """Check for equality by comparing the underlying DOM element."""
111
- return isinstance(obj, Element) and obj._dom_element == self._dom_element
112
-
113
124
  @property
114
125
  def children(self):
115
- return ElementCollection(
116
- [Element.from_dom_element(el) for el in self._dom_element.children]
117
- )
126
+ """Return the element's children as an `ElementCollection`."""
127
+ return ElementCollection.wrap_dom_elements(self._dom_element.children)
118
128
 
119
129
  @property
120
130
  def classes(self):
131
+ """Return the element's `classList` as a `Classes` instance."""
121
132
  return self._classes
122
133
 
123
134
  @property
124
135
  def parent(self):
125
- if self._parent:
126
- return self._parent
127
-
128
- if self._dom_element.parentElement:
129
- self._parent = Element.from_dom_element(self._dom_element.parentElement)
136
+ """Return the element's `parent `Element`."""
137
+ if self._dom_element.parentElement is None:
138
+ return None
130
139
 
131
- return self._parent
140
+ return Element.wrap_dom_element(self._dom_element.parentElement)
132
141
 
133
142
  @property
134
143
  def style(self):
144
+ """Return the element's `style` attribute as a `Style` instance."""
135
145
  return self._style
136
146
 
137
- def append(self, child):
138
- if isinstance(child, Element):
139
- self._dom_element.appendChild(child._dom_element)
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)
140
152
 
141
- elif isinstance(child, ElementCollection):
142
- for el in child:
143
- self._dom_element.appendChild(el._dom_element)
153
+ elif isinstance(item, ElementCollection):
154
+ for element in item:
155
+ self._dom_element.appendChild(element._dom_element)
144
156
 
145
- else:
146
- # In this case we know it's not an Element or an ElementCollection, so we
147
- # guess that it's either a DOM element or NodeList returned via the ffi.
148
- try:
149
- # First, we try to see if it's an element by accessing the 'tagName'
150
- # attribute.
151
- child.tagName
152
- self._dom_element.appendChild(child)
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)
153
163
 
154
- except AttributeError:
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.
155
168
  try:
156
- # Ok, it's not an element, so let's see if it's a NodeList by
157
- # accessing the 'length' attribute.
158
- child.length
159
- for element_ in child:
160
- self._dom_element.appendChild(element_)
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)
161
173
 
162
174
  except AttributeError:
163
- # Nope! This is not an element or a NodeList.
164
- raise TypeError(
165
- f'Element "{child}" is a proxy object, "'
166
- f"but not a valid element or a NodeList."
167
- )
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
+ )
168
188
 
169
189
  def clone(self, clone_id=None):
170
190
  """Make a clone of the element (clones the underlying DOM object too)."""
171
- clone = Element.from_dom_element(self._dom_element.cloneNode(True))
191
+ clone = Element.wrap_dom_element(self._dom_element.cloneNode(True))
172
192
  clone.id = clone_id
173
193
  return clone
174
194
 
175
195
  def find(self, selector):
176
- """Return an ElementCollection representing all the child elements that
177
- match the specified selector.
196
+ """Find all elements that match the specified selector.
178
197
 
179
- Args:
180
- selector (str): A string containing a selector expression
181
-
182
- Returns:
183
- ElementCollection: A collection of elements matching the selector
198
+ Return the results as a (possibly empty) `ElementCollection`.
184
199
  """
185
- return ElementCollection(
186
- [
187
- Element.from_dom_element(dom_element)
188
- for dom_element in self._dom_element.querySelectorAll(selector)
189
- ]
200
+ return ElementCollection.wrap_dom_elements(
201
+ self._dom_element.querySelectorAll(selector)
190
202
  )
191
203
 
192
204
  def show_me(self):
193
- """Scroll the element into view."""
205
+ """Convenience method for 'element.scrollIntoView()'."""
194
206
  self._dom_element.scrollIntoView()
195
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
+
196
220
 
197
221
  class Classes:
198
222
  """A set-like interface to an element's `classList`."""
@@ -233,6 +257,7 @@ class Classes:
233
257
  return " ".join(self._class_list)
234
258
 
235
259
  def add(self, *class_names):
260
+ """Add one or more classes to the element."""
236
261
  for class_name in class_names:
237
262
  if isinstance(class_name, list):
238
263
  for item in class_name:
@@ -242,9 +267,11 @@ class Classes:
242
267
  self._class_list.add(class_name)
243
268
 
244
269
  def contains(self, class_name):
270
+ """Check if the element has the specified class."""
245
271
  return class_name in self
246
272
 
247
273
  def remove(self, *class_names):
274
+ """Remove one or more classes from the element."""
248
275
  for class_name in class_names:
249
276
  if isinstance(class_name, list):
250
277
  for item in class_name:
@@ -254,10 +281,12 @@ class Classes:
254
281
  self._class_list.remove(class_name)
255
282
 
256
283
  def replace(self, old_class, new_class):
284
+ """Replace one of the element's classes with another."""
257
285
  self.remove(old_class)
258
286
  self.add(new_class)
259
287
 
260
288
  def toggle(self, *class_names):
289
+ """Toggle one or more of the element's classes."""
261
290
  for class_name in class_names:
262
291
  if class_name in self:
263
292
  self.remove(class_name)
@@ -274,6 +303,7 @@ class HasOptions:
274
303
 
275
304
  @property
276
305
  def options(self):
306
+ """Return the element's options as an `Options"""
277
307
  if not hasattr(self, "_options"):
278
308
  self._options = Options(self)
279
309
 
@@ -281,81 +311,70 @@ class HasOptions:
281
311
 
282
312
 
283
313
  class Options:
284
- """This class represents the <option>s of a <datalist>, <optgroup> or <select>
285
- element.
314
+ """This class represents the <option>s of a <datalist>, <optgroup> or <select>.
286
315
 
287
- It allows to access to add and remove <option>s by using the `add` and `remove`
288
- methods.
316
+ It allows access to add and remove <option>s by using the `add`, `remove` and
317
+ `clear` methods.
289
318
  """
290
319
 
291
- def __init__(self, element: Element) -> None:
320
+ def __init__(self, element):
292
321
  self._element = element
293
322
 
294
- def add(
295
- self,
296
- value: Any = None,
297
- html: str = None,
298
- text: str = None,
299
- before: Element | int = None,
300
- **kws,
301
- ) -> None:
302
- """Add a new option to the select element"""
303
-
304
- option = document.createElement("option")
305
- if value is not None:
306
- kws["value"] = value
307
- if html is not None:
308
- option.innerHTML = html
309
- if text is not None:
310
- kws["text"] = text
311
-
312
- for key, value in kws.items():
313
- option.setAttribute(key, value)
314
-
315
- if before:
316
- if isinstance(before, Element):
317
- before = before._dom_element
323
+ def __getitem__(self, key):
324
+ return self.options[key]
318
325
 
319
- self._element._dom_element.add(option, before)
326
+ def __iter__(self):
327
+ yield from self.options
320
328
 
321
- def remove(self, item: int) -> None:
322
- """Remove the option at the specified index"""
323
- self._element._dom_element.remove(item)
329
+ def __len__(self):
330
+ return len(self.options)
324
331
 
325
- def clear(self) -> None:
326
- """Remove all the options"""
327
- for i in range(len(self)):
328
- self.remove(0)
332
+ def __repr__(self):
333
+ return f"{self.__class__.__name__} (length: {len(self)}) {self.options}"
329
334
 
330
335
  @property
331
336
  def options(self):
332
- """Return the list of options"""
333
- return [
334
- Element.from_dom_element(opt) for opt in self._element._dom_element.options
335
- ]
337
+ """Return the list of options."""
338
+ return [Element.wrap_dom_element(o) for o in self._element._dom_element.options]
336
339
 
337
340
  @property
338
341
  def selected(self):
339
- """Return the selected option"""
342
+ """Return the selected option."""
340
343
  return self.options[self._element._dom_element.selectedIndex]
341
344
 
342
- def __iter__(self):
343
- yield from self.options
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
344
349
 
345
- def __len__(self):
346
- return len(self.options)
350
+ if html is not None:
351
+ kwargs["innerHTML"] = html
347
352
 
348
- def __repr__(self):
349
- return f"{self.__class__.__name__} (length: {len(self)}) {self.options}"
353
+ if text is not None:
354
+ kwargs["text"] = text
350
355
 
351
- def __getitem__(self, key):
352
- return self.options[key]
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)
353
372
 
354
373
 
355
374
  class Style:
356
- """A dict-like interface to an element's css style."""
375
+ """A dict-like interface to an element's `style` attribute."""
357
376
 
358
- def __init__(self, element: Element) -> None:
377
+ def __init__(self, element: Element):
359
378
  self._element = element
360
379
  self._style = self._element._dom_element.style
361
380
 
@@ -366,9 +385,11 @@ class Style:
366
385
  self._style.setProperty(key, value)
367
386
 
368
387
  def remove(self, key):
388
+ """Remove a CSS property from the element."""
369
389
  self._style.removeProperty(key)
370
390
 
371
391
  def set(self, **kwargs):
392
+ """Set one or more CSS properties on the element."""
372
393
  for key, value in kwargs.items():
373
394
  self._element._dom_element.style.setProperty(key, value)
374
395
 
@@ -402,10 +423,188 @@ class ContainerElement(Element):
402
423
  else:
403
424
  self.innerHTML += child
404
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)
405
577
 
406
- # Classes for every element type. If the element type (e.g. "input") clashes with
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
407
606
  # either a Python keyword or common symbol, then we suffix the class name with an "_"
408
- # (e.g. "input_").
607
+ # (e.g. the class for the "input" element is "input_").
409
608
 
410
609
 
411
610
  class a(ContainerElement):
@@ -463,7 +662,7 @@ class button(ContainerElement):
463
662
  class canvas(ContainerElement):
464
663
  """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas"""
465
664
 
466
- def download(self, filename: str = "snapped.png") -> None:
665
+ def download(self, filename: str = "snapped.png"):
467
666
  """Download the current element with the filename provided in input.
468
667
 
469
668
  Inputs:
@@ -905,167 +1104,6 @@ class wbr(Element):
905
1104
  """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/wbr"""
906
1105
 
907
1106
 
908
- class ClassesCollection:
909
- def __init__(self, collection: "ElementCollection") -> None:
910
- self._collection = collection
911
-
912
- def __contains__(self, class_name):
913
- for element in self._collection:
914
- if class_name in element.classes:
915
- return True
916
-
917
- return False
918
-
919
- def __eq__(self, other):
920
- return (
921
- isinstance(other, ClassesCollection)
922
- and self._collection == other._collection
923
- )
924
-
925
- def __iter__(self):
926
- for class_name in self._all_class_names():
927
- yield class_name
928
-
929
- def __len__(self):
930
- return len(self._all_class_names())
931
-
932
- def __repr__(self):
933
- return f"ClassesCollection({repr(self._collection)})"
934
-
935
- def __str__(self):
936
- return " ".join(self._all_class_names())
937
-
938
- def add(self, *class_names):
939
- for element in self._collection:
940
- element.classes.add(*class_names)
941
-
942
- def contains(self, class_name):
943
- return class_name in self
944
-
945
- def remove(self, *class_names):
946
- for element in self._collection:
947
- element.classes.remove(*class_names)
948
-
949
- def replace(self, old_class, new_class):
950
- for element in self._collection:
951
- element.classes.replace(old_class, new_class)
952
-
953
- def toggle(self, *class_names):
954
- for element in self._collection:
955
- element.classes.toggle(*class_names)
956
-
957
- def _all_class_names(self):
958
- all_class_names = set()
959
- for element in self._collection:
960
- for class_name in element.classes:
961
- all_class_names.add(class_name)
962
-
963
- return all_class_names
964
-
965
-
966
- class StyleCollection:
967
- def __init__(self, collection: "ElementCollection") -> None:
968
- self._collection = collection
969
-
970
- def __get__(self, obj, objtype=None):
971
- return obj._get_attribute("style")
972
-
973
- def __getitem__(self, key):
974
- return self._collection._get_attribute("style")[key]
975
-
976
- def __setitem__(self, key, value):
977
- for element in self._collection._elements:
978
- element.style[key] = value
979
-
980
- def __repr__(self):
981
- return f"StyleCollection({repr(self._collection)})"
982
-
983
- def remove(self, key):
984
- for element in self._collection._elements:
985
- element.style.remove(key)
986
-
987
-
988
- class ElementCollection:
989
- def __init__(self, elements: [Element]) -> None:
990
- self._elements = elements
991
- self._classes = ClassesCollection(self)
992
- self._style = StyleCollection(self)
993
-
994
- def __eq__(self, obj):
995
- """Check for equality by comparing the underlying DOM elements."""
996
- return isinstance(obj, ElementCollection) and obj._elements == self._elements
997
-
998
- def __getitem__(self, key):
999
- # If it's an integer we use it to access the elements in the collection
1000
- if isinstance(key, int):
1001
- return self._elements[key]
1002
-
1003
- # If it's a slice we use it to support slice operations over the elements
1004
- # in the collection
1005
- elif isinstance(key, slice):
1006
- return ElementCollection(self._elements[key])
1007
-
1008
- # If it's anything else (basically a string) we use it as a query selector.
1009
- return self.find(key)
1010
-
1011
- def __iter__(self):
1012
- yield from self._elements
1013
-
1014
- def __len__(self):
1015
- return len(self._elements)
1016
-
1017
- def __repr__(self):
1018
- return (
1019
- f"{self.__class__.__name__} (length: {len(self._elements)}) "
1020
- f"{self._elements}"
1021
- )
1022
-
1023
- def __getattr__(self, item):
1024
- return self._get_attribute(item)
1025
-
1026
- def __setattr__(self, key, value):
1027
- # This class overrides `__setattr__` to delegate "public" attributes to the
1028
- # elements in the collection. BUT, we don't use the usual Python pattern where
1029
- # we set attributes on the collection itself via `self.__dict__` as that is not
1030
- # yet supported in our build of MicroPython. Instead, we handle it here by
1031
- # using super for all "private" attributes (those starting with an underscore).
1032
- if key.startswith("_"):
1033
- super().__setattr__(key, value)
1034
-
1035
- else:
1036
- self._set_attribute(key, value)
1037
-
1038
- @property
1039
- def children(self):
1040
- return self._elements
1041
-
1042
- @property
1043
- def classes(self):
1044
- return self._classes
1045
-
1046
- @property
1047
- def style(self):
1048
- return self._style
1049
-
1050
- def find(self, selector):
1051
- elements = []
1052
- for element in self._elements:
1053
- elements.extend(element.find(selector))
1054
-
1055
- return ElementCollection(elements)
1056
-
1057
- def _get_attribute(self, attr, index=None):
1058
- if index is None:
1059
- return [getattr(el, attr) for el in self._elements]
1060
-
1061
- # As JQuery, when getting an attr, only return it for the first element
1062
- return getattr(self._elements[index], attr)
1063
-
1064
- def _set_attribute(self, attr, value):
1065
- for el in self._elements:
1066
- setattr(el, attr, value)
1067
-
1068
-
1069
1107
  # fmt: off
1070
1108
  ELEMENT_CLASSES = [
1071
1109
  a, abbr, address, area, article, aside, audio,
@@ -1092,7 +1130,47 @@ ELEMENT_CLASSES = [
1092
1130
  # fmt: on
1093
1131
 
1094
1132
 
1095
- # Lookup table to get an element class by its tag name.
1096
- ELEMENT_CLASSES_BY_TAG_NAME = {
1097
- cls.__name__.replace("_", ""): cls for cls in ELEMENT_CLASSES
1098
- }
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()