From f98e912f15e31d9d094dc93517198dc32f0970e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Ci=C4=99=C5=BCarkiewicz?= Date: Tue, 12 May 2020 00:15:27 -0700 Subject: [PATCH] Navigation helpers (shortcuts, etc.) --- resources/mousetrap.min.js | 11 +++++ resources/script.js | 95 ++++++++++++++++++++++++++++++++++++++ src/main.rs | 13 ++++++ src/render.rs | 52 +++++++++------------ 4 files changed, 140 insertions(+), 31 deletions(-) create mode 100644 resources/mousetrap.min.js create mode 100644 resources/script.js diff --git a/resources/mousetrap.min.js b/resources/mousetrap.min.js new file mode 100644 index 0000000..185c42f --- /dev/null +++ b/resources/mousetrap.min.js @@ -0,0 +1,11 @@ +/* mousetrap v1.6.5 craig.is/killing/mice */ +(function(q,u,c){function v(a,b,g){a.addEventListener?a.addEventListener(b,g,!1):a.attachEvent("on"+b,g)}function z(a){if("keypress"==a.type){var b=String.fromCharCode(a.which);a.shiftKey||(b=b.toLowerCase());return b}return n[a.which]?n[a.which]:r[a.which]?r[a.which]:String.fromCharCode(a.which).toLowerCase()}function F(a){var b=[];a.shiftKey&&b.push("shift");a.altKey&&b.push("alt");a.ctrlKey&&b.push("ctrl");a.metaKey&&b.push("meta");return b}function w(a){return"shift"==a||"ctrl"==a||"alt"==a|| +"meta"==a}function A(a,b){var g,d=[];var e=a;"+"===e?e=["+"]:(e=e.replace(/\+{2}/g,"+plus"),e=e.split("+"));for(g=0;gc||n.hasOwnProperty(c)&&(p[n[c]]=c)}g=p[e]?"keydown":"keypress"}"keypress"==g&&d.length&&(g="keydown");return{key:m,modifiers:d,action:g}}function D(a,b){return null===a||a===u?!1:a===b?!0:D(a.parentNode,b)}function d(a){function b(a){a= +a||{};var b=!1,l;for(l in p)a[l]?b=!0:p[l]=0;b||(x=!1)}function g(a,b,t,f,g,d){var l,E=[],h=t.type;if(!k._callbacks[a])return[];"keyup"==h&&w(a)&&(b=[a]);for(l=0;l":".","?":"/","|":"\\"},B={option:"alt",command:"meta","return":"enter", +escape:"esc",plus:"+",mod:/Mac|iPod|iPhone|iPad/.test(navigator.platform)?"meta":"ctrl"},p;for(c=1;20>c;++c)n[111+c]="f"+c;for(c=0;9>=c;++c)n[c+96]=c.toString();d.prototype.bind=function(a,b,c){a=a instanceof Array?a:[a];this._bindMultiple.call(this,a,b,c);return this};d.prototype.unbind=function(a,b){return this.bind.call(this,a,function(){},b)};d.prototype.trigger=function(a,b){if(this._directMap[a+":"+b])this._directMap[a+":"+b]({},a);return this};d.prototype.reset=function(){this._callbacks={}; +this._directMap={};return this};d.prototype.stopCallback=function(a,b){if(-1<(" "+b.className+" ").indexOf(" mousetrap ")||D(b,this.target))return!1;if("composedPath"in a&&"function"===typeof a.composedPath){var c=a.composedPath()[0];c!==a.target&&(b=c)}return"INPUT"==b.tagName||"SELECT"==b.tagName||"TEXTAREA"==b.tagName||b.isContentEditable};d.prototype.handleKey=function(){return this._handleKey.apply(this,arguments)};d.addKeycodes=function(a){for(var b in a)a.hasOwnProperty(b)&&(n[b]=a[b]);p=null}; +d.init=function(){var a=d(u),b;for(b in a)"_"!==b.charAt(0)&&(d[b]=function(b){return function(){return a[b].apply(a,arguments)}}(b))};d.init();q.Mousetrap=d;"undefined"!==typeof module&&module.exports&&(module.exports=d);"function"===typeof define&&define.amd&&define(function(){return d})}})("undefined"!==typeof window?window:null,"undefined"!==typeof window?document:null); diff --git a/resources/script.js b/resources/script.js new file mode 100644 index 0000000..04478f4 --- /dev/null +++ b/resources/script.js @@ -0,0 +1,95 @@ +/* https://raw.githubusercontent.com/ccampbell/mousetrap/master/plugins/global-bind/mousetrap-global-bind.min.js */ +(function(a){var c={},d=a.prototype.stopCallback;a.prototype.stopCallback=function(e,b,a,f){return this.paused?!0:c[a]||c[f]?!1:d.call(this,e,b,a)};a.prototype.bindGlobal=function(a,b,d){this.bind(a,b,d);if(a instanceof Array)for(b=0;b 0) { + var nextElement = focussable[(index + dir + focussable.length) % focussable.length] || focussable[0]; + nextElement.focus(); + } + } +} + +Mousetrap.bind('n', function() { + window.location.href = '/?edit=true'; +}); + +Mousetrap.bind('e', function() { + reliableClickById('edit-button'); +}); + +Mousetrap.bind('d', function() { + reliableClickById('delete-button'); +}); + +Mousetrap.bindGlobal(['alt+enter', 'ctrl+enter'], function() { + reliableClickById('save-button'); +}); + +Mousetrap.bind('s', function() { + reliableClickById('save-button'); +}); + + +Mousetrap.bindGlobal('esc', function() { + let textarea = document.getElementById('source-editor'); + if (textarea && textarea === document.activeElement) { + textarea.blur(); + let button = document.getElementById('cancel-button'); + if (button) { + button.focus(); + } + } else { + reliableClickById('cancel-button'); + reliableClickById('up-button'); + } +}); + +Mousetrap.bind(['ctrl+left', 'h'], function() { + reliableClickById('cancel-button'); + reliableClickById('up-button'); +}); + +Mousetrap.bind(['ctrl+right', 'l'], function() { + const elem = document.activeElement; + if (elem) { + reliableClick(elem); + } +}); + +Mousetrap.bind(['ctrl+down', 'j'], function() { + indexFocusSwitch(1); +}); + +Mousetrap.bind(['ctrl+up', 'k'], function() { + indexFocusSwitch(-1); +}); + +// auto-select first element on index pages +indexFocusSwitch(1); diff --git a/src/main.rs b/src/main.rs index 69941cb..a16ee7f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -106,6 +106,17 @@ async fn handle_style_css() -> std::result::Result, .expect("correct redirect")) } +async fn handle_script_js() -> std::result::Result, warp::Rejection> { + Ok(warp::http::Response::builder() + .status(200) + .header(warp::http::header::CONTENT_TYPE, "application/javascript") + .body( + include_str!("../resources/mousetrap.min.js").to_string() + + include_str!("../resources/script.js"), + ) + .expect("correct response")) +} + async fn handle_post_wrapped( state: Arc, path: FullPath, @@ -270,6 +281,7 @@ async fn handle_get( 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(), ), @@ -287,6 +299,7 @@ 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(with_state(state.clone()) .and(warp::path::full()) .and(warp::query::()) diff --git a/src/render.rs b/src/render.rs index f067559..e0917cc 100644 --- a/src/render.rs +++ b/src/render.rs @@ -23,7 +23,8 @@ pub fn html_page(body: impl RenderOnce) -> impl RenderOnce { link(rel="stylesheet", media="all", href="/_style.css"); } body { - : body + : body; + script(src="/_script.js"); } } } @@ -43,7 +44,7 @@ pub fn page_editing_view(page_state: PageState) -> impl RenderOnce { page_state.clone(), Some( (box_html! { - textarea(name="body") { + textarea(name="body", id="source-editor", autofocus) { : body } }) as Box, @@ -54,7 +55,7 @@ pub fn page_editing_view(page_state: PageState) -> impl RenderOnce { page_state.clone(), Some( (box_html! { - textarea(name="body"); + textarea(name="body", id="source-editor", autofocus); }) as Box, ), ) @@ -74,39 +75,39 @@ pub fn menu(page_state: PageState, subform: Option>) -> impl @ if edit { @ if let Some(id) = id.as_deref() { - a(href=format!("?id={}", id), class="pure-button"){ : "Cancel" } + a(href=format!("?id={}", id), class="pure-button", id="cancel-button"){ : "Cancel" } : " "; } else { - a(href="javascript:history.back()", class="pure-button"){ : "Cancel" } + a(href="javascript:history.back()", class="pure-button", id="cancel-button"){ : "Cancel" } : " "; } } else { - a(href="..",class="pure-button") { : "Up" } + a(href="..",class="pure-button", id="up-button") { : "Up" } : " "; } @ if edit { @ if let Some(_id) = id.as_deref() { - button(type="submit", class="pure-button pure-button-primary", formaction=".", formmethod="post"){ - : "Save" + button(type="submit", id="save-button", class="pure-button pure-button-primary", formaction=".", formmethod="post"){ + : Raw("Save") } } else { - button(type="submit", class="pure-button pure-button-primary", formaction=".", formmethod="post", name="_method", value="put"){ - : "Save" + button(type="submit", id="save-button", class="pure-button pure-button-primary", formaction=".", formmethod="post", name="_method", value="put"){ + : Raw("Save") } } : " "; } else { - a(href="?edit=true", class="pure-button button-green"){ : "New" } + a(href="?edit=true", class="pure-button button-green"){ : Raw("New") } : " "; } - @ if !edit { + @ if !edit && id.is_some() { input(type="hidden", name="edit", value="true"); - button(type="submit", class="pure-button pure-button-primary", formaction=".", formmethod="get"){ - : "Edit" + button(type="submit", id="edit-button", class="pure-button pure-button-primary", formaction=".", formmethod="get"){ + : Raw("Edit") } : " "; - button(type="submit", class="pure-button button-warning", formaction=".", formmethod="post", name="_method", value="delete", onclick="return confirm('Are you sure?');"){ - : "Delete" + button(type="submit", id="delete-button", class="pure-button button-warning", formaction=".", formmethod="post", name="_method", value="delete", onclick="return confirm('Are you sure?');"){ + : Raw("Delete") } } } @@ -126,25 +127,14 @@ pub fn page_view(page_state: PageState) -> impl RenderOnce { } pub fn post_list( + page_state: PageState, unmatched_tags: impl Iterator, posts: impl Iterator + 'static, ) -> impl RenderOnce { + let menu = menu(page_state.clone(), None); owned_html! { - div(class="pure-menu pure-menu-horizontal") { - form(action="..", method="get", class="pure-menu-item pure-form") { - button(type="submit", class="pure-button"){ - : "Up" - } - } - : " "; - form(action="/", method="get", class="pure-menu-item pure-form") { - input(type="hidden", name="edit", value="true"); - button(type="submit", class="pure-button button-green"){ - : "New" - } - } - } - ul { + : menu; + ul(id="index") { @ for tag in unmatched_tags { li { a(href=format!("./{}/", tag.0)) : format!("{} ({})", tag.0, tag.1)