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

547 lines (488 loc) 19.2 kB
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; });