Subversion Repositories SmartDukaan

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
15403 manish.sha 1
/*
2
 * typeahead.js
3
 * https://github.com/twitter/typeahead.js
4
 * Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT
5
 */
6
 
7
var Typeahead = (function() {
8
  'use strict';
9
 
10
  var attrsKey = 'ttAttrs';
11
 
12
  // constructor
13
  // -----------
14
 
15
  // THOUGHT: what if datasets could dynamically be added/removed?
16
  function Typeahead(o) {
17
    var $menu, $input, $hint;
18
 
19
    o = o || {};
20
 
21
    if (!o.input) {
22
      $.error('missing input');
23
    }
24
 
25
    this.isActivated = false;
26
    this.autoselect = !!o.autoselect;
27
    this.minLength = _.isNumber(o.minLength) ? o.minLength : 1;
28
    this.$node = buildDom(o.input, o.withHint);
29
 
30
    $menu = this.$node.find('.tt-dropdown-menu');
31
    $input = this.$node.find('.tt-input');
32
    $hint = this.$node.find('.tt-hint');
33
 
34
    // #705: if there's scrollable overflow, ie doesn't support
35
    // blur cancellations when the scrollbar is clicked
36
    //
37
    // #351: preventDefault won't cancel blurs in ie <= 8
38
    $input.on('blur.tt', function($e) {
39
      var active, isActive, hasActive;
40
 
41
      active = document.activeElement;
42
      isActive = $menu.is(active);
43
      hasActive = $menu.has(active).length > 0;
44
 
45
      if (_.isMsie() && (isActive || hasActive)) {
46
        $e.preventDefault();
47
        // stop immediate in order to prevent Input#_onBlur from
48
        // getting exectued
49
        $e.stopImmediatePropagation();
50
        _.defer(function() { $input.focus(); });
51
      }
52
    });
53
 
54
    // #351: prevents input blur due to clicks within dropdown menu
55
    $menu.on('mousedown.tt', function($e) { $e.preventDefault(); });
56
 
57
    this.eventBus = o.eventBus || new EventBus({ el: $input });
58
 
59
    this.dropdown = new Dropdown({ menu: $menu, datasets: o.datasets })
60
    .onSync('suggestionClicked', this._onSuggestionClicked, this)
61
    .onSync('cursorMoved', this._onCursorMoved, this)
62
    .onSync('cursorRemoved', this._onCursorRemoved, this)
63
    .onSync('opened', this._onOpened, this)
64
    .onSync('closed', this._onClosed, this)
65
    .onAsync('datasetRendered', this._onDatasetRendered, this);
66
 
67
    this.input = new Input({ input: $input, hint: $hint })
68
    .onSync('focused', this._onFocused, this)
69
    .onSync('blurred', this._onBlurred, this)
70
    .onSync('enterKeyed', this._onEnterKeyed, this)
71
    .onSync('tabKeyed', this._onTabKeyed, this)
72
    .onSync('escKeyed', this._onEscKeyed, this)
73
    .onSync('upKeyed', this._onUpKeyed, this)
74
    .onSync('downKeyed', this._onDownKeyed, this)
75
    .onSync('leftKeyed', this._onLeftKeyed, this)
76
    .onSync('rightKeyed', this._onRightKeyed, this)
77
    .onSync('queryChanged', this._onQueryChanged, this)
78
    .onSync('whitespaceChanged', this._onWhitespaceChanged, this);
79
 
80
    this._setLanguageDirection();
81
  }
82
 
83
  // instance methods
84
  // ----------------
85
 
86
  _.mixin(Typeahead.prototype, {
87
 
88
    // ### private
89
 
90
    _onSuggestionClicked: function onSuggestionClicked(type, $el) {
91
      var datum;
92
 
93
      if (datum = this.dropdown.getDatumForSuggestion($el)) {
94
        this._select(datum);
95
      }
96
    },
97
 
98
    _onCursorMoved: function onCursorMoved() {
99
      var datum = this.dropdown.getDatumForCursor();
100
 
101
      this.input.setInputValue(datum.value, true);
102
 
103
      this.eventBus.trigger('cursorchanged', datum.raw, datum.datasetName);
104
    },
105
 
106
    _onCursorRemoved: function onCursorRemoved() {
107
      this.input.resetInputValue();
108
      this._updateHint();
109
    },
110
 
111
    _onDatasetRendered: function onDatasetRendered() {
112
      this._updateHint();
113
    },
114
 
115
    _onOpened: function onOpened() {
116
      this._updateHint();
117
 
118
      this.eventBus.trigger('opened');
119
    },
120
 
121
    _onClosed: function onClosed() {
122
      this.input.clearHint();
123
 
124
      this.eventBus.trigger('closed');
125
    },
126
 
127
    _onFocused: function onFocused() {
128
      this.isActivated = true;
129
      this.dropdown.open();
130
    },
131
 
132
    _onBlurred: function onBlurred() {
133
      this.isActivated = false;
134
      this.dropdown.empty();
135
      this.dropdown.close();
136
    },
137
 
138
    _onEnterKeyed: function onEnterKeyed(type, $e) {
139
      var cursorDatum, topSuggestionDatum;
140
 
141
      cursorDatum = this.dropdown.getDatumForCursor();
142
      topSuggestionDatum = this.dropdown.getDatumForTopSuggestion();
143
 
144
      if (cursorDatum) {
145
        this._select(cursorDatum);
146
        $e.preventDefault();
147
      }
148
 
149
      else if (this.autoselect && topSuggestionDatum) {
150
        this._select(topSuggestionDatum);
151
        $e.preventDefault();
152
      }
153
    },
154
 
155
    _onTabKeyed: function onTabKeyed(type, $e) {
156
      var datum;
157
 
158
      if (datum = this.dropdown.getDatumForCursor()) {
159
        this._select(datum);
160
        $e.preventDefault();
161
      }
162
 
163
      else {
164
        this._autocomplete(true);
165
      }
166
    },
167
 
168
    _onEscKeyed: function onEscKeyed() {
169
      this.dropdown.close();
170
      this.input.resetInputValue();
171
    },
172
 
173
    _onUpKeyed: function onUpKeyed() {
174
      var query = this.input.getQuery();
175
 
176
      this.dropdown.isEmpty && query.length >= this.minLength ?
177
        this.dropdown.update(query) :
178
        this.dropdown.moveCursorUp();
179
 
180
      this.dropdown.open();
181
    },
182
 
183
    _onDownKeyed: function onDownKeyed() {
184
      var query = this.input.getQuery();
185
 
186
      this.dropdown.isEmpty && query.length >= this.minLength ?
187
        this.dropdown.update(query) :
188
        this.dropdown.moveCursorDown();
189
 
190
      this.dropdown.open();
191
    },
192
 
193
    _onLeftKeyed: function onLeftKeyed() {
194
      this.dir === 'rtl' && this._autocomplete();
195
    },
196
 
197
    _onRightKeyed: function onRightKeyed() {
198
      this.dir === 'ltr' && this._autocomplete();
199
    },
200
 
201
    _onQueryChanged: function onQueryChanged(e, query) {
202
      this.input.clearHintIfInvalid();
203
 
204
      query.length >= this.minLength ?
205
        this.dropdown.update(query) :
206
        this.dropdown.empty();
207
 
208
      this.dropdown.open();
209
      this._setLanguageDirection();
210
    },
211
 
212
    _onWhitespaceChanged: function onWhitespaceChanged() {
213
      this._updateHint();
214
      this.dropdown.open();
215
    },
216
 
217
    _setLanguageDirection: function setLanguageDirection() {
218
      var dir;
219
 
220
      if (this.dir !== (dir = this.input.getLanguageDirection())) {
221
        this.dir = dir;
222
        this.$node.css('direction', dir);
223
        this.dropdown.setLanguageDirection(dir);
224
      }
225
    },
226
 
227
    _updateHint: function updateHint() {
228
      var datum, val, query, escapedQuery, frontMatchRegEx, match;
229
 
230
      datum = this.dropdown.getDatumForTopSuggestion();
231
 
232
      if (datum && this.dropdown.isVisible() && !this.input.hasOverflow()) {
233
        val = this.input.getInputValue();
234
        query = Input.normalizeQuery(val);
235
        escapedQuery = _.escapeRegExChars(query);
236
 
237
        // match input value, then capture trailing text
238
        frontMatchRegEx = new RegExp('^(?:' + escapedQuery + ')(.+$)', 'i');
239
        match = frontMatchRegEx.exec(datum.value);
240
 
241
        // clear hint if there's no trailing text
242
        match ? this.input.setHint(val + match[1]) : this.input.clearHint();
243
      }
244
 
245
      else {
246
        this.input.clearHint();
247
      }
248
    },
249
 
250
    _autocomplete: function autocomplete(laxCursor) {
251
      var hint, query, isCursorAtEnd, datum;
252
 
253
      hint = this.input.getHint();
254
      query = this.input.getQuery();
255
      isCursorAtEnd = laxCursor || this.input.isCursorAtEnd();
256
 
257
      if (hint && query !== hint && isCursorAtEnd) {
258
        datum = this.dropdown.getDatumForTopSuggestion();
259
        datum && this.input.setInputValue(datum.value);
260
 
261
        this.eventBus.trigger('autocompleted', datum.raw, datum.datasetName);
262
      }
263
    },
264
 
265
    _select: function select(datum) {
266
      this.input.setQuery(datum.value);
267
      this.input.setInputValue(datum.value, true);
268
 
269
      this._setLanguageDirection();
270
 
271
      this.eventBus.trigger('selected', datum.raw, datum.datasetName);
272
      this.dropdown.close();
273
 
274
      // #118: allow click event to bubble up to the body before removing
275
      // the suggestions otherwise we break event delegation
276
      _.defer(_.bind(this.dropdown.empty, this.dropdown));
277
    },
278
 
279
    // ### public
280
 
281
    open: function open() {
282
      this.dropdown.open();
283
    },
284
 
285
    close: function close() {
286
      this.dropdown.close();
287
    },
288
 
289
    setVal: function setVal(val) {
290
      // expect val to be a string, so be safe, and coerce
291
      val = _.toStr(val);
292
 
293
      if (this.isActivated) {
294
        this.input.setInputValue(val);
295
      }
296
 
297
      else {
298
        this.input.setQuery(val);
299
        this.input.setInputValue(val, true);
300
      }
301
 
302
      this._setLanguageDirection();
303
    },
304
 
305
    getVal: function getVal() {
306
      return this.input.getQuery();
307
    },
308
 
309
    destroy: function destroy() {
310
      this.input.destroy();
311
      this.dropdown.destroy();
312
 
313
      destroyDomStructure(this.$node);
314
 
315
      this.$node = null;
316
    }
317
  });
318
 
319
  return Typeahead;
320
 
321
  function buildDom(input, withHint) {
322
    var $input, $wrapper, $dropdown, $hint;
323
 
324
    $input = $(input);
325
    $wrapper = $(html.wrapper).css(css.wrapper);
326
    $dropdown = $(html.dropdown).css(css.dropdown);
327
    $hint = $input.clone().css(css.hint).css(getBackgroundStyles($input));
328
 
329
    $hint
330
    .val('')
331
    .removeData()
332
    .addClass('tt-hint')
333
    .removeAttr('id name placeholder required')
334
    .prop('readonly', true)
335
    .attr({ autocomplete: 'off', spellcheck: 'false', tabindex: -1 });
336
 
337
    // store the original values of the attrs that get modified
338
    // so modifications can be reverted on destroy
339
    $input.data(attrsKey, {
340
      dir: $input.attr('dir'),
341
      autocomplete: $input.attr('autocomplete'),
342
      spellcheck: $input.attr('spellcheck'),
343
      style: $input.attr('style')
344
    });
345
 
346
    $input
347
    .addClass('tt-input')
348
    .attr({ autocomplete: 'off', spellcheck: false })
349
    .css(withHint ? css.input : css.inputWithNoHint);
350
 
351
    // ie7 does not like it when dir is set to auto
352
    try { !$input.attr('dir') && $input.attr('dir', 'auto'); } catch (e) {}
353
 
354
    return $input
355
    .wrap($wrapper)
356
    .parent()
357
    .prepend(withHint ? $hint : null)
358
    .append($dropdown);
359
  }
360
 
361
  function getBackgroundStyles($el) {
362
    return {
363
      backgroundAttachment: $el.css('background-attachment'),
364
      backgroundClip: $el.css('background-clip'),
365
      backgroundColor: $el.css('background-color'),
366
      backgroundImage: $el.css('background-image'),
367
      backgroundOrigin: $el.css('background-origin'),
368
      backgroundPosition: $el.css('background-position'),
369
      backgroundRepeat: $el.css('background-repeat'),
370
      backgroundSize: $el.css('background-size')
371
    };
372
  }
373
 
374
  function destroyDomStructure($node) {
375
    var $input = $node.find('.tt-input');
376
 
377
    // need to remove attrs that weren't previously defined and
378
    // revert attrs that originally had a value
379
    _.each($input.data(attrsKey), function(val, key) {
380
      _.isUndefined(val) ? $input.removeAttr(key) : $input.attr(key, val);
381
    });
382
 
383
    $input
384
    .detach()
385
    .removeData(attrsKey)
386
    .removeClass('tt-input')
387
    .insertAfter($node);
388
 
389
    $node.remove();
390
  }
391
})();