import datetime as dt import uuid from .bootstrap_HTML import * from js import document, FileReader, btoa, Uint8Array # type: ignore from pyodide.ffi import create_proxy # type: ignore import io import base64 class InputLabel(HTML.Label, BootstrapContainer): _default_class_name: str = "form-label" class InputLabelCheckbox(InputLabel): _default_class_name: str = "form-check-label" class InputHelp(BootstrapContainer): _default_class_name: str = "form-text" class InputFormControl(HTML.Input, BootstrapContainer): _default_class_name: str = "form-control" class InputFormControlColor(InputFormControl): _default_class_name: str = "form-control form-control-color" class InputFormControlNumber(InputFormControl): _default_class_name: str = "form-control" @property def min(self): return self.element.getAttribute("min") @min.setter def min(self, value): self.element.setAttribute("min", str(value)) @property def max(self): return self.element.getAttribute("max") @max.setter def max(self, value): self.element.setAttribute("max", str(value)) class InputFormControlRange(InputFormControl): _default_class_name: str = "form-range" class InputFormControlSelect(InputFormControl): _default_class_name: str = "form-select" _tag_type: str = "select" class InputFormControlCheckbox(InputFormControl): _default_class_name: str = "form-check-input" class InputElement(BootstrapContainer): _default_input_type = "text" _default_input_class = InputFormControl _default_label_class = InputLabel def __init__(self, label_text: str = None, help_text: str = None, floating_label: bool = False, placeholder: str = None, input_type=None, id=None, parent=None): super().__init__(parent=parent, id=id) if floating_label and placeholder is None: placeholder = " " # create a dummy placeholder self._input = self.__class__._default_input_class( parent=None, id=self.id+"_input", type=input_type if input_type is not None else self._default_input_type, placeholder=placeholder ) self._label = self.__class__._default_label_class( parent=None, id=self.id+"_label", inner_html=label_text) # if floating_label is True, add the input first, then the label if floating_label: self.append_child(self._input) self.append_child(self._label) else: self.append_child(self._label) self.append_child(self._input) self._help_text = None if help_text is not None: self._help_text = InputHelp( parent=self, id=self.id+"_help_text", inner_html=help_text) self.floating_label = floating_label @property def is_small(self) -> bool: return self._input.has_class("form-control-sm") @is_small.setter def is_small(self, value: bool): self._input.set_class_name("form-control-sm", value) @property def is_large(self) -> bool: return self._input.has_class("form-control-lg") @is_large.setter def is_large(self, value: bool): self._input.set_class_name("form-control-lg", value) @property def readonly(self) -> bool: return self._input.has_attribute("readonly") @readonly.setter def readonly(self, value: bool): self._input.set_attribute("readonly", value, is_boolean_attribute=True) @property def value(self): return self._input.value @value.setter def value(self, value): self._input.value = value @property def onchange(self): return self._input.onchange @onchange.setter def onchange(self, value): self._input.onchange = value @property def floating_label(self) -> bool: return self.has_class("form-floating") @floating_label.setter def floating_label(self, value: bool): self.set_class("form-floating", value) class InputEmail(InputElement): _default_input_type = "email" class InputPassword(InputElement): _default_input_type = "password" class InputText(InputElement): _default_input_type = "text" class InputFloat(InputElement): _default_input_type = "number" _default_number_class = float def __init__(self, label_text: str = None, help_text: str = None, floating_label: bool = False, placeholder: str = None, min: _default_number_class = None, max: _default_number_class = None, step: _default_number_class = None, input_type=None, id=None, parent=None): super().__init__(label_text=label_text, help_text=help_text, floating_label=floating_label, placeholder=placeholder, input_type=input_type, id=id, parent=parent) if min is not None: self.min = min if max is not None: self.max = max if step is not None: self.step = step @property def value(self) -> _default_number_class: val = self._input.value if val == "": return None return self.__class__._default_number_class(self._input.value) @value.setter def value(self, value: _default_number_class): self._input.value = str(value) if value is not None else "" @property def min(self) -> _default_number_class: return self.__class__._default_number_class(self._input.get_attribute("min")) @min.setter def min(self, value: _default_number_class): self._input.set_attribute("min", str(value)) @property def max(self) -> _default_number_class: return self.__class__._default_number_class(self._input.get_attribute("max")) @max.setter def max(self, value: _default_number_class): self._input.set_attribute("max", str(value)) @property def step(self) -> _default_number_class: return self.__class__._default_number_class(self._input.get_attribute("step")) @step.setter def step(self, value: _default_number_class): self._input.set_attribute("step", str(value)) class InputRangeFloat(InputFloat): _default_input_type = "range" _default_input_class = InputFormControlRange class InputInt(InputFloat): _default_number_class = int class InputRangeInt(InputInt): _default_input_type = "range" _default_input_class = InputFormControlRange class InputDate(InputElement): _default_input_type = "date" @property def value(self) -> dt.datetime: if self._input.value == "": return None return dt.datetime.strptime(self._input.value, "%Y-%m-%d").date() @value.setter def value(self, value: Union[dt.datetime, dt.date, str, None]): if isinstance(value, str): self._input.value = value self._input.value = value.strftime( "%Y-%m-%d") if value is not None else "" class InputTime(InputElement): _default_input_type = "time" @property def value(self) -> dt.datetime: if self._input.value == "": return None return dt.datetime.strptime(self._input.value, "%H:%M").time() @value.setter def value(self, value: Union[dt.datetime, dt.time, str, None]): if isinstance(value, str): self._input.value = value self._input.value = value.strftime( "%H:%M") if value is not None else "" class InputRange(InputElement): _default_input_type = "range" class InputFile(InputElement): _default_input_type = "file" def __init__(self, label_text: str = None, help_text: str = None, floating_label: bool = False, placeholder: str = None, input_type=None, id=None, parent=None): super().__init__(label_text=label_text, help_text=help_text, floating_label=floating_label, placeholder=placeholder, input_type=input_type, id=id, parent=parent) self._files = {} self._input.onchange = self._load_file self._on_file_change = None def _load_file(self, *args): async def read_file(event, f): # FIXME: i think i am doing things too complicated here, # but my understanding of loading raw bytes from file # inputs in javascript (and their pyodide interacions) is very much limited name = f.name print("loaded ", name) uint8_array = Uint8Array.new(await f.arrayBuffer()) buffer = io.BytesIO(bytearray(uint8_array)) self._files[name] = buffer buffer.seek(0) buffer.name = name # not standardized, but works here # TODO: create base class for that purpose if self._on_file_change is not None: self._on_file_change(buffer) file_list = self._input.element.files.to_py() self._files = {} for f in file_list: # reader is a pyodide.JsProxy reader = FileReader.new() # Create a Python proxy for the callback function onload_event = create_proxy(lambda event, f=f: read_file(event, f)) reader.onload = onload_event reader.readAsBinaryString(f) return @property def value(self): return self._files @property def onchange(self): return self._on_file_change @onchange.setter def onchange(self, value): self._on_file_change = value class InputMultiFile(InputFile): def __init__(self, label_text: str = None, help_text: str = None, floating_label: bool = False, placeholder: str = None, input_type=None, id=None, parent=None): super().__init__(label_text=label_text, help_text=help_text, floating_label=floating_label, placeholder=placeholder, input_type=input_type, id=id, parent=parent) self._input.set_attribute("multiple", True, is_boolean_attribute=True) @property def value(self): files = [] for i in range(self._input.element.files.length): files.append(self._input.element.files.item(i).name) return files class InputColor(InputElement): _default_input_type = "color" _default_input_class = InputFormControlColor class InputDatalist(InputElement): def __init__(self, options: List[str], label_text: str = None, help_text: str = None, floating_label: bool = False, placeholder: str = None, input_type=None, id=None, parent=None): super().__init__(label_text=label_text, help_text=help_text, floating_label=floating_label, placeholder=placeholder, input_type=input_type, id=id, parent=parent) self._input.type = None self._options = options self._datalist = HTML.DataList(parent=self, id=self.id+"_datalist") for option in options: HTML.Option(inner_html=option, parent=self._datalist) self._input.set_attribute("list", self._datalist.id) @property def options(self): return self._options @options.setter def options(self, value): self._options = value for c in self._datalist.children: c.destroy() for option in self._options: HTML.Option(inner_html=option, parent=self._datalist) class InputSelect(InputElement): _default_input_class = InputFormControlSelect def __init__(self, options: List[str], label_text: str = None, help_text: str = None, floating_label: bool = False, placeholder: str = None, multiple: bool = False, input_type=None, id=None, parent=None): super().__init__(label_text=label_text, help_text=help_text, floating_label=floating_label, placeholder=placeholder, input_type=input_type, id=id, parent=parent) self._input.type = None self._options = options self.multiple = multiple for option in options: HTML.Option(inner_html=option, parent=self._input) @property def options(self): return self._options @options.setter def options(self, value): self._options = value for c in self._input.children: c.destroy() for option in self._options: HTML.Option(inner_html=option, parent=self._input) @property def multiple(self): return self._input.has_attribute("multiple") @multiple.setter def multiple(self, value): self._input.set_attribute("multiple", value, is_boolean_attribute=True) class InputCheckboxSingle(InputElement): _default_class_name: str = "form-check" _default_input_type = "checkbox" _default_input_class = InputFormControlCheckbox _default_label_class = InputLabelCheckbox def __init__(self, label_text: str = None, help_text: str = None, floating_label: bool = False, placeholder: str = None, name: str = None, input_type=None, id=None, parent=None): super().__init__(label_text=label_text, help_text=help_text, floating_label=floating_label, placeholder=placeholder, input_type=input_type, id=id, parent=parent) if name is not None: self._input.set_attribute("name", name) @property def checked(self) -> bool: return self._input.element.checked @checked.setter def checked(self, value: bool): self._input.element.checked = value @property def value(self) -> bool: return self.checked @value.setter def value(self, value: bool): self.checked = value @property def inline(self) -> bool: return self.has_class("inline") @inline.setter def inline(self, value: bool): self.set_class("inline", value) class InputRadioSingle(InputCheckboxSingle): _default_input_type = "radio" class InputSwitchSingle(InputCheckboxSingle): _default_class_name: str = "form-check form-switch" class InputCheckboxGroup(BootstrapContainer): _default_input_class = InputCheckboxSingle def __init__(self, options: List[str], inline: bool = False, id: str = None, label_text: Union[str, HTML.Element] = None, group_name: str = None, class_name: str = None, parent: HTML.Element = None, inner_html: str = None,) -> None: super().__init__(inner_html=inner_html, id=id, class_name=class_name, parent=parent) if group_name is None: group_name = "id-" + str(uuid.uuid4()) if label_text is not None: if isinstance(label_text, str): label_text = HTML.Label(inner_html=label_text, parent=self) else: self.append_child(label_text) self._options = options self._option_checkboxes = {} self._group_name = group_name for option in options: cb = self.__class__._default_input_class( option, name=group_name, parent=self) cb.inline = inline self._option_checkboxes[option] = cb @property def options(self): return self._options @options.setter def options(self, value): self._options = value for c in self._option_checkboxes.values(): c.destroy() self._option_checkboxes = {} for option in self._options: cb = self.__class__._default_input_class( option, name=self._group_name, parent=self) self._option_checkboxes[option] = cb @property def value(self) -> List[str]: """ returns a list of checked options """ values = [] for option in self._options: if self._option_checkboxes[option].checked: values.append(option) return values @value.setter def value(self, value: List[str]): for option in self._options: self._option_checkboxes[option].checked = option in value @property def option_checkboxes(self) -> Dict[str, InputCheckboxSingle]: """ returns the form elements for each option as dictionary """ return self._option_checkboxes class InputRadioGroup(InputCheckboxGroup): _default_input_class = InputRadioSingle class InputSwitchGroup(InputCheckboxGroup): _default_input_class = InputSwitchSingle class Form(HTML.Form, BootstrapContainer): pass # TODO: implement more Form and form validation methods