search.py 4.2 KB

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