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.
This commit is contained in:
parent
c27ea12b66
commit
b0d74b1466
@ -280,64 +280,44 @@ getMessageOr mnewmsg = do
|
|||||||
-- | Add transaction form.
|
-- | Add transaction form.
|
||||||
addform :: Text -> ViewData -> HtmlUrl AppRoute
|
addform :: Text -> ViewData -> HtmlUrl AppRoute
|
||||||
addform _ vd@VD{..} = [hamlet|
|
addform _ vd@VD{..} = [hamlet|
|
||||||
|
|
||||||
<script language="javascript">
|
<script language="javascript">
|
||||||
jQuery(document).ready(function() {
|
jQuery(document).ready(function() {
|
||||||
|
|
||||||
/* set up type-ahead fields */
|
/* set up typeahead fields */
|
||||||
|
|
||||||
datesSuggester = new Bloodhound({
|
datesSuggester = new Bloodhound({
|
||||||
local:#{listToJsonValueObjArrayStr dates},
|
local:#{listToJsonValueObjArrayStr dates},
|
||||||
limit:100,
|
limit:100,
|
||||||
datumTokenizer: function(d) { return [d.value]; },
|
datumTokenizer: function(d) { return [d.value]; },
|
||||||
queryTokenizer: function(q) { return [q]; }
|
queryTokenizer: function(q) { return [q]; }
|
||||||
});
|
});
|
||||||
datesSuggester.initialize();
|
datesSuggester.initialize();
|
||||||
jQuery('#date').typeahead(
|
|
||||||
{
|
descriptionsSuggester = new Bloodhound({
|
||||||
highlight: true
|
local:#{listToJsonValueObjArrayStr descriptions},
|
||||||
},
|
limit:100,
|
||||||
{
|
datumTokenizer: function(d) { return [d.value]; },
|
||||||
source: datesSuggester.ttAdapter()
|
queryTokenizer: function(q) { return [q]; }
|
||||||
}
|
});
|
||||||
);
|
descriptionsSuggester.initialize();
|
||||||
|
|
||||||
accountsSuggester = new Bloodhound({
|
accountsSuggester = new Bloodhound({
|
||||||
local:#{listToJsonValueObjArrayStr accts},
|
local:#{listToJsonValueObjArrayStr accts},
|
||||||
limit:100,
|
limit:100,
|
||||||
datumTokenizer: function(d) { return [d.value]; },
|
datumTokenizer: function(d) { return [d.value]; },
|
||||||
queryTokenizer: function(q) { return [q]; }
|
queryTokenizer: function(q) { return [q]; }
|
||||||
/*
|
/*
|
||||||
datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'),
|
datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'),
|
||||||
datumTokenizer: Bloodhound.tokenizers.whitespace(d.value)
|
datumTokenizer: Bloodhound.tokenizers.whitespace(d.value)
|
||||||
queryTokenizer: Bloodhound.tokenizers.whitespace
|
queryTokenizer: Bloodhound.tokenizers.whitespace
|
||||||
*/
|
*/
|
||||||
});
|
});
|
||||||
accountsSuggester.initialize();
|
accountsSuggester.initialize();
|
||||||
jQuery('#account1,#account2').typeahead(
|
|
||||||
{
|
|
||||||
/* minLength: 3, */
|
|
||||||
highlight: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
source: accountsSuggester.ttAdapter()
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
descriptionsSuggester = new Bloodhound({
|
enableTypeahead(jQuery('input#date'), datesSuggester);
|
||||||
local:#{listToJsonValueObjArrayStr descriptions},
|
enableTypeahead(jQuery('input#description'), descriptionsSuggester);
|
||||||
limit:100,
|
enableTypeahead(jQuery('input#account1, input#account2'), accountsSuggester);
|
||||||
datumTokenizer: function(d) { return [d.value]; },
|
|
||||||
queryTokenizer: function(q) { return [q]; }
|
|
||||||
});
|
|
||||||
descriptionsSuggester.initialize();
|
|
||||||
jQuery('#description').typeahead(
|
|
||||||
{
|
|
||||||
highlight: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
source: descriptionsSuggester.ttAdapter()
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -348,11 +328,14 @@ addform _ vd@VD{..} = [hamlet|
|
|||||||
<table style="width:100%;">
|
<table style="width:100%;">
|
||||||
<tr#descriptionrow>
|
<tr#descriptionrow>
|
||||||
<td>
|
<td>
|
||||||
<input #date .form-control .input-lg type=text size=15 name=date placeholder="Date" value=#{date}>
|
<input #date .typeahead .form-control .input-lg type=text size=15 name=date placeholder="Date" value=#{date}>
|
||||||
<td>
|
<td>
|
||||||
<input #description .form-control .input-lg type=text size=40 name=description placeholder="Description">
|
<input #description .typeahead .form-control .input-lg type=text size=40 name=description placeholder="Description">
|
||||||
$forall n <- postingnums
|
$forall n <- postingnums
|
||||||
^{postingfields vd n}
|
^{postingfields vd n}
|
||||||
|
<span style="padding-left:2em;">
|
||||||
|
<span .small>
|
||||||
|
Tab in last field for <a .small href="#" onclick="addformAddPosting(); return false;">more</a> (or ctrl +, ctrl -)
|
||||||
|]
|
|]
|
||||||
where
|
where
|
||||||
date = "today" :: String
|
date = "today" :: String
|
||||||
@ -364,20 +347,19 @@ addform _ vd@VD{..} = [hamlet|
|
|||||||
postingnums = [1..numpostings]
|
postingnums = [1..numpostings]
|
||||||
postingfields :: ViewData -> Int -> HtmlUrl AppRoute
|
postingfields :: ViewData -> Int -> HtmlUrl AppRoute
|
||||||
postingfields _ n = [hamlet|
|
postingfields _ n = [hamlet|
|
||||||
<tr .posting .#{lastclass}>
|
<tr .posting>
|
||||||
<td style="padding-left:2em;">
|
<td style="padding-left:2em;">
|
||||||
<input ##{acctvar} .form-control .input-lg style="width:100%;" type=text name=#{acctvar} placeholder="#{acctph}">
|
<input ##{acctvar} .account-input .typeahead .form-control .input-lg style="width:100%;" type=text name=#{acctvar} placeholder="#{acctph}">
|
||||||
^{amtfieldorsubmitbtn}
|
^{amtfieldorsubmitbtn}
|
||||||
|]
|
|]
|
||||||
where
|
where
|
||||||
islast = n == numpostings
|
islast = n == numpostings
|
||||||
lastclass = if islast then "lastrow" else "" :: String
|
|
||||||
acctvar = "account" ++ show n
|
acctvar = "account" ++ show n
|
||||||
acctph = "Account " ++ show n
|
acctph = "Account " ++ show n
|
||||||
amtfieldorsubmitbtn
|
amtfieldorsubmitbtn
|
||||||
| not islast = [hamlet|
|
| not islast = [hamlet|
|
||||||
<td>
|
<td>
|
||||||
<input ##{amtvar} .form-control .input-lg type=text size=10 name=#{amtvar} placeholder="#{amtph}">
|
<input ##{amtvar} .amount-input .form-control .input-lg type=text size=10 name=#{amtvar} placeholder="#{amtph}">
|
||||||
|]
|
|]
|
||||||
| otherwise = [hamlet|
|
| otherwise = [hamlet|
|
||||||
<td #addbtncell style="text-align:right;">
|
<td #addbtncell style="text-align:right;">
|
||||||
@ -395,7 +377,7 @@ addform _ vd@VD{..} = [hamlet|
|
|||||||
|
|
||||||
journalselect :: [(FilePath,String)] -> HtmlUrl AppRoute
|
journalselect :: [(FilePath,String)] -> HtmlUrl AppRoute
|
||||||
journalselect journalfiles = [hamlet|
|
journalselect journalfiles = [hamlet|
|
||||||
<select id=journalselect name=journal onchange="editformJournalSelect(event)">
|
<select id=journalselect name=journal onchange="journalSelect(event)">
|
||||||
$forall f <- journalfiles
|
$forall f <- journalfiles
|
||||||
<option value=#{fst f}>#{fst f}
|
<option value=#{fst f}>#{fst f}
|
||||||
|]
|
|]
|
||||||
|
|||||||
@ -6,11 +6,12 @@ import Import
|
|||||||
|
|
||||||
import Control.Applicative
|
import Control.Applicative
|
||||||
import Data.Either (lefts,rights)
|
import Data.Either (lefts,rights)
|
||||||
import Data.List (intercalate)
|
import Data.List (intercalate, sort)
|
||||||
import qualified Data.List as L (head) -- qualified keeps dev & prod builds warning-free
|
import qualified Data.List as L (head) -- qualified keeps dev & prod builds warning-free
|
||||||
|
import Data.Maybe
|
||||||
import Data.Text (unpack)
|
import Data.Text (unpack)
|
||||||
import qualified Data.Text as T (null)
|
import qualified Data.Text as T
|
||||||
import Text.Parsec (eof)
|
import Text.Parsec (digit, eof, many1, string)
|
||||||
import Text.Printf (printf)
|
import Text.Printf (printf)
|
||||||
|
|
||||||
import Hledger.Utils
|
import Hledger.Utils
|
||||||
@ -32,26 +33,13 @@ handlePost = do
|
|||||||
handleAdd :: Handler Html
|
handleAdd :: Handler Html
|
||||||
handleAdd = do
|
handleAdd = do
|
||||||
VD{..} <- getViewData
|
VD{..} <- getViewData
|
||||||
|
-- XXX port to yesod-form later
|
||||||
-- get form input values. M means a Maybe value.
|
-- get form input values. M means a Maybe value.
|
||||||
|
journalM <- lookupPostParam "journal"
|
||||||
dateM <- lookupPostParam "date"
|
dateM <- lookupPostParam "date"
|
||||||
descM <- lookupPostParam "description"
|
descM <- lookupPostParam "description"
|
||||||
acct1M <- lookupPostParam "account1"
|
let dateE = maybe (Left "date required") (either (\e -> Left $ showDateParseError e) Right . fixSmartDateStrEither today . strip . unpack) dateM
|
||||||
amt1M <- lookupPostParam "amount1"
|
|
||||||
acct2M <- lookupPostParam "account2"
|
|
||||||
amt2M <- lookupPostParam "amount2"
|
|
||||||
journalM <- lookupPostParam "journal"
|
|
||||||
-- supply defaults and parse date and amounts, or get errors.
|
|
||||||
let dateE = maybe (Left "date required") (either (\e -> Left $ showDateParseError e) Right . fixSmartDateStrEither today . unpack) dateM
|
|
||||||
descE = Right $ maybe "" unpack descM
|
descE = Right $ maybe "" unpack descM
|
||||||
-- XXX simplify...
|
|
||||||
maybeNothing = maybe Nothing (\t -> if T.null t then Nothing else Just t)
|
|
||||||
acct1E = maybe (Left "To account required") (Right . strip . unpack) (maybeNothing acct1M)
|
|
||||||
>>= \a -> either (Left . ("could not parse To account: "++) . show) Right (parsewith (accountnamep <* eof) a)
|
|
||||||
acct2E = maybe (Left "From account required") (Right . strip . unpack) (maybeNothing acct2M)
|
|
||||||
>>= \a -> either (Left . ("could not parse From account: "++) . show) Right (parsewith (accountnamep <* eof) a)
|
|
||||||
amt1E = maybe (Left "Amount 1 required") (Right . strip . unpack) (maybeNothing amt1M)
|
|
||||||
>>= \a -> either (Left . ("could not parse To account: "++) . show) Right (parseWithCtx nullctx (amountp <* eof) a)
|
|
||||||
amt2E = maybe (Right missingamt) (either (Left . ("could not parse amount 2: "++) . show) Right . parseWithCtx nullctx amountp . strip . unpack) amt2M
|
|
||||||
journalE = maybe (Right $ journalFilePath j)
|
journalE = maybe (Right $ journalFilePath j)
|
||||||
(\f -> let f' = unpack f in
|
(\f -> let f' = unpack f in
|
||||||
if f' `elem` journalFilePaths j
|
if f' `elem` journalFilePaths j
|
||||||
@ -59,26 +47,51 @@ handleAdd = do
|
|||||||
else Left $ "unrecognised journal file path: " ++ f'
|
else Left $ "unrecognised journal file path: " ++ f'
|
||||||
)
|
)
|
||||||
journalM
|
journalM
|
||||||
strEs = [dateE, descE, acct1E, acct2E, journalE]
|
estrs = [dateE, descE, journalE]
|
||||||
amtEs = [amt1E, amt2E]
|
(errs1, [date,desc,journalpath]) = (lefts estrs, rights estrs) -- XXX irrefutable
|
||||||
errs = lefts strEs ++ lefts amtEs
|
|
||||||
[date,desc,acct1,acct2,journalpath] = rights strEs
|
(params,_) <- runRequestBody
|
||||||
[amt1,amt2] = rights amtEs
|
-- mtrace params
|
||||||
|
let paramnamep s = do {string s; n <- many1 digit; eof; return (read n :: Int)}
|
||||||
|
acctparams = sort
|
||||||
|
[ (n,v) | (k,v) <- params
|
||||||
|
, let en = parsewith (paramnamep "account") $ T.unpack k
|
||||||
|
, isRight en
|
||||||
|
, let Right n = en
|
||||||
|
]
|
||||||
|
amtparams = sort
|
||||||
|
[ (n,v) | (k,v) <- params
|
||||||
|
, let en = parsewith (paramnamep "amount") $ T.unpack k
|
||||||
|
, isRight en
|
||||||
|
, let Right n = en
|
||||||
|
]
|
||||||
|
num = length acctparams
|
||||||
|
paramErrs | not $ length amtparams `elem` [num, num-1] = ["different number of account and amount parameters"]
|
||||||
|
| otherwise = catMaybes
|
||||||
|
[if map fst acctparams == [1..num] then Nothing else Just "misnumbered account parameters"
|
||||||
|
,if map fst amtparams == [1..num] || map fst amtparams == [1..(num-1)] then Nothing else Just "misnumbered amount parameters"
|
||||||
|
]
|
||||||
|
eaccts = map (parsewith (accountnamep <* eof) . strip . T.unpack . snd) acctparams
|
||||||
|
eamts = map (parseWithCtx nullctx (amountp <* eof) . strip . T.unpack . snd) amtparams
|
||||||
|
(accts, acctErrs) = (rights eaccts, map show $ lefts eaccts)
|
||||||
|
(amts', amtErrs) = (rights eamts, map show $ lefts eamts)
|
||||||
|
amts | length amts' == num = amts'
|
||||||
|
| otherwise = amts' ++ [missingamt]
|
||||||
|
|
||||||
-- if no errors so far, generate a transaction and balance it or get the error.
|
-- if no errors so far, generate a transaction and balance it or get the error.
|
||||||
tE | not $ null errs = Left errs
|
errs = errs1 ++ if null paramErrs then (acctErrs ++ amtErrs) else paramErrs
|
||||||
|
et | not $ null errs = Left errs
|
||||||
| otherwise = either (\e -> Left ["unbalanced postings: " ++ (L.head $ lines e)]) Right
|
| otherwise = either (\e -> Left ["unbalanced postings: " ++ (L.head $ lines e)]) Right
|
||||||
(balanceTransaction Nothing $ nulltransaction { -- imprecise balancing
|
(balanceTransaction Nothing $ nulltransaction {
|
||||||
tdate=parsedate date
|
tdate=parsedate date
|
||||||
,tdescription=desc
|
,tdescription=desc
|
||||||
,tpostings=[
|
,tpostings=[nullposting{paccount=acct, pamount=mixed amt} | (acct,amt) <- zip accts amts]
|
||||||
nullposting{paccount=acct1, pamount=mixed amt1}
|
})
|
||||||
,nullposting{paccount=acct2, pamount=mixed amt2}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
-- display errors or add transaction
|
-- display errors or add transaction
|
||||||
-- XXX currently it's still possible to write an invalid entry, eg by adding space space ; after the first account name
|
case et of
|
||||||
case tE of
|
|
||||||
Left errs' -> do
|
Left errs' -> do
|
||||||
|
error $ show errs' -- XXX
|
||||||
-- save current form values in session
|
-- save current form values in session
|
||||||
-- setMessage $ toHtml $ intercalate "; " errs
|
-- setMessage $ toHtml $ intercalate "; " errs
|
||||||
setMessage [shamlet|
|
setMessage [shamlet|
|
||||||
@ -94,6 +107,38 @@ handleAdd = do
|
|||||||
|
|
||||||
redirect (JournalR) -- , [("add","1")])
|
redirect (JournalR) -- , [("add","1")])
|
||||||
|
|
||||||
|
-- personForm :: Html -> MForm Handler (FormResult Person, Widget)
|
||||||
|
-- personForm extra = do
|
||||||
|
-- (nameRes, nameView) <- mreq textField "this is not used" Nothing
|
||||||
|
-- (ageRes, ageView) <- mreq intField "neither is this" Nothing
|
||||||
|
-- let personRes = Person <$> nameRes <*> ageRes
|
||||||
|
-- let widget = do
|
||||||
|
-- toWidget
|
||||||
|
-- [lucius|
|
||||||
|
-- ##{fvId ageView} {
|
||||||
|
-- width: 3em;
|
||||||
|
-- }
|
||||||
|
-- |]
|
||||||
|
-- [whamlet|
|
||||||
|
-- #{extra}
|
||||||
|
-- <p>
|
||||||
|
-- Hello, my name is #
|
||||||
|
-- ^{fvInput nameView}
|
||||||
|
-- \ and I am #
|
||||||
|
-- ^{fvInput ageView}
|
||||||
|
-- \ years old. #
|
||||||
|
-- <input type=submit value="Introduce myself">
|
||||||
|
-- |]
|
||||||
|
-- return (personRes, widget)
|
||||||
|
--
|
||||||
|
-- ((res, widget), enctype) <- runFormGet personForm
|
||||||
|
-- defaultLayout
|
||||||
|
-- [whamlet|
|
||||||
|
-- <p>Result: #{show res}
|
||||||
|
-- <form enctype=#{enctype}>
|
||||||
|
-- ^{widget}
|
||||||
|
-- |]
|
||||||
|
|
||||||
-- | Handle a post from the journal edit form.
|
-- | Handle a post from the journal edit form.
|
||||||
handleEdit :: Handler Html
|
handleEdit :: Handler Html
|
||||||
handleEdit = do
|
handleEdit = do
|
||||||
|
|||||||
@ -1,59 +1,200 @@
|
|||||||
/* hledger web ui javascript */
|
/* hledger web ui javascript */
|
||||||
/* depends on jquery etc. */
|
|
||||||
|
|
||||||
// /* show/hide things based on locally-saved state */
|
//----------------------------------------------------------------------
|
||||||
// happens too late with large main content in chrome, visible glitch
|
// STARTUP
|
||||||
// if (localStorage.getItem('sidebarVisible') == "false")
|
|
||||||
// $('#sidebar').hide();
|
|
||||||
// /* or request parameters */
|
|
||||||
// if ($.url.param('sidebar')=='' || $.url.param('sidebar')=='0')
|
|
||||||
// $('#sidebar').hide();
|
|
||||||
// else if ($.url.param('sidebar')=='1')
|
|
||||||
// $('#sidebar').show();
|
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
|
|
||||||
/* show add form if ?add=1 */
|
// show add form if ?add=1
|
||||||
if ($.url.param('add')) { addformShow(); }
|
if ($.url.param('add')) { addformShow(); }
|
||||||
|
|
||||||
/* sidebar account hover handlers */
|
// sidebar account hover handlers
|
||||||
$('#sidebar td a').mouseenter(function(){ $(this).parent().addClass('mouseover'); });
|
$('#sidebar td a').mouseenter(function(){ $(this).parent().addClass('mouseover'); });
|
||||||
$('#sidebar td').mouseleave(function(){ $(this).removeClass('mouseover'); });
|
$('#sidebar td').mouseleave(function(){ $(this).removeClass('mouseover'); });
|
||||||
|
|
||||||
/* keyboard shortcuts */
|
// keyboard shortcuts
|
||||||
$(document).bind('keydown', 'shift+/', function(){ $('#helpmodal').modal('toggle'); return false; });
|
// 'body' seems to hold focus better than document in FF
|
||||||
$(document).bind('keydown', 'h', function(){ $('#helpmodal').modal('toggle'); return false; });
|
$('body').bind('keydown', 'shift+/', function(){ $('#helpmodal').modal('toggle'); return false; });
|
||||||
$(document).bind('keydown', 'j', function(){ location.href = '/journal'; return false; });
|
$('body').bind('keydown', 'h', function(){ $('#helpmodal').modal('toggle'); return false; });
|
||||||
$(document).bind('keydown', 's', function(){ sidebarToggle(); return false; });
|
$('body').bind('keydown', 'j', function(){ location.href = '/journal'; return false; });
|
||||||
$(document).bind('keydown', 'a', function(){ addformShow(); return false; });
|
$('body').bind('keydown', 's', function(){ sidebarToggle(); return false; });
|
||||||
$(document).bind('keydown', '/', function(){ $('#searchform input').focus(); return false; });
|
$('body').bind('keydown', 'a', function(){ addformShow(); return false; });
|
||||||
$('#addform input,#addform button,#addformlink').bind('keydown', 'ctrl+shift+=', addformAddPosting);
|
$('body').bind('keydown', 'n', function(){ addformShow(); return false; });
|
||||||
$('#addform input,#addform button,#addformlink').bind('keydown', 'ctrl+=', addformAddPosting);
|
$('body').bind('keydown', '/', function(){ $('#searchform input').focus(); return false; });
|
||||||
$('#addform input,#addform button,#addformlink').bind('keydown', 'ctrl+-', addformDeletePosting);
|
$('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() {
|
function sidebarToggle() {
|
||||||
console.log('sidebarToggle');
|
//console.log('sidebarToggle');
|
||||||
var visible = $('#sidebar').is(':visible');
|
var visible = $('#sidebar').is(':visible');
|
||||||
console.log('sidebar visibility was',visible);
|
//console.log('sidebar visibility was',visible);
|
||||||
// if opening sidebar, start an ajax fetch of its content
|
// if opening sidebar, start an ajax fetch of its content
|
||||||
if (!visible) {
|
if (!visible) {
|
||||||
//console.log('getting sidebar content');
|
//console.log('getting sidebar content');
|
||||||
$.get("sidebar"
|
$.get("sidebar"
|
||||||
,null
|
,null
|
||||||
,function(data) {
|
,function(data) {
|
||||||
//console.log( "success" );
|
//console.log( "success" );
|
||||||
$("#sidebar-body" ).html(data);
|
$("#sidebar-body" ).html(data);
|
||||||
})
|
})
|
||||||
.done(function() {
|
.done(function() {
|
||||||
//console.log( "success 2" );
|
//console.log( "success 2" );
|
||||||
})
|
})
|
||||||
.fail(function() {
|
.fail(function() {
|
||||||
//console.log( "error" );
|
//console.log( "error" );
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// localStorage.setItem('sidebarVisible', !visible);
|
// localStorage.setItem('sidebarVisible', !visible);
|
||||||
// set a cookie to communicate the new sidebar state to the server
|
// set a cookie to communicate the new sidebar state to the server
|
||||||
$.cookie('showsidebar', visible ? '0' : '1');
|
$.cookie('showsidebar', visible ? '0' : '1');
|
||||||
// horizontally slide the sidebar in or out
|
// horizontally slide the sidebar in or out
|
||||||
@ -64,53 +205,18 @@ function sidebarToggle() {
|
|||||||
$('#sidebar').animate({'width': visible ? 'hide' : 'show'});
|
$('#sidebar').animate({'width': visible ? 'hide' : 'show'});
|
||||||
}
|
}
|
||||||
|
|
||||||
function addformShow() {
|
//----------------------------------------------------------------------
|
||||||
$('#addmodal').modal('show').on('shown.bs.modal', function (e) {
|
// MISC
|
||||||
$('#addform input[name=date]').focus();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function addformAddPosting() {
|
function enableTypeahead($el, suggester) {
|
||||||
var rownum = $('#addform tr.posting').length + 1;
|
return $el.typeahead(
|
||||||
// XXX duplicates markup in Common.hs
|
{
|
||||||
// duplicate last row
|
highlight: true
|
||||||
$('#addform > table').append($('#addform > table tr:last').clone());
|
},
|
||||||
// fix up second-last row
|
{
|
||||||
$('#addform > table > tr.lastrow:first > td:last').html('');
|
source: suggester.ttAdapter()
|
||||||
$('#addform > table > tr.lastrow:first').removeClass('lastrow');
|
}
|
||||||
|
);
|
||||||
// fix up last row
|
|
||||||
$('#addform table').append($('#addform table tr:last').clone());
|
|
||||||
// '<tr class="posting">' +
|
|
||||||
// '<td style="padding-left:2em;">' +
|
|
||||||
// '<input id="account'+rownum+'" class="form-control input-lg" style="width:100%;" type="text"' +
|
|
||||||
// ' name=account'+rownum+'" placeholder="Account '+rownum+'">'
|
|
||||||
// );
|
|
||||||
|
|
||||||
// $('#addbtncell').appendTo($('#addform table tr:last'))
|
|
||||||
// );
|
|
||||||
}
|
|
||||||
|
|
||||||
function addformDeletePosting() {
|
|
||||||
}
|
|
||||||
|
|
||||||
function editformJournalSelect(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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user