nicegui-bootstrap-components 0.1.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.
- nicegui_bootstrap_components/__init__.py +7 -0
- nicegui_bootstrap_components/_base.py +590 -0
- nicegui_bootstrap_components/_host.py +259 -0
- nicegui_bootstrap_components/assets.py +371 -0
- nicegui_bootstrap_components/bs/__init__.py +267 -0
- nicegui_bootstrap_components/bs/_actions.py +232 -0
- nicegui_bootstrap_components/bs/_alert_spinner.py +423 -0
- nicegui_bootstrap_components/bs/_carousel.py +312 -0
- nicegui_bootstrap_components/bs/_choices.py +779 -0
- nicegui_bootstrap_components/bs/_content.py +1343 -0
- nicegui_bootstrap_components/bs/_forms.py +378 -0
- nicegui_bootstrap_components/bs/_inputs.py +560 -0
- nicegui_bootstrap_components/bs/_layout.py +341 -0
- nicegui_bootstrap_components/bs/_nav.py +650 -0
- nicegui_bootstrap_components/bs/_navbar.py +445 -0
- nicegui_bootstrap_components/bs/_overlay_root.py +73 -0
- nicegui_bootstrap_components/bs/_overlays.py +2633 -0
- nicegui_bootstrap_components/bs/_scope.py +28 -0
- nicegui_bootstrap_components/bs/_tabs.py +605 -0
- nicegui_bootstrap_components/bs/_toast.py +137 -0
- nicegui_bootstrap_components/bs/_tooltip_popover.py +537 -0
- nicegui_bootstrap_components/contracts/dbc-2.0.4.json +16811 -0
- nicegui_bootstrap_components/dbc/__init__.py +108 -0
- nicegui_bootstrap_components/icons.py +29 -0
- nicegui_bootstrap_components/persist.py +201 -0
- nicegui_bootstrap_components/py.typed +0 -0
- nicegui_bootstrap_components/static/dist/fonts/bootstrap-icons.woff +0 -0
- nicegui_bootstrap_components/static/dist/fonts/bootstrap-icons.woff2 +0 -0
- nicegui_bootstrap_components/static/dist/ngbs-bootstrap-unscoped.css +12047 -0
- nicegui_bootstrap_components/static/dist/ngbs-bootstrap.css +12057 -0
- nicegui_bootstrap_components/static/dist/ngbs-cerulean-unscoped.css +12338 -0
- nicegui_bootstrap_components/static/dist/ngbs-cerulean.css +12348 -0
- nicegui_bootstrap_components/static/dist/ngbs-compat-unscoped.css +12 -0
- nicegui_bootstrap_components/static/dist/ngbs-compat.css +12 -0
- nicegui_bootstrap_components/static/dist/ngbs-cosmo-unscoped.css +11825 -0
- nicegui_bootstrap_components/static/dist/ngbs-cosmo.css +11835 -0
- nicegui_bootstrap_components/static/dist/ngbs-cyborg-unscoped.css +12213 -0
- nicegui_bootstrap_components/static/dist/ngbs-cyborg.css +12223 -0
- nicegui_bootstrap_components/static/dist/ngbs-darkly-unscoped.css +12143 -0
- nicegui_bootstrap_components/static/dist/ngbs-darkly.css +12153 -0
- nicegui_bootstrap_components/static/dist/ngbs-flatly-unscoped.css +12129 -0
- nicegui_bootstrap_components/static/dist/ngbs-flatly.css +12139 -0
- nicegui_bootstrap_components/static/dist/ngbs-grid-unscoped.css +4084 -0
- nicegui_bootstrap_components/static/dist/ngbs-grid.css +4094 -0
- nicegui_bootstrap_components/static/dist/ngbs-host-unscoped.css +9 -0
- nicegui_bootstrap_components/static/dist/ngbs-host.css +9 -0
- nicegui_bootstrap_components/static/dist/ngbs-icons-bootstrap.css +5 -0
- nicegui_bootstrap_components/static/dist/ngbs-icons-fontawesome.css +9 -0
- nicegui_bootstrap_components/static/dist/ngbs-journal-unscoped.css +12112 -0
- nicegui_bootstrap_components/static/dist/ngbs-journal.css +12122 -0
- nicegui_bootstrap_components/static/dist/ngbs-litera-unscoped.css +12214 -0
- nicegui_bootstrap_components/static/dist/ngbs-litera.css +12224 -0
- nicegui_bootstrap_components/static/dist/ngbs-lumen-unscoped.css +12377 -0
- nicegui_bootstrap_components/static/dist/ngbs-lumen.css +12387 -0
- nicegui_bootstrap_components/static/dist/ngbs-lux-unscoped.css +12048 -0
- nicegui_bootstrap_components/static/dist/ngbs-lux.css +12058 -0
- nicegui_bootstrap_components/static/dist/ngbs-materia-unscoped.css +13197 -0
- nicegui_bootstrap_components/static/dist/ngbs-materia.css +13207 -0
- nicegui_bootstrap_components/static/dist/ngbs-minty-unscoped.css +12184 -0
- nicegui_bootstrap_components/static/dist/ngbs-minty.css +12194 -0
- nicegui_bootstrap_components/static/dist/ngbs-morph-unscoped.css +12829 -0
- nicegui_bootstrap_components/static/dist/ngbs-morph.css +12839 -0
- nicegui_bootstrap_components/static/dist/ngbs-pulse-unscoped.css +11901 -0
- nicegui_bootstrap_components/static/dist/ngbs-pulse.css +11911 -0
- nicegui_bootstrap_components/static/dist/ngbs-quartz-unscoped.css +12625 -0
- nicegui_bootstrap_components/static/dist/ngbs-quartz.css +12635 -0
- nicegui_bootstrap_components/static/dist/ngbs-sandstone-unscoped.css +12214 -0
- nicegui_bootstrap_components/static/dist/ngbs-sandstone.css +12224 -0
- nicegui_bootstrap_components/static/dist/ngbs-simplex-unscoped.css +12199 -0
- nicegui_bootstrap_components/static/dist/ngbs-simplex.css +12209 -0
- nicegui_bootstrap_components/static/dist/ngbs-sketchy-unscoped.css +12518 -0
- nicegui_bootstrap_components/static/dist/ngbs-sketchy.css +12528 -0
- nicegui_bootstrap_components/static/dist/ngbs-slate-unscoped.css +12527 -0
- nicegui_bootstrap_components/static/dist/ngbs-slate.css +12537 -0
- nicegui_bootstrap_components/static/dist/ngbs-solar-unscoped.css +12152 -0
- nicegui_bootstrap_components/static/dist/ngbs-solar.css +12162 -0
- nicegui_bootstrap_components/static/dist/ngbs-spacelab-unscoped.css +12279 -0
- nicegui_bootstrap_components/static/dist/ngbs-spacelab.css +12289 -0
- nicegui_bootstrap_components/static/dist/ngbs-superhero-unscoped.css +12223 -0
- nicegui_bootstrap_components/static/dist/ngbs-superhero.css +12233 -0
- nicegui_bootstrap_components/static/dist/ngbs-united-unscoped.css +12090 -0
- nicegui_bootstrap_components/static/dist/ngbs-united.css +12100 -0
- nicegui_bootstrap_components/static/dist/ngbs-vapor-unscoped.css +12556 -0
- nicegui_bootstrap_components/static/dist/ngbs-vapor.css +12566 -0
- nicegui_bootstrap_components/static/dist/ngbs-yeti-unscoped.css +12333 -0
- nicegui_bootstrap_components/static/dist/ngbs-yeti.css +12343 -0
- nicegui_bootstrap_components/static/dist/ngbs-zephyr-unscoped.css +12296 -0
- nicegui_bootstrap_components/static/dist/ngbs-zephyr.css +12306 -0
- nicegui_bootstrap_components/static/prototype-ngbs.css +541 -0
- nicegui_bootstrap_components/static/webfonts/fa-brands-400.ttf +0 -0
- nicegui_bootstrap_components/static/webfonts/fa-brands-400.woff2 +0 -0
- nicegui_bootstrap_components/static/webfonts/fa-regular-400.ttf +0 -0
- nicegui_bootstrap_components/static/webfonts/fa-regular-400.woff2 +0 -0
- nicegui_bootstrap_components/static/webfonts/fa-solid-900.ttf +0 -0
- nicegui_bootstrap_components/static/webfonts/fa-solid-900.woff2 +0 -0
- nicegui_bootstrap_components/static/webfonts/fa-v4compatibility.ttf +0 -0
- nicegui_bootstrap_components/static/webfonts/fa-v4compatibility.woff2 +0 -0
- nicegui_bootstrap_components/theme.py +238 -0
- nicegui_bootstrap_components/themes.py +131 -0
- nicegui_bootstrap_components-0.1.0.dist-info/METADATA +234 -0
- nicegui_bootstrap_components-0.1.0.dist-info/RECORD +103 -0
- nicegui_bootstrap_components-0.1.0.dist-info/WHEEL +4 -0
- nicegui_bootstrap_components-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,779 @@
|
|
|
1
|
+
"""Choice family: Select, Checkbox, Switch, RadioButton, Checklist, RadioItems."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from nicegui.element import Element
|
|
9
|
+
|
|
10
|
+
from .._base import (
|
|
11
|
+
BootstrapValueElement,
|
|
12
|
+
ChildrenError,
|
|
13
|
+
PropConflictError,
|
|
14
|
+
make_surface_classes,
|
|
15
|
+
normalize_style,
|
|
16
|
+
style_to_css,
|
|
17
|
+
)
|
|
18
|
+
from .._host import set_element_text
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"Select",
|
|
22
|
+
"DbcSelect",
|
|
23
|
+
"select",
|
|
24
|
+
"Checkbox",
|
|
25
|
+
"DbcCheckbox",
|
|
26
|
+
"checkbox",
|
|
27
|
+
"RadioButton",
|
|
28
|
+
"DbcRadioButton",
|
|
29
|
+
"radio_button",
|
|
30
|
+
"Switch",
|
|
31
|
+
"DbcSwitch",
|
|
32
|
+
"switch",
|
|
33
|
+
"Checklist",
|
|
34
|
+
"DbcChecklist",
|
|
35
|
+
"checklist",
|
|
36
|
+
"RadioItems",
|
|
37
|
+
"DbcRadioItems",
|
|
38
|
+
"radio_items",
|
|
39
|
+
"normalize_choice_options",
|
|
40
|
+
"select_structural_classes",
|
|
41
|
+
"form_check_item_classes",
|
|
42
|
+
"choice_input_classes",
|
|
43
|
+
"choice_label_classes",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
_SELECT_SIZES = {"sm", "md", "lg"}
|
|
47
|
+
|
|
48
|
+
_JS_SELECT_CHANGE = """(e) => {
|
|
49
|
+
const root = e.currentTarget || e.target;
|
|
50
|
+
let sel = e.target;
|
|
51
|
+
if (!sel || sel.tagName !== 'SELECT') {
|
|
52
|
+
sel = root && root.tagName === 'SELECT' ? root : sel;
|
|
53
|
+
}
|
|
54
|
+
if (!sel || sel.tagName !== 'SELECT') return;
|
|
55
|
+
emit(sel.value == null ? '' : String(sel.value));
|
|
56
|
+
}"""
|
|
57
|
+
|
|
58
|
+
_JS_CHECK_CHANGE = """(e) => {
|
|
59
|
+
const root = e.currentTarget || e.target;
|
|
60
|
+
let input = e.target;
|
|
61
|
+
if (!input || input.tagName !== 'INPUT') {
|
|
62
|
+
input = root && root.querySelector ? root.querySelector('input') : input;
|
|
63
|
+
}
|
|
64
|
+
if (!input) return;
|
|
65
|
+
emit(!!input.checked);
|
|
66
|
+
}"""
|
|
67
|
+
|
|
68
|
+
_JS_CHECKLIST_CHANGE = """(e) => {
|
|
69
|
+
const root = e.currentTarget || e.target;
|
|
70
|
+
if (!root || !root.querySelectorAll) return;
|
|
71
|
+
const checked = [];
|
|
72
|
+
root.querySelectorAll('input[type="checkbox"]').forEach((el) => {
|
|
73
|
+
if (el.checked) checked.push(el.value == null ? '' : String(el.value));
|
|
74
|
+
});
|
|
75
|
+
emit(checked);
|
|
76
|
+
}"""
|
|
77
|
+
|
|
78
|
+
_JS_RADIO_ITEMS_CHANGE = """(e) => {
|
|
79
|
+
const root = e.currentTarget || e.target;
|
|
80
|
+
if (!root || !root.querySelector) return;
|
|
81
|
+
const selected = root.querySelector('input[type="radio"]:checked');
|
|
82
|
+
emit(selected && selected.value != null ? String(selected.value) : null);
|
|
83
|
+
}"""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _split_classes(value: str | None) -> list[str]:
|
|
87
|
+
if not value:
|
|
88
|
+
return []
|
|
89
|
+
return [part for part in value.split() if part]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _resolve_alias(primary: Any, fallback: Any) -> Any:
|
|
93
|
+
if primary is not None:
|
|
94
|
+
return primary
|
|
95
|
+
return fallback
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _html_value(value: Any) -> str:
|
|
99
|
+
if value is None:
|
|
100
|
+
return ""
|
|
101
|
+
return str(value)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _payload_from_event(e: Any) -> Any:
|
|
105
|
+
return getattr(e, "args", e)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _set_flag(element: Any, name: str, value: bool) -> None:
|
|
109
|
+
props = element._props
|
|
110
|
+
if value:
|
|
111
|
+
props[name] = True
|
|
112
|
+
else:
|
|
113
|
+
props.pop(name, None)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _apply_style_dict(element: Element, style: dict[str, Any] | str | None) -> None:
|
|
117
|
+
if not style:
|
|
118
|
+
return
|
|
119
|
+
if isinstance(style, str):
|
|
120
|
+
element.style(style)
|
|
121
|
+
return
|
|
122
|
+
normalized = normalize_style(style)
|
|
123
|
+
if isinstance(normalized, str):
|
|
124
|
+
if normalized:
|
|
125
|
+
element.style(normalized)
|
|
126
|
+
return
|
|
127
|
+
css = style_to_css(normalized)
|
|
128
|
+
if css:
|
|
129
|
+
element.style(css)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _has_children(children: object) -> bool:
|
|
133
|
+
if children is None:
|
|
134
|
+
return False
|
|
135
|
+
return not (children == [] or children == ())
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _normalize_one_option(item: Any) -> dict[str, Any]:
|
|
139
|
+
if isinstance(item, dict):
|
|
140
|
+
if "value" not in item and "label" not in item:
|
|
141
|
+
raise ValueError("Option dict requires 'label' or 'value'")
|
|
142
|
+
value = item["value"] if "value" in item else item["label"]
|
|
143
|
+
raw_label = item.get("label", value)
|
|
144
|
+
title = item.get("title")
|
|
145
|
+
return {
|
|
146
|
+
"value": value,
|
|
147
|
+
"label": "" if raw_label is None else str(raw_label),
|
|
148
|
+
"disabled": bool(item.get("disabled", False)),
|
|
149
|
+
"title": None if title is None else str(title),
|
|
150
|
+
"input_id": item.get("input_id"),
|
|
151
|
+
"label_id": item.get("label_id"),
|
|
152
|
+
}
|
|
153
|
+
if isinstance(item, (list, tuple)):
|
|
154
|
+
raise ValueError(f"Invalid option {item!r}; expected a string, number, or dict")
|
|
155
|
+
return {
|
|
156
|
+
"value": item,
|
|
157
|
+
"label": "" if item is None else str(item),
|
|
158
|
+
"disabled": False,
|
|
159
|
+
"title": None,
|
|
160
|
+
"input_id": None,
|
|
161
|
+
"label_id": None,
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def normalize_choice_options(
|
|
166
|
+
options: list[Any] | dict[Any, Any] | tuple[Any, ...] | None,
|
|
167
|
+
) -> list[dict[str, Any]]:
|
|
168
|
+
"""Normalize choice options to dicts with label, value, disabled, title, and ids."""
|
|
169
|
+
if options is None:
|
|
170
|
+
return []
|
|
171
|
+
if isinstance(options, dict):
|
|
172
|
+
items = [{"value": key, "label": val} for key, val in options.items()]
|
|
173
|
+
return [_normalize_one_option(item) for item in items]
|
|
174
|
+
if isinstance(options, (list, tuple)):
|
|
175
|
+
return [_normalize_one_option(item) for item in options]
|
|
176
|
+
raise ValueError(f"options must be a list, tuple, or dict, not {type(options).__name__}")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def select_structural_classes(
|
|
180
|
+
*,
|
|
181
|
+
size: str | None = None,
|
|
182
|
+
valid: bool | None = None,
|
|
183
|
+
invalid: bool | None = None,
|
|
184
|
+
) -> list[str]:
|
|
185
|
+
"""Return Bootstrap ``form-select`` classes. Unknown sizes raise ``ValueError``."""
|
|
186
|
+
if size is not None and size not in _SELECT_SIZES:
|
|
187
|
+
raise ValueError(f"Invalid Select size {size!r}; expected one of 'sm', 'md', 'lg'")
|
|
188
|
+
if valid and invalid:
|
|
189
|
+
raise PropConflictError("valid", "invalid")
|
|
190
|
+
classes = ["form-select"]
|
|
191
|
+
if size in {"sm", "lg"}:
|
|
192
|
+
classes.append(f"form-select-{size}")
|
|
193
|
+
if valid:
|
|
194
|
+
classes.append("is-valid")
|
|
195
|
+
if invalid:
|
|
196
|
+
classes.append("is-invalid")
|
|
197
|
+
return classes
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def form_check_item_classes(*, inline: bool = False, switch: bool = False) -> list[str]:
|
|
201
|
+
"""Return Bootstrap ``form-check`` wrapper classes for a choice item."""
|
|
202
|
+
classes = ["form-check"]
|
|
203
|
+
if inline:
|
|
204
|
+
classes.append("form-check-inline")
|
|
205
|
+
if switch:
|
|
206
|
+
classes.append("form-switch")
|
|
207
|
+
return classes
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def choice_input_classes(
|
|
211
|
+
*,
|
|
212
|
+
input_class_name: str | None = None,
|
|
213
|
+
input_checked_class_name: str | None = None,
|
|
214
|
+
checked: bool = False,
|
|
215
|
+
) -> list[str]:
|
|
216
|
+
"""Return classes for a ``form-check-input`` element."""
|
|
217
|
+
classes = ["form-check-input"]
|
|
218
|
+
classes.extend(_split_classes(input_class_name))
|
|
219
|
+
if checked:
|
|
220
|
+
classes.extend(_split_classes(input_checked_class_name))
|
|
221
|
+
return classes
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def choice_label_classes(
|
|
225
|
+
*,
|
|
226
|
+
label_class_name: str | None = None,
|
|
227
|
+
label_checked_class_name: str | None = None,
|
|
228
|
+
checked: bool = False,
|
|
229
|
+
) -> list[str]:
|
|
230
|
+
"""Return classes for a ``form-check-label`` element."""
|
|
231
|
+
classes = ["form-check-label"]
|
|
232
|
+
classes.extend(_split_classes(label_class_name))
|
|
233
|
+
if checked:
|
|
234
|
+
classes.extend(_split_classes(label_checked_class_name))
|
|
235
|
+
return classes
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _option_is_selected(option_value: Any, current: Any, *, multi: bool) -> bool:
|
|
239
|
+
if multi:
|
|
240
|
+
if current is None:
|
|
241
|
+
return False
|
|
242
|
+
for item in current:
|
|
243
|
+
if item == option_value or _html_value(item) == _html_value(option_value):
|
|
244
|
+
return True
|
|
245
|
+
return False
|
|
246
|
+
if current is None:
|
|
247
|
+
return False
|
|
248
|
+
return current == option_value or _html_value(current) == _html_value(option_value)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _match_option_value(value: Any, normalized: list[dict[str, Any]]) -> Any | None:
|
|
252
|
+
for opt in normalized:
|
|
253
|
+
if opt["value"] == value or _html_value(opt["value"]) == _html_value(value):
|
|
254
|
+
return opt["value"]
|
|
255
|
+
return None
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _filter_selected(values: list[Any], normalized: list[dict[str, Any]]) -> list[Any]:
|
|
259
|
+
filtered: list[Any] = []
|
|
260
|
+
seen: set[int] = set()
|
|
261
|
+
for item in values:
|
|
262
|
+
for index, opt in enumerate(normalized):
|
|
263
|
+
if opt["value"] == item or _html_value(opt["value"]) == _html_value(item):
|
|
264
|
+
if index not in seen:
|
|
265
|
+
filtered.append(opt["value"])
|
|
266
|
+
seen.add(index)
|
|
267
|
+
break
|
|
268
|
+
return filtered
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _make_option_element(
|
|
272
|
+
*,
|
|
273
|
+
value: str,
|
|
274
|
+
label: str,
|
|
275
|
+
disabled: bool = False,
|
|
276
|
+
title: str | None = None,
|
|
277
|
+
selected: bool = False,
|
|
278
|
+
hidden: bool = False,
|
|
279
|
+
) -> Element:
|
|
280
|
+
element = Element("option")
|
|
281
|
+
element._props["value"] = value
|
|
282
|
+
set_element_text(element, label)
|
|
283
|
+
if disabled:
|
|
284
|
+
element._props["disabled"] = True
|
|
285
|
+
if selected:
|
|
286
|
+
element._props["selected"] = True
|
|
287
|
+
if hidden:
|
|
288
|
+
element._props["hidden"] = True
|
|
289
|
+
if title:
|
|
290
|
+
element._props["title"] = title
|
|
291
|
+
return element
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _mount_form_check(
|
|
295
|
+
parent: BootstrapValueElement,
|
|
296
|
+
*,
|
|
297
|
+
input_type: str,
|
|
298
|
+
checked: bool,
|
|
299
|
+
disabled: bool,
|
|
300
|
+
label: str | None,
|
|
301
|
+
name: str | None,
|
|
302
|
+
input_id: str | None,
|
|
303
|
+
label_id: str | None,
|
|
304
|
+
input_class_name: str | None,
|
|
305
|
+
input_style: dict[str, Any] | str | None,
|
|
306
|
+
label_class_name: str | None,
|
|
307
|
+
label_style: dict[str, Any] | str | None,
|
|
308
|
+
) -> tuple[Element, Element]:
|
|
309
|
+
with parent:
|
|
310
|
+
input_el = Element("input")
|
|
311
|
+
input_el._classes.extend(
|
|
312
|
+
choice_input_classes(input_class_name=input_class_name, checked=checked)
|
|
313
|
+
)
|
|
314
|
+
input_el._props["type"] = input_type
|
|
315
|
+
if checked:
|
|
316
|
+
input_el._props["checked"] = True
|
|
317
|
+
if disabled:
|
|
318
|
+
input_el._props["disabled"] = True
|
|
319
|
+
if name is not None:
|
|
320
|
+
input_el._props["name"] = name
|
|
321
|
+
if input_id is not None:
|
|
322
|
+
input_el._props["id"] = input_id
|
|
323
|
+
_apply_style_dict(input_el, input_style)
|
|
324
|
+
label_el = Element("label")
|
|
325
|
+
label_el._classes.extend(
|
|
326
|
+
choice_label_classes(label_class_name=label_class_name, checked=checked)
|
|
327
|
+
)
|
|
328
|
+
set_element_text(label_el, "" if label is None else label)
|
|
329
|
+
if label_id is not None:
|
|
330
|
+
label_el._props["id"] = label_id
|
|
331
|
+
if input_id is not None:
|
|
332
|
+
label_el._props["for"] = input_id
|
|
333
|
+
_apply_style_dict(label_el, label_style)
|
|
334
|
+
return input_el, label_el
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _merged_style(
|
|
338
|
+
base: dict[str, Any] | str | None,
|
|
339
|
+
extra: dict[str, Any] | str | None,
|
|
340
|
+
*,
|
|
341
|
+
use_extra: bool,
|
|
342
|
+
) -> dict[str, Any] | str | None:
|
|
343
|
+
if not use_extra or extra is None:
|
|
344
|
+
return base
|
|
345
|
+
if base is None:
|
|
346
|
+
return extra
|
|
347
|
+
if isinstance(base, dict) and isinstance(extra, dict):
|
|
348
|
+
merged = dict(base)
|
|
349
|
+
merged.update(extra)
|
|
350
|
+
return merged
|
|
351
|
+
return extra
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _mount_choice_item(
|
|
355
|
+
parent: BootstrapValueElement,
|
|
356
|
+
*,
|
|
357
|
+
input_type: str,
|
|
358
|
+
option: dict[str, Any],
|
|
359
|
+
checked: bool,
|
|
360
|
+
inline: bool,
|
|
361
|
+
switch: bool,
|
|
362
|
+
name: str | None,
|
|
363
|
+
input_class_name: str | None,
|
|
364
|
+
input_checked_class_name: str | None,
|
|
365
|
+
input_style: dict[str, Any] | str | None,
|
|
366
|
+
input_checked_style: dict[str, Any] | str | None,
|
|
367
|
+
label_class_name: str | None,
|
|
368
|
+
label_checked_class_name: str | None,
|
|
369
|
+
label_style: dict[str, Any] | str | None,
|
|
370
|
+
label_checked_style: dict[str, Any] | str | None,
|
|
371
|
+
input_id: str | None,
|
|
372
|
+
) -> tuple[Element, Element, Element]:
|
|
373
|
+
with parent:
|
|
374
|
+
wrapper = Element("div")
|
|
375
|
+
wrapper._classes.extend(form_check_item_classes(inline=inline, switch=switch))
|
|
376
|
+
with wrapper:
|
|
377
|
+
input_el = Element("input")
|
|
378
|
+
input_el._classes.extend(
|
|
379
|
+
choice_input_classes(
|
|
380
|
+
input_class_name=input_class_name,
|
|
381
|
+
input_checked_class_name=input_checked_class_name,
|
|
382
|
+
checked=checked,
|
|
383
|
+
)
|
|
384
|
+
)
|
|
385
|
+
input_el._props["type"] = input_type
|
|
386
|
+
input_el._props["value"] = _html_value(option["value"])
|
|
387
|
+
if checked:
|
|
388
|
+
input_el._props["checked"] = True
|
|
389
|
+
if option["disabled"]:
|
|
390
|
+
input_el._props["disabled"] = True
|
|
391
|
+
if name is not None:
|
|
392
|
+
input_el._props["name"] = name
|
|
393
|
+
if input_id is not None:
|
|
394
|
+
input_el._props["id"] = input_id
|
|
395
|
+
_apply_style_dict(
|
|
396
|
+
input_el,
|
|
397
|
+
_merged_style(input_style, input_checked_style, use_extra=checked),
|
|
398
|
+
)
|
|
399
|
+
label_el = Element("label")
|
|
400
|
+
label_el._classes.extend(
|
|
401
|
+
choice_label_classes(
|
|
402
|
+
label_class_name=label_class_name,
|
|
403
|
+
label_checked_class_name=label_checked_class_name,
|
|
404
|
+
checked=checked,
|
|
405
|
+
)
|
|
406
|
+
)
|
|
407
|
+
set_element_text(label_el, option["label"])
|
|
408
|
+
label_id = option.get("label_id")
|
|
409
|
+
if label_id is not None:
|
|
410
|
+
label_el._props["id"] = label_id
|
|
411
|
+
if input_id is not None:
|
|
412
|
+
label_el._props["for"] = input_id
|
|
413
|
+
_apply_style_dict(
|
|
414
|
+
label_el,
|
|
415
|
+
_merged_style(label_style, label_checked_style, use_extra=checked),
|
|
416
|
+
)
|
|
417
|
+
return wrapper, input_el, label_el
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
class _SelectImpl(BootstrapValueElement):
|
|
421
|
+
"""Private Select implementation."""
|
|
422
|
+
|
|
423
|
+
component_name = "Select"
|
|
424
|
+
children_kind = "grouping"
|
|
425
|
+
reserved_classes = ("form-select",)
|
|
426
|
+
|
|
427
|
+
def __init__(
|
|
428
|
+
self,
|
|
429
|
+
children: object = None,
|
|
430
|
+
*,
|
|
431
|
+
_surface: str,
|
|
432
|
+
value: Any = None,
|
|
433
|
+
options: list[Any] | dict[Any, Any] | tuple[Any, ...] | None = None,
|
|
434
|
+
placeholder: str | None = None,
|
|
435
|
+
size: str | None = None,
|
|
436
|
+
html_size: int | None = None,
|
|
437
|
+
disabled: bool = False,
|
|
438
|
+
required: bool = False,
|
|
439
|
+
valid: bool | None = None,
|
|
440
|
+
invalid: bool | None = None,
|
|
441
|
+
name: str | None = None,
|
|
442
|
+
on_change: Callable[..., Any] | None = None,
|
|
443
|
+
**kwargs: Any,
|
|
444
|
+
) -> None:
|
|
445
|
+
self._surface = _surface
|
|
446
|
+
self._normalized_options = normalize_choice_options(options)
|
|
447
|
+
self._structural_classes = tuple(
|
|
448
|
+
select_structural_classes(size=size, valid=valid, invalid=invalid)
|
|
449
|
+
)
|
|
450
|
+
use_options = options is not None
|
|
451
|
+
super().__init__(
|
|
452
|
+
None if use_options else children,
|
|
453
|
+
value=value,
|
|
454
|
+
on_change=on_change,
|
|
455
|
+
tag="select",
|
|
456
|
+
**kwargs,
|
|
457
|
+
)
|
|
458
|
+
if html_size is not None:
|
|
459
|
+
self._props["size"] = int(html_size)
|
|
460
|
+
_set_flag(self, "disabled", disabled)
|
|
461
|
+
_set_flag(self, "required", required)
|
|
462
|
+
if name is not None:
|
|
463
|
+
self._props["name"] = name
|
|
464
|
+
if use_options:
|
|
465
|
+
current = self.value
|
|
466
|
+
current_html = None if current is None or current == "" else _html_value(current)
|
|
467
|
+
matched = False
|
|
468
|
+
if current_html is not None:
|
|
469
|
+
matched = any(
|
|
470
|
+
_html_value(opt["value"]) == current_html for opt in self._normalized_options
|
|
471
|
+
)
|
|
472
|
+
with self:
|
|
473
|
+
if placeholder is not None:
|
|
474
|
+
_make_option_element(
|
|
475
|
+
value="",
|
|
476
|
+
label=placeholder,
|
|
477
|
+
disabled=True,
|
|
478
|
+
hidden=True,
|
|
479
|
+
selected=not matched,
|
|
480
|
+
)
|
|
481
|
+
for opt in self._normalized_options:
|
|
482
|
+
selected = (
|
|
483
|
+
current_html is not None and _html_value(opt["value"]) == current_html
|
|
484
|
+
)
|
|
485
|
+
_make_option_element(
|
|
486
|
+
value=_html_value(opt["value"]),
|
|
487
|
+
label=opt["label"],
|
|
488
|
+
disabled=bool(opt["disabled"]),
|
|
489
|
+
title=opt["title"],
|
|
490
|
+
selected=selected,
|
|
491
|
+
)
|
|
492
|
+
self.on("change", self._on_dom_change, js_handler=_JS_SELECT_CHANGE)
|
|
493
|
+
|
|
494
|
+
def _on_dom_change(self, e: Any) -> None:
|
|
495
|
+
payload = _payload_from_event(e)
|
|
496
|
+
if isinstance(payload, list):
|
|
497
|
+
payload = payload[0] if payload else ""
|
|
498
|
+
if payload is None or payload == "":
|
|
499
|
+
self.value = None
|
|
500
|
+
return
|
|
501
|
+
self.value = str(payload)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
Select, DbcSelect = make_surface_classes("Select", globals())
|
|
505
|
+
select = Select
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
class _FormCheckControlImpl(BootstrapValueElement):
|
|
509
|
+
"""Shared boolean form-check control (checkbox, switch, radio)."""
|
|
510
|
+
|
|
511
|
+
component_name = "Checkbox"
|
|
512
|
+
children_kind = "grouping"
|
|
513
|
+
reserved_classes: tuple[str, ...] = ("form-check",)
|
|
514
|
+
_input_type: str = "checkbox"
|
|
515
|
+
_switch: bool = False
|
|
516
|
+
|
|
517
|
+
def __init__(
|
|
518
|
+
self,
|
|
519
|
+
children: object = None,
|
|
520
|
+
*,
|
|
521
|
+
_surface: str,
|
|
522
|
+
value: bool = False,
|
|
523
|
+
disabled: bool = False,
|
|
524
|
+
label: str | None = None,
|
|
525
|
+
name: str | None = None,
|
|
526
|
+
input_id: str | None = None,
|
|
527
|
+
label_id: str | None = None,
|
|
528
|
+
input_class_name: str | None = None,
|
|
529
|
+
inputClassName: str | None = None,
|
|
530
|
+
input_style: dict[str, Any] | str | None = None,
|
|
531
|
+
label_class_name: str | None = None,
|
|
532
|
+
labelClassName: str | None = None,
|
|
533
|
+
label_style: dict[str, Any] | str | None = None,
|
|
534
|
+
on_change: Callable[..., Any] | None = None,
|
|
535
|
+
**kwargs: Any,
|
|
536
|
+
) -> None:
|
|
537
|
+
self._surface = _surface
|
|
538
|
+
if _has_children(children):
|
|
539
|
+
raise ChildrenError(
|
|
540
|
+
f"{self.component_name} does not accept children; use the label property instead"
|
|
541
|
+
)
|
|
542
|
+
self._structural_classes = tuple(form_check_item_classes(switch=self._switch))
|
|
543
|
+
super().__init__(
|
|
544
|
+
None,
|
|
545
|
+
value=bool(value),
|
|
546
|
+
on_change=on_change,
|
|
547
|
+
tag="div",
|
|
548
|
+
**kwargs,
|
|
549
|
+
)
|
|
550
|
+
resolved_input_id = input_id or f"nbc-{self.component_name.lower()}-{self.id}"
|
|
551
|
+
self._input, self._label = _mount_form_check(
|
|
552
|
+
self,
|
|
553
|
+
input_type=self._input_type,
|
|
554
|
+
checked=bool(self.value),
|
|
555
|
+
disabled=disabled,
|
|
556
|
+
label=label,
|
|
557
|
+
name=name,
|
|
558
|
+
input_id=resolved_input_id,
|
|
559
|
+
label_id=label_id,
|
|
560
|
+
input_class_name=_resolve_alias(input_class_name, inputClassName),
|
|
561
|
+
input_style=input_style,
|
|
562
|
+
label_class_name=_resolve_alias(label_class_name, labelClassName),
|
|
563
|
+
label_style=label_style,
|
|
564
|
+
)
|
|
565
|
+
self.on("change", self._on_dom_change, js_handler=_JS_CHECK_CHANGE)
|
|
566
|
+
|
|
567
|
+
def _handle_value_change(self, *args: Any, **kwargs: Any) -> None:
|
|
568
|
+
inherited = getattr(super(), "_handle_value_change", None)
|
|
569
|
+
if callable(inherited):
|
|
570
|
+
inherited(*args, **kwargs)
|
|
571
|
+
self._sync_to_dom()
|
|
572
|
+
|
|
573
|
+
def _sync_to_dom(self) -> None:
|
|
574
|
+
input_el = getattr(self, "_input", None)
|
|
575
|
+
if input_el is None:
|
|
576
|
+
return
|
|
577
|
+
_set_flag(input_el, "checked", bool(self.value))
|
|
578
|
+
updater = getattr(input_el, "update", None)
|
|
579
|
+
if callable(updater):
|
|
580
|
+
updater()
|
|
581
|
+
|
|
582
|
+
def _on_dom_change(self, e: Any) -> None:
|
|
583
|
+
payload = _payload_from_event(e)
|
|
584
|
+
if isinstance(payload, list):
|
|
585
|
+
payload = payload[0] if payload else False
|
|
586
|
+
self.value = bool(payload)
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
class _CheckboxImpl(_FormCheckControlImpl):
|
|
590
|
+
"""Private Checkbox implementation."""
|
|
591
|
+
|
|
592
|
+
component_name = "Checkbox"
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
class _SwitchImpl(_FormCheckControlImpl):
|
|
596
|
+
"""Private Switch implementation."""
|
|
597
|
+
|
|
598
|
+
component_name = "Switch"
|
|
599
|
+
reserved_classes = ("form-check", "form-switch")
|
|
600
|
+
_switch = True
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
class _RadioButtonImpl(_FormCheckControlImpl):
|
|
604
|
+
"""Private RadioButton implementation."""
|
|
605
|
+
|
|
606
|
+
component_name = "RadioButton"
|
|
607
|
+
_input_type = "radio"
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
Checkbox, DbcCheckbox = make_surface_classes("Checkbox", globals())
|
|
611
|
+
checkbox = Checkbox
|
|
612
|
+
|
|
613
|
+
Switch, DbcSwitch = make_surface_classes("Switch", globals())
|
|
614
|
+
switch = Switch
|
|
615
|
+
|
|
616
|
+
RadioButton, DbcRadioButton = make_surface_classes("RadioButton", globals())
|
|
617
|
+
radio_button = RadioButton
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
class _ChoiceGroupImpl(BootstrapValueElement):
|
|
621
|
+
"""Shared options group for Checklist and RadioItems."""
|
|
622
|
+
|
|
623
|
+
component_name = "Checklist"
|
|
624
|
+
children_kind = "grouping"
|
|
625
|
+
reserved_classes: tuple[str, ...] = ()
|
|
626
|
+
_input_type: str = "checkbox"
|
|
627
|
+
_multi: bool = True
|
|
628
|
+
|
|
629
|
+
def __init__(
|
|
630
|
+
self,
|
|
631
|
+
children: object = None,
|
|
632
|
+
*,
|
|
633
|
+
_surface: str,
|
|
634
|
+
value: Any = None,
|
|
635
|
+
options: list[Any] | dict[Any, Any] | tuple[Any, ...] | None = None,
|
|
636
|
+
inline: bool = False,
|
|
637
|
+
switch: bool = False,
|
|
638
|
+
disabled: bool = False,
|
|
639
|
+
name: str | None = None,
|
|
640
|
+
input_class_name: str | None = None,
|
|
641
|
+
inputClassName: str | None = None,
|
|
642
|
+
input_style: dict[str, Any] | str | None = None,
|
|
643
|
+
input_checked_class_name: str | None = None,
|
|
644
|
+
inputCheckedClassName: str | None = None,
|
|
645
|
+
input_checked_style: dict[str, Any] | str | None = None,
|
|
646
|
+
inputCheckedStyle: dict[str, Any] | str | None = None,
|
|
647
|
+
label_class_name: str | None = None,
|
|
648
|
+
labelClassName: str | None = None,
|
|
649
|
+
label_style: dict[str, Any] | str | None = None,
|
|
650
|
+
label_checked_class_name: str | None = None,
|
|
651
|
+
labelCheckedClassName: str | None = None,
|
|
652
|
+
label_checked_style: dict[str, Any] | str | None = None,
|
|
653
|
+
labelCheckedStyle: dict[str, Any] | str | None = None,
|
|
654
|
+
on_change: Callable[..., Any] | None = None,
|
|
655
|
+
**kwargs: Any,
|
|
656
|
+
) -> None:
|
|
657
|
+
self._surface = _surface
|
|
658
|
+
if _has_children(children):
|
|
659
|
+
raise ChildrenError(
|
|
660
|
+
f"{self.component_name} does not accept children; use the options property instead"
|
|
661
|
+
)
|
|
662
|
+
self._normalized_options = normalize_choice_options(options)
|
|
663
|
+
if self._multi:
|
|
664
|
+
incoming = list(value) if value is not None else []
|
|
665
|
+
value = _filter_selected(incoming, self._normalized_options)
|
|
666
|
+
elif value is not None:
|
|
667
|
+
value = _match_option_value(value, self._normalized_options)
|
|
668
|
+
self._structural_classes = ()
|
|
669
|
+
self._inline = inline
|
|
670
|
+
self._switch = switch
|
|
671
|
+
self._name = name
|
|
672
|
+
self._input_class_name = _resolve_alias(input_class_name, inputClassName)
|
|
673
|
+
self._input_style = input_style
|
|
674
|
+
self._input_checked_class_name = _resolve_alias(
|
|
675
|
+
input_checked_class_name, inputCheckedClassName
|
|
676
|
+
)
|
|
677
|
+
self._input_checked_style = _resolve_alias(input_checked_style, inputCheckedStyle)
|
|
678
|
+
self._label_class_name = _resolve_alias(label_class_name, labelClassName)
|
|
679
|
+
self._label_style = label_style
|
|
680
|
+
self._label_checked_class_name = _resolve_alias(
|
|
681
|
+
label_checked_class_name, labelCheckedClassName
|
|
682
|
+
)
|
|
683
|
+
self._label_checked_style = _resolve_alias(label_checked_style, labelCheckedStyle)
|
|
684
|
+
self._html_to_value = {
|
|
685
|
+
_html_value(opt["value"]): opt["value"] for opt in self._normalized_options
|
|
686
|
+
}
|
|
687
|
+
super().__init__(None, value=value, on_change=on_change, tag="div", **kwargs)
|
|
688
|
+
group_name = self._name
|
|
689
|
+
if group_name is None and not self._multi:
|
|
690
|
+
group_name = f"nbc-radioitems-{self.id}"
|
|
691
|
+
self._item_refs: list[tuple[Element, Element, dict[str, Any]]] = []
|
|
692
|
+
for index, opt in enumerate(self._normalized_options):
|
|
693
|
+
checked = _option_is_selected(opt["value"], self.value, multi=self._multi)
|
|
694
|
+
option_disabled = bool(opt["disabled"]) or disabled
|
|
695
|
+
option = dict(opt)
|
|
696
|
+
option["disabled"] = option_disabled
|
|
697
|
+
input_id = opt["input_id"] or f"nbc-{self.component_name.lower()}-{self.id}-{index}"
|
|
698
|
+
_wrapper, input_el, label_el = _mount_choice_item(
|
|
699
|
+
self,
|
|
700
|
+
input_type=self._input_type,
|
|
701
|
+
option=option,
|
|
702
|
+
checked=checked,
|
|
703
|
+
inline=self._inline,
|
|
704
|
+
switch=self._switch,
|
|
705
|
+
name=group_name,
|
|
706
|
+
input_class_name=self._input_class_name,
|
|
707
|
+
input_checked_class_name=self._input_checked_class_name,
|
|
708
|
+
input_style=self._input_style,
|
|
709
|
+
input_checked_style=self._input_checked_style,
|
|
710
|
+
label_class_name=self._label_class_name,
|
|
711
|
+
label_checked_class_name=self._label_checked_class_name,
|
|
712
|
+
label_style=self._label_style,
|
|
713
|
+
label_checked_style=self._label_checked_style,
|
|
714
|
+
input_id=input_id,
|
|
715
|
+
)
|
|
716
|
+
self._item_refs.append((input_el, label_el, opt))
|
|
717
|
+
js_handler = _JS_CHECKLIST_CHANGE if self._multi else _JS_RADIO_ITEMS_CHANGE
|
|
718
|
+
self.on("change", self._on_dom_change, js_handler=js_handler)
|
|
719
|
+
|
|
720
|
+
def _restore_value(self, raw: Any) -> Any:
|
|
721
|
+
text = _html_value(raw)
|
|
722
|
+
if text in self._html_to_value:
|
|
723
|
+
return self._html_to_value[text]
|
|
724
|
+
return raw
|
|
725
|
+
|
|
726
|
+
def _handle_value_change(self, *args: Any, **kwargs: Any) -> None:
|
|
727
|
+
inherited = getattr(super(), "_handle_value_change", None)
|
|
728
|
+
if callable(inherited):
|
|
729
|
+
inherited(*args, **kwargs)
|
|
730
|
+
self._sync_to_dom()
|
|
731
|
+
|
|
732
|
+
def _sync_to_dom(self) -> None:
|
|
733
|
+
items = getattr(self, "_item_refs", None)
|
|
734
|
+
if not items:
|
|
735
|
+
return
|
|
736
|
+
current = self.value
|
|
737
|
+
for input_el, _label_el, opt in items:
|
|
738
|
+
checked = _option_is_selected(opt["value"], current, multi=self._multi)
|
|
739
|
+
_set_flag(input_el, "checked", checked)
|
|
740
|
+
updater = getattr(input_el, "update", None)
|
|
741
|
+
if callable(updater):
|
|
742
|
+
updater()
|
|
743
|
+
|
|
744
|
+
def _on_dom_change(self, e: Any) -> None:
|
|
745
|
+
payload = _payload_from_event(e)
|
|
746
|
+
if self._multi:
|
|
747
|
+
if payload is None:
|
|
748
|
+
payload = []
|
|
749
|
+
if not isinstance(payload, list):
|
|
750
|
+
payload = [payload]
|
|
751
|
+
self.value = [self._restore_value(item) for item in payload]
|
|
752
|
+
return
|
|
753
|
+
if isinstance(payload, list):
|
|
754
|
+
payload = payload[0] if payload else None
|
|
755
|
+
if payload is None or payload == "":
|
|
756
|
+
self.value = None
|
|
757
|
+
return
|
|
758
|
+
self.value = self._restore_value(payload)
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
class _ChecklistImpl(_ChoiceGroupImpl):
|
|
762
|
+
"""Private Checklist implementation."""
|
|
763
|
+
|
|
764
|
+
component_name = "Checklist"
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
class _RadioItemsImpl(_ChoiceGroupImpl):
|
|
768
|
+
"""Private RadioItems implementation."""
|
|
769
|
+
|
|
770
|
+
component_name = "RadioItems"
|
|
771
|
+
_input_type = "radio"
|
|
772
|
+
_multi = False
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
Checklist, DbcChecklist = make_surface_classes("Checklist", globals())
|
|
776
|
+
checklist = Checklist
|
|
777
|
+
|
|
778
|
+
RadioItems, DbcRadioItems = make_surface_classes("RadioItems", globals())
|
|
779
|
+
radio_items = RadioItems
|