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,650 @@
|
|
|
1
|
+
"""Navigation family: Nav, NavItem, NavLink, Breadcrumb, Pagination."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable, Mapping
|
|
6
|
+
from contextlib import suppress
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from nicegui import ui
|
|
10
|
+
|
|
11
|
+
from .._base import (
|
|
12
|
+
BootstrapElement,
|
|
13
|
+
ChildrenError,
|
|
14
|
+
make_surface_classes,
|
|
15
|
+
normalize_style,
|
|
16
|
+
style_to_css,
|
|
17
|
+
)
|
|
18
|
+
from .._host import set_element_text
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"Breadcrumb",
|
|
22
|
+
"DbcBreadcrumb",
|
|
23
|
+
"DbcNav",
|
|
24
|
+
"DbcNavItem",
|
|
25
|
+
"DbcNavLink",
|
|
26
|
+
"DbcPagination",
|
|
27
|
+
"Nav",
|
|
28
|
+
"NavItem",
|
|
29
|
+
"NavLink",
|
|
30
|
+
"Pagination",
|
|
31
|
+
"breadcrumb",
|
|
32
|
+
"breadcrumb_item_classes",
|
|
33
|
+
"clamp_page",
|
|
34
|
+
"nav",
|
|
35
|
+
"nav_classes",
|
|
36
|
+
"nav_item",
|
|
37
|
+
"nav_link",
|
|
38
|
+
"nav_link_classes",
|
|
39
|
+
"pagination",
|
|
40
|
+
"pagination_tokens",
|
|
41
|
+
"validate_breadcrumb_items",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
_VERTICAL_BPS = frozenset({"xs", "sm", "md", "lg", "xl"})
|
|
45
|
+
_HORIZONTAL = frozenset({"start", "center", "end", "between", "around"})
|
|
46
|
+
_PAGINATION_SIZES = frozenset({"sm", "lg"})
|
|
47
|
+
_PAGINATION_GLYPHS = {
|
|
48
|
+
"first": "«",
|
|
49
|
+
"prev": "‹",
|
|
50
|
+
"next": "›",
|
|
51
|
+
"last": "»",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def nav_classes(
|
|
56
|
+
*,
|
|
57
|
+
pills: bool = False,
|
|
58
|
+
vertical: bool | str | None = False,
|
|
59
|
+
horizontal: str | None = None,
|
|
60
|
+
fill: bool = False,
|
|
61
|
+
justified: bool = False,
|
|
62
|
+
card: bool = False,
|
|
63
|
+
navbar: bool = False,
|
|
64
|
+
navbar_scroll: bool = False,
|
|
65
|
+
) -> list[str]:
|
|
66
|
+
"""Return Bootstrap ``nav`` classes."""
|
|
67
|
+
classes = ["navbar-nav" if navbar else "nav"]
|
|
68
|
+
if pills:
|
|
69
|
+
classes.append("nav-pills")
|
|
70
|
+
if navbar and navbar_scroll:
|
|
71
|
+
classes.append("navbar-nav-scroll")
|
|
72
|
+
if fill:
|
|
73
|
+
classes.append("nav-fill")
|
|
74
|
+
if justified:
|
|
75
|
+
classes.append("nav-justified")
|
|
76
|
+
if card and pills:
|
|
77
|
+
classes.append("card-header-pills")
|
|
78
|
+
if vertical is True:
|
|
79
|
+
classes.append("flex-column")
|
|
80
|
+
elif isinstance(vertical, str):
|
|
81
|
+
if vertical not in _VERTICAL_BPS:
|
|
82
|
+
raise ValueError(f"Invalid nav vertical breakpoint: {vertical!r}")
|
|
83
|
+
classes.append(f"flex-{vertical}-column")
|
|
84
|
+
elif vertical not in (False, None):
|
|
85
|
+
raise ValueError(f"Invalid nav vertical: {vertical!r}")
|
|
86
|
+
if horizontal is not None:
|
|
87
|
+
if horizontal not in _HORIZONTAL:
|
|
88
|
+
raise ValueError(f"Invalid nav horizontal: {horizontal!r}")
|
|
89
|
+
classes.append(f"justify-content-{horizontal}")
|
|
90
|
+
return classes
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def nav_link_classes(*, active: bool | str = False, disabled: bool = False) -> list[str]:
|
|
94
|
+
"""Return Bootstrap ``nav-link`` classes."""
|
|
95
|
+
classes = ["nav-link"]
|
|
96
|
+
if active:
|
|
97
|
+
classes.append("active")
|
|
98
|
+
if disabled:
|
|
99
|
+
classes.append("disabled")
|
|
100
|
+
return classes
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def breadcrumb_item_classes(active: bool = False) -> list[str]:
|
|
104
|
+
"""Return Bootstrap ``breadcrumb-item`` classes."""
|
|
105
|
+
classes = ["breadcrumb-item"]
|
|
106
|
+
if active:
|
|
107
|
+
classes.append("active")
|
|
108
|
+
return classes
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def validate_breadcrumb_items(items: list[Any] | None) -> list[dict[str, Any]]:
|
|
112
|
+
"""Copy and validate breadcrumb item dicts."""
|
|
113
|
+
if items is None:
|
|
114
|
+
return []
|
|
115
|
+
validated: list[dict[str, Any]] = []
|
|
116
|
+
for entry in items:
|
|
117
|
+
if not isinstance(entry, Mapping):
|
|
118
|
+
raise TypeError(f"Breadcrumb items must be mappings, got {type(entry)!r}")
|
|
119
|
+
if "label" not in entry:
|
|
120
|
+
raise ValueError("Breadcrumb item missing label")
|
|
121
|
+
label = entry["label"]
|
|
122
|
+
if label is None or label == "":
|
|
123
|
+
raise ValueError("Breadcrumb item missing label")
|
|
124
|
+
validated.append(dict(entry))
|
|
125
|
+
return validated
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _rounded_max_value(min_value: int, max_value: int, step: int) -> int:
|
|
129
|
+
remainder = (max_value - min_value) % step
|
|
130
|
+
if remainder != 0:
|
|
131
|
+
return max_value + step - remainder
|
|
132
|
+
return max_value
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def pagination_tokens(
|
|
136
|
+
*,
|
|
137
|
+
min_value: int,
|
|
138
|
+
max_value: int,
|
|
139
|
+
step: int,
|
|
140
|
+
active_page: int,
|
|
141
|
+
fully_expanded: bool = True,
|
|
142
|
+
previous_next: bool = False,
|
|
143
|
+
first_last: bool = False,
|
|
144
|
+
) -> list[str | int]:
|
|
145
|
+
"""Return ordered pagination tokens (page numbers and sentinels)."""
|
|
146
|
+
max_value = _rounded_max_value(min_value, max_value, step)
|
|
147
|
+
tokens: list[str | int] = []
|
|
148
|
+
if first_last:
|
|
149
|
+
tokens.append("first")
|
|
150
|
+
if previous_next:
|
|
151
|
+
tokens.append("prev")
|
|
152
|
+
page_count = (max_value - min_value) // step + 1
|
|
153
|
+
if fully_expanded or page_count <= 7:
|
|
154
|
+
page = min_value
|
|
155
|
+
while page <= max_value:
|
|
156
|
+
tokens.append(page)
|
|
157
|
+
page += step
|
|
158
|
+
else:
|
|
159
|
+
tokens.append(min_value)
|
|
160
|
+
if active_page <= min_value + 3 * step:
|
|
161
|
+
tokens.extend(
|
|
162
|
+
[
|
|
163
|
+
min_value + step,
|
|
164
|
+
min_value + 2 * step,
|
|
165
|
+
min_value + 3 * step,
|
|
166
|
+
min_value + 4 * step,
|
|
167
|
+
]
|
|
168
|
+
)
|
|
169
|
+
tokens.append("ellipsis")
|
|
170
|
+
elif active_page >= max_value - 3 * step:
|
|
171
|
+
tokens.append("ellipsis")
|
|
172
|
+
tokens.extend(
|
|
173
|
+
[
|
|
174
|
+
max_value - 4 * step,
|
|
175
|
+
max_value - 3 * step,
|
|
176
|
+
max_value - 2 * step,
|
|
177
|
+
max_value - step,
|
|
178
|
+
]
|
|
179
|
+
)
|
|
180
|
+
else:
|
|
181
|
+
tokens.extend(
|
|
182
|
+
[
|
|
183
|
+
"ellipsis",
|
|
184
|
+
active_page - step,
|
|
185
|
+
active_page,
|
|
186
|
+
active_page + step,
|
|
187
|
+
"ellipsis",
|
|
188
|
+
]
|
|
189
|
+
)
|
|
190
|
+
tokens.append(max_value)
|
|
191
|
+
if previous_next:
|
|
192
|
+
tokens.append("next")
|
|
193
|
+
if first_last:
|
|
194
|
+
tokens.append("last")
|
|
195
|
+
return tokens
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def clamp_page(value: int, *, min_value: int, max_value: int) -> int:
|
|
199
|
+
"""Clamp ``value`` into ``[min_value, max_value]``."""
|
|
200
|
+
if value < min_value:
|
|
201
|
+
return min_value
|
|
202
|
+
if value > max_value:
|
|
203
|
+
return max_value
|
|
204
|
+
return int(value)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _apply_dom_attrs(element: Any, attrs: dict[str, Any]) -> None:
|
|
208
|
+
parts: list[str] = []
|
|
209
|
+
for key, val in attrs.items():
|
|
210
|
+
if val is True:
|
|
211
|
+
parts.append(str(key))
|
|
212
|
+
elif val is False or val is None or val == "":
|
|
213
|
+
continue
|
|
214
|
+
else:
|
|
215
|
+
parts.append(f"{key}={val}")
|
|
216
|
+
if parts:
|
|
217
|
+
element.props(" ".join(parts))
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _set_text(element: Any, text: str) -> None:
|
|
221
|
+
setter = getattr(element, "set_text", None)
|
|
222
|
+
if callable(setter):
|
|
223
|
+
setter(text)
|
|
224
|
+
return
|
|
225
|
+
set_element_text(element, text)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _flatten_children(children: object) -> list[object]:
|
|
229
|
+
if children is None:
|
|
230
|
+
return []
|
|
231
|
+
if isinstance(children, str):
|
|
232
|
+
return [children]
|
|
233
|
+
if isinstance(children, (list, tuple)):
|
|
234
|
+
flat: list[object] = []
|
|
235
|
+
for child in children:
|
|
236
|
+
flat.extend(_flatten_children(child))
|
|
237
|
+
return flat
|
|
238
|
+
return [children]
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _nav_link_dom_attrs(
|
|
242
|
+
*,
|
|
243
|
+
href: str | None,
|
|
244
|
+
external_link: bool | None,
|
|
245
|
+
disabled: bool,
|
|
246
|
+
target: str | None,
|
|
247
|
+
) -> tuple[str, dict[str, Any]]:
|
|
248
|
+
if href:
|
|
249
|
+
attrs: dict[str, Any] = {"href": href}
|
|
250
|
+
if external_link is True:
|
|
251
|
+
attrs["target"] = "_blank"
|
|
252
|
+
attrs["rel"] = "noopener noreferrer"
|
|
253
|
+
elif target:
|
|
254
|
+
attrs["target"] = target
|
|
255
|
+
if disabled:
|
|
256
|
+
attrs["aria-disabled"] = "true"
|
|
257
|
+
attrs["tabindex"] = "-1"
|
|
258
|
+
return ("a", attrs)
|
|
259
|
+
button_attrs: dict[str, Any] = {"type": "button"}
|
|
260
|
+
if disabled:
|
|
261
|
+
button_attrs["disabled"] = True
|
|
262
|
+
return ("button", button_attrs)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _add_breadcrumb_item(
|
|
266
|
+
entry: dict[str, Any],
|
|
267
|
+
*,
|
|
268
|
+
item_style: dict[str, Any] | None,
|
|
269
|
+
item_class_name: str | None,
|
|
270
|
+
) -> None:
|
|
271
|
+
active = bool(entry.get("active"))
|
|
272
|
+
label = entry.get("label")
|
|
273
|
+
href = entry.get("href")
|
|
274
|
+
li = ui.element("li")
|
|
275
|
+
classes = breadcrumb_item_classes(active=active)
|
|
276
|
+
if item_class_name:
|
|
277
|
+
classes.append(str(item_class_name))
|
|
278
|
+
li.classes(" ".join(classes))
|
|
279
|
+
if item_style:
|
|
280
|
+
css = style_to_css(normalize_style(dict(item_style)))
|
|
281
|
+
if css:
|
|
282
|
+
li.style(css)
|
|
283
|
+
if active:
|
|
284
|
+
_apply_dom_attrs(li, {"aria-current": "page"})
|
|
285
|
+
_set_text(li, str(label))
|
|
286
|
+
return
|
|
287
|
+
if href:
|
|
288
|
+
with li:
|
|
289
|
+
anchor = ui.element("a")
|
|
290
|
+
attrs: dict[str, Any] = {"href": href}
|
|
291
|
+
if entry.get("external_link") is True:
|
|
292
|
+
attrs["target"] = "_blank"
|
|
293
|
+
attrs["rel"] = "noopener noreferrer"
|
|
294
|
+
elif entry.get("target"):
|
|
295
|
+
attrs["target"] = entry.get("target")
|
|
296
|
+
title = entry.get("title")
|
|
297
|
+
if title is not None:
|
|
298
|
+
attrs["title"] = title
|
|
299
|
+
_apply_dom_attrs(anchor, attrs)
|
|
300
|
+
_set_text(anchor, str(label))
|
|
301
|
+
return
|
|
302
|
+
_set_text(li, str(label))
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _adopt_breadcrumb_child(host: Any, child: object) -> None:
|
|
306
|
+
if isinstance(child, (str, int, float)):
|
|
307
|
+
with host:
|
|
308
|
+
li = ui.element("li")
|
|
309
|
+
li.classes("breadcrumb-item")
|
|
310
|
+
_set_text(li, str(child))
|
|
311
|
+
return
|
|
312
|
+
mover = getattr(child, "move", None)
|
|
313
|
+
if callable(mover):
|
|
314
|
+
mover(host)
|
|
315
|
+
return
|
|
316
|
+
raise TypeError(f"Unsupported Breadcrumb child type: {type(child)!r}")
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class _NavImpl(BootstrapElement):
|
|
320
|
+
"""Nav can be used to group together a collection of navigation links."""
|
|
321
|
+
|
|
322
|
+
component_name = "Nav"
|
|
323
|
+
children_kind = "auto"
|
|
324
|
+
|
|
325
|
+
def __init__(
|
|
326
|
+
self,
|
|
327
|
+
children: object = None,
|
|
328
|
+
*,
|
|
329
|
+
_surface: str,
|
|
330
|
+
pills: bool = False,
|
|
331
|
+
vertical: bool | str = False,
|
|
332
|
+
horizontal: str | None = None,
|
|
333
|
+
fill: bool = False,
|
|
334
|
+
justified: bool = False,
|
|
335
|
+
card: bool = False,
|
|
336
|
+
navbar: bool = False,
|
|
337
|
+
navbar_scroll: bool = False,
|
|
338
|
+
**kwargs: Any,
|
|
339
|
+
) -> None:
|
|
340
|
+
self._surface = _surface
|
|
341
|
+
self._structural_classes = tuple(
|
|
342
|
+
nav_classes(
|
|
343
|
+
pills=pills,
|
|
344
|
+
vertical=vertical,
|
|
345
|
+
horizontal=horizontal,
|
|
346
|
+
fill=fill,
|
|
347
|
+
justified=justified,
|
|
348
|
+
card=card,
|
|
349
|
+
navbar=navbar,
|
|
350
|
+
navbar_scroll=navbar_scroll,
|
|
351
|
+
)
|
|
352
|
+
)
|
|
353
|
+
super().__init__(children, tag="ul", **kwargs)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
class _NavItemImpl(BootstrapElement):
|
|
357
|
+
"""A list item for grouping navigation links."""
|
|
358
|
+
|
|
359
|
+
component_name = "NavItem"
|
|
360
|
+
children_kind = "auto"
|
|
361
|
+
reserved_classes = ("nav-item",)
|
|
362
|
+
|
|
363
|
+
def __init__(
|
|
364
|
+
self,
|
|
365
|
+
children: object = None,
|
|
366
|
+
*,
|
|
367
|
+
_surface: str,
|
|
368
|
+
**kwargs: Any,
|
|
369
|
+
) -> None:
|
|
370
|
+
self._surface = _surface
|
|
371
|
+
self._structural_classes = ("nav-item",)
|
|
372
|
+
super().__init__(children, tag="li", **kwargs)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
class _NavLinkImpl(BootstrapElement):
|
|
376
|
+
"""Add a link to a Nav. Can be used as a child of NavItem or of Nav directly."""
|
|
377
|
+
|
|
378
|
+
component_name = "NavLink"
|
|
379
|
+
children_kind = "phrasing"
|
|
380
|
+
reserved_classes = ("nav-link",)
|
|
381
|
+
|
|
382
|
+
def __init__(
|
|
383
|
+
self,
|
|
384
|
+
children: object = None,
|
|
385
|
+
*,
|
|
386
|
+
_surface: str,
|
|
387
|
+
href: str | None = None,
|
|
388
|
+
n_clicks: int = 0,
|
|
389
|
+
active: bool | str = False,
|
|
390
|
+
disabled: bool = False,
|
|
391
|
+
external_link: bool | None = None,
|
|
392
|
+
target: str | None = None,
|
|
393
|
+
on_click: Callable[..., Any] | None = None,
|
|
394
|
+
**kwargs: Any,
|
|
395
|
+
) -> None:
|
|
396
|
+
self._surface = _surface
|
|
397
|
+
tag, attrs = _nav_link_dom_attrs(
|
|
398
|
+
href=href,
|
|
399
|
+
external_link=external_link,
|
|
400
|
+
disabled=disabled,
|
|
401
|
+
target=target,
|
|
402
|
+
)
|
|
403
|
+
self._structural_classes = tuple(nav_link_classes(active=active, disabled=disabled))
|
|
404
|
+
self._n_clicks = int(n_clicks)
|
|
405
|
+
self.disabled = bool(disabled)
|
|
406
|
+
self._on_click = on_click
|
|
407
|
+
super().__init__(children, tag=tag, **kwargs)
|
|
408
|
+
_apply_dom_attrs(self, attrs)
|
|
409
|
+
self.on("click", self._handle_click)
|
|
410
|
+
|
|
411
|
+
@property
|
|
412
|
+
def n_clicks(self) -> int:
|
|
413
|
+
"""Compat click counter (read-only observable)."""
|
|
414
|
+
return self._n_clicks
|
|
415
|
+
|
|
416
|
+
def _handle_click(self, *_args: Any, **_kwargs: Any) -> None:
|
|
417
|
+
if self.disabled:
|
|
418
|
+
return
|
|
419
|
+
self._n_clicks += 1
|
|
420
|
+
if self._on_click is not None:
|
|
421
|
+
self._on_click()
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
class _BreadcrumbImpl(BootstrapElement):
|
|
425
|
+
"""Use breadcrumbs to create a navigation breadcrumb in your app."""
|
|
426
|
+
|
|
427
|
+
component_name = "Breadcrumb"
|
|
428
|
+
children_kind = "auto"
|
|
429
|
+
|
|
430
|
+
def __init__(
|
|
431
|
+
self,
|
|
432
|
+
children: object = None,
|
|
433
|
+
*,
|
|
434
|
+
_surface: str,
|
|
435
|
+
items: list[dict[str, Any]] | None = None,
|
|
436
|
+
item_style: dict[str, Any] | None = None,
|
|
437
|
+
item_class_name: str | None = None,
|
|
438
|
+
tag: str = "nav",
|
|
439
|
+
**kwargs: Any,
|
|
440
|
+
) -> None:
|
|
441
|
+
self._surface = _surface
|
|
442
|
+
alias = kwargs.pop("itemClassName", None)
|
|
443
|
+
if item_class_name is None:
|
|
444
|
+
item_class_name = alias
|
|
445
|
+
validated = validate_breadcrumb_items(items)
|
|
446
|
+
super().__init__(None, tag=tag, **kwargs)
|
|
447
|
+
_apply_dom_attrs(self, {"aria-label": "breadcrumb"})
|
|
448
|
+
with self:
|
|
449
|
+
self._list = ui.element("ol")
|
|
450
|
+
self._list.classes("breadcrumb")
|
|
451
|
+
with self._list:
|
|
452
|
+
for entry in validated:
|
|
453
|
+
_add_breadcrumb_item(
|
|
454
|
+
entry,
|
|
455
|
+
item_style=item_style,
|
|
456
|
+
item_class_name=item_class_name,
|
|
457
|
+
)
|
|
458
|
+
for child in _flatten_children(children):
|
|
459
|
+
_adopt_breadcrumb_child(self._list, child)
|
|
460
|
+
self._slot_host = self._list
|
|
461
|
+
|
|
462
|
+
def __enter__(self) -> Any:
|
|
463
|
+
host = getattr(self, "_slot_host", None)
|
|
464
|
+
if host is not None:
|
|
465
|
+
host.__enter__()
|
|
466
|
+
return self
|
|
467
|
+
return super().__enter__()
|
|
468
|
+
|
|
469
|
+
def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> Any:
|
|
470
|
+
host = getattr(self, "_slot_host", None)
|
|
471
|
+
if host is not None:
|
|
472
|
+
return host.__exit__(exc_type, exc, tb)
|
|
473
|
+
parent_exit = getattr(super(), "__exit__", None)
|
|
474
|
+
if parent_exit is not None:
|
|
475
|
+
return parent_exit(exc_type, exc, tb)
|
|
476
|
+
return None
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
class _PaginationImpl(BootstrapElement):
|
|
480
|
+
"""The container for presentational components for building a pagination UI."""
|
|
481
|
+
|
|
482
|
+
component_name = "Pagination"
|
|
483
|
+
children_kind = "grouping"
|
|
484
|
+
|
|
485
|
+
def __init__(
|
|
486
|
+
self,
|
|
487
|
+
children: object = None,
|
|
488
|
+
*,
|
|
489
|
+
_surface: str,
|
|
490
|
+
max_value: int,
|
|
491
|
+
min_value: int = 1,
|
|
492
|
+
step: int = 1,
|
|
493
|
+
active_page: int = 1,
|
|
494
|
+
size: str | None = None,
|
|
495
|
+
fully_expanded: bool = True,
|
|
496
|
+
previous_next: bool = False,
|
|
497
|
+
first_last: bool = False,
|
|
498
|
+
on_change: Callable[..., Any] | None = None,
|
|
499
|
+
**kwargs: Any,
|
|
500
|
+
) -> None:
|
|
501
|
+
self._surface = _surface
|
|
502
|
+
if children is not None:
|
|
503
|
+
raise ChildrenError("Pagination generates its own items; do not pass children")
|
|
504
|
+
if isinstance(max_value, bool) or not isinstance(max_value, int):
|
|
505
|
+
raise ValueError(f"max_value must be an int, got {max_value!r}")
|
|
506
|
+
if isinstance(min_value, bool) or not isinstance(min_value, int):
|
|
507
|
+
raise ValueError(f"min_value must be an int, got {min_value!r}")
|
|
508
|
+
if max_value < min_value:
|
|
509
|
+
raise ValueError(f"max_value must be >= min_value, got {max_value} < {min_value}")
|
|
510
|
+
if isinstance(step, bool) or not isinstance(step, int) or step < 1:
|
|
511
|
+
raise ValueError(f"step must be an int >= 1, got {step!r}")
|
|
512
|
+
if size is not None and size not in _PAGINATION_SIZES:
|
|
513
|
+
raise ValueError(f"Invalid Pagination size: {size!r}")
|
|
514
|
+
max_value = _rounded_max_value(min_value, max_value, step)
|
|
515
|
+
self._min_value = min_value
|
|
516
|
+
self._max_value = max_value
|
|
517
|
+
self._step = step
|
|
518
|
+
self._size = size
|
|
519
|
+
self._fully_expanded = bool(fully_expanded)
|
|
520
|
+
self._previous_next = bool(previous_next)
|
|
521
|
+
self._first_last = bool(first_last)
|
|
522
|
+
self._on_change = on_change
|
|
523
|
+
self._active_page = clamp_page(int(active_page), min_value=min_value, max_value=max_value)
|
|
524
|
+
super().__init__(None, tag="nav", **kwargs)
|
|
525
|
+
_apply_dom_attrs(self, {"aria-label": "pagination"})
|
|
526
|
+
self._render_items()
|
|
527
|
+
|
|
528
|
+
@property
|
|
529
|
+
def active_page(self) -> int:
|
|
530
|
+
"""Currently selected page number (clamped)."""
|
|
531
|
+
return self._active_page
|
|
532
|
+
|
|
533
|
+
@active_page.setter
|
|
534
|
+
def active_page(self, value: int) -> None:
|
|
535
|
+
clamped = clamp_page(int(value), min_value=self._min_value, max_value=self._max_value)
|
|
536
|
+
if clamped == self._active_page:
|
|
537
|
+
return
|
|
538
|
+
self._active_page = clamped
|
|
539
|
+
self._render_items()
|
|
540
|
+
if self._on_change is not None:
|
|
541
|
+
self._on_change(clamped)
|
|
542
|
+
|
|
543
|
+
def _render_items(self) -> None:
|
|
544
|
+
clearer = getattr(self, "clear", None)
|
|
545
|
+
if callable(clearer):
|
|
546
|
+
with suppress(Exception):
|
|
547
|
+
clearer()
|
|
548
|
+
classes = ["pagination"]
|
|
549
|
+
if self._size == "sm":
|
|
550
|
+
classes.append("pagination-sm")
|
|
551
|
+
elif self._size == "lg":
|
|
552
|
+
classes.append("pagination-lg")
|
|
553
|
+
tokens = pagination_tokens(
|
|
554
|
+
min_value=self._min_value,
|
|
555
|
+
max_value=self._max_value,
|
|
556
|
+
step=self._step,
|
|
557
|
+
active_page=self._active_page,
|
|
558
|
+
fully_expanded=self._fully_expanded,
|
|
559
|
+
previous_next=self._previous_next,
|
|
560
|
+
first_last=self._first_last,
|
|
561
|
+
)
|
|
562
|
+
with self:
|
|
563
|
+
self._list = ui.element("ul")
|
|
564
|
+
self._list.classes(" ".join(classes))
|
|
565
|
+
with self._list:
|
|
566
|
+
for token in tokens:
|
|
567
|
+
self._render_token(token)
|
|
568
|
+
|
|
569
|
+
def _special_disabled(self, token: str) -> bool:
|
|
570
|
+
if token in {"first", "prev"}:
|
|
571
|
+
return self._active_page <= self._min_value
|
|
572
|
+
return self._active_page >= self._max_value
|
|
573
|
+
|
|
574
|
+
def _target_for_token(self, token: str) -> int:
|
|
575
|
+
if token == "first":
|
|
576
|
+
return self._min_value
|
|
577
|
+
if token == "prev":
|
|
578
|
+
return self._active_page - self._step
|
|
579
|
+
if token == "next":
|
|
580
|
+
return self._active_page + self._step
|
|
581
|
+
return self._max_value
|
|
582
|
+
|
|
583
|
+
def _bind_page_click(self, element: Any, page: int) -> None:
|
|
584
|
+
def _on_click(*_args: Any, **_kwargs: Any) -> None:
|
|
585
|
+
self.active_page = page
|
|
586
|
+
|
|
587
|
+
element.on("click", _on_click)
|
|
588
|
+
|
|
589
|
+
def _render_token(self, token: str | int) -> None:
|
|
590
|
+
if token == "ellipsis":
|
|
591
|
+
li = ui.element("li")
|
|
592
|
+
li.classes("page-item disabled")
|
|
593
|
+
with li:
|
|
594
|
+
span = ui.element("span")
|
|
595
|
+
span.classes("page-link")
|
|
596
|
+
_set_text(span, "…")
|
|
597
|
+
return
|
|
598
|
+
is_active = isinstance(token, int) and token == self._active_page
|
|
599
|
+
is_disabled = isinstance(token, str) and self._special_disabled(token)
|
|
600
|
+
li_classes = ["page-item"]
|
|
601
|
+
if is_active:
|
|
602
|
+
li_classes.append("active")
|
|
603
|
+
if is_disabled:
|
|
604
|
+
li_classes.append("disabled")
|
|
605
|
+
li = ui.element("li")
|
|
606
|
+
li.classes(" ".join(li_classes))
|
|
607
|
+
if is_active:
|
|
608
|
+
_apply_dom_attrs(li, {"aria-current": "page"})
|
|
609
|
+
with li:
|
|
610
|
+
anchor = ui.element("a")
|
|
611
|
+
anchor.classes("page-link")
|
|
612
|
+
if isinstance(token, int):
|
|
613
|
+
if is_active:
|
|
614
|
+
with anchor:
|
|
615
|
+
_set_text(ui.element("span"), str(token))
|
|
616
|
+
hidden = ui.element("span")
|
|
617
|
+
hidden.classes("visually-hidden")
|
|
618
|
+
_set_text(hidden, "(current)")
|
|
619
|
+
else:
|
|
620
|
+
_set_text(anchor, str(token))
|
|
621
|
+
self._bind_page_click(anchor, token)
|
|
622
|
+
return
|
|
623
|
+
glyph = _PAGINATION_GLYPHS[token]
|
|
624
|
+
hidden_label: str | None = None
|
|
625
|
+
if not is_disabled and token == "prev":
|
|
626
|
+
hidden_label = "Previous"
|
|
627
|
+
elif not is_disabled and token == "next":
|
|
628
|
+
hidden_label = "Next"
|
|
629
|
+
if hidden_label is None:
|
|
630
|
+
_set_text(anchor, glyph)
|
|
631
|
+
else:
|
|
632
|
+
with anchor:
|
|
633
|
+
_set_text(ui.element("span"), glyph)
|
|
634
|
+
hidden = ui.element("span")
|
|
635
|
+
hidden.classes("visually-hidden")
|
|
636
|
+
_set_text(hidden, hidden_label)
|
|
637
|
+
if not is_disabled:
|
|
638
|
+
self._bind_page_click(anchor, self._target_for_token(token))
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
Nav, DbcNav = make_surface_classes("Nav", globals())
|
|
642
|
+
nav = Nav
|
|
643
|
+
NavItem, DbcNavItem = make_surface_classes("NavItem", globals())
|
|
644
|
+
nav_item = NavItem
|
|
645
|
+
NavLink, DbcNavLink = make_surface_classes("NavLink", globals())
|
|
646
|
+
nav_link = NavLink
|
|
647
|
+
Breadcrumb, DbcBreadcrumb = make_surface_classes("Breadcrumb", globals())
|
|
648
|
+
breadcrumb = Breadcrumb
|
|
649
|
+
Pagination, DbcPagination = make_surface_classes("Pagination", globals())
|
|
650
|
+
pagination = Pagination
|