Refactoring & rework

This commit is contained in:
Dawid Ciężarkiewicz 2020-05-12 23:53:33 -07:00
parent f98e912f15
commit 70163f9cc7
6 changed files with 226 additions and 62 deletions

View File

@ -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 // Modified: https://stackoverflow.com/a/35173443/12271202
// dir: 1 for down, -1 for up // dir: 1 for down, -1 for up
function indexFocusSwitch(dir) { function indexFocusSwitch(dir) {
let indexList = document.getElementById('index'); const parentArea = document.getElementById('page-content');
if (indexList) { if (parentArea) {
var focussableElements = 'a:not([disabled])'; 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) { function (element) {
return true; return true;
} }
@ -37,7 +70,8 @@ function indexFocusSwitch(dir) {
} }
Mousetrap.bind('n', function() { Mousetrap.bind('n', function() {
window.location.href = '/?edit=true'; // window.location.href = '?edit=true';
reliableClickById('new-button');
}); });
Mousetrap.bind('e', function() { Mousetrap.bind('e', function() {
@ -59,12 +93,15 @@ Mousetrap.bind('s', function() {
Mousetrap.bindGlobal('esc', function() { Mousetrap.bindGlobal('esc', function() {
let textarea = document.getElementById('source-editor'); let textarea = document.getElementById('source-editor');
let queryText = document.getElementById('query-text');
if (textarea && textarea === document.activeElement) { if (textarea && textarea === document.activeElement) {
textarea.blur(); textarea.blur();
let button = document.getElementById('cancel-button'); let button = document.getElementById('cancel-button');
if (button) { if (button) {
button.focus(); button.focus();
} }
} else if (queryText && queryText === document.activeElement) {
queryText.blur();
} else { } else {
reliableClickById('cancel-button'); reliableClickById('cancel-button');
reliableClickById('up-button'); reliableClickById('up-button');
@ -91,5 +128,16 @@ Mousetrap.bind(['ctrl+up', 'k'], function() {
indexFocusSwitch(-1); 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 // auto-select first element on index pages
indexFocusSwitch(1); indexFocusSwitch(1);
// if we're editing a page, let's start at the the right placeA
smartFocusEditById('source-editor');

View File

@ -1,5 +1,5 @@
body { body {
max-width: 40em; max-width: 50em;
margin: 0 auto; margin: 0 auto;
} }
@ -16,8 +16,36 @@ a.pure-button {
background: rgb(28, 184, 65); background: rgb(28, 184, 65);
} }
.float-right {
float: right;
}
/* TOOD: make the whole form exactly fit the screen (including the buttons) */ /* TOOD: make the whole form exactly fit the screen (including the buttons) */
textarea { textarea {
height: 90vh; height: 90vh;
width: 100%; 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);
*/
}

View File

@ -43,7 +43,7 @@ pub struct CompactResults {
// all tags that were not already filtered on // all tags that were not already filtered on
pub tags: Vec<(Tag, usize)>, pub tags: Vec<(Tag, usize)>,
// all pages that can't be reached by one of the `tags` // all pages that can't be reached by one of the `tags`
pub pages: Vec<PageInfo>, pub direct_hit_pages: Vec<PageInfo>,
} }
impl<T> Index<T> impl<T> Index<T>
@ -77,7 +77,7 @@ where
} }
/// Compact the results to a shorter form /// 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<String> = results.matching_tags.iter().cloned().collect(); let matching_tags: HashSet<String> = results.matching_tags.iter().cloned().collect();
let mut unmatched_tags: HashMap<Tag, usize> = Default::default(); let mut unmatched_tags: HashMap<Tag, usize> = Default::default();
for page_info in &results.matching_pages { for page_info in &results.matching_pages {
@ -88,25 +88,29 @@ where
} }
} }
let mut pages: Vec<PageInfo> = results let mut direct_hit_pages: Vec<PageInfo> = results
.matching_pages .matching_pages
.into_iter() .iter()
.filter(|page_info| { .filter(|page_info| {
self.tags_by_page_id[&page_info.id] self.tags_by_page_id[&page_info.id]
.iter() .iter()
.filter(|page_tag| !matching_tags.contains(page_tag.as_str())) .filter(|page_tag| !matching_tags.contains(page_tag.as_str()))
.count() .count()
< 5 == 0
}) })
.cloned()
.collect(); .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(); 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))); 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,
}
} }
} }

View File

@ -63,6 +63,7 @@ fn get_rid_of_windows_newlines(s: String) -> String {
struct GetParams { struct GetParams {
edit: Option<bool>, edit: Option<bool>,
id: Option<String>, id: Option<String>,
q: Option<String>,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
@ -117,6 +118,20 @@ async fn handle_script_js() -> std::result::Result<warp::http::Response<String>,
.expect("correct response")) .expect("correct response"))
} }
async fn handle_query(
query: GetParams,
) -> std::result::Result<warp::http::Response<&'static str>, warp::Rejection> {
let q = "/".to_string()
+ &query
.q
.unwrap_or_else(|| String::new())
.split(" ")
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join("/");
Ok(warp_temporary_redirect_to_get_method(&q))
}
async fn handle_post_wrapped( async fn handle_post_wrapped(
state: Arc<State>, state: Arc<State>,
path: FullPath, path: FullPath,
@ -241,52 +256,71 @@ async fn handle_get(
path.as_str() 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 tags = path_to_tags(&path);
let page_state = render::PageState { let page_state = render::PageState {
page: None, page: None,
edit: query.edit.is_some(), edit: query.edit.is_some(),
path: path.as_str().to_string(), path: path.as_str().to_string(),
subtags: vec![],
}; };
let read = state.page_store.read().await; 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()); let results = read.find(tags.as_slice());
if results.matching_tags != tags { if results.matching_tags != tags {
return Ok(Box::new(warp_temporary_redirect( return Ok(Box::new(warp_temporary_redirect(
&("/".to_string() + &results.matching_tags.join("/")), &("/".to_string() + &results.matching_tags.join("/")),
))); )));
} }
if results.matching_pages.len() == 1 {
let page = read.get(results.matching_pages[0].id.clone()).await?; let (page_id, subtags) = if query.edit.is_some() {
Ok(warp_reply_from_render(render::html_page(render::page( (query.id, vec![])
render::PageState { } else if results.matching_pages.len() == 1 {
page: Some(page), (Some(results.matching_pages[0].id.clone()), vec![])
..page_state
},
))))
} else { } else {
let compact_results = read.compact_results(results); let compact_results = read.compact_results(&results);
Ok(warp_reply_from_render(render::html_page( if let Some(q_id) = query.id {
render::post_list( (Some(q_id), compact_results.tags)
page_state, } else if compact_results.direct_hit_pages.len() == 1 {
compact_results.tags.into_iter(), (
compact_results.pages.into_iter(), 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<()> { async fn start(opts: &cli::Opts) -> Result<()> {
@ -300,6 +334,9 @@ async fn start(opts: &cli::Opts) -> Result<()> {
let handler = warp::any() let handler = warp::any()
.and(warp::path!("_style.css").and_then(handle_style_css)) .and(warp::path!("_style.css").and_then(handle_style_css))
.or(warp::path!("_script.js").and_then(handle_script_js)) .or(warp::path!("_script.js").and_then(handle_script_js))
.or(warp::path!("_query")
.and(warp::query::<GetParams>())
.and_then(handle_query))
.or(with_state(state.clone()) .or(with_state(state.clone())
.and(warp::path::full()) .and(warp::path::full())
.and(warp::query::<GetParams>()) .and(warp::query::<GetParams>())

View File

@ -31,7 +31,7 @@ pub struct Parsed {
fn split_headers_and_body(source: &Source) -> (&str, &str) { fn split_headers_and_body(source: &Source) -> (&str, &str) {
lazy_static! { lazy_static! {
static ref RE: regex::Regex = static ref RE: regex::Regex =
regex::RegexBuilder::new(r"\A[[:space:]]*<!--+(.*)--+>(.*)\z") regex::RegexBuilder::new(r"\A[[:space:]]*<!--+(.*?)--+>(.*)\z")
.multi_line(true) .multi_line(true)
.dot_matches_new_line(true) .dot_matches_new_line(true)
.build() .build()

View File

@ -11,6 +11,7 @@ pub struct PageState {
pub path: String, pub path: String,
pub edit: bool, pub edit: bool,
pub page: Option<Parsed>, pub page: Option<Parsed>,
pub subtags: Vec<(String, usize)>,
} }
pub fn html_page(body: impl RenderOnce) -> impl RenderOnce { pub fn html_page(body: impl RenderOnce) -> impl RenderOnce {
@ -30,10 +31,23 @@ pub fn html_page(body: impl RenderOnce) -> impl RenderOnce {
} }
pub fn page(page_state: PageState) -> Box<dyn RenderBox> { pub fn page(page_state: PageState) -> Box<dyn RenderBox> {
if page_state.edit { if page_state.edit.clone() {
Box::new(page_editing_view(page_state)) as Box<dyn RenderBox> Box::new(page_editing_view(page_state.clone())) as Box<dyn RenderBox>
} else { } else {
Box::new(page_view(page_state)) as Box<dyn RenderBox> let page_state_clone = page_state.clone();
let sub_pages = owned_html! {
@ if !page_state_clone.subtags.is_empty() {
h1 { : "Subpages" }
ul {
@ for tag in &page_state_clone.subtags {
li {
a(href=format!("./{}/", tag.0)) : format!("{} ({})", tag.0, tag.1)
}
}
}
}
};
Box::new(page_view(page_state.clone(), sub_pages)) as Box<dyn RenderBox>
} }
} }
@ -44,18 +58,28 @@ pub fn page_editing_view(page_state: PageState) -> impl RenderOnce {
page_state.clone(), page_state.clone(),
Some( Some(
(box_html! { (box_html! {
textarea(name="body", id="source-editor", autofocus) { textarea(name="body", id="source-editor", class="append", autofocus) {
: body : body
} }
}) as Box<dyn RenderBox>, }) as Box<dyn RenderBox>,
), ),
) )
} else { } else {
let starting_tags = page_state
.path
.split("/")
.filter(|t| !t.trim().is_empty())
.map(|t| format!("#{}", t))
.collect::<Vec<_>>()
.join(" ");
let starting_text = "\n\n\n".to_string() + &starting_tags;
menu( menu(
page_state.clone(), page_state.clone(),
Some( Some(
(box_html! { (box_html! {
textarea(name="body", id="source-editor", autofocus); textarea(name="body", id="source-editor", class="prepend", autofocus) {
: starting_text
}
}) as Box<dyn RenderBox>, }) as Box<dyn RenderBox>,
), ),
) )
@ -65,9 +89,22 @@ pub fn page_editing_view(page_state: PageState) -> impl RenderOnce {
pub fn menu(page_state: PageState, subform: Option<Box<dyn RenderBox>>) -> impl RenderOnce { pub fn menu(page_state: PageState, subform: Option<Box<dyn RenderBox>>) -> impl RenderOnce {
let id = page_state.page.map(|p| p.id().to_owned()); let id = page_state.page.map(|p| p.id().to_owned());
let edit = page_state.edit; let edit = page_state.edit;
let path_tags: String = page_state
.path
.split("/")
.filter(|f| !f.trim().is_empty())
.collect::<Vec<&str>>()
.join(" ");
// # The sucky menu mega-form
// I really want one top-bar with all the buttons, and because I want
// everything to work even without JS enabled, all stuff here is quirky
// * GET queries are just links to avoid conflicting with other inputs
// * other buttons use `_method=METHOD` and `formaction` and `formmethod` + server-side redirect
// * query text uses a server-side redirect on `q`
// If more stuff is cramed in here, this will all eventually fall appart. :)
owned_html! { owned_html! {
form { form(class="pure-form") {
div(class="pure-menu pure-menu-horizontal") { div(class="pure-menu pure-menu-horizontal") {
@ if let Some(id) = id.as_deref() { @ if let Some(id) = id.as_deref() {
input(type="hidden", name="id", value=id); input(type="hidden", name="id", value=id);
@ -97,12 +134,11 @@ pub fn menu(page_state: PageState, subform: Option<Box<dyn RenderBox>>) -> impl
} }
: " "; : " ";
} else { } else {
a(href="?edit=true", class="pure-button button-green"){ : Raw("<u>N</u>ew") } a(href="?edit=true", id="new-button", class="pure-button button-green"){ : Raw("<u>N</u>ew") }
: " "; : " ";
} }
@ if !edit && id.is_some() { @ if !edit && id.is_some() {
input(type="hidden", name="edit", value="true"); a(type="submit", href=format!("?id={}&edit=true", id.as_ref().unwrap()), id="edit-button", class="pure-button pure-button-primary"){
button(type="submit", id="edit-button", class="pure-button pure-button-primary", formaction=".", formmethod="get"){
: Raw("<u>E</u>dit") : Raw("<u>E</u>dit")
} }
: " "; : " ";
@ -110,19 +146,27 @@ pub fn menu(page_state: PageState, subform: Option<Box<dyn RenderBox>>) -> impl
: Raw("<u>D</u>elete") : Raw("<u>D</u>elete")
} }
} }
: " ";
button(type="submit", id="query-button", class="pure-button float-right", formaction="/_query", formmethod="get") {
: "Search"
}
input(type="text", class="float-right", id="query-text", name="q", placeholder="tag1 tag2...", value=path_tags);
} }
: subform : subform
} }
} }
} }
pub fn page_view(page_state: PageState) -> impl RenderOnce { pub fn page_view(page_state: PageState, sub_pages: impl RenderOnce) -> impl RenderOnce {
let menu = menu(page_state.clone(), None); let menu = menu(page_state.clone(), None);
let page = page_state.page.expect("always some"); let page = page_state.page.expect("always some");
let page_html = page.html.clone(); let page_html = page.html.clone();
owned_html! { owned_html! {
: menu; : menu;
: Raw(page_html) article(id="page-content") {
: Raw(page_html);
: sub_pages;
}
} }
} }
@ -134,15 +178,18 @@ pub fn post_list(
let menu = menu(page_state.clone(), None); let menu = menu(page_state.clone(), None);
owned_html! { owned_html! {
: menu; : menu;
ul(id="index") { div(id="page-content") {
@ for tag in unmatched_tags { h1 { : "Subpages" }
li { ul(id="index") {
a(href=format!("./{}/", tag.0)) : format!("{} ({})", tag.0, tag.1) @ for post in posts {
li {
a(href=format!("./?id={}", post.id)) : post.title
}
} }
} @ for tag in unmatched_tags {
@ for post in posts { li {
li { a(href=format!("./{}/", tag.0)) : format!("{} ({})", tag.0, tag.1)
a(href=format!("./?id={}", post.id)) : post.title }
} }
} }
} }