diff --git a/resources/script.js b/resources/script.js index 04478f4..9e81f8f 100644 --- a/resources/script.js +++ b/resources/script.js @@ -17,13 +17,46 @@ function reliableClickById(id) { } } +function focusById(id) { + const elem = document.getElementById(id); + if (elem) { + elem.focus(); + } +} + +function focusAppendToById(id) { + const elem = document.getElementById(id); + if (elem) { + elem.focus(); + elem.setSelectionRange(elem.value.length, elem.value.length); + } +} + +function focusPrependToById(id) { + const elem = document.getElementById(id); + if (elem) { + elem.focus(); + elem.setSelectionRange(0, 0); + } +} +function smartFocusEditById(id) { + const elem = document.getElementById(id); + if (elem) { + if (elem.classList.contains("prepend")) { + focusPrependToById(id); + } else { + focusAppendToById(id); + } + } +} + // Modified: https://stackoverflow.com/a/35173443/12271202 // dir: 1 for down, -1 for up function indexFocusSwitch(dir) { - let indexList = document.getElementById('index'); - if (indexList) { + const parentArea = document.getElementById('page-content'); + if (parentArea) { var focussableElements = 'a:not([disabled])'; - var focussable = Array.prototype.filter.call(indexList.querySelectorAll(focussableElements), + var focussable = Array.prototype.filter.call(parentArea.querySelectorAll(focussableElements), function (element) { return true; } @@ -37,7 +70,8 @@ function indexFocusSwitch(dir) { } Mousetrap.bind('n', function() { - window.location.href = '/?edit=true'; + // window.location.href = '?edit=true'; + reliableClickById('new-button'); }); Mousetrap.bind('e', function() { @@ -59,12 +93,15 @@ Mousetrap.bind('s', function() { Mousetrap.bindGlobal('esc', function() { let textarea = document.getElementById('source-editor'); + let queryText = document.getElementById('query-text'); if (textarea && textarea === document.activeElement) { textarea.blur(); let button = document.getElementById('cancel-button'); if (button) { button.focus(); } + } else if (queryText && queryText === document.activeElement) { + queryText.blur(); } else { reliableClickById('cancel-button'); reliableClickById('up-button'); @@ -91,5 +128,16 @@ Mousetrap.bind(['ctrl+up', 'k'], function() { indexFocusSwitch(-1); }); +Mousetrap.bind(['/'], function() { + focusAppendToById('query-text'); +}); + +Mousetrap.bindGlobal(['alt+/', 'alt+f', 'alt+q'], function() { + focusAppendToById('query-text'); +}); + // auto-select first element on index pages indexFocusSwitch(1); + +// if we're editing a page, let's start at the the right placeA +smartFocusEditById('source-editor'); diff --git a/resources/style.css b/resources/style.css index d83cc76..d15e087 100644 --- a/resources/style.css +++ b/resources/style.css @@ -1,5 +1,5 @@ body { - max-width: 40em; + max-width: 50em; margin: 0 auto; } @@ -16,8 +16,36 @@ a.pure-button { background: rgb(28, 184, 65); } +.float-right { + float: right; +} + /* TOOD: make the whole form exactly fit the screen (including the buttons) */ textarea { height: 90vh; width: 100%; } + +/* nice glow on selected stuff */ +input[type=text], textarea { + /* + -webkit-transition: all 0.30s ease-in-out; + -moz-transition: all 0.30s ease-in-out; + -ms-transition: all 0.30s ease-in-out; + -o-transition: all 0.30s ease-in-out; + outline: none; + padding: 3px 0px 3px 3px; + margin: 5px 1px 3px 0px; + border: 1px solid #DDDDDD; + */ +} + +input[type=text]:focus, textarea:focus, a:focus { + + box-shadow: 0 0 10px rgba(81, 203, 238, 1); + /* + padding: 3px 0px 3px 3px; + margin: 5px 1px 3px 0px; + border: 1px solid rgba(81, 203, 238, 1); + */ +} diff --git a/src/index.rs b/src/index.rs index 80dfb55..2d17879 100644 --- a/src/index.rs +++ b/src/index.rs @@ -43,7 +43,7 @@ pub struct CompactResults { // all tags that were not already filtered on pub tags: Vec<(Tag, usize)>, // all pages that can't be reached by one of the `tags` - pub pages: Vec, + pub direct_hit_pages: Vec, } impl Index @@ -77,7 +77,7 @@ where } /// Compact the results to a shorter form - pub fn compact_results(&self, results: FindResults) -> CompactResults { + pub fn compact_results(&self, results: &FindResults) -> CompactResults { let matching_tags: HashSet = results.matching_tags.iter().cloned().collect(); let mut unmatched_tags: HashMap = Default::default(); for page_info in &results.matching_pages { @@ -88,25 +88,29 @@ where } } - let mut pages: Vec = results + let mut direct_hit_pages: Vec = results .matching_pages - .into_iter() + .iter() .filter(|page_info| { self.tags_by_page_id[&page_info.id] .iter() .filter(|page_tag| !matching_tags.contains(page_tag.as_str())) .count() - < 5 + == 0 }) + .cloned() .collect(); - pages.sort_by(|a, b| a.title.cmp(&b.title)); + direct_hit_pages.sort_by(|a, b| a.title.cmp(&b.title)); let mut tags: Vec<_> = unmatched_tags.into_iter().collect(); tags.sort_by(|a, b| a.1.cmp(&b.1).reverse().then_with(|| a.0.cmp(&b.0))); - CompactResults { tags, pages } + CompactResults { + tags, + direct_hit_pages, + } } } diff --git a/src/main.rs b/src/main.rs index a16ee7f..cddd4a4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -63,6 +63,7 @@ fn get_rid_of_windows_newlines(s: String) -> String { struct GetParams { edit: Option, id: Option, + q: Option, } #[derive(Deserialize, Debug)] @@ -117,6 +118,20 @@ async fn handle_script_js() -> std::result::Result, .expect("correct response")) } +async fn handle_query( + query: GetParams, +) -> std::result::Result, warp::Rejection> { + let q = "/".to_string() + + &query + .q + .unwrap_or_else(|| String::new()) + .split(" ") + .filter(|s| !s.is_empty()) + .collect::>() + .join("/"); + Ok(warp_temporary_redirect_to_get_method(&q)) +} + async fn handle_post_wrapped( state: Arc, path: FullPath, @@ -241,52 +256,71 @@ async fn handle_get( path.as_str() )))); } + + // because of this sucky mega-form, are getting here on enter + // (which submits values of all inputs in the form) + // to help with it - redirect to the right place, just like + // if the user hit the "Search" button + if let Some(q) = query.q.as_ref() { + return Ok(Box::new(warp_temporary_redirect(&format!( + "/_query?q={}", + q + )))); + } let tags = path_to_tags(&path); let page_state = render::PageState { page: None, edit: query.edit.is_some(), path: path.as_str().to_string(), + subtags: vec![], }; let read = state.page_store.read().await; - if let Some(q_id) = query.id { - let page = read.get(q_id).await?; - return Ok(warp_reply_from_render(render::html_page(render::page( - render::PageState { - page: Some(page), - ..page_state - }, - )))); - } else if query.edit.is_some() { - return Ok(warp_reply_from_render(render::html_page(render::page( - page_state, - )))); - } let results = read.find(tags.as_slice()); if results.matching_tags != tags { return Ok(Box::new(warp_temporary_redirect( &("/".to_string() + &results.matching_tags.join("/")), ))); } - if results.matching_pages.len() == 1 { - let page = read.get(results.matching_pages[0].id.clone()).await?; - Ok(warp_reply_from_render(render::html_page(render::page( - render::PageState { - page: Some(page), - ..page_state - }, - )))) + + let (page_id, subtags) = if query.edit.is_some() { + (query.id, vec![]) + } else if results.matching_pages.len() == 1 { + (Some(results.matching_pages[0].id.clone()), vec![]) } else { - let compact_results = read.compact_results(results); - Ok(warp_reply_from_render(render::html_page( - render::post_list( - page_state, - compact_results.tags.into_iter(), - compact_results.pages.into_iter(), - ), - ))) - } + let compact_results = read.compact_results(&results); + if let Some(q_id) = query.id { + (Some(q_id), compact_results.tags) + } else if compact_results.direct_hit_pages.len() == 1 { + ( + Some(compact_results.direct_hit_pages[0].id.clone()), + compact_results.tags, + ) + } else { + return Ok(warp_reply_from_render(render::html_page( + render::post_list( + page_state, + compact_results.tags.into_iter(), + compact_results.direct_hit_pages.into_iter(), + ), + ))); + } + }; + + let page = if let Some(page_id) = page_id { + Some(read.get(page_id).await?) + } else { + None + }; + + Ok(warp_reply_from_render(render::html_page(render::page( + render::PageState { + page, + subtags, + ..page_state + }, + )))) } async fn start(opts: &cli::Opts) -> Result<()> { @@ -300,6 +334,9 @@ async fn start(opts: &cli::Opts) -> Result<()> { let handler = warp::any() .and(warp::path!("_style.css").and_then(handle_style_css)) .or(warp::path!("_script.js").and_then(handle_script_js)) + .or(warp::path!("_query") + .and(warp::query::()) + .and_then(handle_query)) .or(with_state(state.clone()) .and(warp::path::full()) .and(warp::query::()) diff --git a/src/page.rs b/src/page.rs index 34427b2..ab3264d 100644 --- a/src/page.rs +++ b/src/page.rs @@ -31,7 +31,7 @@ pub struct Parsed { fn split_headers_and_body(source: &Source) -> (&str, &str) { lazy_static! { static ref RE: regex::Regex = - regex::RegexBuilder::new(r"\A[[:space:]]*