forked from posit-dev/py-shiny
-
Notifications
You must be signed in to change notification settings - Fork 0
/
_input_select.py
331 lines (279 loc) · 9.32 KB
/
_input_select.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
# pyright: reportUnnecessaryComparison=false
from __future__ import annotations
from ..types import Jsonifiable
__all__ = (
"input_select",
"input_selectize",
)
import copy
from json import dumps
from typing import Any, Mapping, Optional, Union, cast
from htmltools import Tag, TagChild, TagList, css, div, tags
from .._docstring import add_example
from .._namespaces import resolve_id
from ._html_deps_external import selectize_deps
from ._utils import JSEval, extract_js_keys, shiny_input_label
_Choices = Mapping[str, TagChild]
_OptGrpChoices = Mapping[str, _Choices]
# Canonical format for representing select options.
_SelectChoices = Union[_Choices, _OptGrpChoices]
# Formats available to the user
SelectChoicesArg = Union[
# ["a", "b", "c"]
"list[str]",
# ("a", "b", "c")
"tuple[str, ...]",
# {"a": "Choice A", "b": tags.i("Choice B")}
_Choices,
# optgroup {"Group A": {"a1": "Choice A1", "a2": tags.i("Choice A2")}, "Group B": {}}
_OptGrpChoices,
]
_topics = {
"Server value": """
If `multiple=False`, the server value is a string with the value of the selected item.
If `multiple=True`, the server value is a tuple containing the values of the
selected items. When ``multiple=True`` and nothing is selected, this value
will be ``None``.
"""
}
@add_example()
def input_selectize(
id: str,
label: TagChild,
choices: SelectChoicesArg,
*,
selected: Optional[str | list[str]] = None,
multiple: bool = False,
width: Optional[str] = None,
remove_button: Optional[bool] = None,
options: Optional[dict[str, Jsonifiable | JSEval]] = None,
) -> Tag:
"""
Create a select list that can be used to choose a single or multiple items from a
list of values.
Parameters
----------
id
An input id.
label
An input label.
choices
Either a list of choices or a dictionary mapping choice values to labels. Note
that if a dictionary is provided, the keys are used as the (input) values so
that the dictionary values can hold HTML labels. A dictionary of dictionaries is
also supported, and in that case, the top-level keys are treated as
``<optgroup>`` labels.
selected
The values that should be initially selected, if any.
multiple
Is selection of multiple items allowed?
width
The CSS width, e.g. '400px', or '100%'
remove_button
Whether to add a remove button. This uses the `clear_button` and `remove_button`
selectize plugins which can also be supplied as options. By default it will apply a
remove button to multiple selections, but not single selections.
options
A dictionary of options. See the documentation of selectize.js for possible options.
If you want to pass a JavaScript function, wrap the string in `ui.JS`.
Returns
-------
:
A UI element.
Notes
------
::: {.callout-note title="Server value"}
If `multiple=False`, the server value is a string with the value of the selected item.
If `multiple=True`, the server value is a tuple containing the values of the
selected items. When ``multiple=True`` and nothing is selected, this value
will be ``None``.
:::
See Also
--------
* :func:`~shiny.ui.input_select`
* :func:`~shiny.ui.input_radio_buttons`
* :func:`~shiny.ui.input_checkbox_group`
"""
x = input_select(
id,
label,
choices,
selected=selected,
multiple=multiple,
selectize=True,
width=width,
remove_button=remove_button,
options=options,
)
return x
@add_example()
def input_select(
id: str,
label: TagChild,
choices: SelectChoicesArg,
*,
selected: Optional[str | list[str]] = None,
multiple: bool = False,
selectize: bool = False,
width: Optional[str] = None,
size: Optional[str] = None,
remove_button: Optional[bool] = None,
options: Optional[dict[str, Jsonifiable | JSEval]] = None,
) -> Tag:
"""
Create a select list that can be used to choose a single or multiple items from a
list of values.
Parameters
----------
id
An input id.
label
An input label.
choices
Either a list of choices or a dictionary mapping choice values to labels. Note
that if a dictionary is provided, the keys are used as the (input) values so
that the dictionary values can hold HTML labels. A dictionary of dictionaries is
also supported, and in that case, the top-level keys are treated as
``<optgroup>`` labels.
selected
The values that should be initially selected, if any.
multiple
Is selection of multiple items allowed?
selectize
Whether to use selectize.js or not.
width
The CSS width, e.g. '400px', or '100%'
size
Number of items to show in the selection box; a larger number will result in a
taller box. Normally, when ``multiple=False``, a select input will be a
drop-down list, but when size is set, it will be a box instead.
Returns
-------
:
A UI element.
Notes
------
::: {.callout-note title="Server value"}
If `multiple=False`, the server value is a string with the value of the selected item.
If `multiple=True`, the server value is a tuple containing the values of the
selected items. When ``multiple=True`` and nothing is selected, this value
will be ``None``.
:::
See Also
--------
* :func:`~shiny.ui.input_selectize`
* :func:`~shiny.ui.update_select`
* :func:`~shiny.ui.input_radio_buttons`
* :func:`~shiny.ui.input_checkbox_group`
"""
if options is not None and selectize is False:
raise Exception("Options can only be set when selectize is `True`.")
remove_button = _resolve_remove_button(remove_button, multiple)
choices_ = _normalize_choices(choices)
if selected is None and not multiple:
selected = _find_first_option(choices_)
if options is None:
options = {}
opts = _update_options(options, remove_button, multiple)
choices_tags = _render_choices(choices_, selected)
resolved_id = resolve_id(id)
return div(
shiny_input_label(resolved_id, label),
div(
tags.select(
*choices_tags,
{"class": "shiny-input-select"},
class_=None if selectize else "form-select",
id=resolved_id,
multiple=multiple,
size=size,
),
(
TagList(
tags.script(
dumps(opts),
type="application/json",
data_for=resolved_id,
data_eval=dumps(extract_js_keys(opts)),
),
selectize_deps(),
)
if selectize
else None
),
),
class_="form-group shiny-input-container",
style=css(width=width),
)
def _resolve_remove_button(remove_button: Optional[bool], multiple: bool) -> bool:
if remove_button is None:
if multiple:
return True
else:
return False
return remove_button
def _update_options(
options: dict[str, Any], remove_button: bool, multiple: bool
) -> dict[str, Any]:
opts = copy.deepcopy(options)
plugins = opts.get("plugins", [])
if remove_button:
if multiple:
to_add = "remove_button"
else:
to_add = "clear_button"
if to_add not in plugins:
plugins.append(to_add)
if not plugins:
return options
opts["plugins"] = plugins
return opts
def _normalize_choices(x: SelectChoicesArg) -> _SelectChoices:
if x is None:
raise TypeError("`choices` must be a list, tuple, or dict.")
elif isinstance(x, (list, tuple)):
return {k: k for k in x}
else:
return x
def _render_choices(
x: _SelectChoices, selected: Optional[str | list[str]] = None
) -> TagList:
result = TagList()
if x is None:
return result
for k, v in x.items():
if isinstance(v, Mapping):
result.append(
tags.optgroup(
*(_render_choices(cast(_SelectChoices, v), selected)), label=k
)
)
else:
is_selected = False
if isinstance(selected, list):
is_selected = k in selected
else:
is_selected = k == selected
result.append(tags.option(v, value=k, selected=is_selected))
return result
# Returns the first option in a _SelectChoices object. For most cases, this is
# straightforward. In the following, the first option is "a":
# {"a": "Choice A", "b": "Choice B", "c": "Choice C"}
#
# Sometimes the first option is nested within an optgroup. For example, in the
# following, the first option is "b1":
# {
# "Group A": {},
# "Group B": {"Choice B1": "b1", "Choice B2": "b2"},
# }
def _find_first_option(x: _SelectChoices) -> Optional[str]:
if x is None:
return None
for k, v in x.items():
if isinstance(v, dict):
result = _find_first_option(cast(_SelectChoices, v))
if result is not None:
return result
else:
return k
return None