search.py 3.4 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
  1. from nicegui import __version__, app, background_tasks, events, ui
  2. class Search:
  3. def __init__(self) -> None:
  4. ui.add_head_html(r'''
  5. <script>
  6. async function loadSearchData() {
  7. const response = await fetch("/static/search_index.json?version=''' + __version__ + r'''");
  8. if (!response.ok) {
  9. throw new Error(`HTTP error! status: ${response.status}`);
  10. }
  11. const searchData = await response.json();
  12. const options = {
  13. keys: [
  14. { name: "title", weight: 0.7 },
  15. { name: "content", weight: 0.3 },
  16. ],
  17. tokenize: true, // each word is ranked individually
  18. threshold: 0.3,
  19. location: 0,
  20. distance: 10000,
  21. };
  22. window.fuse = new Fuse(searchData, options);
  23. }
  24. loadSearchData();
  25. </script>
  26. ''')
  27. with ui.dialog() as self.dialog, ui.card().tight().classes('w-[800px] h-[600px]'):
  28. with ui.row().classes('w-full items-center px-4'):
  29. ui.icon('search', size='2em')
  30. ui.input(placeholder='Search documentation', on_change=self.handle_input) \
  31. .classes('flex-grow').props('borderless autofocus')
  32. ui.button('ESC', on_click=self.dialog.close) \
  33. .props('padding="2px 8px" outline size=sm color=grey-5').classes('shadow')
  34. ui.separator()
  35. self.results = ui.element('q-list').classes('w-full').props('separator')
  36. ui.keyboard(self.handle_keypress)
  37. def create_button(self) -> ui.button:
  38. return ui.button(on_click=self.dialog.open, icon='search').props('flat color=white') \
  39. .tooltip('Press Ctrl+K or / to search the documentation')
  40. def handle_keypress(self, e: events.KeyEventArguments) -> None:
  41. if not e.action.keydown:
  42. return
  43. if e.key == '/':
  44. self.dialog.open()
  45. if e.key == 'k' and (e.modifiers.ctrl or e.modifiers.meta):
  46. self.dialog.open()
  47. def handle_input(self, e: events.ValueChangeEventArguments) -> None:
  48. async def handle_input():
  49. with self.results:
  50. results = await ui.run_javascript(f'return window.fuse.search("{e.value}").slice(0, 100)', timeout=6)
  51. self.results.clear()
  52. for result in results:
  53. if result['item']['content']:
  54. href: str = result['item']['url']
  55. with ui.element('q-item').props('clickable') \
  56. .on('click', lambda href=href: self.open_url(href), []):
  57. with ui.element('q-item-section'):
  58. ui.label(result['item']['title']).style('font-weight: 500')
  59. ui.label(result['item']['content'][:200] + '...').classes('text-grey')
  60. background_tasks.create_lazy(handle_input(), name='handle_search_input')
  61. def open_url(self, url: str) -> None:
  62. ui.run_javascript(f'''
  63. const url = "{url}"
  64. if (url.startsWith("http"))
  65. window.open(url, "_blank");
  66. else
  67. window.location.href = url;
  68. ''')
  69. self.dialog.close()