UNPKG

dijit

Version:

Dijit provides a complete collection of user interface controls based on Dojo, giving you the power to create web applications that are highly optimized for usability, performance, internationalization, accessibility, but above all deliver an incredible u

419 lines (377 loc) 17.4 kB
define([ "dojo/_base/declare", // declare "dojo/_base/lang", // lang.hitch lang.mixin "dojo/i18n", // i18n.normalizeLocale, i18n.getLocalization "dojo/string", // string.rep "dojo/number", // number._realNumberRegexp number.format number.parse number.regexp "./RangeBoundTextBox" ], function(declare, lang, i18n, string, number, RangeBoundTextBox){ // module: // dijit/form/NumberTextBox // A private helper function to determine decimal information // Returns an object with "sep" and "places" properties var getDecimalInfo = function(constraints){ var constraints = constraints || {}, bundle = i18n.getLocalization("dojo.cldr", "number", i18n.normalizeLocale(constraints.locale)), pattern = constraints.pattern ? constraints.pattern : bundle[(constraints.type || "decimal")+"Format"]; // The number of places in the constraint can be specified in several ways, // the resolution order is: // // 1. If constraints.places is a number, use that // 2. If constraints.places is a string, which specifies a range, use the range max (e.g. 0,4) // 3. If a pattern is specified, use the implicit number of places in the pattern. // 4. If neither constraints.pattern or constraints.places is specified, use the locale default pattern var places; if(typeof constraints.places == "number"){ places = constraints.places; }else if(typeof constraints.places === "string" && constraints.places.length > 0){ places = constraints.places.replace(/.*,/, ""); }else{ places = (pattern.indexOf(".") != -1 ? pattern.split(".")[1].replace(/[^#0]/g, "").length : 0); } return { sep: bundle.decimal, places: places }; }; var NumberTextBoxMixin = declare("dijit.form.NumberTextBoxMixin", null, { // summary: // A mixin for all number textboxes // tags: // protected // Override ValidationTextBox.pattern.... we use a reg-ex generating function rather // than a straight regexp to deal with locale (plus formatting options too?) pattern: function(constraints){ // if focused, accept either currency data or NumberTextBox format return '(' + (this.focused && this.editOptions ? this._regExpGenerator(lang.delegate(constraints, this.editOptions)) + '|' : '') + this._regExpGenerator(constraints) + ')'; }, /*===== // constraints: NumberTextBox.__Constraints // Despite the name, this parameter specifies both constraints on the input // (including minimum/maximum allowed values) as well as // formatting options like places (the number of digits to display after // the decimal point). constraints: {}, ======*/ // value: Number // The value of this NumberTextBox as a Javascript Number (i.e., not a String). // If the displayed value is blank, the value is NaN, and if the user types in // an gibberish value (like "hello world"), the value is undefined // (i.e. get('value') returns undefined). // // Symmetrically, set('value', NaN) will clear the displayed value, // whereas set('value', undefined) will have no effect. value: NaN, // editOptions: [protected] Object // Properties to mix into constraints when the value is being edited. // This is here because we edit the number in the format "12345", which is // different than the display value (ex: "12,345") editOptions: { pattern: '#.######' }, /*===== _formatter: function(value, options){ // summary: // _formatter() is called by format(). It's the base routine for formatting a number, // as a string, for example converting 12345 into "12,345". // value: Number // The number to be converted into a string. // options: number.__FormatOptions? // Formatting options // tags: // protected extension return "12345"; // String }, =====*/ _formatter: number.format, /*===== _regExpGenerator: function(constraints){ // summary: // Generate a localized regular expression as a string, according to constraints. // constraints: number.__ParseOptions // Formatting options // tags: // protected return "(\d*).(\d*)"; // string }, =====*/ _regExpGenerator: number.regexp, // _decimalInfo: Object // summary: // An object containing decimal related properties relevant to this TextBox. // tags: // private _decimalInfo: getDecimalInfo(), postMixInProperties: function(){ this.inherited(arguments); this._set("type", "text"); // in case type="number" was specified which messes up parse/format }, _setConstraintsAttr: function(/*Object*/ constraints){ var places = typeof constraints.places == "number"? constraints.places : 0; if(places){ places++; } // decimal rounding errors take away another digit of precision if(typeof constraints.max != "number"){ constraints.max = 9 * Math.pow(10, 15-places); } if(typeof constraints.min != "number"){ constraints.min = -9 * Math.pow(10, 15-places); } this.inherited(arguments, [ constraints ]); if(this.focusNode && this.focusNode.value && !isNaN(this.value)){ this.set('value', this.value); } // Capture decimal information based on the constraint locale and pattern. this._decimalInfo = getDecimalInfo(constraints); }, _onFocus: function(/*String*/ by){ if(this.disabled || this.readOnly){ return; } var val = this.get('value'); if(typeof val == "number" && !isNaN(val)){ var formattedValue = this.format(val, this.constraints); if(formattedValue !== undefined){ this.textbox.value = formattedValue; // when NumberTextBox or descendants (i.e. CurrencyTextBox) format textbox.value when focused // all browsers except Chrome will select textbox contents when tabbed to by keyboard // force selection if not focused by mouse if (by !== "mouse") { this.textbox.select(); } } } this.inherited(arguments); }, format: function(/*Number*/ value, /*number.__FormatOptions*/ constraints){ // summary: // Formats the value as a Number, according to constraints. // tags: // protected var formattedValue = String(value); if(typeof value != "number"){ return formattedValue; } if(isNaN(value)){ return ""; } // check for exponential notation that dojo/number.format() chokes on if(!("rangeCheck" in this && this.rangeCheck(value, constraints)) && constraints.exponent !== false && /\de[-+]?\d/i.test(formattedValue)){ return formattedValue; } if(this.editOptions && this.focused){ constraints = lang.mixin({}, constraints, this.editOptions); } return this._formatter(value, constraints); }, /*===== _parser: function(value, constraints){ // summary: // Parses the string value as a Number, according to constraints. // value: String // String representing a number // constraints: number.__ParseOptions // Formatting options // tags: // protected return 123.45; // Number }, =====*/ _parser: number.parse, parse: function(/*String*/ value, /*number.__FormatOptions*/ constraints){ // summary: // Replaceable function to convert a formatted string to a number value // tags: // protected extension var parserOptions = lang.mixin({}, constraints, (this.editOptions && this.focused) ? this.editOptions : {}) if(this.focused && parserOptions.places != null /* or undefined */){ var places = parserOptions.places; var maxPlaces = typeof places === "number" ? places : Number(places.split(",").pop()); // handle number and range parserOptions.places = "0," + maxPlaces; } var v = this._parser(value, parserOptions); if(this.editOptions && this.focused && isNaN(v)){ v = this._parser(value, constraints); // parse w/o editOptions: not technically needed but is nice for the user } return v; }, _getDisplayedValueAttr: function(){ var v = this.inherited(arguments); return isNaN(v) ? this.textbox.value : v; }, filter: function(/*Number*/ value){ // summary: // This is called with both the display value (string), and the actual value (a number). // When called with the actual value it does corrections so that '' etc. are represented as NaN. // Otherwise it dispatches to the superclass's filter() method. // // See `dijit/form/TextBox.filter()` for more details. if(value == null /* or undefined */ || typeof value == "string" && value ==''){ return NaN; }else if(typeof value == "number" && !isNaN(value) && value != 0){ value = number.round(value, this._decimalInfo.places); } return this.inherited(arguments, [value]); }, serialize: function(/*Number*/ value, /*Object?*/ options){ // summary: // Convert value (a Number) into a canonical string (ie, how the number literal is written in javascript/java/C/etc.) // tags: // protected return (typeof value != "number" || isNaN(value)) ? '' : this.inherited(arguments); }, _setBlurValue: function(){ var val = lang.hitch(lang.delegate(this, { focused: true }), "get")('value'); // parse with editOptions this._setValueAttr(val, true); }, _setValueAttr: function(/*Number*/ value, /*Boolean?*/ priorityChange, /*String?*/ formattedValue){ // summary: // Hook so set('value', ...) works. if(value !== undefined && formattedValue === undefined){ formattedValue = String(value); if(typeof value == "number"){ if(isNaN(value)){ formattedValue = '' } // check for exponential notation that number.format chokes on else if(("rangeCheck" in this && this.rangeCheck(value, this.constraints)) || this.constraints.exponent === false || !/\de[-+]?\d/i.test(formattedValue)){ formattedValue = undefined; // lets format compute a real string value } }else if(!value){ // 0 processed in if branch above, ''|null|undefined flows through here formattedValue = ''; value = NaN; }else{ // non-numeric values value = undefined; } } this.inherited(arguments, [value, priorityChange, formattedValue]); }, _getValueAttr: function(){ // summary: // Hook so get('value') works. // Returns Number, NaN for '', or undefined for unparseable text var v = this.inherited(arguments); // returns Number for all values accepted by parse() or NaN for all other displayed values // If the displayed value of the textbox is gibberish (ex: "hello world"), this.inherited() above // returns NaN; this if() branch converts the return value to undefined. // Returning undefined prevents user text from being overwritten when doing _setValueAttr(_getValueAttr()). // A blank displayed value is still returned as NaN. if(isNaN(v) && this.textbox.value !== ''){ if(this.constraints.exponent !== false && /\de[-+]?\d/i.test(this.textbox.value) && (new RegExp("^"+number._realNumberRegexp(lang.delegate(this.constraints))+"$").test(this.textbox.value))){ // check for exponential notation that parse() rejected (erroneously?) var n = Number(this.textbox.value); return isNaN(n) ? undefined : n; // return exponential Number or undefined for random text (may not be possible to do with the above RegExp check) }else{ return undefined; // gibberish } }else{ return v; // Number or NaN for '' } }, isValid: function(/*Boolean*/ isFocused){ // Overrides dijit/form/RangeBoundTextBox.isValid() to check that the editing-mode value is valid since // it may not be formatted according to the regExp validation rules if(!this.focused || this._isEmpty(this.textbox.value)){ return this.inherited(arguments); }else{ var v = this.get('value'); if(!isNaN(v) && this.rangeCheck(v, this.constraints)){ if(this.constraints.exponent !== false && /\de[-+]?\d/i.test(this.textbox.value)){ // exponential, parse doesn't like it return true; // valid exponential number in range }else{ return this.inherited(arguments); } }else{ return false; } } }, _isValidSubset: function(){ // Overrides dijit/form/ValidationTextBox._isValidSubset() // // The inherited method only checks that the computed regex pattern is valid, which doesn't // take into account that numbers are a special case. Specifically: // // (1) An arbitrary amount of leading or trailing zero's can be ignored. // (2) Since numeric input always occurs in the order of most significant to least significant // digits, the maximum and minimum possible values for partially inputted numbers can easily // be determined by using the number of remaining digit spaces available. // // For example, if an input has a maxLength of 5, and a min value of greater than 100, then the subset // is invalid if there are 3 leading 0s. It remains valid for the first two. // // Another example is if the min value is 1.1. Once a value of 1.0 is entered, no additional trailing digits // could possibly satisify the min requirement. // // See ticket #17923 var hasMinConstraint = (typeof this.constraints.min == "number"), hasMaxConstraint = (typeof this.constraints.max == "number"), curVal = this.get('value'); // If there is no parsable number, or there are no min or max bounds, then we can safely // skip all remaining checks if(isNaN(curVal) || (!hasMinConstraint && !hasMaxConstraint)){ return this.inherited(arguments); } // This block picks apart the values in the text box to be used later to compute the min and max possible // values based on the current value and the remaining available digits. // // Warning: The use of a "num|0" expression, can be confusing. See the link below // for an explanation. // // http://stackoverflow.com/questions/12125421/why-does-a-shift-by-0-truncate-the-decimal var integerDigits = curVal|0, valNegative = curVal < 0, // Check if the current number has a decimal based on its locale hasDecimal = this.textbox.value.indexOf(this._decimalInfo.sep) != -1, // Determine the max digits based on the textbox length. If no length is // specified, chose a huge number to account for crazy formatting. maxDigits = this.maxLength || 20, // Determine the remaining digits, based on the max digits remainingDigitsCount = maxDigits - this.textbox.value.length, // avoid approximation issues by capturing the decimal portion of the value as the user-entered string fractionalDigitStr = hasDecimal ? this.textbox.value.split(this._decimalInfo.sep)[1].replace(/[^0-9]/g, "") : ""; // Create a normalized value string in the form of #.### var normalizedValueStr = hasDecimal ? integerDigits+"."+fractionalDigitStr : integerDigits+""; // The min and max values for the field can be determined using the following // logic: // // If the number is positive: // min value = the current value // max value = the current value with 9s appended for all remaining possible digits // else // min value = the current value with 9s appended for all remaining possible digits // max value = the current value // var ninePaddingStr = string.rep("9", remainingDigitsCount), minPossibleValue = curVal, maxPossibleValue = curVal; if (valNegative){ minPossibleValue = Number(normalizedValueStr+ninePaddingStr); } else{ maxPossibleValue = Number(normalizedValueStr+ninePaddingStr); } return !((hasMinConstraint && maxPossibleValue < this.constraints.min) || (hasMaxConstraint && minPossibleValue > this.constraints.max)); } }); var NumberTextBox = declare("dijit.form.NumberTextBox", [RangeBoundTextBox, NumberTextBoxMixin], { // summary: // A TextBox for entering numbers, with formatting and range checking // description: // NumberTextBox is a textbox for entering and displaying numbers, supporting // the following main features: // // 1. Enforce minimum/maximum allowed values (as well as enforcing that the user types // a number rather than a random string) // 2. NLS support (altering roles of comma and dot as "thousands-separator" and "decimal-point" // depending on locale). // 3. Separate modes for editing the value and displaying it, specifically that // the thousands separator character (typically comma) disappears when editing // but reappears after the field is blurred. // 4. Formatting and constraints regarding the number of places (digits after the decimal point) // allowed on input, and number of places displayed when blurred (see `constraints` parameter). baseClass: "dijitTextBox dijitNumberTextBox" }); NumberTextBox.Mixin = NumberTextBoxMixin; // for monkey patching /*===== NumberTextBox.__Constraints = declare([RangeBoundTextBox.__Constraints, number.__FormatOptions, number.__ParseOptions], { // summary: // Specifies both the rules on valid/invalid values (minimum, maximum, // number of required decimal places), and also formatting options for // displaying the value when the field is not focused. // example: // Minimum/maximum: // To specify a field between 0 and 120: // | {min:0,max:120} // To specify a field that must be an integer: // | {fractional:false} // To specify a field where 0 to 3 decimal places are allowed on input: // | {places:'0,3'} }); =====*/ return NumberTextBox; });