1
0

select.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. from copy import deepcopy
  2. from typing import Any, Callable, Dict, List, Literal, Optional, Union
  3. from ..events import GenericEventArguments
  4. from .choice_element import ChoiceElement
  5. from .mixins.disableable_element import DisableableElement
  6. class Select(ChoiceElement, DisableableElement, component='select.js'):
  7. def __init__(self,
  8. options: Union[List, Dict], *,
  9. label: Optional[str] = None,
  10. value: Any = None,
  11. on_change: Optional[Callable[..., Any]] = None,
  12. with_input: bool = False,
  13. new_value_mode: Optional[Literal['add', 'add-unique', 'toggle']] = None,
  14. multiple: bool = False,
  15. clearable: bool = False,
  16. ) -> None:
  17. """Dropdown Selection
  18. This element is based on Quasar's `QSelect <https://quasar.dev/vue-components/select>`_ component.
  19. The options can be specified as a list of values, or as a dictionary mapping values to labels.
  20. After manipulating the options, call `update()` to update the options in the UI.
  21. If `with_input` is True, an input field is shown to filter the options.
  22. If `new_value_mode` is not None, it implies `with_input=True` and the user can enter new values in the input field.
  23. See `Quasar's documentation <https://quasar.dev/vue-components/select#the-new-value-mode-prop>`_ for details.
  24. :param options: a list ['value1', ...] or dictionary `{'value1':'label1', ...}` specifying the options
  25. :param label: the label to display above the selection
  26. :param value: the initial value
  27. :param on_change: callback to execute when selection changes
  28. :param with_input: whether to show an input field to filter the options
  29. :param new_value_mode: handle new values from user input (default: None, i.e. no new values)
  30. :param multiple: whether to allow multiple selections
  31. :param clearable: whether to add a button to clear the selection
  32. """
  33. self.multiple = multiple
  34. if multiple:
  35. if value is None:
  36. value = []
  37. elif not isinstance(value, list):
  38. value = [value]
  39. super().__init__(options=options, value=value, on_change=on_change)
  40. if label is not None:
  41. self._props['label'] = label
  42. if new_value_mode is not None:
  43. if isinstance(options, dict) and new_value_mode == 'add':
  44. raise ValueError('new_value_mode "add" is not supported for dict options')
  45. self._props['new-value-mode'] = new_value_mode
  46. with_input = True
  47. if with_input:
  48. self.original_options = deepcopy(options)
  49. self._props['use-input'] = True
  50. self._props['hide-selected'] = not multiple
  51. self._props['fill-input'] = True
  52. self._props['input-debounce'] = 0
  53. self._props['multiple'] = multiple
  54. self._props['clearable'] = clearable
  55. def _event_args_to_value(self, e: GenericEventArguments) -> Any:
  56. # pylint: disable=no-else-return
  57. if self.multiple:
  58. if e.args is None:
  59. return []
  60. else:
  61. args = [self._values[arg['value']] if isinstance(arg, dict) else arg for arg in e.args]
  62. for arg in e.args:
  63. if isinstance(arg, str):
  64. self._handle_new_value(arg)
  65. return [arg for arg in args if arg in self._values]
  66. else:
  67. if e.args is None:
  68. return None
  69. else:
  70. if isinstance(e.args, str):
  71. self._handle_new_value(e.args)
  72. return e.args if e.args in self._values else None
  73. else:
  74. return self._values[e.args['value']]
  75. def _value_to_model_value(self, value: Any) -> Any:
  76. # pylint: disable=no-else-return
  77. if self.multiple:
  78. result = []
  79. for item in value or []:
  80. try:
  81. index = self._values.index(item)
  82. result.append({'value': index, 'label': self._labels[index]})
  83. except ValueError:
  84. pass
  85. return result
  86. else:
  87. try:
  88. index = self._values.index(value)
  89. return {'value': index, 'label': self._labels[index]}
  90. except ValueError:
  91. return None
  92. def _handle_new_value(self, value: str) -> None:
  93. mode = self._props['new-value-mode']
  94. if isinstance(self.options, list):
  95. if mode == 'add':
  96. self.options.append(value)
  97. elif mode == 'add-unique':
  98. if value not in self.options:
  99. self.options.append(value)
  100. elif mode == 'toggle':
  101. if value in self.options:
  102. self.options.remove(value)
  103. else:
  104. self.options.append(value)
  105. # NOTE: self._labels and self._values are updated via self.options since they share the same references
  106. else:
  107. if mode in 'add-unique':
  108. if value not in self.options:
  109. self.options[value] = value
  110. elif mode == 'toggle':
  111. if value in self.options:
  112. self.options.pop(value)
  113. else:
  114. self.options.update({value: value})
  115. self._update_values_and_labels()