浏览代码

Merge pull request #1063 from zauberzeug/autocomplete

Autocomplete for mobile devices
Rodja Trappe 1 年之前
父节点
当前提交
79d9a1a381
共有 3 个文件被更改,包括 76 次插入25 次删除
  1. 57 0
      nicegui/elements/input.js
  2. 10 21
      nicegui/elements/input.py
  3. 9 4
      tests/test_input.py

+ 57 - 0
nicegui/elements/input.js

@@ -0,0 +1,57 @@
+export default {
+  template: `
+    <q-input
+      v-bind="$attrs"
+      v-model="inputValue"
+      :shadow-text="shadowText"
+      @keydown.tab="perform_autocomplete"
+      :list="id + '-datalist'"
+    >
+      <template v-for="(_, slot) in $slots" v-slot:[slot]="slotProps">
+        <slot :name="slot" v-bind="slotProps || {}" />
+      </template>
+    </q-input>
+    <datalist v-if="withDatalist" :id="id + '-datalist'">
+      <option v-for="option in autocomplete" :value="option"></option>
+    </datalist>
+  `,
+  props: {
+    id: String,
+    autocomplete: Array,
+    value: String,
+  },
+  data() {
+    return {
+      inputValue: this.value,
+    };
+  },
+  watch: {
+    value(newValue) {
+      this.inputValue = newValue;
+    },
+    inputValue(newValue) {
+      this.$emit("update:value", newValue);
+    },
+  },
+  computed: {
+    shadowText() {
+      if (!this.inputValue) return "";
+      const matchingOption = this.autocomplete.find((option) =>
+        option.toLowerCase().startsWith(this.inputValue.toLowerCase())
+      );
+      return matchingOption ? matchingOption.slice(this.inputValue.length) : "";
+    },
+    withDatalist() {
+      const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
+      return isMobile && this.autocomplete && this.autocomplete.length > 0;
+    },
+  },
+  methods: {
+    perform_autocomplete(e) {
+      if (this.shadowText) {
+        this.inputValue += this.shadowText;
+        e.preventDefault();
+      }
+    },
+  },
+};

+ 10 - 21
nicegui/elements/input.py

@@ -1,11 +1,15 @@
 from typing import Any, Callable, Dict, List, Optional
 
+from ..dependencies import register_component
 from .icon import Icon
 from .mixins.disableable_element import DisableableElement
 from .mixins.validation_element import ValidationElement
 
+register_component('nicegui-input', __file__, 'input.js')
+
 
 class Input(ValidationElement, DisableableElement):
+    VALUE_PROP: str = 'value'
     LOOPBACK = False
 
     def __init__(self,
@@ -37,7 +41,7 @@ class Input(ValidationElement, DisableableElement):
         :param autocomplete: optional list of strings for autocompletion
         :param validation: dictionary of validation rules, e.g. ``{'Too long!': lambda value: len(value) < 3}``
         """
-        super().__init__(tag='q-input', value=value, on_value_change=on_change, validation=validation)
+        super().__init__(tag='nicegui-input', value=value, on_value_change=on_change, validation=validation)
         if label is not None:
             self._props['label'] = label
         if placeholder is not None:
@@ -52,24 +56,9 @@ class Input(ValidationElement, DisableableElement):
                     self.props(f'type={"text" if is_hidden else "password"}')
                 icon = Icon('visibility_off').classes('cursor-pointer').on('click', toggle_type)
 
-        if autocomplete:
-            def find_autocompletion() -> Optional[str]:
-                if self.value:
-                    needle = str(self.value).casefold()
-                    for item in autocomplete or []:
-                        if item.casefold().startswith(needle):
-                            return item
-                return None  # required by mypy
-
-            def autocomplete_input() -> None:
-                match = find_autocompletion() or ''
-                self.props(f'shadow-text="{match[len(self.value):]}"')
-
-            def complete_input() -> None:
-                match = find_autocompletion()
-                if match:
-                    self.set_value(match)
-                self.props('shadow-text=""')
+        self._props['autocomplete'] = autocomplete or []
 
-            self.on('keyup', autocomplete_input)
-            self.on('keydown.tab', complete_input)
+    def set_autocomplete(self, autocomplete: Optional[List[str]]) -> None:
+        """Set the autocomplete list."""
+        self._props['autocomplete'] = autocomplete
+        self.update()

+ 9 - 4
tests/test_input.py

@@ -83,7 +83,7 @@ def test_input_with_multi_word_error_message(screen: Screen):
 
 
 def test_autocompletion(screen: Screen):
-    ui.input('Input', autocomplete=['foo', 'bar', 'baz'])
+    input = ui.input('Input', autocomplete=['foo', 'bar', 'baz'])
 
     screen.open('/')
     element = screen.selenium.find_element(By.XPATH, '//*[@aria-label="Input"]')
@@ -100,16 +100,21 @@ def test_autocompletion(screen: Screen):
     element.send_keys(Keys.TAB)
     screen.wait(0.2)
     assert element.get_attribute('value') == 'foo'
+    assert input.value == 'foo'
 
     element.send_keys(Keys.BACKSPACE)
-    screen.wait(0.2)
     element.send_keys(Keys.BACKSPACE)
-    screen.wait(0.2)
     element.send_keys('x')
-    screen.wait(0.2)
     element.send_keys(Keys.TAB)
     screen.wait(0.5)
     assert element.get_attribute('value') == 'fx'
+    assert input.value == 'fx'
+
+    input.set_autocomplete(['one', 'two'])
+    element.send_keys(Keys.BACKSPACE)
+    element.send_keys(Keys.BACKSPACE)
+    element.send_keys('o')
+    screen.should_contain('ne')
 
 
 def test_clearable_input(screen: Screen):