bootstrap-python 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.
- bootstrap_python-0.1.0.dist-info/METADATA +226 -0
- bootstrap_python-0.1.0.dist-info/RECORD +15 -0
- bootstrap_python-0.1.0.dist-info/WHEEL +5 -0
- bootstrap_python-0.1.0.dist-info/licenses/LICENSE +21 -0
- bootstrap_python-0.1.0.dist-info/top_level.txt +1 -0
- bootstrap_ui/__init__.py +162 -0
- bootstrap_ui/constants.py +85 -0
- bootstrap_ui/content.py +562 -0
- bootstrap_ui/data.py +411 -0
- bootstrap_ui/forms.py +489 -0
- bootstrap_ui/layout.py +260 -0
- bootstrap_ui/nav.py +356 -0
- bootstrap_ui/overlays.py +199 -0
- bootstrap_ui/templates/page.jinja2 +37 -0
- bootstrap_ui/types.py +236 -0
bootstrap_ui/content.py
ADDED
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
"""基础内容组件:标题、文本、链接、图片、图标、徽标、提示框、卡片、列表组、按钮。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, ClassVar, Literal
|
|
6
|
+
|
|
7
|
+
from pydantic import Field
|
|
8
|
+
|
|
9
|
+
from .constants import Color, Size, value as enum_value
|
|
10
|
+
from .types import Node, NodeLike, escape, render_attrs, render_children
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"Heading",
|
|
14
|
+
"Paragraph",
|
|
15
|
+
"Span",
|
|
16
|
+
"Strong",
|
|
17
|
+
"Small",
|
|
18
|
+
"Link",
|
|
19
|
+
"Image",
|
|
20
|
+
"Icon",
|
|
21
|
+
"Badge",
|
|
22
|
+
"Alert",
|
|
23
|
+
"Card",
|
|
24
|
+
"ListGroup",
|
|
25
|
+
"ListItem",
|
|
26
|
+
"Button",
|
|
27
|
+
"ButtonGroup",
|
|
28
|
+
"CloseButton",
|
|
29
|
+
"icon_name",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def icon_name(name: str) -> str:
|
|
34
|
+
"""把 ``plus``、``bi-plus``、``bi bi-plus`` 统一成 ``bi bi-plus``。"""
|
|
35
|
+
cleaned = name.strip().removeprefix("bi ").removeprefix("bi-")
|
|
36
|
+
return f"bi bi-{cleaned}"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Heading(Node):
|
|
40
|
+
"""标题,``<h1>`` ~ ``<h6>``。"""
|
|
41
|
+
|
|
42
|
+
TAG: ClassVar[str | None] = None
|
|
43
|
+
|
|
44
|
+
text: str = ""
|
|
45
|
+
level: int = 2
|
|
46
|
+
"""1-6,默认 2"""
|
|
47
|
+
|
|
48
|
+
size: Literal["1", "2", "3", "4", "5", "6"] | None = None
|
|
49
|
+
"""视觉字号(class ``h1``~``h6``),与语义 level 解耦"""
|
|
50
|
+
|
|
51
|
+
body: list[NodeLike] = Field(default_factory=list)
|
|
52
|
+
|
|
53
|
+
def tag_name(self) -> str | None:
|
|
54
|
+
level = min(max(self.level, 1), 6)
|
|
55
|
+
return f"h{level}"
|
|
56
|
+
|
|
57
|
+
def css_classes(self) -> list[str]:
|
|
58
|
+
return [f"h{self.size}"] if self.size else []
|
|
59
|
+
|
|
60
|
+
def render_content(self) -> str:
|
|
61
|
+
return escape(self.text) + render_children(self.body)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class Paragraph(Node):
|
|
65
|
+
"""段落。"""
|
|
66
|
+
|
|
67
|
+
TAG: ClassVar[str | None] = "p"
|
|
68
|
+
|
|
69
|
+
text: str = ""
|
|
70
|
+
body: list[NodeLike] = Field(default_factory=list)
|
|
71
|
+
|
|
72
|
+
lead: bool = False
|
|
73
|
+
"""引导段落(``.lead``)"""
|
|
74
|
+
|
|
75
|
+
muted: bool = False
|
|
76
|
+
"""次要文字色(``text-body-secondary``)"""
|
|
77
|
+
|
|
78
|
+
def css_classes(self) -> list[str]:
|
|
79
|
+
classes: list[str] = []
|
|
80
|
+
if self.lead:
|
|
81
|
+
classes.append("lead")
|
|
82
|
+
if self.muted:
|
|
83
|
+
classes.append("text-body-secondary")
|
|
84
|
+
return classes
|
|
85
|
+
|
|
86
|
+
def render_content(self) -> str:
|
|
87
|
+
return escape(self.text) + render_children(self.body)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class Span(Node):
|
|
91
|
+
"""行内文本片段。"""
|
|
92
|
+
|
|
93
|
+
TAG: ClassVar[str | None] = "span"
|
|
94
|
+
|
|
95
|
+
text: str = ""
|
|
96
|
+
body: list[NodeLike] = Field(default_factory=list)
|
|
97
|
+
|
|
98
|
+
color: str | None = None
|
|
99
|
+
"""``text-*`` 的颜色名,如 ``danger``、``body-secondary``"""
|
|
100
|
+
|
|
101
|
+
muted: bool = False
|
|
102
|
+
weight: Literal["bold", "semibold", "normal", "light"] | None = None
|
|
103
|
+
nowrap: bool = False
|
|
104
|
+
|
|
105
|
+
def css_classes(self) -> list[str]:
|
|
106
|
+
classes: list[str] = []
|
|
107
|
+
if self.color:
|
|
108
|
+
classes.append(f"text-{self.color}")
|
|
109
|
+
if self.muted:
|
|
110
|
+
classes.append("text-body-secondary")
|
|
111
|
+
if self.weight:
|
|
112
|
+
classes.append(f"fw-{self.weight}")
|
|
113
|
+
if self.nowrap:
|
|
114
|
+
classes.append("text-nowrap")
|
|
115
|
+
return classes
|
|
116
|
+
|
|
117
|
+
def render_content(self) -> str:
|
|
118
|
+
return escape(self.text) + render_children(self.body)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class Strong(Span):
|
|
122
|
+
"""加粗文本。"""
|
|
123
|
+
|
|
124
|
+
TAG: ClassVar[str | None] = "strong"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class Small(Span):
|
|
128
|
+
"""小号文本。"""
|
|
129
|
+
|
|
130
|
+
TAG: ClassVar[str | None] = "small"
|
|
131
|
+
|
|
132
|
+
def css_classes(self) -> list[str]:
|
|
133
|
+
classes = super().css_classes()
|
|
134
|
+
if not any(cls.startswith("text-") for cls in classes):
|
|
135
|
+
classes.append("text-body-secondary")
|
|
136
|
+
return classes
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class Link(Node):
|
|
140
|
+
"""超链接。"""
|
|
141
|
+
|
|
142
|
+
TAG: ClassVar[str | None] = "a"
|
|
143
|
+
|
|
144
|
+
text: str = ""
|
|
145
|
+
body: list[NodeLike] = Field(default_factory=list)
|
|
146
|
+
|
|
147
|
+
href: str = "#"
|
|
148
|
+
target: Literal["_blank", "_self", "_parent", "_top"] | None = None
|
|
149
|
+
color: str | None = None
|
|
150
|
+
no_underline: bool = False
|
|
151
|
+
icon: str | None = None
|
|
152
|
+
|
|
153
|
+
def html_attrs(self) -> dict[str, Any]:
|
|
154
|
+
attrs = super().html_attrs()
|
|
155
|
+
attrs["href"] = self.href
|
|
156
|
+
if self.target:
|
|
157
|
+
attrs["target"] = self.target
|
|
158
|
+
if self.target == "_blank":
|
|
159
|
+
attrs.setdefault("rel", "noopener noreferrer")
|
|
160
|
+
return attrs
|
|
161
|
+
|
|
162
|
+
def css_classes(self) -> list[str]:
|
|
163
|
+
classes: list[str] = []
|
|
164
|
+
if self.color:
|
|
165
|
+
classes.append(f"link-{self.color}")
|
|
166
|
+
if self.no_underline:
|
|
167
|
+
classes.append("text-decoration-none")
|
|
168
|
+
return classes
|
|
169
|
+
|
|
170
|
+
def render_content(self) -> str:
|
|
171
|
+
icon = f'<i class="{icon_name(self.icon)} me-1"></i>' if self.icon else ""
|
|
172
|
+
return icon + escape(self.text) + render_children(self.body)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class Image(Node):
|
|
176
|
+
"""图片。"""
|
|
177
|
+
|
|
178
|
+
TAG: ClassVar[str | None] = "img"
|
|
179
|
+
|
|
180
|
+
src: str = ""
|
|
181
|
+
alt: str = ""
|
|
182
|
+
|
|
183
|
+
width: int | str | None = None
|
|
184
|
+
height: int | str | None = None
|
|
185
|
+
|
|
186
|
+
fluid: bool = False
|
|
187
|
+
"""响应式图片(``img-fluid``)"""
|
|
188
|
+
|
|
189
|
+
rounded: bool | str = False
|
|
190
|
+
"""``True`` → ``rounded``;字符串 → ``rounded-3`` 之类"""
|
|
191
|
+
|
|
192
|
+
circle: bool = False
|
|
193
|
+
"""圆形(``rounded-circle``)"""
|
|
194
|
+
|
|
195
|
+
thumbnail: bool = False
|
|
196
|
+
"""缩略图边框(``img-thumbnail``)"""
|
|
197
|
+
|
|
198
|
+
lazy: bool = True
|
|
199
|
+
|
|
200
|
+
def html_attrs(self) -> dict[str, Any]:
|
|
201
|
+
attrs = super().html_attrs()
|
|
202
|
+
attrs["src"] = self.src
|
|
203
|
+
attrs["alt"] = self.alt
|
|
204
|
+
if self.width is not None:
|
|
205
|
+
attrs["width"] = self.width
|
|
206
|
+
if self.height is not None:
|
|
207
|
+
attrs["height"] = self.height
|
|
208
|
+
if self.lazy:
|
|
209
|
+
attrs["loading"] = "lazy"
|
|
210
|
+
return attrs
|
|
211
|
+
|
|
212
|
+
def css_classes(self) -> list[str]:
|
|
213
|
+
classes: list[str] = []
|
|
214
|
+
if self.fluid:
|
|
215
|
+
classes.append("img-fluid")
|
|
216
|
+
if self.circle:
|
|
217
|
+
classes.append("rounded-circle")
|
|
218
|
+
elif self.rounded:
|
|
219
|
+
classes.append("rounded" if self.rounded is True else f"rounded-{self.rounded}")
|
|
220
|
+
if self.thumbnail:
|
|
221
|
+
classes.append("img-thumbnail")
|
|
222
|
+
return classes
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class Icon(Node):
|
|
226
|
+
"""Bootstrap Icons 图标。"""
|
|
227
|
+
|
|
228
|
+
TAG: ClassVar[str | None] = "i"
|
|
229
|
+
|
|
230
|
+
name: str = "circle"
|
|
231
|
+
size: str | int | None = None
|
|
232
|
+
color: str | None = None
|
|
233
|
+
spin: bool = False
|
|
234
|
+
|
|
235
|
+
def html_attrs(self) -> dict[str, Any]:
|
|
236
|
+
attrs = super().html_attrs()
|
|
237
|
+
attrs["aria-hidden"] = "true"
|
|
238
|
+
styles: list[str] = []
|
|
239
|
+
if self.size is not None:
|
|
240
|
+
size = f"{self.size}px" if isinstance(self.size, int) else str(self.size)
|
|
241
|
+
styles.append(f"font-size: {size}")
|
|
242
|
+
if self.color:
|
|
243
|
+
styles.append(f"color: var(--bs-{self.color})")
|
|
244
|
+
if styles:
|
|
245
|
+
attrs["style"] = "; ".join(styles) + (f"; {attrs['style']}" if "style" in attrs else "")
|
|
246
|
+
return attrs
|
|
247
|
+
|
|
248
|
+
def css_classes(self) -> list[str]:
|
|
249
|
+
classes = icon_name(self.name).split()
|
|
250
|
+
if self.spin:
|
|
251
|
+
classes.append("spinner-border spinner-border-sm")
|
|
252
|
+
return classes
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class Badge(Node):
|
|
256
|
+
"""徽标。"""
|
|
257
|
+
|
|
258
|
+
TAG: ClassVar[str | None] = "span"
|
|
259
|
+
|
|
260
|
+
text: str = ""
|
|
261
|
+
color: Color = Color.primary
|
|
262
|
+
pill: bool = False
|
|
263
|
+
icon: str | None = None
|
|
264
|
+
|
|
265
|
+
def css_classes(self) -> list[str]:
|
|
266
|
+
classes = [f"badge text-bg-{enum_value(self.color)}"]
|
|
267
|
+
if self.pill:
|
|
268
|
+
classes.append("rounded-pill")
|
|
269
|
+
return classes
|
|
270
|
+
|
|
271
|
+
def render_content(self) -> str:
|
|
272
|
+
icon = f'<i class="{icon_name(self.icon)} me-1"></i>' if self.icon else ""
|
|
273
|
+
return icon + escape(self.text)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class Alert(Node):
|
|
277
|
+
"""提示框。"""
|
|
278
|
+
|
|
279
|
+
color: Color = Color.info
|
|
280
|
+
body: list[NodeLike] = Field(default_factory=list)
|
|
281
|
+
title: str | None = None
|
|
282
|
+
icon: str | None = None
|
|
283
|
+
dismissible: bool = True
|
|
284
|
+
show: bool = True
|
|
285
|
+
|
|
286
|
+
def css_classes(self) -> list[str]:
|
|
287
|
+
classes = [f"alert alert-{enum_value(self.color)}"]
|
|
288
|
+
if self.dismissible:
|
|
289
|
+
classes.append("alert-dismissible")
|
|
290
|
+
if self.show and self.dismissible:
|
|
291
|
+
classes.append("fade show")
|
|
292
|
+
return classes
|
|
293
|
+
|
|
294
|
+
def html_attrs(self) -> dict[str, Any]:
|
|
295
|
+
attrs = super().html_attrs()
|
|
296
|
+
attrs.setdefault("role", "alert")
|
|
297
|
+
return attrs
|
|
298
|
+
|
|
299
|
+
def render_content(self) -> str:
|
|
300
|
+
parts: list[str] = []
|
|
301
|
+
if self.icon:
|
|
302
|
+
parts.append(f'<i class="{icon_name(self.icon)} me-2"></i>')
|
|
303
|
+
if self.title:
|
|
304
|
+
parts.append(f"<strong>{escape(self.title)}</strong> ")
|
|
305
|
+
parts.append(render_children(self.body))
|
|
306
|
+
if self.dismissible:
|
|
307
|
+
parts.append(
|
|
308
|
+
'<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="关闭"></button>'
|
|
309
|
+
)
|
|
310
|
+
return "".join(parts)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
class Card(Node):
|
|
314
|
+
"""卡片。"""
|
|
315
|
+
|
|
316
|
+
body: list[NodeLike] = Field(default_factory=list)
|
|
317
|
+
title: str | None = None
|
|
318
|
+
subtitle: str | None = None
|
|
319
|
+
header: list[NodeLike] | None = None
|
|
320
|
+
"""自定义页眉内容,设置后忽略 title/subtitle"""
|
|
321
|
+
|
|
322
|
+
footer: list[NodeLike] | None = None
|
|
323
|
+
actions: list[NodeLike] = Field(default_factory=list)
|
|
324
|
+
"""放在页眉右侧的操作按钮"""
|
|
325
|
+
|
|
326
|
+
image: str | None = None
|
|
327
|
+
image_position: Literal["top", "bottom"] = "top"
|
|
328
|
+
color: Color | None = None
|
|
329
|
+
body_class: str | None = None
|
|
330
|
+
|
|
331
|
+
def css_classes(self) -> list[str]:
|
|
332
|
+
classes = ["card", "h-100"]
|
|
333
|
+
if self.color:
|
|
334
|
+
classes.append(f"text-bg-{enum_value(self.color)}")
|
|
335
|
+
return classes
|
|
336
|
+
|
|
337
|
+
def _render_header(self) -> str:
|
|
338
|
+
left: list[str] = []
|
|
339
|
+
if self.header:
|
|
340
|
+
left.append(render_children(self.header))
|
|
341
|
+
else:
|
|
342
|
+
if self.title:
|
|
343
|
+
left.append(f'<h5 class="card-title mb-0">{escape(self.title)}</h5>')
|
|
344
|
+
if self.subtitle:
|
|
345
|
+
left.append(f'<div class="small opacity-75 mt-1">{escape(self.subtitle)}</div>')
|
|
346
|
+
actions = render_children(self.actions)
|
|
347
|
+
if not left and not actions:
|
|
348
|
+
return ""
|
|
349
|
+
|
|
350
|
+
left_html = "".join(left)
|
|
351
|
+
if actions:
|
|
352
|
+
return (
|
|
353
|
+
'<div class="card-header d-flex justify-content-between align-items-center gap-2">'
|
|
354
|
+
f'<div>{left_html}</div><div class="d-flex gap-2">{actions}</div></div>'
|
|
355
|
+
)
|
|
356
|
+
return f'<div class="card-header">{left_html}</div>'
|
|
357
|
+
|
|
358
|
+
def render_content(self) -> str:
|
|
359
|
+
parts: list[str] = []
|
|
360
|
+
image = f'<img src="{escape(self.image)}" class="card-img-top" alt="">' if self.image else ""
|
|
361
|
+
if image and self.image_position == "top":
|
|
362
|
+
parts.append(image)
|
|
363
|
+
parts.append(self._render_header())
|
|
364
|
+
body_class = f"card-body {self.body_class}" if self.body_class else "card-body"
|
|
365
|
+
parts.append(f'<div class="{body_class}">{render_children(self.body)}</div>')
|
|
366
|
+
if self.footer:
|
|
367
|
+
parts.append(f'<div class="card-footer">{render_children(self.footer)}</div>')
|
|
368
|
+
if image and self.image_position == "bottom":
|
|
369
|
+
parts.append(image)
|
|
370
|
+
return "".join(parts)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
class ListItem(Node):
|
|
374
|
+
"""列表组中的一项。"""
|
|
375
|
+
|
|
376
|
+
TAG: ClassVar[str | None] = None
|
|
377
|
+
|
|
378
|
+
text: str = ""
|
|
379
|
+
body: list[NodeLike] = Field(default_factory=list)
|
|
380
|
+
href: str | None = None
|
|
381
|
+
active: bool = False
|
|
382
|
+
disabled: bool = False
|
|
383
|
+
color: str | None = None
|
|
384
|
+
badge: str | None = None
|
|
385
|
+
icon: str | None = None
|
|
386
|
+
|
|
387
|
+
def render(self) -> str:
|
|
388
|
+
classes = ["list-group-item"]
|
|
389
|
+
if self.href:
|
|
390
|
+
classes.append("list-group-item-action")
|
|
391
|
+
if self.active:
|
|
392
|
+
classes.append("active")
|
|
393
|
+
if self.disabled:
|
|
394
|
+
classes.append("disabled")
|
|
395
|
+
if self.color:
|
|
396
|
+
classes.append(f"list-group-item-{self.color}")
|
|
397
|
+
if self.class_name:
|
|
398
|
+
classes.append(self.class_name)
|
|
399
|
+
|
|
400
|
+
content: list[str] = []
|
|
401
|
+
if self.icon:
|
|
402
|
+
content.append(f'<i class="{icon_name(self.icon)} me-2"></i>')
|
|
403
|
+
content.append(escape(self.text))
|
|
404
|
+
content.append(render_children(self.body))
|
|
405
|
+
if self.badge:
|
|
406
|
+
content.append(f'<span class="badge text-bg-secondary float-end">{escape(self.badge)}</span>')
|
|
407
|
+
|
|
408
|
+
attrs = {"class": " ".join(classes)}
|
|
409
|
+
if self.id:
|
|
410
|
+
attrs["id"] = self.id
|
|
411
|
+
if self.disabled:
|
|
412
|
+
attrs["aria-disabled"] = "true"
|
|
413
|
+
inner = "".join(content)
|
|
414
|
+
if self.href and not self.disabled:
|
|
415
|
+
return f'<a href="{escape(self.href)}"{render_attrs(attrs)}>{inner}</a>'
|
|
416
|
+
return f'<div{render_attrs(attrs)}>{inner}</div>'
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
class ListGroup(Node):
|
|
420
|
+
"""列表组。"""
|
|
421
|
+
|
|
422
|
+
items: list[ListItem | str] = Field(default_factory=list)
|
|
423
|
+
flush: bool = False
|
|
424
|
+
numbered: bool = False
|
|
425
|
+
horizontal: bool = False
|
|
426
|
+
|
|
427
|
+
def css_classes(self) -> list[str]:
|
|
428
|
+
classes = ["list-group"]
|
|
429
|
+
if self.flush:
|
|
430
|
+
classes.append("list-group-flush")
|
|
431
|
+
if self.numbered:
|
|
432
|
+
classes.append("list-group-numbered")
|
|
433
|
+
if self.horizontal:
|
|
434
|
+
classes.append("list-group-horizontal")
|
|
435
|
+
return classes
|
|
436
|
+
|
|
437
|
+
def render_content(self) -> str:
|
|
438
|
+
parts: list[str] = []
|
|
439
|
+
for item in self.items:
|
|
440
|
+
if isinstance(item, ListItem):
|
|
441
|
+
parts.append(item.render())
|
|
442
|
+
else:
|
|
443
|
+
parts.append(ListItem(text=str(item)).render())
|
|
444
|
+
return "".join(parts)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
class Button(Node):
|
|
448
|
+
"""按钮。``href`` 存在时渲染为 ``<a>``。"""
|
|
449
|
+
|
|
450
|
+
text: str = ""
|
|
451
|
+
color: Color = Color.primary
|
|
452
|
+
outline: bool = False
|
|
453
|
+
size: Size | None = None
|
|
454
|
+
type: Literal["button", "submit", "reset"] = "button"
|
|
455
|
+
href: str | None = None
|
|
456
|
+
icon: str | None = None
|
|
457
|
+
block: bool = False
|
|
458
|
+
disabled: bool = False
|
|
459
|
+
active: bool = False
|
|
460
|
+
|
|
461
|
+
toggle_modal: str | None = None
|
|
462
|
+
"""点击打开指定 id 的模态框,自动补 ``data-bs-toggle/target``"""
|
|
463
|
+
|
|
464
|
+
toggle_offcanvas: str | None = None
|
|
465
|
+
"""点击打开指定 id 的抽屉"""
|
|
466
|
+
|
|
467
|
+
dismiss: Literal["modal", "offcanvas", "alert", "toast"] | None = None
|
|
468
|
+
"""点击关闭指定类型的浮层"""
|
|
469
|
+
|
|
470
|
+
tooltip: str | None = None
|
|
471
|
+
"""鼠标悬停提示(由页面脚本初始化)"""
|
|
472
|
+
|
|
473
|
+
def tag_name(self) -> str | None:
|
|
474
|
+
return "a" if self.href else "button"
|
|
475
|
+
|
|
476
|
+
def html_attrs(self) -> dict[str, Any]:
|
|
477
|
+
attrs = super().html_attrs()
|
|
478
|
+
if self.href:
|
|
479
|
+
attrs["href"] = self.href
|
|
480
|
+
else:
|
|
481
|
+
attrs["type"] = self.type
|
|
482
|
+
if self.disabled:
|
|
483
|
+
if self.href:
|
|
484
|
+
attrs["aria-disabled"] = "true"
|
|
485
|
+
attrs["tabindex"] = "-1"
|
|
486
|
+
else:
|
|
487
|
+
attrs["disabled"] = True
|
|
488
|
+
if self.toggle_modal:
|
|
489
|
+
attrs["data-bs-toggle"] = "modal"
|
|
490
|
+
attrs["data-bs-target"] = f"#{self.toggle_modal.lstrip('#')}"
|
|
491
|
+
elif self.toggle_offcanvas:
|
|
492
|
+
attrs["data-bs-toggle"] = "offcanvas"
|
|
493
|
+
attrs["data-bs-target"] = f"#{self.toggle_offcanvas.lstrip('#')}"
|
|
494
|
+
if self.dismiss:
|
|
495
|
+
attrs["data-bs-dismiss"] = self.dismiss
|
|
496
|
+
if self.tooltip:
|
|
497
|
+
attrs["title"] = self.tooltip
|
|
498
|
+
attrs.setdefault("data-bs-toggle", "tooltip")
|
|
499
|
+
return attrs
|
|
500
|
+
|
|
501
|
+
def css_classes(self) -> list[str]:
|
|
502
|
+
variant = f"btn-outline-{enum_value(self.color)}" if self.outline else f"btn-{enum_value(self.color)}"
|
|
503
|
+
classes = ["btn", variant]
|
|
504
|
+
if self.size:
|
|
505
|
+
classes.append(f"btn-{enum_value(self.size)}")
|
|
506
|
+
if self.block:
|
|
507
|
+
classes.append("w-100")
|
|
508
|
+
if self.active:
|
|
509
|
+
classes.append("active")
|
|
510
|
+
if self.disabled:
|
|
511
|
+
classes.append("disabled")
|
|
512
|
+
return classes
|
|
513
|
+
|
|
514
|
+
def render_content(self) -> str:
|
|
515
|
+
icon = f'<i class="{icon_name(self.icon)} me-1"></i>' if self.icon else ""
|
|
516
|
+
return icon + escape(self.text)
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
class ButtonGroup(Node):
|
|
520
|
+
"""按钮组。"""
|
|
521
|
+
|
|
522
|
+
buttons: list[NodeLike] = Field(default_factory=list)
|
|
523
|
+
vertical: bool = False
|
|
524
|
+
size: Size | None = None
|
|
525
|
+
|
|
526
|
+
def css_classes(self) -> list[str]:
|
|
527
|
+
classes = ["btn-group"]
|
|
528
|
+
if self.vertical:
|
|
529
|
+
classes.append("btn-group-vertical")
|
|
530
|
+
if self.size:
|
|
531
|
+
classes.append(f"btn-group-{enum_value(self.size)}")
|
|
532
|
+
return classes
|
|
533
|
+
|
|
534
|
+
def html_attrs(self) -> dict[str, Any]:
|
|
535
|
+
attrs = super().html_attrs()
|
|
536
|
+
attrs.setdefault("role", "group")
|
|
537
|
+
return attrs
|
|
538
|
+
|
|
539
|
+
def render_content(self) -> str:
|
|
540
|
+
return render_children(self.buttons)
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
class CloseButton(Node):
|
|
544
|
+
"""关闭按钮(``.btn-close``)。"""
|
|
545
|
+
|
|
546
|
+
TAG: ClassVar[str | None] = "button"
|
|
547
|
+
|
|
548
|
+
dismiss: Literal["modal", "offcanvas", "alert", "toast"] = "modal"
|
|
549
|
+
white: bool = False
|
|
550
|
+
|
|
551
|
+
def html_attrs(self) -> dict[str, Any]:
|
|
552
|
+
attrs = super().html_attrs()
|
|
553
|
+
attrs["type"] = "button"
|
|
554
|
+
attrs["data-bs-dismiss"] = self.dismiss
|
|
555
|
+
attrs["aria-label"] = "关闭"
|
|
556
|
+
return attrs
|
|
557
|
+
|
|
558
|
+
def css_classes(self) -> list[str]:
|
|
559
|
+
classes = ["btn-close"]
|
|
560
|
+
if self.white:
|
|
561
|
+
classes.append("btn-close-white")
|
|
562
|
+
return classes
|