dioxus_docs_kit/components/
search_modal.rs1use dioxus::prelude::*;
2use dioxus_mdx::HttpMethod;
3
4use super::search_shell::{SearchHit, SearchModalShell};
5use crate::DocsContext;
6use crate::registry::DocsRegistry;
7use crate::search::{SNIPPET_WINDOW, build_snippet, split_terms};
8
9#[component]
11pub fn SearchModal() -> Element {
12 let ctx = use_context::<DocsContext>();
13 let registry = use_context::<&'static DocsRegistry>();
14
15 let search = use_callback(move |query: String| {
16 let terms = split_terms(&query);
17 registry
18 .search_docs(&query)
19 .into_iter()
20 .map(|entry| {
21 let target = if entry.anchor.is_empty() {
24 entry.path.clone()
25 } else {
26 format!("{}#{}", entry.path, entry.anchor)
27 };
28 let (title, context) = if entry.heading.is_empty() {
30 (entry.title.clone(), None)
31 } else {
32 (entry.heading.clone(), Some(entry.title.clone()))
33 };
34 let snippet_src = if entry.body.is_empty() {
35 &entry.description
36 } else {
37 &entry.body
38 };
39 SearchHit {
40 target,
41 title,
42 context,
43 badge: entry.api_method.map(method_badge),
44 meta: entry.breadcrumb.clone(),
45 tags: Vec::new(),
46 snippet: build_snippet(snippet_src, &terms, SNIPPET_WINDOW),
47 }
48 })
49 .collect()
50 });
51
52 let on_select = use_callback(move |target: String| {
53 let mut parts = target.splitn(2, '#');
55 let path = parts.next().unwrap_or_default().to_string();
56 let anchor = parts.next().unwrap_or_default().to_string();
57
58 (ctx.navigate)(path);
59
60 #[cfg(target_arch = "wasm32")]
62 if !anchor.is_empty() {
63 scroll_to_anchor(anchor);
64 }
65 #[cfg(not(target_arch = "wasm32"))]
66 let _ = anchor;
67 });
68
69 rsx! {
70 SearchModalShell {
71 placeholder: "Search documentation...",
72 search,
73 on_select,
74 }
75 }
76}
77
78#[cfg(target_arch = "wasm32")]
83fn scroll_to_anchor(anchor: String) {
84 spawn(async move {
85 let js = format!(
86 r#"
87 (function() {{
88 const id = {};
89 let attempts = 0;
90 function tryScroll() {{
91 const el = document.getElementById(id);
92 if (el) {{
93 el.scrollIntoView({{ behavior: 'smooth', block: 'start' }});
94 history.replaceState(null, '', '#' + id);
95 return;
96 }}
97 if (attempts++ < 20) {{
98 requestAnimationFrame(tryScroll);
99 }}
100 }}
101 requestAnimationFrame(tryScroll);
102 }})();
103 "#,
104 serde_json::to_string(&anchor).unwrap_or_default()
105 );
106 let _ = document::eval(&js);
107 });
108}
109
110fn method_badge(method: HttpMethod) -> (&'static str, &'static str) {
111 match method {
112 HttpMethod::Get => ("GET", "badge-soft badge-success"),
113 HttpMethod::Post => ("POST", "badge-soft badge-primary"),
114 HttpMethod::Put => ("PUT", "badge-soft badge-warning"),
115 HttpMethod::Delete => ("DEL", "badge-soft badge-error"),
116 HttpMethod::Patch => ("PATCH", "badge-soft badge-info"),
117 _ => ("???", "badge-soft badge-ghost"),
118 }
119}