646 lines
18 KiB
Python
646 lines
18 KiB
Python
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"
|
|
|
|
@property
|
|
def value(self) -> str:
|
|
return self._parse_value()
|
|
|
|
@value.setter
|
|
def value(self, value: str):
|
|
self._set_value(value)
|
|
|
|
|
|
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 |