yzt
2023-05-26 de4278af2fd46705a40bac58ec01122db6b7f3d7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
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(){
            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;
                }
            }
            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 v = this._parser(value, lang.mixin({}, constraints, (this.editOptions && this.focused) ? this.editOptions : {}));
            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;
});