hledger/hledger-web/static/hledger.js
Simon Michael b0d74b1466 web: support more than 2 postings in the add form
- ctrl plus/ctrl minus adds/removes posting fields
- clicking the more link or pressing tab in the last field also works
- the focus is adjusted sensibly
- the add form is reset if closed and reopened, except the number of postings
- keyboard shortcuts should be less dependent on focus now
- tested in recent firefox, chrome, safari
- things should be robust with typeahead, with one notable exception:
  typeahead is not enabled in the new account fields when you add
  postings. I tried hard, help welcome.
2014-07-14 14:30:30 -07:00

234 lines
8.0 KiB
JavaScript

/* hledger web ui javascript */
//----------------------------------------------------------------------
// STARTUP
$(document).ready(function() {
// show add form if ?add=1
if ($.url.param('add')) { addformShow(); }
// sidebar account hover handlers
$('#sidebar td a').mouseenter(function(){ $(this).parent().addClass('mouseover'); });
$('#sidebar td').mouseleave(function(){ $(this).removeClass('mouseover'); });
// keyboard shortcuts
// 'body' seems to hold focus better than document in FF
$('body').bind('keydown', 'shift+/', function(){ $('#helpmodal').modal('toggle'); return false; });
$('body').bind('keydown', 'h', function(){ $('#helpmodal').modal('toggle'); return false; });
$('body').bind('keydown', 'j', function(){ location.href = '/journal'; return false; });
$('body').bind('keydown', 's', function(){ sidebarToggle(); return false; });
$('body').bind('keydown', 'a', function(){ addformShow(); return false; });
$('body').bind('keydown', 'n', function(){ addformShow(); return false; });
$('body').bind('keydown', '/', function(){ $('#searchform input').focus(); return false; });
$('body, #addform input').bind('keydown', 'ctrl+shift+=', addformAddPosting);
$('body, #addform input').bind('keydown', 'ctrl+=', addformAddPosting);
$('body, #addform input').bind('keydown', 'ctrl+-', addformDeletePosting);
$('#addform tr.posting:last > td:first input').bind('keydown', 'tab', addformAddPostingWithTab);
});
//----------------------------------------------------------------------
// ADD FORM
function addformShow() {
addformReset();
$('#addmodal')
.on('shown.bs.modal', function (e) {
addformFocus();
})
.modal('show');
}
// Make sure the add form is empty and clean for display.
function addformReset() {
if ($('form#addform').length > 0) {
$('form#addform')[0].reset();
$('input#date').val('today');
// reset typehead state (though not fetched completions)
$('.typeahead').typeahead('val', '');
$('.tt-dropdown-menu').hide();
}
}
// Focus the first add form field.
function addformFocus() {
focus($('#addform input#date'));
}
// Focus a jquery-wrapped element, working around http://stackoverflow.com/a/7046837.
function focus($el) {
setTimeout(function (){
$el.focus();
}, 0);
}
// Insert another posting row in the add form.
function addformAddPosting() {
// do nothing if it's not currently visible
if (!$('#addform').is(':visible')) return;
// save a copy of last row
var lastrow = $('#addform tr.posting:last').clone();
// replace the submit button with an amount field, clear and renumber it, add the keybindings
$('#addform tr.posting:last > td:last')
.html( $('#addform tr.posting:first > td:last').html() );
var num = $('#addform tr.posting').length;
$('#addform tr.posting:last > td:last input:last') // input:last here and elsewhere is to avoid autocomplete's extra input
.val('')
.prop('id','amount'+num)
.prop('name','amount'+num)
.prop('placeholder','Amount '+num)
.bind('keydown', 'ctrl+shift+=', addformAddPosting)
.bind('keydown', 'ctrl+=', addformAddPosting)
.bind('keydown', 'ctrl+-', addformDeletePosting);
// set up the new last row's account field.
// First typehead, it's hard to enable on new DOM elements
var $acctinput = lastrow.find('.account-input:last');
// XXX nothing works
// $acctinput.typeahead('destroy'); //,'NoCached');
// lastrow.on("DOMNodeInserted", function () {
// //$(this).find(".typeahead").typeahead();
// console.log('DOMNodeInserted');
// // infinite loop
// console.log($(this).find('.typeahead'));
// //enableTypeahead($(this).find('.typeahead'), accountsSuggester);
// });
// setTimeout(function (){
// $('#addform tr.posting:last input.account-input').typeahead('destroy');
// enableTypeahead($('#addform tr.posting:last input.account-input:last'), accountsSuggester);
// }, 1000);
// insert the new last row
$('#addform > table > tbody').append(lastrow);
// clear and renumber the field, add keybindings
$acctinput
.val('')
.prop('id','account'+(num+1))
.prop('name','account'+(num+1))
.prop('placeholder','Account '+(num+1));
//lastrow.find('input') // not :last this time
$acctinput
.bind('keydown', 'ctrl+shift+=', addformAddPosting)
.bind('keydown', 'ctrl+=', addformAddPosting)
.bind('keydown', 'ctrl+-', addformDeletePosting)
.bind('keydown', 'tab', addformAddPostingWithTab);
}
// Insert another posting row by tabbing within the last field, also advancing the focus.
function addformAddPostingWithTab(ev) {
// do nothing if called from a non-last row (don't know how to remove keybindings)
if ($(ev.target).is('#addform input.account-input:last')) {
addformAddPosting();
focus($('#addform input.amount-input:last')); // help FF
return false;
}
else
return true;
}
// Remove the add form's last posting row, if empty, keeping at least two.
function addformDeletePosting() {
var num = $('#addform tr.posting').length;
if (num <= 2
|| $('#addform tr.posting:last > td:first input:last').val() != ''
) return;
// copy submit button
var btn = $('#addform tr.posting:last > td:last').html();
// remember if the last row's field or button had focus
var focuslost =
$('#addform tr.posting:last > td:first input:last').is(':focus')
|| $('#addform tr.posting:last button').is(':focus');
// delete last row
$('#addform tr.posting:last').remove();
// remember if the last amount field had focus
focuslost = focuslost ||
$('#addform tr.posting:last > td:last input:last').is(':focus');
// replace new last row's amount field with the button
$('#addform tr.posting:last > td:last').html(btn);
// if deleted row had focus, focus the new last row
if (focuslost) $('#addform tr.posting:last > td:first input:last').focus();
}
function journalSelect(ev) {
var textareas = $('textarea', $('form#editform'));
for (i=0; i<textareas.length; i++) {
textareas[i].style.display = 'none';
textareas[i].disabled = true;
}
var targ = getTarget(ev);
if (targ.value) {
var journalid = targ.value+'_textarea';
var textarea = document.getElementById(journalid);
}
else {
var textarea = textareas[0];
}
textarea.style.display = 'block';
textarea.disabled = false;
return true;
}
//----------------------------------------------------------------------
// SIDEBAR
function sidebarToggle() {
//console.log('sidebarToggle');
var visible = $('#sidebar').is(':visible');
//console.log('sidebar visibility was',visible);
// if opening sidebar, start an ajax fetch of its content
if (!visible) {
//console.log('getting sidebar content');
$.get("sidebar"
,null
,function(data) {
//console.log( "success" );
$("#sidebar-body" ).html(data);
})
.done(function() {
//console.log( "success 2" );
})
.fail(function() {
//console.log( "error" );
});
}
// localStorage.setItem('sidebarVisible', !visible);
// set a cookie to communicate the new sidebar state to the server
$.cookie('showsidebar', visible ? '0' : '1');
// horizontally slide the sidebar in or out
// how to make it smooth, without delayed content pop-in ?
//$('#sidebar').animate({'width': 'toggle'});
//$('#sidebar').animate({'width': visible ? 'hide' : '+=20m'});
//$('#sidebar-spacer').width(200);
$('#sidebar').animate({'width': visible ? 'hide' : 'show'});
}
//----------------------------------------------------------------------
// MISC
function enableTypeahead($el, suggester) {
return $el.typeahead(
{
highlight: true
},
{
source: suggester.ttAdapter()
}
);
}
/*
// Get the current event's target in a robust way.
// http://www.quirksmode.org/js/events_properties.html
function getTarget(ev) {
var targ;
if (!ev) var ev = window.event;
if (ev.target) targ = ev.target;
else if (ev.srcElement) targ = ev.srcElement;
if (targ.nodeType == 3) targ = targ.parentNode;
return targ;
}
*/