Navigation helpers (shortcuts, etc.)

This commit is contained in:
Dawid Ciężarkiewicz 2020-05-12 00:15:27 -07:00
parent 088bf5dd9e
commit f98e912f15
4 changed files with 140 additions and 31 deletions

11
resources/mousetrap.min.js vendored Normal file
View File

@ -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;g<e.length;++g){var m=e[g];B[m]&&(m=B[m]);b&&"keypress"!=b&&C[m]&&(m=C[m],d.push("shift"));w(m)&&d.push(m)}e=m;g=b;if(!g){if(!p){p={};for(var c in n)95<c&&112>c||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<k._callbacks[a].length;++l){var c=k._callbacks[a][l];if((f||!c.seq||p[c.seq]==c.level)&&h==c.action){var e;(e="keypress"==h&&!t.metaKey&&!t.ctrlKey)||(e=c.modifiers,e=b.sort().join(",")===e.sort().join(","));e&&(e=f&&c.seq==f&&c.level==d,(!f&&c.combo==g||e)&&k._callbacks[a].splice(l,1),E.push(c))}}return E}function c(a,b,c,f){k.stopCallback(b,
b.target||b.srcElement,c,f)||!1!==a(b,c)||(b.preventDefault?b.preventDefault():b.returnValue=!1,b.stopPropagation?b.stopPropagation():b.cancelBubble=!0)}function e(a){"number"!==typeof a.which&&(a.which=a.keyCode);var b=z(a);b&&("keyup"==a.type&&y===b?y=!1:k.handleKey(b,F(a),a))}function m(a,g,t,f){function h(c){return function(){x=c;++p[a];clearTimeout(q);q=setTimeout(b,1E3)}}function l(g){c(t,g,a);"keyup"!==f&&(y=z(g));setTimeout(b,10)}for(var d=p[a]=0;d<g.length;++d){var e=d+1===g.length?l:h(f||
A(g[d+1]).action);n(g[d],e,f,a,d)}}function n(a,b,c,f,d){k._directMap[a+":"+c]=b;a=a.replace(/\s+/g," ");var e=a.split(" ");1<e.length?m(a,e,b,c):(c=A(a,c),k._callbacks[c.key]=k._callbacks[c.key]||[],g(c.key,c.modifiers,{type:c.action},f,a,d),k._callbacks[c.key][f?"unshift":"push"]({callback:b,modifiers:c.modifiers,action:c.action,seq:f,level:d,combo:a}))}var k=this;a=a||u;if(!(k instanceof d))return new d(a);k.target=a;k._callbacks={};k._directMap={};var p={},q,y=!1,r=!1,x=!1;k._handleKey=function(a,
d,e){var f=g(a,d,e),h;d={};var k=0,l=!1;for(h=0;h<f.length;++h)f[h].seq&&(k=Math.max(k,f[h].level));for(h=0;h<f.length;++h)f[h].seq?f[h].level==k&&(l=!0,d[f[h].seq]=1,c(f[h].callback,e,f[h].combo,f[h].seq)):l||c(f[h].callback,e,f[h].combo);f="keypress"==e.type&&r;e.type!=x||w(a)||f||b(d);r=l&&"keydown"==e.type};k._bindMultiple=function(a,b,c){for(var d=0;d<a.length;++d)n(a[d],b,c)};v(a,"keypress",e);v(a,"keydown",e);v(a,"keyup",e)}if(q){var n={8:"backspace",9:"tab",13:"enter",16:"shift",17:"ctrl",
18:"alt",20:"capslock",27:"esc",32:"space",33:"pageup",34:"pagedown",35:"end",36:"home",37:"left",38:"up",39:"right",40:"down",45:"ins",46:"del",91:"meta",93:"meta",224:"meta"},r={106:"*",107:"+",109:"-",110:".",111:"/",186:";",187:"=",188:",",189:"-",190:".",191:"/",192:"`",219:"[",220:"\\",221:"]",222:"'"},C={"~":"`","!":"1","@":"2","#":"3",$:"4","%":"5","^":"6","&":"7","*":"8","(":"9",")":"0",_:"-","+":"=",":":";",'"':"'","<":",",">":".","?":"/","|":"\\"},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);

95
resources/script.js Normal file
View File

@ -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<a.length;b++)c[a[b]]=!0;else c[a]=!0};a.init()})(Mousetrap);
// OWN, CUSTOM CODE BELOW
// for some odd reason, without using a timeout, `.click()` does not always work
function reliableClick(domElement) {
window.setTimeout(function () {
domElement.click();
}, 1);
}
function reliableClickById(id) {
const elem = document.getElementById(id);
if (elem) {
reliableClick(elem);
}
}
// Modified: https://stackoverflow.com/a/35173443/12271202
// dir: 1 for down, -1 for up
function indexFocusSwitch(dir) {
let indexList = document.getElementById('index');
if (indexList) {
var focussableElements = 'a:not([disabled])';
var focussable = Array.prototype.filter.call(indexList.querySelectorAll(focussableElements),
function (element) {
return true;
}
);
var index = focussable.indexOf(document.activeElement);
if(focussable.length > 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);

View File

@ -106,6 +106,17 @@ async fn handle_style_css() -> std::result::Result<warp::http::Response<String>,
.expect("correct redirect"))
}
async fn handle_script_js() -> std::result::Result<warp::http::Response<String>, 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<State>,
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::<GetParams>())

View File

@ -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<dyn RenderBox>,
@ -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<dyn RenderBox>,
),
)
@ -74,39 +75,39 @@ pub fn menu(page_state: PageState, subform: Option<Box<dyn RenderBox>>) -> 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("<u>S</u>ave")
}
} 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("<u>S</u>ave")
}
}
: " ";
} else {
a(href="?edit=true", class="pure-button button-green"){ : "New" }
a(href="?edit=true", class="pure-button button-green"){ : Raw("<u>N</u>ew") }
: " ";
}
@ 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("<u>E</u>dit")
}
: " ";
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("<u>D</u>elete")
}
}
}
@ -126,25 +127,14 @@ pub fn page_view(page_state: PageState) -> impl RenderOnce {
}
pub fn post_list(
page_state: PageState,
unmatched_tags: impl Iterator<Item = (Tag, usize)>,
posts: impl Iterator<Item = index::PageInfo> + '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)