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