search.py 3.9 KB

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