1 | define([
|
---|
2 | "dojo/_base/array", // array.forEach
|
---|
3 | "dojo/_base/declare", // declare
|
---|
4 | "dojo/dom", // dom.byId
|
---|
5 | "dojo/has",
|
---|
6 | "dojo/keys", // keys.ALT keys.CAPS_LOCK keys.CTRL keys.META keys.SHIFT
|
---|
7 | "dojo/_base/lang", // lang.mixin
|
---|
8 | "dojo/on", // on
|
---|
9 | "../main" // for exporting dijit._setSelectionRange, dijit.selectInputText
|
---|
10 | ], function(array, declare, dom, has, keys, lang, on, dijit){
|
---|
11 |
|
---|
12 | // module:
|
---|
13 | // dijit/form/_TextBoxMixin
|
---|
14 |
|
---|
15 | var _TextBoxMixin = declare("dijit.form._TextBoxMixin" + (has("dojo-bidi") ? "_NoBidi" : ""), null, {
|
---|
16 | // summary:
|
---|
17 | // A mixin for textbox form input widgets
|
---|
18 |
|
---|
19 | // trim: Boolean
|
---|
20 | // Removes leading and trailing whitespace if true. Default is false.
|
---|
21 | trim: false,
|
---|
22 |
|
---|
23 | // uppercase: Boolean
|
---|
24 | // Converts all characters to uppercase if true. Default is false.
|
---|
25 | uppercase: false,
|
---|
26 |
|
---|
27 | // lowercase: Boolean
|
---|
28 | // Converts all characters to lowercase if true. Default is false.
|
---|
29 | lowercase: false,
|
---|
30 |
|
---|
31 | // propercase: Boolean
|
---|
32 | // Converts the first character of each word to uppercase if true.
|
---|
33 | propercase: false,
|
---|
34 |
|
---|
35 | // maxLength: String
|
---|
36 | // HTML INPUT tag maxLength declaration.
|
---|
37 | maxLength: "",
|
---|
38 |
|
---|
39 | // selectOnClick: [const] Boolean
|
---|
40 | // If true, all text will be selected when focused with mouse
|
---|
41 | selectOnClick: false,
|
---|
42 |
|
---|
43 | // placeHolder: String
|
---|
44 | // Defines a hint to help users fill out the input field (as defined in HTML 5).
|
---|
45 | // This should only contain plain text (no html markup).
|
---|
46 | placeHolder: "",
|
---|
47 |
|
---|
48 | _getValueAttr: function(){
|
---|
49 | // summary:
|
---|
50 | // Hook so get('value') works as we like.
|
---|
51 | // description:
|
---|
52 | // For `dijit/form/TextBox` this basically returns the value of the `<input>`.
|
---|
53 | //
|
---|
54 | // For `dijit/form/MappedTextBox` subclasses, which have both
|
---|
55 | // a "displayed value" and a separate "submit value",
|
---|
56 | // This treats the "displayed value" as the master value, computing the
|
---|
57 | // submit value from it via this.parse().
|
---|
58 | return this.parse(this.get('displayedValue'), this.constraints);
|
---|
59 | },
|
---|
60 |
|
---|
61 | _setValueAttr: function(value, /*Boolean?*/ priorityChange, /*String?*/ formattedValue){
|
---|
62 | // summary:
|
---|
63 | // Hook so set('value', ...) works.
|
---|
64 | //
|
---|
65 | // description:
|
---|
66 | // Sets the value of the widget to "value" which can be of
|
---|
67 | // any type as determined by the widget.
|
---|
68 | //
|
---|
69 | // value:
|
---|
70 | // The visual element value is also set to a corresponding,
|
---|
71 | // but not necessarily the same, value.
|
---|
72 | //
|
---|
73 | // formattedValue:
|
---|
74 | // If specified, used to set the visual element value,
|
---|
75 | // otherwise a computed visual value is used.
|
---|
76 | //
|
---|
77 | // priorityChange:
|
---|
78 | // If true, an onChange event is fired immediately instead of
|
---|
79 | // waiting for the next blur event.
|
---|
80 |
|
---|
81 | var filteredValue;
|
---|
82 | if(value !== undefined){
|
---|
83 | // TODO: this is calling filter() on both the display value and the actual value.
|
---|
84 | // I added a comment to the filter() definition about this, but it should be changed.
|
---|
85 | filteredValue = this.filter(value);
|
---|
86 | if(typeof formattedValue != "string"){
|
---|
87 | if(filteredValue !== null && ((typeof filteredValue != "number") || !isNaN(filteredValue))){
|
---|
88 | formattedValue = this.filter(this.format(filteredValue, this.constraints));
|
---|
89 | }else{
|
---|
90 | formattedValue = '';
|
---|
91 | }
|
---|
92 | }
|
---|
93 | }
|
---|
94 | if(formattedValue != null /* and !undefined */ && ((typeof formattedValue) != "number" || !isNaN(formattedValue)) && this.textbox.value != formattedValue){
|
---|
95 | this.textbox.value = formattedValue;
|
---|
96 | this._set("displayedValue", this.get("displayedValue"));
|
---|
97 | }
|
---|
98 |
|
---|
99 | this.inherited(arguments, [filteredValue, priorityChange]);
|
---|
100 | },
|
---|
101 |
|
---|
102 | // displayedValue: String
|
---|
103 | // For subclasses like ComboBox where the displayed value
|
---|
104 | // (ex: Kentucky) and the serialized value (ex: KY) are different,
|
---|
105 | // this represents the displayed value.
|
---|
106 | //
|
---|
107 | // Setting 'displayedValue' through set('displayedValue', ...)
|
---|
108 | // updates 'value', and vice-versa. Otherwise 'value' is updated
|
---|
109 | // from 'displayedValue' periodically, like onBlur etc.
|
---|
110 | //
|
---|
111 | // TODO: move declaration to MappedTextBox?
|
---|
112 | // Problem is that ComboBox references displayedValue,
|
---|
113 | // for benefit of FilteringSelect.
|
---|
114 | displayedValue: "",
|
---|
115 |
|
---|
116 | _getDisplayedValueAttr: function(){
|
---|
117 | // summary:
|
---|
118 | // Hook so get('displayedValue') works.
|
---|
119 | // description:
|
---|
120 | // Returns the displayed value (what the user sees on the screen),
|
---|
121 | // after filtering (ie, trimming spaces etc.).
|
---|
122 | //
|
---|
123 | // For some subclasses of TextBox (like ComboBox), the displayed value
|
---|
124 | // is different from the serialized value that's actually
|
---|
125 | // sent to the server (see `dijit/form/ValidationTextBox.serialize()`)
|
---|
126 |
|
---|
127 | // TODO: maybe we should update this.displayedValue on every keystroke so that we don't need
|
---|
128 | // this method
|
---|
129 | // TODO: this isn't really the displayed value when the user is typing
|
---|
130 | return this.filter(this.textbox.value);
|
---|
131 | },
|
---|
132 |
|
---|
133 | _setDisplayedValueAttr: function(/*String*/ value){
|
---|
134 | // summary:
|
---|
135 | // Hook so set('displayedValue', ...) works.
|
---|
136 | // description:
|
---|
137 | // Sets the value of the visual element to the string "value".
|
---|
138 | // The widget value is also set to a corresponding,
|
---|
139 | // but not necessarily the same, value.
|
---|
140 |
|
---|
141 | if(value == null /* or undefined */){
|
---|
142 | value = ''
|
---|
143 | }
|
---|
144 | else if(typeof value != "string"){
|
---|
145 | value = String(value)
|
---|
146 | }
|
---|
147 |
|
---|
148 | this.textbox.value = value;
|
---|
149 |
|
---|
150 | // sets the serialized value to something corresponding to specified displayedValue
|
---|
151 | // (if possible), and also updates the textbox.value, for example converting "123"
|
---|
152 | // to "123.00"
|
---|
153 | this._setValueAttr(this.get('value'), undefined);
|
---|
154 |
|
---|
155 | this._set("displayedValue", this.get('displayedValue'));
|
---|
156 | },
|
---|
157 |
|
---|
158 | format: function(value /*=====, constraints =====*/){
|
---|
159 | // summary:
|
---|
160 | // Replaceable function to convert a value to a properly formatted string.
|
---|
161 | // value: String
|
---|
162 | // constraints: Object
|
---|
163 | // tags:
|
---|
164 | // protected extension
|
---|
165 | return value == null /* or undefined */ ? "" : (value.toString ? value.toString() : value);
|
---|
166 | },
|
---|
167 |
|
---|
168 | parse: function(value /*=====, constraints =====*/){
|
---|
169 | // summary:
|
---|
170 | // Replaceable function to convert a formatted string to a value
|
---|
171 | // value: String
|
---|
172 | // constraints: Object
|
---|
173 | // tags:
|
---|
174 | // protected extension
|
---|
175 |
|
---|
176 | return value; // String
|
---|
177 | },
|
---|
178 |
|
---|
179 | _refreshState: function(){
|
---|
180 | // summary:
|
---|
181 | // After the user types some characters, etc., this method is
|
---|
182 | // called to check the field for validity etc. The base method
|
---|
183 | // in `dijit/form/TextBox` does nothing, but subclasses override.
|
---|
184 | // tags:
|
---|
185 | // protected
|
---|
186 | },
|
---|
187 |
|
---|
188 | onInput: function(/*===== event =====*/){
|
---|
189 | // summary:
|
---|
190 | // Connect to this function to receive notifications of various user data-input events.
|
---|
191 | // Return false to cancel the event and prevent it from being processed.
|
---|
192 | // event:
|
---|
193 | // keydown | keypress | cut | paste | input
|
---|
194 | // tags:
|
---|
195 | // callback
|
---|
196 | },
|
---|
197 |
|
---|
198 | __skipInputEvent: false,
|
---|
199 | _onInput: function(/*Event*/ evt){
|
---|
200 | // summary:
|
---|
201 | // Called AFTER the input event has happened
|
---|
202 |
|
---|
203 | this._processInput(evt);
|
---|
204 |
|
---|
205 | if(this.intermediateChanges){
|
---|
206 | // allow the key to post to the widget input box
|
---|
207 | this.defer(function(){
|
---|
208 | this._handleOnChange(this.get('value'), false);
|
---|
209 | });
|
---|
210 | }
|
---|
211 | },
|
---|
212 |
|
---|
213 | _processInput: function(/*Event*/ evt){
|
---|
214 | // summary:
|
---|
215 | // Default action handler for user input events
|
---|
216 |
|
---|
217 | this._refreshState();
|
---|
218 |
|
---|
219 | // In case someone is watch()'ing for changes to displayedValue
|
---|
220 | this._set("displayedValue", this.get("displayedValue"));
|
---|
221 | },
|
---|
222 |
|
---|
223 | postCreate: function(){
|
---|
224 | // setting the value here is needed since value="" in the template causes "undefined"
|
---|
225 | // and setting in the DOM (instead of the JS object) helps with form reset actions
|
---|
226 | this.textbox.setAttribute("value", this.textbox.value); // DOM and JS values should be the same
|
---|
227 |
|
---|
228 | this.inherited(arguments);
|
---|
229 |
|
---|
230 | // normalize input events to reduce spurious event processing
|
---|
231 | // onkeydown: do not forward modifier keys
|
---|
232 | // set charOrCode to numeric keycode
|
---|
233 | // onkeypress: do not forward numeric charOrCode keys (already sent through onkeydown)
|
---|
234 | // onpaste & oncut: set charOrCode to 229 (IME)
|
---|
235 | // oninput: if primary event not already processed, set charOrCode to 229 (IME), else do not forward
|
---|
236 | var handleEvent = function(e){
|
---|
237 | var charOrCode;
|
---|
238 | if(e.type == "keydown"){
|
---|
239 | charOrCode = e.keyCode;
|
---|
240 | switch(charOrCode){ // ignore state keys
|
---|
241 | case keys.SHIFT:
|
---|
242 | case keys.ALT:
|
---|
243 | case keys.CTRL:
|
---|
244 | case keys.META:
|
---|
245 | case keys.CAPS_LOCK:
|
---|
246 | case keys.NUM_LOCK:
|
---|
247 | case keys.SCROLL_LOCK:
|
---|
248 | return;
|
---|
249 | }
|
---|
250 | if(!e.ctrlKey && !e.metaKey && !e.altKey){ // no modifiers
|
---|
251 | switch(charOrCode){ // ignore location keys
|
---|
252 | case keys.NUMPAD_0:
|
---|
253 | case keys.NUMPAD_1:
|
---|
254 | case keys.NUMPAD_2:
|
---|
255 | case keys.NUMPAD_3:
|
---|
256 | case keys.NUMPAD_4:
|
---|
257 | case keys.NUMPAD_5:
|
---|
258 | case keys.NUMPAD_6:
|
---|
259 | case keys.NUMPAD_7:
|
---|
260 | case keys.NUMPAD_8:
|
---|
261 | case keys.NUMPAD_9:
|
---|
262 | case keys.NUMPAD_MULTIPLY:
|
---|
263 | case keys.NUMPAD_PLUS:
|
---|
264 | case keys.NUMPAD_ENTER:
|
---|
265 | case keys.NUMPAD_MINUS:
|
---|
266 | case keys.NUMPAD_PERIOD:
|
---|
267 | case keys.NUMPAD_DIVIDE:
|
---|
268 | return;
|
---|
269 | }
|
---|
270 | if((charOrCode >= 65 && charOrCode <= 90) || (charOrCode >= 48 && charOrCode <= 57) || charOrCode == keys.SPACE){
|
---|
271 | return; // keypress will handle simple non-modified printable keys
|
---|
272 | }
|
---|
273 | var named = false;
|
---|
274 | for(var i in keys){
|
---|
275 | if(keys[i] === e.keyCode){
|
---|
276 | named = true;
|
---|
277 | break;
|
---|
278 | }
|
---|
279 | }
|
---|
280 | if(!named){
|
---|
281 | return;
|
---|
282 | } // only allow named ones through
|
---|
283 | }
|
---|
284 | }
|
---|
285 | charOrCode = e.charCode >= 32 ? String.fromCharCode(e.charCode) : e.charCode;
|
---|
286 | if(!charOrCode){
|
---|
287 | charOrCode = (e.keyCode >= 65 && e.keyCode <= 90) || (e.keyCode >= 48 && e.keyCode <= 57) || e.keyCode == keys.SPACE ? String.fromCharCode(e.keyCode) : e.keyCode;
|
---|
288 | }
|
---|
289 | if(!charOrCode){
|
---|
290 | charOrCode = 229; // IME
|
---|
291 | }
|
---|
292 | if(e.type == "keypress"){
|
---|
293 | if(typeof charOrCode != "string"){
|
---|
294 | return;
|
---|
295 | }
|
---|
296 | if((charOrCode >= 'a' && charOrCode <= 'z') || (charOrCode >= 'A' && charOrCode <= 'Z') || (charOrCode >= '0' && charOrCode <= '9') || (charOrCode === ' ')){
|
---|
297 | if(e.ctrlKey || e.metaKey || e.altKey){
|
---|
298 | return;
|
---|
299 | } // can only be stopped reliably in keydown
|
---|
300 | }
|
---|
301 | }
|
---|
302 | if(e.type == "input"){
|
---|
303 | if(this.__skipInputEvent){ // duplicate event
|
---|
304 | this.__skipInputEvent = false;
|
---|
305 | return;
|
---|
306 | }
|
---|
307 | }else{
|
---|
308 | this.__skipInputEvent = true;
|
---|
309 | }
|
---|
310 | // create fake event to set charOrCode and to know if preventDefault() was called
|
---|
311 | var faux = { faux: true }, attr;
|
---|
312 | for(attr in e){
|
---|
313 | if(attr != "layerX" && attr != "layerY"){ // prevent WebKit warnings
|
---|
314 | var v = e[attr];
|
---|
315 | if(typeof v != "function" && typeof v != "undefined"){
|
---|
316 | faux[attr] = v;
|
---|
317 | }
|
---|
318 | }
|
---|
319 | }
|
---|
320 | lang.mixin(faux, {
|
---|
321 | charOrCode: charOrCode,
|
---|
322 | _wasConsumed: false,
|
---|
323 | preventDefault: function(){
|
---|
324 | faux._wasConsumed = true;
|
---|
325 | e.preventDefault();
|
---|
326 | },
|
---|
327 | stopPropagation: function(){
|
---|
328 | e.stopPropagation();
|
---|
329 | }
|
---|
330 | });
|
---|
331 | // give web page author a chance to consume the event
|
---|
332 | //console.log(faux.type + ', charOrCode = (' + (typeof charOrCode) + ') ' + charOrCode + ', ctrl ' + !!faux.ctrlKey + ', alt ' + !!faux.altKey + ', meta ' + !!faux.metaKey + ', shift ' + !!faux.shiftKey);
|
---|
333 | if(this.onInput(faux) === false){ // return false means stop
|
---|
334 | faux.preventDefault();
|
---|
335 | faux.stopPropagation();
|
---|
336 | }
|
---|
337 | if(faux._wasConsumed){
|
---|
338 | return;
|
---|
339 | } // if preventDefault was called
|
---|
340 | this.defer(function(){
|
---|
341 | this._onInput(faux);
|
---|
342 | }); // widget notification after key has posted
|
---|
343 | if(e.type == "keypress"){
|
---|
344 | e.stopPropagation(); // don't allow parents to stop printables from being typed
|
---|
345 | }
|
---|
346 | };
|
---|
347 | this.own(on(this.textbox, "keydown, keypress, paste, cut, input, compositionend", lang.hitch(this, handleEvent)));
|
---|
348 | },
|
---|
349 |
|
---|
350 | _blankValue: '', // if the textbox is blank, what value should be reported
|
---|
351 | filter: function(val){
|
---|
352 | // summary:
|
---|
353 | // Auto-corrections (such as trimming) that are applied to textbox
|
---|
354 | // value on blur or form submit.
|
---|
355 | // description:
|
---|
356 | // For MappedTextBox subclasses, this is called twice
|
---|
357 | //
|
---|
358 | // - once with the display value
|
---|
359 | // - once the value as set/returned by set('value', ...)
|
---|
360 | //
|
---|
361 | // and get('value'), ex: a Number for NumberTextBox.
|
---|
362 | //
|
---|
363 | // In the latter case it does corrections like converting null to NaN. In
|
---|
364 | // the former case the NumberTextBox.filter() method calls this.inherited()
|
---|
365 | // to execute standard trimming code in TextBox.filter().
|
---|
366 | //
|
---|
367 | // TODO: break this into two methods in 2.0
|
---|
368 | //
|
---|
369 | // tags:
|
---|
370 | // protected extension
|
---|
371 | if(val === null){
|
---|
372 | return this._blankValue;
|
---|
373 | }
|
---|
374 | if(typeof val != "string"){
|
---|
375 | return val;
|
---|
376 | }
|
---|
377 | if(this.trim){
|
---|
378 | val = lang.trim(val);
|
---|
379 | }
|
---|
380 | if(this.uppercase){
|
---|
381 | val = val.toUpperCase();
|
---|
382 | }
|
---|
383 | if(this.lowercase){
|
---|
384 | val = val.toLowerCase();
|
---|
385 | }
|
---|
386 | if(this.propercase){
|
---|
387 | val = val.replace(/[^\s]+/g, function(word){
|
---|
388 | return word.substring(0, 1).toUpperCase() + word.substring(1);
|
---|
389 | });
|
---|
390 | }
|
---|
391 | return val;
|
---|
392 | },
|
---|
393 |
|
---|
394 | _setBlurValue: function(){
|
---|
395 | // Format the displayed value, for example (for NumberTextBox) convert 1.4 to 1.400,
|
---|
396 | // or (for CurrencyTextBox) 2.50 to $2.50
|
---|
397 |
|
---|
398 | this._setValueAttr(this.get('value'), true);
|
---|
399 | },
|
---|
400 |
|
---|
401 | _onBlur: function(e){
|
---|
402 | if(this.disabled){
|
---|
403 | return;
|
---|
404 | }
|
---|
405 | this._setBlurValue();
|
---|
406 | this.inherited(arguments);
|
---|
407 | },
|
---|
408 |
|
---|
409 | _isTextSelected: function(){
|
---|
410 | return this.textbox.selectionStart != this.textbox.selectionEnd;
|
---|
411 | },
|
---|
412 |
|
---|
413 | _onFocus: function(/*String*/ by){
|
---|
414 | if(this.disabled || this.readOnly){
|
---|
415 | return;
|
---|
416 | }
|
---|
417 |
|
---|
418 | // Select all text on focus via click if nothing already selected.
|
---|
419 | // Since mouse-up will clear the selection, need to defer selection until after mouse-up.
|
---|
420 | // Don't do anything on focus by tabbing into the widget since there's no associated mouse-up event.
|
---|
421 | if(this.selectOnClick && by == "mouse"){
|
---|
422 | // Use on.once() to only select all text on first click only; otherwise users would have no way to clear
|
---|
423 | // the selection.
|
---|
424 | this._selectOnClickHandle = on.once(this.domNode, "mouseup, touchend", lang.hitch(this, function(evt){
|
---|
425 | // Check if the user selected some text manually (mouse-down, mouse-move, mouse-up)
|
---|
426 | // and if not, then select all the text
|
---|
427 | if(!this._isTextSelected()){
|
---|
428 | _TextBoxMixin.selectInputText(this.textbox);
|
---|
429 | }
|
---|
430 | }));
|
---|
431 | this.own(this._selectOnClickHandle);
|
---|
432 |
|
---|
433 | // in case the mouseup never comes
|
---|
434 | this.defer(function(){
|
---|
435 | if(this._selectOnClickHandle){
|
---|
436 | this._selectOnClickHandle.remove();
|
---|
437 | this._selectOnClickHandle = null;
|
---|
438 | }
|
---|
439 | }, 500); // if mouseup not received soon, then treat it as some gesture
|
---|
440 | }
|
---|
441 | // call this.inherited() before refreshState(), since this.inherited() will possibly scroll the viewport
|
---|
442 | // (to scroll the TextBox into view), which will affect how _refreshState() positions the tooltip
|
---|
443 | this.inherited(arguments);
|
---|
444 |
|
---|
445 | this._refreshState();
|
---|
446 | },
|
---|
447 |
|
---|
448 | reset: function(){
|
---|
449 | // Overrides `dijit/_FormWidget/reset()`.
|
---|
450 | // Additionally resets the displayed textbox value to ''
|
---|
451 | this.textbox.value = '';
|
---|
452 | this.inherited(arguments);
|
---|
453 | }
|
---|
454 | });
|
---|
455 |
|
---|
456 | if(has("dojo-bidi")){
|
---|
457 | _TextBoxMixin = declare("dijit.form._TextBoxMixin", _TextBoxMixin, {
|
---|
458 | _setValueAttr: function(){
|
---|
459 | this.inherited(arguments);
|
---|
460 | this.applyTextDir(this.focusNode);
|
---|
461 | },
|
---|
462 | _setDisplayedValueAttr: function(){
|
---|
463 | this.inherited(arguments);
|
---|
464 | this.applyTextDir(this.focusNode);
|
---|
465 | },
|
---|
466 | _onInput: function(){
|
---|
467 | this.applyTextDir(this.focusNode);
|
---|
468 | this.inherited(arguments);
|
---|
469 | }
|
---|
470 | });
|
---|
471 | }
|
---|
472 |
|
---|
473 | _TextBoxMixin._setSelectionRange = dijit._setSelectionRange = function(/*DomNode*/ element, /*Number?*/ start, /*Number?*/ stop){
|
---|
474 | if(element.setSelectionRange){
|
---|
475 | element.setSelectionRange(start, stop);
|
---|
476 | }
|
---|
477 | };
|
---|
478 |
|
---|
479 | _TextBoxMixin.selectInputText = dijit.selectInputText = function(/*DomNode*/ element, /*Number?*/ start, /*Number?*/ stop){
|
---|
480 | // summary:
|
---|
481 | // Select text in the input element argument, from start (default 0), to stop (default end).
|
---|
482 |
|
---|
483 | // TODO: use functions in _editor/selection.js?
|
---|
484 | element = dom.byId(element);
|
---|
485 | if(isNaN(start)){
|
---|
486 | start = 0;
|
---|
487 | }
|
---|
488 | if(isNaN(stop)){
|
---|
489 | stop = element.value ? element.value.length : 0;
|
---|
490 | }
|
---|
491 | try{
|
---|
492 | element.focus();
|
---|
493 | _TextBoxMixin._setSelectionRange(element, start, stop);
|
---|
494 | }catch(e){ /* squelch random errors (esp. on IE) from unexpected focus changes or DOM nodes being hidden */
|
---|
495 | }
|
---|
496 | };
|
---|
497 |
|
---|
498 | return _TextBoxMixin;
|
---|
499 | });
|
---|