1 | define([ |
---|
2 | "dojo/_base/declare", // declare |
---|
3 | "dojo/_base/lang", // lang.hitch lang.mixin |
---|
4 | "dojo/number", // number._realNumberRegexp number.format number.parse number.regexp |
---|
5 | "./RangeBoundTextBox" |
---|
6 | ], function(declare, lang, number, RangeBoundTextBox){ |
---|
7 | |
---|
8 | // module: |
---|
9 | // dijit/form/NumberTextBox |
---|
10 | |
---|
11 | |
---|
12 | var NumberTextBoxMixin = declare("dijit.form.NumberTextBoxMixin", null, { |
---|
13 | // summary: |
---|
14 | // A mixin for all number textboxes |
---|
15 | // tags: |
---|
16 | // protected |
---|
17 | |
---|
18 | // Override ValidationTextBox.pattern.... we use a reg-ex generating function rather |
---|
19 | // than a straight regexp to deal with locale (plus formatting options too?) |
---|
20 | pattern: function(constraints){ |
---|
21 | // if focused, accept either currency data or NumberTextBox format |
---|
22 | return '(' + (this.focused && this.editOptions ? this._regExpGenerator(lang.delegate(constraints, this.editOptions)) + '|' : '') |
---|
23 | + this._regExpGenerator(constraints) + ')'; |
---|
24 | }, |
---|
25 | |
---|
26 | /*===== |
---|
27 | // constraints: NumberTextBox.__Constraints |
---|
28 | // Despite the name, this parameter specifies both constraints on the input |
---|
29 | // (including minimum/maximum allowed values) as well as |
---|
30 | // formatting options like places (the number of digits to display after |
---|
31 | // the decimal point). |
---|
32 | constraints: {}, |
---|
33 | ======*/ |
---|
34 | |
---|
35 | // value: Number |
---|
36 | // The value of this NumberTextBox as a Javascript Number (i.e., not a String). |
---|
37 | // If the displayed value is blank, the value is NaN, and if the user types in |
---|
38 | // an gibberish value (like "hello world"), the value is undefined |
---|
39 | // (i.e. get('value') returns undefined). |
---|
40 | // |
---|
41 | // Symmetrically, set('value', NaN) will clear the displayed value, |
---|
42 | // whereas set('value', undefined) will have no effect. |
---|
43 | value: NaN, |
---|
44 | |
---|
45 | // editOptions: [protected] Object |
---|
46 | // Properties to mix into constraints when the value is being edited. |
---|
47 | // This is here because we edit the number in the format "12345", which is |
---|
48 | // different than the display value (ex: "12,345") |
---|
49 | editOptions: { pattern: '#.######' }, |
---|
50 | |
---|
51 | /*===== |
---|
52 | _formatter: function(value, options){ |
---|
53 | // summary: |
---|
54 | // _formatter() is called by format(). It's the base routine for formatting a number, |
---|
55 | // as a string, for example converting 12345 into "12,345". |
---|
56 | // value: Number |
---|
57 | // The number to be converted into a string. |
---|
58 | // options: number.__FormatOptions? |
---|
59 | // Formatting options |
---|
60 | // tags: |
---|
61 | // protected extension |
---|
62 | |
---|
63 | return "12345"; // String |
---|
64 | }, |
---|
65 | =====*/ |
---|
66 | _formatter: number.format, |
---|
67 | |
---|
68 | /*===== |
---|
69 | _regExpGenerator: function(constraints){ |
---|
70 | // summary: |
---|
71 | // Generate a localized regular expression as a string, according to constraints. |
---|
72 | // constraints: number.__ParseOptions |
---|
73 | // Formatting options |
---|
74 | // tags: |
---|
75 | // protected |
---|
76 | |
---|
77 | return "(\d*).(\d*)"; // string |
---|
78 | }, |
---|
79 | =====*/ |
---|
80 | _regExpGenerator: number.regexp, |
---|
81 | |
---|
82 | postMixInProperties: function(){ |
---|
83 | this.inherited(arguments); |
---|
84 | this._set("type", "text"); // in case type="number" was specified which messes up parse/format |
---|
85 | }, |
---|
86 | |
---|
87 | _setConstraintsAttr: function(/*Object*/ constraints){ |
---|
88 | var places = typeof constraints.places == "number"? constraints.places : 0; |
---|
89 | if(places){ places++; } // decimal rounding errors take away another digit of precision |
---|
90 | if(typeof constraints.max != "number"){ |
---|
91 | constraints.max = 9 * Math.pow(10, 15-places); |
---|
92 | } |
---|
93 | if(typeof constraints.min != "number"){ |
---|
94 | constraints.min = -9 * Math.pow(10, 15-places); |
---|
95 | } |
---|
96 | this.inherited(arguments, [ constraints ]); |
---|
97 | if(this.focusNode && this.focusNode.value && !isNaN(this.value)){ |
---|
98 | this.set('value', this.value); |
---|
99 | } |
---|
100 | }, |
---|
101 | |
---|
102 | _onFocus: function(){ |
---|
103 | if(this.disabled || this.readOnly){ return; } |
---|
104 | var val = this.get('value'); |
---|
105 | if(typeof val == "number" && !isNaN(val)){ |
---|
106 | var formattedValue = this.format(val, this.constraints); |
---|
107 | if(formattedValue !== undefined){ |
---|
108 | this.textbox.value = formattedValue; |
---|
109 | } |
---|
110 | } |
---|
111 | this.inherited(arguments); |
---|
112 | }, |
---|
113 | |
---|
114 | format: function(/*Number*/ value, /*number.__FormatOptions*/ constraints){ |
---|
115 | // summary: |
---|
116 | // Formats the value as a Number, according to constraints. |
---|
117 | // tags: |
---|
118 | // protected |
---|
119 | |
---|
120 | var formattedValue = String(value); |
---|
121 | if(typeof value != "number"){ return formattedValue; } |
---|
122 | if(isNaN(value)){ return ""; } |
---|
123 | // check for exponential notation that dojo/number.format() chokes on |
---|
124 | if(!("rangeCheck" in this && this.rangeCheck(value, constraints)) && constraints.exponent !== false && /\de[-+]?\d/i.test(formattedValue)){ |
---|
125 | return formattedValue; |
---|
126 | } |
---|
127 | if(this.editOptions && this.focused){ |
---|
128 | constraints = lang.mixin({}, constraints, this.editOptions); |
---|
129 | } |
---|
130 | return this._formatter(value, constraints); |
---|
131 | }, |
---|
132 | |
---|
133 | /*===== |
---|
134 | _parser: function(value, constraints){ |
---|
135 | // summary: |
---|
136 | // Parses the string value as a Number, according to constraints. |
---|
137 | // value: String |
---|
138 | // String representing a number |
---|
139 | // constraints: number.__ParseOptions |
---|
140 | // Formatting options |
---|
141 | // tags: |
---|
142 | // protected |
---|
143 | |
---|
144 | return 123.45; // Number |
---|
145 | }, |
---|
146 | =====*/ |
---|
147 | _parser: number.parse, |
---|
148 | |
---|
149 | parse: function(/*String*/ value, /*number.__FormatOptions*/ constraints){ |
---|
150 | // summary: |
---|
151 | // Replaceable function to convert a formatted string to a number value |
---|
152 | // tags: |
---|
153 | // protected extension |
---|
154 | |
---|
155 | var v = this._parser(value, lang.mixin({}, constraints, (this.editOptions && this.focused) ? this.editOptions : {})); |
---|
156 | if(this.editOptions && this.focused && isNaN(v)){ |
---|
157 | v = this._parser(value, constraints); // parse w/o editOptions: not technically needed but is nice for the user |
---|
158 | } |
---|
159 | return v; |
---|
160 | }, |
---|
161 | |
---|
162 | _getDisplayedValueAttr: function(){ |
---|
163 | var v = this.inherited(arguments); |
---|
164 | return isNaN(v) ? this.textbox.value : v; |
---|
165 | }, |
---|
166 | |
---|
167 | filter: function(/*Number*/ value){ |
---|
168 | // summary: |
---|
169 | // This is called with both the display value (string), and the actual value (a number). |
---|
170 | // When called with the actual value it does corrections so that '' etc. are represented as NaN. |
---|
171 | // Otherwise it dispatches to the superclass's filter() method. |
---|
172 | // |
---|
173 | // See `dijit/form/TextBox.filter()` for more details. |
---|
174 | return (value == null /* or undefined */ || value === '') ? NaN : this.inherited(arguments); // set('value', null||''||undefined) should fire onChange(NaN) |
---|
175 | }, |
---|
176 | |
---|
177 | serialize: function(/*Number*/ value, /*Object?*/ options){ |
---|
178 | // summary: |
---|
179 | // Convert value (a Number) into a canonical string (ie, how the number literal is written in javascript/java/C/etc.) |
---|
180 | // tags: |
---|
181 | // protected |
---|
182 | return (typeof value != "number" || isNaN(value)) ? '' : this.inherited(arguments); |
---|
183 | }, |
---|
184 | |
---|
185 | _setBlurValue: function(){ |
---|
186 | var val = lang.hitch(lang.delegate(this, { focused: true }), "get")('value'); // parse with editOptions |
---|
187 | this._setValueAttr(val, true); |
---|
188 | }, |
---|
189 | |
---|
190 | _setValueAttr: function(/*Number*/ value, /*Boolean?*/ priorityChange, /*String?*/ formattedValue){ |
---|
191 | // summary: |
---|
192 | // Hook so set('value', ...) works. |
---|
193 | if(value !== undefined && formattedValue === undefined){ |
---|
194 | formattedValue = String(value); |
---|
195 | if(typeof value == "number"){ |
---|
196 | if(isNaN(value)){ formattedValue = '' } |
---|
197 | // check for exponential notation that number.format chokes on |
---|
198 | else if(("rangeCheck" in this && this.rangeCheck(value, this.constraints)) || this.constraints.exponent === false || !/\de[-+]?\d/i.test(formattedValue)){ |
---|
199 | formattedValue = undefined; // lets format compute a real string value |
---|
200 | } |
---|
201 | }else if(!value){ // 0 processed in if branch above, ''|null|undefined flows through here |
---|
202 | formattedValue = ''; |
---|
203 | value = NaN; |
---|
204 | }else{ // non-numeric values |
---|
205 | value = undefined; |
---|
206 | } |
---|
207 | } |
---|
208 | this.inherited(arguments, [value, priorityChange, formattedValue]); |
---|
209 | }, |
---|
210 | |
---|
211 | _getValueAttr: function(){ |
---|
212 | // summary: |
---|
213 | // Hook so get('value') works. |
---|
214 | // Returns Number, NaN for '', or undefined for unparseable text |
---|
215 | var v = this.inherited(arguments); // returns Number for all values accepted by parse() or NaN for all other displayed values |
---|
216 | |
---|
217 | // If the displayed value of the textbox is gibberish (ex: "hello world"), this.inherited() above |
---|
218 | // returns NaN; this if() branch converts the return value to undefined. |
---|
219 | // Returning undefined prevents user text from being overwritten when doing _setValueAttr(_getValueAttr()). |
---|
220 | // A blank displayed value is still returned as NaN. |
---|
221 | if(isNaN(v) && this.textbox.value !== ''){ |
---|
222 | 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?) |
---|
223 | var n = Number(this.textbox.value); |
---|
224 | return isNaN(n) ? undefined : n; // return exponential Number or undefined for random text (may not be possible to do with the above RegExp check) |
---|
225 | }else{ |
---|
226 | return undefined; // gibberish |
---|
227 | } |
---|
228 | }else{ |
---|
229 | return v; // Number or NaN for '' |
---|
230 | } |
---|
231 | }, |
---|
232 | |
---|
233 | isValid: function(/*Boolean*/ isFocused){ |
---|
234 | // Overrides dijit/form/RangeBoundTextBox.isValid() to check that the editing-mode value is valid since |
---|
235 | // it may not be formatted according to the regExp validation rules |
---|
236 | if(!this.focused || this._isEmpty(this.textbox.value)){ |
---|
237 | return this.inherited(arguments); |
---|
238 | }else{ |
---|
239 | var v = this.get('value'); |
---|
240 | if(!isNaN(v) && this.rangeCheck(v, this.constraints)){ |
---|
241 | if(this.constraints.exponent !== false && /\de[-+]?\d/i.test(this.textbox.value)){ // exponential, parse doesn't like it |
---|
242 | return true; // valid exponential number in range |
---|
243 | }else{ |
---|
244 | return this.inherited(arguments); |
---|
245 | } |
---|
246 | }else{ |
---|
247 | return false; |
---|
248 | } |
---|
249 | } |
---|
250 | } |
---|
251 | }); |
---|
252 | |
---|
253 | var NumberTextBox = declare("dijit.form.NumberTextBox", [RangeBoundTextBox, NumberTextBoxMixin], { |
---|
254 | // summary: |
---|
255 | // A TextBox for entering numbers, with formatting and range checking |
---|
256 | // description: |
---|
257 | // NumberTextBox is a textbox for entering and displaying numbers, supporting |
---|
258 | // the following main features: |
---|
259 | // |
---|
260 | // 1. Enforce minimum/maximum allowed values (as well as enforcing that the user types |
---|
261 | // a number rather than a random string) |
---|
262 | // 2. NLS support (altering roles of comma and dot as "thousands-separator" and "decimal-point" |
---|
263 | // depending on locale). |
---|
264 | // 3. Separate modes for editing the value and displaying it, specifically that |
---|
265 | // the thousands separator character (typically comma) disappears when editing |
---|
266 | // but reappears after the field is blurred. |
---|
267 | // 4. Formatting and constraints regarding the number of places (digits after the decimal point) |
---|
268 | // allowed on input, and number of places displayed when blurred (see `constraints` parameter). |
---|
269 | |
---|
270 | baseClass: "dijitTextBox dijitNumberTextBox" |
---|
271 | }); |
---|
272 | |
---|
273 | NumberTextBox.Mixin = NumberTextBoxMixin; // for monkey patching |
---|
274 | |
---|
275 | /*===== |
---|
276 | NumberTextBox.__Constraints = declare([RangeBoundTextBox.__Constraints, number.__FormatOptions, number.__ParseOptions], { |
---|
277 | // summary: |
---|
278 | // Specifies both the rules on valid/invalid values (minimum, maximum, |
---|
279 | // number of required decimal places), and also formatting options for |
---|
280 | // displaying the value when the field is not focused. |
---|
281 | // example: |
---|
282 | // Minimum/maximum: |
---|
283 | // To specify a field between 0 and 120: |
---|
284 | // | {min:0,max:120} |
---|
285 | // To specify a field that must be an integer: |
---|
286 | // | {fractional:false} |
---|
287 | // To specify a field where 0 to 3 decimal places are allowed on input: |
---|
288 | // | {places:'0,3'} |
---|
289 | }); |
---|
290 | =====*/ |
---|
291 | |
---|
292 | return NumberTextBox; |
---|
293 | }); |
---|