From 063c9fcfe3cfcabc51449967b0474ea306e66c7d Mon Sep 17 00:00:00 2001 From: Jonathan Rosenbaum Date: Sat, 24 Jan 2015 06:43:00 +0000 Subject: [PATCH] Adds all deposits to the range per year, and last deposit from prev year. --- include_header.html | 4 +- js/jquery.liblink.js | 311 +++++++++++++++++++++++++++++++++++++++++ js/transaction.js | 62 +++++++-- js/wNumb.js | 321 +++++++++++++++++++++++++++++++++++++++++++ transaction_log.php | 3 +- 5 files changed, 687 insertions(+), 14 deletions(-) create mode 100644 js/jquery.liblink.js create mode 100644 js/wNumb.js diff --git a/include_header.html b/include_header.html index cad0bee..fa5bd19 100644 --- a/include_header.html +++ b/include_header.html @@ -32,7 +32,9 @@ function resetTimer() - + + + diff --git a/js/jquery.liblink.js b/js/jquery.liblink.js new file mode 100644 index 0000000..47b8917 --- /dev/null +++ b/js/jquery.liblink.js @@ -0,0 +1,311 @@ +/*jslint browser: true */ +/*jslint white: true */ + +(function( $ ){ + + 'use strict'; + +// Helpers + + // Test in an object is an instance of jQuery or Zepto. + function isInstance ( a ) { + return a instanceof $ || ( $.zepto && $.zepto.isZ(a) ); + } + + +// Link types + + function fromPrefix ( target, method ) { + + // If target is a string, a new hidden input will be created. + if ( typeof target === 'string' && target.indexOf('-inline-') === 0 ) { + + // By default, use the 'html' method. + this.method = method || 'html'; + + // Use jQuery to create the element + this.target = this.el = $( target.replace('-inline-', '') || '
' ); + + return true; + } + } + + function fromString ( target ) { + + // If the string doesn't begin with '-', which is reserved, add a new hidden input. + if ( typeof target === 'string' && target.indexOf('-') !== 0 ) { + + this.method = 'val'; + + var element = document.createElement('input'); + element.name = target; + element.type = 'hidden'; + this.target = this.el = $(element); + + return true; + } + } + + function fromFunction ( target ) { + + // The target can also be a function, which will be called. + if ( typeof target === 'function' ) { + this.target = false; + this.method = target; + + return true; + } + } + + function fromInstance ( target, method ) { + + if ( isInstance( target ) && !method ) { + + // If a jQuery/Zepto input element is provided, but no method is set, + // the element can assume it needs to respond to 'change'... + if ( target.is('input, select, textarea') ) { + + // Default to .val if this is an input element. + this.method = 'val'; + + // Fire the API changehandler when the target changes. + this.target = target.on('change.liblink', this.changeHandler); + + } else { + + this.target = target; + + // If no method is set, and we are not auto-binding an input, default to 'html'. + this.method = 'html'; + } + + return true; + } + } + + function fromInstanceMethod ( target, method ) { + + // The method must exist on the element. + if ( isInstance( target ) && + (typeof method === 'function' || + (typeof method === 'string' && target[method])) + ) { + this.method = method; + this.target = target; + + return true; + } + } + +var +/** @const */ + creationFunctions = [fromPrefix, fromString, fromFunction, fromInstance, fromInstanceMethod]; + + +// Link Instance + +/** @constructor */ + function Link ( target, method, format ) { + + var that = this, valid = false; + + // Forward calls within scope. + this.changeHandler = function ( changeEvent ) { + var decodedValue = that.formatInstance.from( $(this).val() ); + + // If the value is invalid, stop this event, as well as it's propagation. + if ( decodedValue === false || isNaN(decodedValue) ) { + + // Reset the value. + $(this).val(that.lastSetValue); + return false; + } + + that.changeHandlerMethod.call( '', changeEvent, decodedValue ); + }; + + // See if this Link needs individual targets based on its usage. + // If so, return the element that needs to be copied by the + // implementing interface. + // Default the element to false. + this.el = false; + + // Store the formatter, or use the default. + this.formatInstance = format; + + // Try all Link types. + /*jslint unparam: true*/ + $.each(creationFunctions, function(i, fn){ + valid = fn.call(that, target, method); + return !valid; + }); + /*jslint unparam: false*/ + + // Nothing matched, throw error. + if ( !valid ) { + throw new RangeError("(Link) Invalid Link."); + } + } + + // Provides external items with the object value. + Link.prototype.set = function ( value ) { + + // Ignore the value, so only the passed-on arguments remain. + var args = Array.prototype.slice.call( arguments ), + additionalArgs = args.slice(1); + + // Store some values. The actual, numerical value, + // the formatted value and the parameters for use in 'resetValue'. + // Slice additionalArgs to break the relation. + this.lastSetValue = this.formatInstance.to( value ); + + // Prepend the value to the function arguments. + additionalArgs.unshift( + this.lastSetValue + ); + + // When target is undefined, the target was a function. + // In that case, provided the object as the calling scope. + // Branch between writing to a function or an object. + ( typeof this.method === 'function' ? + this.method : + this.target[ this.method ] ).apply( this.target, additionalArgs ); + }; + + +// Developer API + +/** @constructor */ + function LinkAPI ( origin ) { + this.items = []; + this.elements = []; + this.origin = origin; + } + + LinkAPI.prototype.push = function( item, element ) { + this.items.push(item); + + // Prevent 'false' elements + if ( element ) { + this.elements.push(element); + } + }; + + LinkAPI.prototype.reconfirm = function ( flag ) { + var i; + for ( i = 0; i < this.elements.length; i += 1 ) { + this.origin.LinkConfirm(flag, this.elements[i]); + } + }; + + LinkAPI.prototype.remove = function ( flag ) { + var i; + for ( i = 0; i < this.items.length; i += 1 ) { + this.items[i].target.off('.liblink'); + } + for ( i = 0; i < this.elements.length; i += 1 ) { + this.elements[i].remove(); + } + }; + + LinkAPI.prototype.change = function ( value ) { + + if ( this.origin.LinkIsEmitting ) { + return false; + } + + this.origin.LinkIsEmitting = true; + + var args = Array.prototype.slice.call( arguments, 1 ), i; + args.unshift( value ); + + // Write values to serialization Links. + // Convert the value to the correct relative representation. + for ( i = 0; i < this.items.length; i += 1 ) { + this.items[i].set.apply(this.items[i], args); + } + + this.origin.LinkIsEmitting = false; + }; + + +// jQuery plugin + + function binder ( flag, target, method, format ){ + + if ( flag === 0 ) { + flag = this.LinkDefaultFlag; + } + + // Create a list of API's (if it didn't exist yet); + if ( !this.linkAPI ) { + this.linkAPI = {}; + } + + // Add an API point. + if ( !this.linkAPI[flag] ) { + this.linkAPI[flag] = new LinkAPI(this); + } + + var linkInstance = new Link ( target, method, format || this.LinkDefaultFormatter ); + + // Default the calling scope to the linked object. + if ( !linkInstance.target ) { + linkInstance.target = $(this); + } + + // If the Link requires creation of a new element, + // Pass the element and request confirmation to get the changehandler. + // Set the method to be called when a Link changes. + linkInstance.changeHandlerMethod = this.LinkConfirm( flag, linkInstance.el ); + + // Store the linkInstance in the flagged list. + this.linkAPI[flag].push( linkInstance, linkInstance.el ); + + // Now that Link have been connected, request an update. + this.LinkUpdate( flag ); + } + + /** @export */ + $.fn.Link = function( flag ){ + + var that = this; + + // Delete all linkAPI + if ( flag === false ) { + + return that.each(function(){ + + // .Link(false) can be called on elements without Links. + // When that happens, the objects can't be looped. + if ( !this.linkAPI ) { + return; + } + + $.map(this.linkAPI, function(api){ + api.remove(); + }); + + delete this.linkAPI; + }); + } + + if ( flag === undefined ) { + + flag = 0; + + } else if ( typeof flag !== 'string') { + + throw new Error("Flag must be string."); + } + + return { + to: function( a, b, c ){ + return that.each(function(){ + binder.call(this, flag, a, b, c); + }); + } + }; + }; + +}( window.jQuery || window.Zepto )); diff --git a/js/transaction.js b/js/transaction.js index 88b1b32..5b891b9 100644 --- a/js/transaction.js +++ b/js/transaction.js @@ -157,12 +157,15 @@ $(function() { function transaction_slider() { // Establish range - transaction_id | deposited (yes or no) | date - var range = []; + var range = []; + var range_last_year = []; $.post("json/transaction.php",{ transaction_slider: 1}, function(data) { var obj = $.parseJSON(data); - // currently go by a Jan - Dec year as fiscal year + // currently go by a Jan - Dec year as fiscal year + // max deposit from previous year needs to be included in range var year = $("[name='gnucash_csv_year']").val(); + var last_year = year - 1; $.each(obj,function(k,v){ var trans = obj[k]; @@ -173,33 +176,68 @@ $(function() { if (trans.deposited == "yes" && trans_year == year) { range.push(trans.transaction_id); } + // find max for last year + if (trans.deposited == "yes" && trans_year == last_year) { + range_last_year.push(trans.transaction_id); + } }); } ); - // gnucash deposit range + // gnucash deposit range - min, max, and previous to max var min_range = Number(range[0]); var max_range = Number(range[range.length - 1]); + var max_range_last_year = Number(range_last_year[range_last_year.length - 1]); var prev_trans = Number(range[range.length - 2]); + + // ranges between min and max in percentages with min prepended and max appended as an object + var range_obj = {}; + console.log(max_range_last_year); + console.log(range.toString()); + console.log(percentage_amounts); + + // add last deposit from last year if it exists + if(max_range_last_year) { + range.unshift(max_range_last_year); + min_range = max_range_last_year; + } + var percentage_amounts = 100 / (range.length - 1); + var percentage = percentage_amounts; + + $.each(range,function(k,v) { + if (v == min_range) { + range_obj["min"] = min_range; + } else if (v == max_range) { + range_obj["max"] = max_range; + } else { + range_obj[percentage_amounts + '%'] = Number(v); + percentage_amounts = percentage_amounts + percentage; + } + + }); + + console.dir(range_obj); //initialize slider if (!slider) { slider = $('#gnucash_csv_range').noUiSlider({ start: [ prev_trans, max_range ], - range: { - "min": min_range, - "max": max_range - } + range: range_obj, + format: wNumb({decimals:0, prefix: "Transaction ID: "}), + snap: true }); - } else { + slider.Link('lower').to($('#slider_lower')); + slider.Link('upper').to($('#slider_upper')); + } else { // on change slider.noUiSlider({ start: [ prev_trans, max_range ], - range: { - "min": min_range, - "max": max_range - } + range: range_obj, + format: wNumb({decimals:0, prefix: "Transaction ID: "}), + snap: true }, true); + slider.Link('lower').to($('#slider_lower')); + slider.Link('upper').to($('#slider_upper')); } } // end function transaction_slider diff --git a/js/wNumb.js b/js/wNumb.js new file mode 100644 index 0000000..89902a7 --- /dev/null +++ b/js/wNumb.js @@ -0,0 +1,321 @@ +(function(){ + + 'use strict'; + +var +/** @const */ FormatOptions = [ + 'decimals', + 'thousand', + 'mark', + 'prefix', + 'postfix', + 'encoder', + 'decoder', + 'negativeBefore', + 'negative', + 'edit', + 'undo' +]; + +// General + + // Reverse a string + function strReverse ( a ) { + return a.split('').reverse().join(''); + } + + // Check if a string starts with a specified prefix. + function strStartsWith ( input, match ) { + return input.substring(0, match.length) === match; + } + + // Check is a string ends in a specified postfix. + function strEndsWith ( input, match ) { + return input.slice(-1 * match.length) === match; + } + + // Throw an error if formatting options are incompatible. + function throwEqualError( F, a, b ) { + if ( (F[a] || F[b]) && (F[a] === F[b]) ) { + throw new Error(a); + } + } + + // Check if a number is finite and not NaN + function isValidNumber ( input ) { + return typeof input === 'number' && isFinite( input ); + } + + // Provide rounding-accurate toFixed method. + function toFixed ( value, decimals ) { + var scale = Math.pow(10, decimals); + return ( Math.round(value * scale) / scale).toFixed( decimals ); + } + + +// Formatting + + // Accept a number as input, output formatted string. + function formatTo ( decimals, thousand, mark, prefix, postfix, encoder, decoder, negativeBefore, negative, edit, undo, input ) { + + var originalInput = input, inputIsNegative, inputPieces, inputBase, inputDecimals = '', output = ''; + + // Apply user encoder to the input. + // Expected outcome: number. + if ( encoder ) { + input = encoder(input); + } + + // Stop if no valid number was provided, the number is infinite or NaN. + if ( !isValidNumber(input) ) { + return false; + } + + // Rounding away decimals might cause a value of -0 + // when using very small ranges. Remove those cases. + if ( decimals && parseFloat(input.toFixed(decimals)) === 0 ) { + input = 0; + } + + // Formatting is done on absolute numbers, + // decorated by an optional negative symbol. + if ( input < 0 ) { + inputIsNegative = true; + input = Math.abs(input); + } + + // Reduce the number of decimals to the specified option. + if ( decimals ) { + input = toFixed( input, decimals ); + } + + // Transform the number into a string, so it can be split. + input = input.toString(); + + // Break the number on the decimal separator. + if ( input.indexOf('.') !== -1 ) { + inputPieces = input.split('.'); + + inputBase = inputPieces[0]; + + if ( mark ) { + inputDecimals = mark + inputPieces[1]; + } + + } else { + + // If it isn't split, the entire number will do. + inputBase = input; + } + + // Group numbers in sets of three. + if ( thousand ) { + inputBase = strReverse(inputBase).match(/.{1,3}/g); + inputBase = strReverse(inputBase.join( strReverse( thousand ) )); + } + + // If the number is negative, prefix with negation symbol. + if ( inputIsNegative && negativeBefore ) { + output += negativeBefore; + } + + // Prefix the number + if ( prefix ) { + output += prefix; + } + + // Normal negative option comes after the prefix. Defaults to '-'. + if ( inputIsNegative && negative ) { + output += negative; + } + + // Append the actual number. + output += inputBase; + output += inputDecimals; + + // Apply the postfix. + if ( postfix ) { + output += postfix; + } + + // Run the output through a user-specified post-formatter. + if ( edit ) { + output = edit ( output, originalInput ); + } + + // All done. + return output; + } + + // Accept a sting as input, output decoded number. + function formatFrom ( decimals, thousand, mark, prefix, postfix, encoder, decoder, negativeBefore, negative, edit, undo, input ) { + + var originalInput = input, inputIsNegative, output = ''; + + // User defined pre-decoder. Result must be a non empty string. + if ( undo ) { + input = undo(input); + } + + // Test the input. Can't be empty. + if ( !input || typeof input !== 'string' ) { + return false; + } + + // If the string starts with the negativeBefore value: remove it. + // Remember is was there, the number is negative. + if ( negativeBefore && strStartsWith(input, negativeBefore) ) { + input = input.replace(negativeBefore, ''); + inputIsNegative = true; + } + + // Repeat the same procedure for the prefix. + if ( prefix && strStartsWith(input, prefix) ) { + input = input.replace(prefix, ''); + } + + // And again for negative. + if ( negative && strStartsWith(input, negative) ) { + input = input.replace(negative, ''); + inputIsNegative = true; + } + + // Remove the postfix. + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/slice + if ( postfix && strEndsWith(input, postfix) ) { + input = input.slice(0, -1 * postfix.length); + } + + // Remove the thousand grouping. + if ( thousand ) { + input = input.split(thousand).join(''); + } + + // Set the decimal separator back to period. + if ( mark ) { + input = input.replace(mark, '.'); + } + + // Prepend the negative symbol. + if ( inputIsNegative ) { + output += '-'; + } + + // Add the number + output += input; + + // Covert to number. + output = Number(output.replace(/[^0-9\.\-.]/g, '')); + + // Run the user-specified post-decoder. + if ( decoder ) { + output = decoder(output); + } + + // Check is the output is valid, otherwise: return false. + if ( !isValidNumber(output) ) { + return false; + } + + return output; + } + + +// Framework + + // Validate formatting options + function validate ( inputOptions ) { + + var i, optionName, optionValue, + filteredOptions = {}; + + for ( i = 0; i < FormatOptions.length; i+=1 ) { + + optionName = FormatOptions[i]; + optionValue = inputOptions[optionName]; + + if ( optionValue === undefined ) { + + // Only default if negativeBefore isn't set. + if ( optionName === 'negative' && !filteredOptions['negativeBefore'] ) { + filteredOptions[optionName] = '-'; + // Don't set a default for mark when 'thousand' is set. + } else if ( optionName === 'mark' && filteredOptions['thousand'] !== '.' ) { + filteredOptions[optionName] = '.'; + } else { + filteredOptions[optionName] = false; + } + + // Floating points in JS are stable up to 7 decimals. + } else if ( optionName === 'decimals' ) { + if ( optionValue > 0 && optionValue < 8 ) { + filteredOptions[optionName] = optionValue; + } + + // These options, when provided, must be functions. + } else if ( optionName === 'encoder' || optionName === 'decoder' || optionName === 'edit' || optionName === 'undo' ) { + if ( typeof optionValue === 'function' ) { + filteredOptions[optionName] = optionValue; + } + + // Other options are strings. + } else { + + if ( typeof optionValue === 'string' ) { + filteredOptions[optionName] = optionValue; + } + } + } + + // Some values can't be extracted from a + // string if certain combinations are present. + throwEqualError(filteredOptions, 'mark', 'thousand'); + throwEqualError(filteredOptions, 'prefix', 'negative'); + throwEqualError(filteredOptions, 'prefix', 'negativeBefore'); + + return filteredOptions; + } + + // Pass all options as function arguments + function passAll ( options, method, input ) { + var i, args = []; + + // Add all options in order of FormatOptions + for ( i = 0; i < FormatOptions.length; i+=1 ) { + args.push(options[FormatOptions[i]]); + } + + // Append the input, then call the method, presenting all + // options as arguments. + args.push(input); + return method.apply('', args); + } + + /** @constructor */ + function wNumb ( options ) { + + if ( !(this instanceof wNumb) ) { + return new wNumb ( options ); + } + + if ( typeof options !== "object" ) { + return; + } + + options = validate(options); + + // Call 'formatTo' with proper arguments. + this['to'] = function ( input ) { + return passAll(options, formatTo, input); + }; + + // Call 'formatFrom' with proper arguments. + this['from'] = function ( input ) { + return passAll(options, formatFrom, input); + }; + } + + /** @export */ + window['wNumb'] = wNumb; + +}()); diff --git a/transaction_log.php b/transaction_log.php index a2747d4..cc4a791 100644 --- a/transaction_log.php +++ b/transaction_log.php @@ -758,7 +758,8 @@ if ((isset($_POST["MM_insert"])) && ($_POST["MM_insert"] == "ChangeDate")) {
"; echo "
"; - echo ""; + echo " +     "; ?>