Browse Source

only use bootstrap_select when desktop and multiple=True

wangweimin 2 years ago
parent
commit
923e7600a4

+ 6 - 3
pywebio/input.py

@@ -330,9 +330,10 @@ def _set_options_selected(options, value):
     return options
 
 
-def select(label: str = '', options: List[Union[Dict[str, Any], Tuple, List, str]] = None, *, multiple: bool = None, validate: Callable[[Any], Optional[str]] = None,
-           name: str = None, value: Union[List, str] = None, onchange: Callable[[Any], None] = None, required: bool = None,
-           help_text: str = None, **other_html_attrs):
+def select(label: str = '', options: List[Union[Dict[str, Any], Tuple, List, str]] = None, *, multiple: bool = None,
+           validate: Callable[[Any], Optional[str]] = None, name: str = None, value: Union[List, str] = None,
+           onchange: Callable[[Any], None] = None, native: bool = True, required: bool = None, help_text: str = None,
+           **other_html_attrs):
     r"""Drop-down selection
 
     By default, only one option can be selected at a time, you can set ``multiple`` parameter to enable multiple selection.
@@ -361,6 +362,8 @@ def select(label: str = '', options: List[Union[Dict[str, Any], Tuple, List, str
        You can also set the initial selected option by setting the ``selected`` field in the ``options`` list item.
     :type value: list or str
     :param bool required: Whether to select at least one item, only available when ``multiple=True``
+    :param bool native: Using browser's native select component rather than
+        `bootstrap-select <https://github.com/snapappointments/bootstrap-select>`_. This is the default behavior.
     :param - label, validate, name, onchange, help_text, other_html_attrs: Those arguments have the same meaning as for `input()`
     :return: If ``multiple=True``, return a list of the values in the ``options`` selected by the user;
         otherwise, return the single value selected by the user.

+ 11 - 4
pywebio/pin.py

@@ -97,7 +97,7 @@ Pin utils
 
     You can use attribute or key index of ``pin`` object to get the current value of a pin widget.
     By default, when accessing the value of a widget that does not exist, it returns ``None`` instead of
-    throwing an exception.
+    throwing an exception. You can enable the error raising by ``pin.use_strict()`` method.
 
     You can also use the ``pin`` object to set the value of pin widget:
 
@@ -170,13 +170,20 @@ def put_textarea(name: str, *, label: str = '', rows: int = 6, code: Union[bool,
 
 
 def put_select(name: str, options: List[Union[Dict[str, Any], Tuple, List, str]] = None, *, label: str = '',
-               multiple: bool = None, value: Union[List, str] = None, help_text: str = None,
+               multiple: bool = None, value: Union[List, str] = None, native: bool = None, help_text: str = None,
                scope: str = None, position: int = OutputPosition.BOTTOM) -> Output:
-    """Output a select widget. Refer to: `pywebio.input.select()`"""
+    """Output a select widget. Refer to: `pywebio.input.select()`
+
+    .. note::
+
+        Unlike `pywebio.input.select()`, when ``multiple=True`` and the user is using PC/macOS, `put_select()` will use
+        `bootstrap-select <https://github.com/snapappointments/bootstrap-select>`_ by default. Setting
+        ``native=True`` will force PyWebIO to use native select component on all platforms and vice versa.
+    """
     from pywebio.input import select
     check_dom_name_value(name, 'pin `name`')
     single_input_return = select(name=name, options=options, label=label, multiple=multiple,
-                                 value=value, help_text=help_text)
+                                 value=value, help_text=help_text, native=native)
     return _pin_output(single_input_return, scope, position)
 
 

+ 1 - 4
pywebio/session/__init__.py

@@ -517,8 +517,6 @@ def set_env(**env_info):
     * ``input_auto_focus`` (bool): Whether to focus on input automatically after showing input panel, default is ``True``
     * ``output_max_width`` (str): The max width of the page content area (in pixel or percentage,
       e.g. ``'1080px'``, ``'80%'``. Default is 880px).
-    * ``native_select`` (bool): Whether to use native select component, default is ``False`` . It's a fallback in case
-        the default select component is not rendered correctly in some cases.
 
     Example::
 
@@ -530,8 +528,7 @@ def set_env(**env_info):
     """
     from ..io_ctrl import send_msg
     assert all(k in ('title', 'output_animation', 'auto_scroll_bottom', 'http_pull_interval', 'output_max_width',
-                     'input_panel_min_height', 'input_panel_init_height', 'input_panel_fixed', 'input_auto_focus',
-                     'native_select')
+                     'input_panel_min_height', 'input_panel_init_height', 'input_panel_fixed', 'input_auto_focus')
                for k in env_info.keys())
     send_msg('set_env', spec=env_info)
 

+ 0 - 4
webiojs/src/handlers/env.ts

@@ -26,10 +26,6 @@ export class EnvSettingHandler implements CommandHandler {
             }
         }
 
-        if (spec.native_select !== undefined) {
-            config.disableSelectPicker = spec.native_select;
-        }
-
         if (spec.http_pull_interval !== undefined) {
             if (state.CurrentSession instanceof HttpSession)
                 state.CurrentSession.change_pull_interval(spec.http_pull_interval);

+ 10 - 6
webiojs/src/models/input/select.ts

@@ -1,6 +1,5 @@
 import {InputItem} from "./base";
-import {deep_copy, make_set} from "../../utils"
-import {config} from "../../state";
+import {deep_copy, is_mobile, make_set} from "../../utils"
 
 const options_tpl = `
 {{#options}}
@@ -24,8 +23,13 @@ $.fn.selectpicker.Constructor.BootstrapVersion = '4';
 export class Select extends InputItem {
     static accept_input_types: string[] = ["select"];
 
+    use_bootstrap_select: boolean = false;
+
     constructor(spec: any, task_id: string, on_input_event: (event_name: string, input_item: InputItem) => void) {
         super(spec, task_id, on_input_event);
+        if (spec.native === false || (spec.native === undefined && spec.multiple && !is_mobile())) {
+            this.use_bootstrap_select = true;
+        }
     }
 
     create_element(): JQuery {
@@ -37,7 +41,7 @@ export class Select extends InputItem {
         this.element = $(html);
         this.setup_select_options(this.element, spec.options);
 
-        if (!config.disableSelectPicker) {
+        if (this.use_bootstrap_select) {
             // @ts-ignore
             this.element.find('select').selectpicker();
         }
@@ -71,7 +75,7 @@ export class Select extends InputItem {
             input_elem.attr(key, this.spec[key]);
         }
 
-        if (!config.disableSelectPicker) {
+        if (this.use_bootstrap_select) {
             // @ts-ignore
             input_elem.selectpicker('refresh');
         }
@@ -99,7 +103,7 @@ export class Select extends InputItem {
                     $(this).prop('selected', true);
                 }
             });
-            if (!config.disableSelectPicker) {
+            if (this.use_bootstrap_select) {
                 // @ts-ignore
                 this.element.find('select').selectpicker('render');
             }
@@ -111,7 +115,7 @@ export class Select extends InputItem {
 
     on_reset(e: any) {
         // need to wait some time to get the select element be reset, and then update `selectpicker`
-        if (!config.disableSelectPicker)
+        if (this.use_bootstrap_select)
             setTimeout(() => {
                 // @ts-ignore
                 this.element.find('select').selectpicker('render');

+ 0 - 1
webiojs/src/state.ts

@@ -16,7 +16,6 @@ export let config = {
     codeMirrorModeURL: "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.52.2/mode/%N/%N.min.js",
     codeMirrorThemeURL: "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.52.2/theme/%N.min.css",
     outputAnimation: true, // 启用内容输出动画
-    disableSelectPicker: false,
     httpPullInterval: 1000,  // HttpSession 拉取消息的周期(ms)
     debug: false,  // 调试模式, 打印所有交互的消息
 };

+ 7 - 0
webiojs/src/utils.ts

@@ -176,4 +176,11 @@ function int2bytes(num: number) {
     dataView.setUint32(0, (num / 4294967296) | 0); // 4294967296 == 2^32
     dataView.setUint32(4, num | 0);
     return buf;
+}
+
+export function is_mobile() {
+    // @ts-ignore
+    if (navigator.userAgentData) return navigator.userAgentData.mobile;
+    const ipadOS = (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); /* iPad OS 13 */
+    return /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(navigator.userAgent.toLowerCase()) || ipadOS;
 }