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
// 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');

View File

@ -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);
*/
}

View File

@ -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<PageInfo>,
pub direct_hit_pages: Vec<PageInfo>,
}
impl<T> Index<T>
@ -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<String> = results.matching_tags.iter().cloned().collect();
let mut unmatched_tags: HashMap<Tag, usize> = Default::default();
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
.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,
}
}
}

View File

@ -63,6 +63,7 @@ fn get_rid_of_windows_newlines(s: String) -> String {
struct GetParams {
edit: Option<bool>,
id: Option<String>,
q: Option<String>,
}
#[derive(Deserialize, Debug)]
@ -117,6 +118,20 @@ async fn handle_script_js() -> std::result::Result<warp::http::Response<String>,
.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(
state: Arc<State>,
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::<GetParams>())
.and_then(handle_query))
.or(with_state(state.clone())
.and(warp::path::full())
.and(warp::query::<GetParams>())

View File

@ -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:]]*<!--+(.*)--+>(.*)\z")
regex::RegexBuilder::new(r"\A[[:space:]]*<!--+(.*?)--+>(.*)\z")
.multi_line(true)
.dot_matches_new_line(true)
.build()

View File

@ -11,6 +11,7 @@ pub struct PageState {
pub path: String,
pub edit: bool,
pub page: Option<Parsed>,
pub subtags: Vec<(String, usize)>,
}
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> {
if page_state.edit {
Box::new(page_editing_view(page_state)) as Box<dyn RenderBox>
if page_state.edit.clone() {
Box::new(page_editing_view(page_state.clone())) as Box<dyn RenderBox>
} 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(),
Some(
(box_html! {
textarea(name="body", id="source-editor", autofocus) {
textarea(name="body", id="source-editor", class="append", autofocus) {
: body
}
}) as Box<dyn RenderBox>,
),
)
} 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(
page_state.clone(),
Some(
(box_html! {
textarea(name="body", id="source-editor", autofocus);
textarea(name="body", id="source-editor", class="prepend", autofocus) {
: starting_text
}
}) 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 {
let id = page_state.page.map(|p| p.id().to_owned());
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! {
form {
form(class="pure-form") {
div(class="pure-menu pure-menu-horizontal") {
@ if let Some(id) = id.as_deref() {
input(type="hidden", name="id", value=id);
@ -97,12 +134,11 @@ pub fn menu(page_state: PageState, subform: Option<Box<dyn RenderBox>>) -> impl
}
: " ";
} 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() {
input(type="hidden", name="edit", value="true");
button(type="submit", id="edit-button", class="pure-button pure-button-primary", formaction=".", formmethod="get"){
a(type="submit", href=format!("?id={}&edit=true", id.as_ref().unwrap()), id="edit-button", class="pure-button pure-button-primary"){
: 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")
}
}
: " ";
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
}
}
}
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 page = page_state.page.expect("always some");
let page_html = page.html.clone();
owned_html! {
: 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);
owned_html! {
: menu;
ul(id="index") {
@ for tag in unmatched_tags {
li {
a(href=format!("./{}/", tag.0)) : format!("{} ({})", tag.0, tag.1)
div(id="page-content") {
h1 { : "Subpages" }
ul(id="index") {
@ for post in posts {
li {
a(href=format!("./?id={}", post.id)) : post.title
}
}
}
@ for post in posts {
li {
a(href=format!("./?id={}", post.id)) : post.title
@ for tag in unmatched_tags {
li {
a(href=format!("./{}/", tag.0)) : format!("{} ({})", tag.0, tag.1)
}
}
}
}