/* * typeahead.js * https://github.com/twitter/typeahead.js * Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT */ var Input = (function() { 'use strict'; var specialKeyCodeMap; specialKeyCodeMap = { 9: 'tab', 27: 'esc', 37: 'left', 39: 'right', 13: 'enter', 38: 'up', 40: 'down' }; // constructor // ----------- function Input(o, www) { o = o || {}; if (!o.input) { $.error('input is missing'); } www.mixin(this); this.$hint = $(o.hint); this.$input = $(o.input); // the query defaults to whatever the value of the input is // on initialization, it'll most likely be an empty string this.query = this.$input.val(); // for tracking when a change event should be triggered this.queryWhenFocused = this.hasFocus() ? this.query : null; // helps with calculating the width of the input's value this.$overflowHelper = buildOverflowHelper(this.$input); // detect the initial lang direction this._checkLanguageDirection(); // if no hint, noop all the hint related functions if (this.$hint.length === 0) { this.setHint = this.getHint = this.clearHint = this.clearHintIfInvalid = _.noop; } } // static methods // -------------- Input.normalizeQuery = function(str) { // strips leading whitespace and condenses all whitespace return (_.toStr(str)).replace(/^\s*/g, '').replace(/\s{2,}/g, ' '); }; // instance methods // ---------------- _.mixin(Input.prototype, EventEmitter, { // ### event handlers _onBlur: function onBlur() { this.resetInputValue(); this.trigger('blurred'); }, _onFocus: function onFocus() { this.queryWhenFocused = this.query; this.trigger('focused'); }, _onKeydown: function onKeydown($e) { // which is normalized and consistent (but not for ie) var keyName = specialKeyCodeMap[$e.which || $e.keyCode]; this._managePreventDefault(keyName, $e); if (keyName && this._shouldTrigger(keyName, $e)) { this.trigger(keyName + 'Keyed', $e); } }, _onInput: function onInput() { this._setQuery(this.getInputValue()); this.clearHintIfInvalid(); this._checkLanguageDirection(); }, // ### private _managePreventDefault: function managePreventDefault(keyName, $e) { var preventDefault; switch (keyName) { case 'up': case 'down': preventDefault = !withModifier($e); break; default: preventDefault = false; } preventDefault && $e.preventDefault(); }, _shouldTrigger: function shouldTrigger(keyName, $e) { var trigger; switch (keyName) { case 'tab': trigger = !withModifier($e); break; default: trigger = true; } return trigger; }, _checkLanguageDirection: function checkLanguageDirection() { var dir = (this.$input.css('direction') || 'ltr').toLowerCase(); if (this.dir !== dir) { this.dir = dir; this.$hint.attr('dir', dir); this.trigger('langDirChanged', dir); } }, _setQuery: function setQuery(val, silent) { var areEquivalent, hasDifferentWhitespace; areEquivalent = areQueriesEquivalent(val, this.query); hasDifferentWhitespace = areEquivalent ? this.query.length !== val.length : false; this.query = val; if (!silent && !areEquivalent) { this.trigger('queryChanged', this.query); } else if (!silent && hasDifferentWhitespace) { this.trigger('whitespaceChanged', this.query); } }, // ### public bind: function() { var that = this, onBlur, onFocus, onKeydown, onInput; // bound functions onBlur = _.bind(this._onBlur, this); onFocus = _.bind(this._onFocus, this); onKeydown = _.bind(this._onKeydown, this); onInput = _.bind(this._onInput, this); this.$input .on('blur.tt', onBlur) .on('focus.tt', onFocus) .on('keydown.tt', onKeydown); // ie8 don't support the input event // ie9 doesn't fire the input event when characters are removed if (!_.isMsie() || _.isMsie() > 9) { this.$input.on('input.tt', onInput); } else { this.$input.on('keydown.tt keypress.tt cut.tt paste.tt', function($e) { // if a special key triggered this, ignore it if (specialKeyCodeMap[$e.which || $e.keyCode]) { return; } // give the browser a chance to update the value of the input // before checking to see if the query changed _.defer(_.bind(that._onInput, that, $e)); }); } return this; }, focus: function focus() { this.$input.focus(); }, blur: function blur() { this.$input.blur(); }, getLangDir: function getLangDir() { return this.dir; }, getQuery: function getQuery() { return this.query || ''; }, setQuery: function setQuery(val, silent) { this.setInputValue(val); this._setQuery(val, silent); }, hasQueryChangedSinceLastFocus: function hasQueryChangedSinceLastFocus() { return this.query !== this.queryWhenFocused; }, getInputValue: function getInputValue() { return this.$input.val(); }, setInputValue: function setInputValue(value) { this.$input.val(value); this.clearHintIfInvalid(); this._checkLanguageDirection(); }, resetInputValue: function resetInputValue() { this.setInputValue(this.query); }, getHint: function getHint() { return this.$hint.val(); }, setHint: function setHint(value) { this.$hint.val(value); }, clearHint: function clearHint() { this.setHint(''); }, clearHintIfInvalid: function clearHintIfInvalid() { var val, hint, valIsPrefixOfHint, isValid; val = this.getInputValue(); hint = this.getHint(); valIsPrefixOfHint = val !== hint && hint.indexOf(val) === 0; isValid = val !== '' && valIsPrefixOfHint && !this.hasOverflow(); !isValid && this.clearHint(); }, hasFocus: function hasFocus() { return this.$input.is(':focus'); }, hasOverflow: function hasOverflow() { // 2 is arbitrary, just picking a small number to handle edge cases var constraint = this.$input.width() - 2; this.$overflowHelper.text(this.getInputValue()); return this.$overflowHelper.width() >= constraint; }, isCursorAtEnd: function() { var valueLength, selectionStart, range; valueLength = this.$input.val().length; selectionStart = this.$input[0].selectionStart; if (_.isNumber(selectionStart)) { return selectionStart === valueLength; } else if (document.selection) { // NOTE: this won't work unless the input has focus, the good news // is this code should only get called when the input has focus range = document.selection.createRange(); range.moveStart('character', -valueLength); return valueLength === range.text.length; } return true; }, destroy: function destroy() { this.$hint.off('.tt'); this.$input.off('.tt'); this.$overflowHelper.remove(); // #970 this.$hint = this.$input = this.$overflowHelper = $('