Jonathan Rosenbaum
10 years ago
5 changed files with 687 additions and 14 deletions
@ -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-', '') || '<div/>' ); |
||||
|
|
||||
|
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 )); |
@ -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; |
||||
|
|
||||
|
}()); |
Loading…
Reference in new issue