瀏覽代碼

implement search UI in NiceGUI

Falko Schindler 1 年之前
父節點
當前提交
1ea7f5c18f
共有 3 個文件被更改,包括 62 次插入98 次删除
  1. 7 6
      main.py
  2. 55 7
      website/search.py
  3. 0 85
      website/search.vue

+ 7 - 6
main.py

@@ -23,7 +23,7 @@ from nicegui import ui
 from website import documentation, example_card, svg
 from website.demo import bash_window, browser_window, python_window
 from website.documentation_tools import create_anchor_name, element_demo, generate_class_doc
-from website.search import search
+from website.search import Search
 from website.star import add_star
 from website.style import example_link, features, heading, link_target, section_heading, side_menu, subtitle, title
 
@@ -80,19 +80,20 @@ def add_header(menu: Optional[ui.left_drawer] = None) -> None:
         if menu:
             ui.button(on_click=menu.toggle).props('flat color=white icon=menu round').classes('lg:hidden')
         with ui.link(target=index_page).classes('row gap-4 items-center no-wrap mr-auto'):
-            svg.face().classes('w-8 stroke-white stroke-2')
+            svg.face().classes('w-8 stroke-white stroke-2 max-[550px]:hidden')
             svg.word().classes('w-24')
         with ui.row().classes('max-lg:hidden'):
             for title, target in menu_items.items():
                 ui.link(title, target).classes(replace='text-lg text-white')
-        search()
-        with ui.link(target='https://discord.gg/TEpFeAaF4f').classes('max-[435px]:hidden').tooltip('Discord'):
+        search = Search()
+        search.create_button()
+        with ui.link(target='https://discord.gg/TEpFeAaF4f').classes('max-[445px]:hidden').tooltip('Discord'):
             svg.discord().classes('fill-white scale-125 m-1')
-        with ui.link(target='https://www.reddit.com/r/nicegui/').classes('max-[385px]:hidden').tooltip('Reddit'):
+        with ui.link(target='https://www.reddit.com/r/nicegui/').classes('max-[395px]:hidden').tooltip('Reddit'):
             svg.reddit().classes('fill-white scale-125 m-1')
         with ui.link(target='https://github.com/zauberzeug/nicegui/').tooltip('GitHub'):
             svg.github().classes('fill-white scale-125 m-1')
-        add_star().classes('max-[480px]:hidden')
+        add_star().classes('max-[490px]:hidden')
         with ui.row().classes('lg:hidden'):
             with ui.button().props('flat color=white icon=more_vert round'):
                 with ui.menu().classes('bg-primary text-white text-lg').props(remove='no-parent-event'):

+ 55 - 7
website/search.py

@@ -1,11 +1,59 @@
-from nicegui.dependencies import register_component
-from nicegui.element import Element
+from nicegui import events, ui
 
-register_component('search', __file__, 'search.vue')
 
-
-class search(Element):
+class Search:
 
     def __init__(self) -> None:
-        """Search NiceGUI documentation"""
-        super().__init__('search')
+        ui.add_head_html(r'''
+            <script>
+            async function loadSearchData() {
+                const response = await fetch("/static/search_index.json");
+                if (!response.ok) {
+                    throw new Error(`HTTP error! status: ${response.status}`);
+                }
+                const searchData = await response.json();
+                const options = {
+                    keys: [
+                        { name: "title", weight: 0.7 },
+                        { name: "content", weight: 0.3 },
+                    ],
+                    tokenize: true, // each word is ranked individually
+                    threshold: 0.3,
+                    location: 0,
+                    distance: 10000,
+                };
+                window.fuse = new Fuse(searchData, options);
+            }
+            loadSearchData();
+            </script>
+        ''')
+        with ui.dialog() as self.dialog, ui.card().tight().classes('w-[800px] h-[600px]'):
+            with ui.row().classes('w-full items-center px-4'):
+                ui.icon('search', size='2em')
+                ui.input(placeholder='Search documentation', on_change=self.handle_input) \
+                    .classes('flex-grow').props('borderless autofocus')
+                ui.button('ESC').props('padding="2px 8px" outline size=sm color=grey-5').classes('shadow')
+            ui.separator()
+            self.results = ui.element('q-list').classes('w-full').props('separator')
+        ui.keyboard(self.handle_keypress)
+
+    def create_button(self) -> ui.button:
+        return ui.button(on_click=self.dialog.open).props('flat icon=search color=white')
+
+    def handle_keypress(self, e: events.KeyEventArguments) -> None:
+        if not e.action.keydown:
+            return
+        if e.key == '/':
+            self.dialog.open()
+        if e.key == 'k' and (e.modifiers.ctrl or e.modifiers.meta):
+            self.dialog.open()
+
+    async def handle_input(self, e: events.ValueChangeEventArguments) -> None:
+        self.results.clear()
+        with self.results:
+            for result in await ui.run_javascript(f'return window.fuse.search("{e.value}").slice(0, 50)'):
+                href: str = result['item']['url']
+                target = 'blank' if href.startswith('http') else ''
+                with ui.element('q-item').props(f'clickable href={href} target={target}'):
+                    with ui.element('q-item-section'):
+                        ui.label(result['item']['title'])

+ 0 - 85
website/search.vue

@@ -1,85 +0,0 @@
-<template>
-  <div class="q-pa-md relative">
-    <q-input
-      v-model="query"
-      dense
-      dark
-      standout
-      :input-class="inputClass"
-      @focus="focused = true"
-      @blur="focused = false"
-    >
-      <template v-slot:append>
-        <q-icon v-if="query === ''" name="search" :class="{ 'text-primary': focused }" />
-        <q-icon v-else name="clear" class="cursor-pointer" @click="query = ''" :class="{ 'text-primary': focused }" />
-      </template>
-    </q-input>
-    <q-list class="bg-primary shadow-lg rounded mt-5 w-64 absolute text-white z-50 max-h-[200px] overflow-y-auto">
-      <q-item clickable v-for="result in results" :key="result.item.title" @click="goTo(result.item.url)">
-        <q-item-section>
-          <q-item-label>{{ result.item.title }}</q-item-label>
-        </q-item-section>
-      </q-item>
-    </q-list>
-  </div>
-</template>
-
-<script>
-export default {
-  data() {
-    return {
-      query: "",
-      focused: false,
-      results: [],
-      searchData: [],
-      fuse: null,
-    };
-  },
-
-  watch: {
-    query() {
-      this.search();
-    },
-  },
-
-  computed: {
-    inputClass() {
-      return this.focused ? "text-primary" : "";
-    },
-  },
-
-  async created() {
-    let response = await fetch("/static/search_index.json");
-    if (!response.ok) {
-      throw new Error(`HTTP error! status: ${response.status}`);
-    }
-    this.searchData = await response.json();
-    let options = {
-      keys: [
-        { name: "title", weight: 0.7 },
-        { name: "content", weight: 0.3 },
-      ],
-      tokenize: true, // each word is ranked individually
-      threshold: 0.3,
-      location: 0,
-      distance: 10000,
-    };
-
-    this.fuse = new Fuse(this.searchData, options);
-  },
-
-  methods: {
-    search() {
-      this.results = this.fuse.search(this.query);
-    },
-    goTo(url) {
-      if (url.startsWith("http")) {
-        window.open(url, "_blank");
-      } else {
-        window.location.href = url;
-      }
-      this.query = "";
-    },
-  },
-};
-</script>