You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
546 lines
19 KiB
546 lines
19 KiB
define([ |
|
"dojo/_base/array", // array.forEach |
|
"dojo/_base/declare", // declare |
|
"dojo/dom", // dom.byId |
|
"dojo/sniff", // has("ie"), has("dojo-bidi") |
|
"dojo/keys", // keys.ALT keys.CAPS_LOCK keys.CTRL keys.META keys.SHIFT |
|
"dojo/_base/lang", // lang.mixin |
|
"dojo/on", // on |
|
"../main" // for exporting dijit._setSelectionRange, dijit.selectInputText |
|
], function(array, declare, dom, has, keys, lang, on, dijit){ |
|
|
|
// module: |
|
// dijit/form/_TextBoxMixin |
|
|
|
var _TextBoxMixin = declare("dijit.form._TextBoxMixin" + (has("dojo-bidi") ? "_NoBidi" : ""), null, { |
|
// summary: |
|
// A mixin for textbox form input widgets |
|
|
|
// trim: Boolean |
|
// Removes leading and trailing whitespace if true. Default is false. |
|
trim: false, |
|
|
|
// uppercase: Boolean |
|
// Converts all characters to uppercase if true. Default is false. |
|
uppercase: false, |
|
|
|
// lowercase: Boolean |
|
// Converts all characters to lowercase if true. Default is false. |
|
lowercase: false, |
|
|
|
// propercase: Boolean |
|
// Converts the first character of each word to uppercase if true. |
|
propercase: false, |
|
|
|
// maxLength: String |
|
// HTML INPUT tag maxLength declaration. |
|
maxLength: "", |
|
|
|
// selectOnClick: [const] Boolean |
|
// If true, all text will be selected when focused with mouse |
|
selectOnClick: false, |
|
|
|
// placeHolder: String |
|
// Defines a hint to help users fill out the input field (as defined in HTML 5). |
|
// This should only contain plain text (no html markup). |
|
placeHolder: "", |
|
|
|
_getValueAttr: function(){ |
|
// summary: |
|
// Hook so get('value') works as we like. |
|
// description: |
|
// For `dijit/form/TextBox` this basically returns the value of the `<input>`. |
|
// |
|
// For `dijit/form/MappedTextBox` subclasses, which have both |
|
// a "displayed value" and a separate "submit value", |
|
// This treats the "displayed value" as the master value, computing the |
|
// submit value from it via this.parse(). |
|
return this.parse(this.get('displayedValue'), this.constraints); |
|
}, |
|
|
|
_setValueAttr: function(value, /*Boolean?*/ priorityChange, /*String?*/ formattedValue){ |
|
// summary: |
|
// Hook so set('value', ...) works. |
|
// |
|
// description: |
|
// Sets the value of the widget to "value" which can be of |
|
// any type as determined by the widget. |
|
// |
|
// value: |
|
// The visual element value is also set to a corresponding, |
|
// but not necessarily the same, value. |
|
// |
|
// formattedValue: |
|
// If specified, used to set the visual element value, |
|
// otherwise a computed visual value is used. |
|
// |
|
// priorityChange: |
|
// If true, an onChange event is fired immediately instead of |
|
// waiting for the next blur event. |
|
|
|
var filteredValue; |
|
if(value !== undefined){ |
|
// TODO: this is calling filter() on both the display value and the actual value. |
|
// I added a comment to the filter() definition about this, but it should be changed. |
|
filteredValue = this.filter(value); |
|
if(typeof formattedValue != "string"){ |
|
if(filteredValue !== null && ((typeof filteredValue != "number") || !isNaN(filteredValue))){ |
|
formattedValue = this.filter(this.format(filteredValue, this.constraints)); |
|
}else{ |
|
formattedValue = ''; |
|
} |
|
// Ensure the filtered value does not change after being formatted. See track #17955. |
|
// |
|
// This check is only applied when the formatted value is not specified by the caller in order to allow the |
|
// behavior to be overriden. This is needed whenever value synonyms cannot be determined using parse/compare. For |
|
// example, dijit/form/FilteringSelect determines the formatted value asynchronously and applies it using a |
|
// callback to this method. |
|
// |
|
// TODO: Should developers be warned that they broke the round trip on format? |
|
if (this.compare(filteredValue, this.filter(this.parse(formattedValue, this.constraints))) != 0){ |
|
formattedValue = null; |
|
} |
|
} |
|
} |
|
if(formattedValue != null /* and !undefined */ && ((typeof formattedValue) != "number" || !isNaN(formattedValue)) && this.textbox.value != formattedValue){ |
|
this.textbox.value = formattedValue; |
|
this._set("displayedValue", this.get("displayedValue")); |
|
} |
|
|
|
this.inherited(arguments, [filteredValue, priorityChange]); |
|
}, |
|
|
|
// displayedValue: String |
|
// For subclasses like ComboBox where the displayed value |
|
// (ex: Kentucky) and the serialized value (ex: KY) are different, |
|
// this represents the displayed value. |
|
// |
|
// Setting 'displayedValue' through set('displayedValue', ...) |
|
// updates 'value', and vice-versa. Otherwise 'value' is updated |
|
// from 'displayedValue' periodically, like onBlur etc. |
|
// |
|
// TODO: move declaration to MappedTextBox? |
|
// Problem is that ComboBox references displayedValue, |
|
// for benefit of FilteringSelect. |
|
displayedValue: "", |
|
|
|
_getDisplayedValueAttr: function(){ |
|
// summary: |
|
// Hook so get('displayedValue') works. |
|
// description: |
|
// Returns the displayed value (what the user sees on the screen), |
|
// after filtering (ie, trimming spaces etc.). |
|
// |
|
// For some subclasses of TextBox (like ComboBox), the displayed value |
|
// is different from the serialized value that's actually |
|
// sent to the server (see `dijit/form/ValidationTextBox.serialize()`) |
|
|
|
// TODO: maybe we should update this.displayedValue on every keystroke so that we don't need |
|
// this method |
|
// TODO: this isn't really the displayed value when the user is typing |
|
return this.filter(this.textbox.value); |
|
}, |
|
|
|
_setDisplayedValueAttr: function(/*String*/ value){ |
|
// summary: |
|
// Hook so set('displayedValue', ...) works. |
|
// description: |
|
// Sets the value of the visual element to the string "value". |
|
// The widget value is also set to a corresponding, |
|
// but not necessarily the same, value. |
|
|
|
if(value == null /* or undefined */){ |
|
value = '' |
|
} |
|
else if(typeof value != "string"){ |
|
value = String(value) |
|
} |
|
|
|
this.textbox.value = value; |
|
|
|
// sets the serialized value to something corresponding to specified displayedValue |
|
// (if possible), and also updates the textbox.value, for example converting "123" |
|
// to "123.00" |
|
this._setValueAttr(this.get('value'), undefined); |
|
|
|
this._set("displayedValue", this.get('displayedValue')); |
|
}, |
|
|
|
format: function(value /*=====, constraints =====*/){ |
|
// summary: |
|
// Replaceable function to convert a value to a properly formatted string. |
|
// value: String |
|
// constraints: Object |
|
// tags: |
|
// protected extension |
|
return value == null /* or undefined */ ? "" : (value.toString ? value.toString() : value); |
|
}, |
|
|
|
parse: function(value /*=====, constraints =====*/){ |
|
// summary: |
|
// Replaceable function to convert a formatted string to a value |
|
// value: String |
|
// constraints: Object |
|
// tags: |
|
// protected extension |
|
|
|
return value; // String |
|
}, |
|
|
|
_refreshState: function(){ |
|
// summary: |
|
// After the user types some characters, etc., this method is |
|
// called to check the field for validity etc. The base method |
|
// in `dijit/form/TextBox` does nothing, but subclasses override. |
|
// tags: |
|
// protected |
|
}, |
|
|
|
onInput: function(/*Event*/ /*===== evt =====*/){ |
|
// summary: |
|
// Connect to this function to receive notifications of various user data-input events. |
|
// Return false to cancel the event and prevent it from being processed. |
|
// Note that although for historical reasons this method is called `onInput()`, it doesn't |
|
// correspond to the standard DOM "input" event, because it occurs before the input has been processed. |
|
// event: |
|
// keydown | keypress | cut | paste | compositionend |
|
// tags: |
|
// callback |
|
}, |
|
|
|
_onInput: function(/*Event*/ evt){ |
|
// summary: |
|
// Called AFTER the input event has happened and this.textbox.value has new value. |
|
|
|
this._lastInputEventValue = this.textbox.value; |
|
|
|
// For Combobox, this needs to be called w/the keydown/keypress event that was passed to onInput(). |
|
// As a backup, use the "input" event itself. |
|
this._processInput(this._lastInputProducingEvent || evt); |
|
delete this._lastInputProducingEvent; |
|
|
|
if(this.intermediateChanges){ |
|
this._handleOnChange(this.get('value'), false); |
|
} |
|
}, |
|
|
|
_processInput: function(/*Event*/ /*===== evt =====*/){ |
|
// summary: |
|
// Default action handler for user input events. |
|
// Called after the "input" event (i.e. after this.textbox.value has been updated), |
|
// but `evt` is the keydown/keypress/etc. event that triggered the "input" event. |
|
// tags: |
|
// protected |
|
|
|
this._refreshState(); |
|
|
|
// In case someone is watch()'ing for changes to displayedValue |
|
this._set("displayedValue", this.get("displayedValue")); |
|
}, |
|
|
|
postCreate: function(){ |
|
// setting the value here is needed since value="" in the template causes "undefined" |
|
// and setting in the DOM (instead of the JS object) helps with form reset actions |
|
this.textbox.setAttribute("value", this.textbox.value); // DOM and JS values should be the same |
|
|
|
this.inherited(arguments); |
|
|
|
// normalize input events to reduce spurious event processing |
|
// keydown: do not forward modifier keys |
|
// set charOrCode to numeric keycode |
|
// keypress: do not forward numeric charOrCode keys (already sent through onkeydown) |
|
// paste, cut, compositionend: set charOrCode to 229 (IME) |
|
function handleEvent(e){ |
|
var charOrCode; |
|
|
|
// Filter out keydown events that will be followed by keypress events. Note that chrome/android |
|
// w/word suggestion has keydown/229 events on typing with no corresponding keypress events. |
|
if(e.type == "keydown" && e.keyCode != 229){ |
|
charOrCode = e.keyCode; |
|
switch(charOrCode){ // ignore state keys |
|
case keys.SHIFT: |
|
case keys.ALT: |
|
case keys.CTRL: |
|
case keys.META: |
|
case keys.CAPS_LOCK: |
|
case keys.NUM_LOCK: |
|
case keys.SCROLL_LOCK: |
|
return; |
|
} |
|
if(!e.ctrlKey && !e.metaKey && !e.altKey){ // no modifiers |
|
switch(charOrCode){ // ignore location keys |
|
case keys.NUMPAD_0: |
|
case keys.NUMPAD_1: |
|
case keys.NUMPAD_2: |
|
case keys.NUMPAD_3: |
|
case keys.NUMPAD_4: |
|
case keys.NUMPAD_5: |
|
case keys.NUMPAD_6: |
|
case keys.NUMPAD_7: |
|
case keys.NUMPAD_8: |
|
case keys.NUMPAD_9: |
|
case keys.NUMPAD_MULTIPLY: |
|
case keys.NUMPAD_PLUS: |
|
case keys.NUMPAD_ENTER: |
|
case keys.NUMPAD_MINUS: |
|
case keys.NUMPAD_PERIOD: |
|
case keys.NUMPAD_DIVIDE: |
|
return; |
|
} |
|
if((charOrCode >= 65 && charOrCode <= 90) || (charOrCode >= 48 && charOrCode <= 57) || charOrCode == keys.SPACE){ |
|
return; // keypress will handle simple non-modified printable keys |
|
} |
|
var named = false; |
|
for(var i in keys){ |
|
if(keys[i] === e.keyCode){ |
|
named = true; |
|
break; |
|
} |
|
} |
|
if(!named){ |
|
return; |
|
} // only allow named ones through |
|
} |
|
} |
|
|
|
charOrCode = e.charCode >= 32 ? String.fromCharCode(e.charCode) : e.charCode; |
|
if(!charOrCode){ |
|
charOrCode = (e.keyCode >= 65 && e.keyCode <= 90) || (e.keyCode >= 48 && e.keyCode <= 57) || e.keyCode == keys.SPACE ? String.fromCharCode(e.keyCode) : e.keyCode; |
|
} |
|
if(!charOrCode){ |
|
charOrCode = 229; // IME |
|
} |
|
if(e.type == "keypress"){ |
|
if(typeof charOrCode != "string"){ |
|
return; |
|
} |
|
if((charOrCode >= 'a' && charOrCode <= 'z') || (charOrCode >= 'A' && charOrCode <= 'Z') || (charOrCode >= '0' && charOrCode <= '9') || (charOrCode === ' ')){ |
|
if(e.ctrlKey || e.metaKey || e.altKey){ |
|
return; |
|
} // can only be stopped reliably in keydown |
|
} |
|
} |
|
|
|
// create fake event to set charOrCode and to know if preventDefault() was called |
|
var faux = { faux: true }, attr; |
|
for(attr in e){ |
|
if(!/^(layer[XY]|returnValue|keyLocation)$/.test(attr)){ // prevent WebKit warnings |
|
var v = e[attr]; |
|
if(typeof v != "function" && typeof v != "undefined"){ |
|
faux[attr] = v; |
|
} |
|
} |
|
} |
|
lang.mixin(faux, { |
|
charOrCode: charOrCode, |
|
_wasConsumed: false, |
|
preventDefault: function(){ |
|
faux._wasConsumed = true; |
|
e.preventDefault(); |
|
}, |
|
stopPropagation: function(){ |
|
e.stopPropagation(); |
|
} |
|
}); |
|
|
|
this._lastInputProducingEvent = faux; |
|
|
|
// Give web page author a chance to consume the event. Note that onInput() may be called multiple times |
|
// for same keystroke: once for keypress event and once for input event. |
|
//console.log(faux.type + ', charOrCode = (' + (typeof charOrCode) + ') ' + charOrCode + ', ctrl ' + !!faux.ctrlKey + ', alt ' + !!faux.altKey + ', meta ' + !!faux.metaKey + ', shift ' + !!faux.shiftKey); |
|
if(this.onInput(faux) === false){ // return false means stop |
|
faux.preventDefault(); |
|
faux.stopPropagation(); |
|
} |
|
if(faux._wasConsumed){ |
|
return; |
|
} // if preventDefault was called |
|
|
|
// IE8 doesn't emit the "input" event at all, and IE9 doesn't emit it for backspace, delete, cut, etc. |
|
// Since the code below (and perhaps user code) depends on that event, emit it synthetically. |
|
// See http://benalpert.com/2013/06/18/a-near-perfect-oninput-shim-for-ie-8-and-9.html. |
|
if(has("ie") <= 9){ |
|
switch(e.keyCode){ |
|
case keys.TAB: |
|
case keys.ESCAPE: |
|
case keys.DOWN_ARROW: |
|
case keys.UP_ARROW: |
|
case keys.LEFT_ARROW: |
|
case keys.RIGHT_ARROW: |
|
// These keys may alter the <input>'s value indirectly, but we don't want to emit an "input" |
|
// event. For example, the up/down arrows in TimeTextBox or ComboBox will cause the next |
|
// dropdown item's value to be copied to the <input>. |
|
break; |
|
default: |
|
if(e.keyCode == keys.ENTER && this.textbox.tagName.toLowerCase() != "textarea"){ |
|
break; |
|
} |
|
this.defer(function(){ |
|
if(this.textbox.value !== this._lastInputEventValue){ |
|
on.emit(this.textbox, "input", {bubbles: true}); |
|
} |
|
}); |
|
} |
|
} |
|
} |
|
this.own( |
|
on(this.textbox, "keydown, keypress, paste, cut, compositionend", lang.hitch(this, handleEvent)), |
|
on(this.textbox, "input", lang.hitch(this, "_onInput")), |
|
|
|
// Allow keypress to bubble to this.domNode, so that TextBox.on("keypress", ...) works, |
|
// but prevent it from further propagating, so that typing into a TextBox inside a Toolbar doesn't |
|
// trigger the Toolbar's letter key navigation. |
|
on(this.domNode, "keypress", function(e){ e.stopPropagation(); }) |
|
); |
|
}, |
|
|
|
_blankValue: '', // if the textbox is blank, what value should be reported |
|
filter: function(val){ |
|
// summary: |
|
// Auto-corrections (such as trimming) that are applied to textbox |
|
// value on blur or form submit. |
|
// description: |
|
// For MappedTextBox subclasses, this is called twice |
|
// |
|
// - once with the display value |
|
// - once the value as set/returned by set('value', ...) |
|
// |
|
// and get('value'), ex: a Number for NumberTextBox. |
|
// |
|
// In the latter case it does corrections like converting null to NaN. In |
|
// the former case the NumberTextBox.filter() method calls this.inherited() |
|
// to execute standard trimming code in TextBox.filter(). |
|
// |
|
// TODO: break this into two methods in 2.0 |
|
// |
|
// tags: |
|
// protected extension |
|
if(val === null){ |
|
return this._blankValue; |
|
} |
|
if(typeof val != "string"){ |
|
return val; |
|
} |
|
if(this.trim){ |
|
val = lang.trim(val); |
|
} |
|
if(this.uppercase){ |
|
val = val.toUpperCase(); |
|
} |
|
if(this.lowercase){ |
|
val = val.toLowerCase(); |
|
} |
|
if(this.propercase){ |
|
val = val.replace(/[^\s]+/g, function(word){ |
|
return word.substring(0, 1).toUpperCase() + word.substring(1); |
|
}); |
|
} |
|
return val; |
|
}, |
|
|
|
_setBlurValue: function(){ |
|
// Format the displayed value, for example (for NumberTextBox) convert 1.4 to 1.400, |
|
// or (for CurrencyTextBox) 2.50 to $2.50 |
|
|
|
this._setValueAttr(this.get('value'), true); |
|
}, |
|
|
|
_onBlur: function(e){ |
|
if(this.disabled){ |
|
return; |
|
} |
|
this._setBlurValue(); |
|
this.inherited(arguments); |
|
}, |
|
|
|
_isTextSelected: function(){ |
|
return this.textbox.selectionStart != this.textbox.selectionEnd; |
|
}, |
|
|
|
_onFocus: function(/*String*/ by){ |
|
if(this.disabled || this.readOnly){ |
|
return; |
|
} |
|
|
|
// Select all text on focus via click if nothing already selected. |
|
// Since mouse-up will clear the selection, need to defer selection until after mouse-up. |
|
// Don't do anything on focus by tabbing into the widget since there's no associated mouse-up event. |
|
if(this.selectOnClick && by == "mouse"){ |
|
// Use on.once() to only select all text on first click only; otherwise users would have no way to clear |
|
// the selection. |
|
this._selectOnClickHandle = on.once(this.domNode, "mouseup, touchend", lang.hitch(this, function(evt){ |
|
// Check if the user selected some text manually (mouse-down, mouse-move, mouse-up) |
|
// and if not, then select all the text |
|
if(!this._isTextSelected()){ |
|
_TextBoxMixin.selectInputText(this.textbox); |
|
} |
|
})); |
|
this.own(this._selectOnClickHandle); |
|
|
|
// in case the mouseup never comes |
|
this.defer(function(){ |
|
if(this._selectOnClickHandle){ |
|
this._selectOnClickHandle.remove(); |
|
this._selectOnClickHandle = null; |
|
} |
|
}, 500); // if mouseup not received soon, then treat it as some gesture |
|
} |
|
// call this.inherited() before refreshState(), since this.inherited() will possibly scroll the viewport |
|
// (to scroll the TextBox into view), which will affect how _refreshState() positions the tooltip |
|
this.inherited(arguments); |
|
|
|
this._refreshState(); |
|
}, |
|
|
|
reset: function(){ |
|
// Overrides `dijit/_FormWidget/reset()`. |
|
// Additionally resets the displayed textbox value to '' |
|
this.textbox.value = ''; |
|
this.inherited(arguments); |
|
} |
|
}); |
|
|
|
if(has("dojo-bidi")){ |
|
_TextBoxMixin = declare("dijit.form._TextBoxMixin", _TextBoxMixin, { |
|
_setValueAttr: function(){ |
|
this.inherited(arguments); |
|
this.applyTextDir(this.focusNode); |
|
}, |
|
_setDisplayedValueAttr: function(){ |
|
this.inherited(arguments); |
|
this.applyTextDir(this.focusNode); |
|
}, |
|
_onInput: function(){ |
|
this.applyTextDir(this.focusNode); |
|
this.inherited(arguments); |
|
} |
|
}); |
|
} |
|
|
|
_TextBoxMixin._setSelectionRange = dijit._setSelectionRange = function(/*DomNode*/ element, /*Number?*/ start, /*Number?*/ stop){ |
|
if(element.setSelectionRange){ |
|
element.setSelectionRange(start, stop); |
|
} |
|
}; |
|
|
|
_TextBoxMixin.selectInputText = dijit.selectInputText = function(/*DomNode*/ element, /*Number?*/ start, /*Number?*/ stop){ |
|
// summary: |
|
// Select text in the input element argument, from start (default 0), to stop (default end). |
|
|
|
// TODO: use functions in _editor/selection.js? |
|
element = dom.byId(element); |
|
if(isNaN(start)){ |
|
start = 0; |
|
} |
|
if(isNaN(stop)){ |
|
stop = element.value ? element.value.length : 0; |
|
} |
|
try{ |
|
element.focus(); |
|
_TextBoxMixin._setSelectionRange(element, start, stop); |
|
}catch(e){ /* squelch random errors (esp. on IE) from unexpected focus changes or DOM nodes being hidden */ |
|
} |
|
}; |
|
|
|
return _TextBoxMixin; |
|
});
|
|
|