mirror of
https://github.com/dpc/tagwiki
synced 2025-04-07 08:06:55 +00:00
Refactoring & rework
This commit is contained in:
parent
f98e912f15
commit
70163f9cc7
@ -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');
|
||||
|
@ -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);
|
||||
*/
|
||||
}
|
||||
|
18
src/index.rs
18
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<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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
97
src/main.rs
97
src/main.rs
@ -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>())
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user