LibreNMS/lib/typeahead/src/typeahead/typeahead.js

439 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* typeahead.js
* https://github.com/twitter/typeahead.js
* Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT
*/
var Typeahead = (function() {
'use strict';
// constructor
// -----------
function Typeahead(o, www) {
var onFocused, onBlurred, onEnterKeyed, onTabKeyed, onEscKeyed, onUpKeyed,
onDownKeyed, onLeftKeyed, onRightKeyed, onQueryChanged,
onWhitespaceChanged;
o = o || {};
if (!o.input) {
$.error('missing input');
}
if (!o.menu) {
$.error('missing menu');
}
if (!o.eventBus) {
$.error('missing event bus');
}
www.mixin(this);
this.eventBus = o.eventBus;
this.minLength = _.isNumber(o.minLength) ? o.minLength : 1;
this.input = o.input;
this.menu = o.menu;
this.enabled = true;
// activate the typeahead on init if the input has focus
this.active = false;
this.input.hasFocus() && this.activate();
// detect the initial lang direction
this.dir = this.input.getLangDir();
this._hacks();
this.menu.bind()
.onSync('selectableClicked', this._onSelectableClicked, this)
.onSync('asyncRequested', this._onAsyncRequested, this)
.onSync('asyncCanceled', this._onAsyncCanceled, this)
.onSync('asyncReceived', this._onAsyncReceived, this)
.onSync('datasetRendered', this._onDatasetRendered, this)
.onSync('datasetCleared', this._onDatasetCleared, this);
// composed event handlers for input
onFocused = c(this, 'activate', 'open', '_onFocused');
onBlurred = c(this, 'deactivate', '_onBlurred');
onEnterKeyed = c(this, 'isActive', 'isOpen', '_onEnterKeyed');
onTabKeyed = c(this, 'isActive', 'isOpen', '_onTabKeyed');
onEscKeyed = c(this, 'isActive', '_onEscKeyed');
onUpKeyed = c(this, 'isActive', 'open', '_onUpKeyed');
onDownKeyed = c(this, 'isActive', 'open', '_onDownKeyed');
onLeftKeyed = c(this, 'isActive', 'isOpen', '_onLeftKeyed');
onRightKeyed = c(this, 'isActive', 'isOpen', '_onRightKeyed');
onQueryChanged = c(this, '_openIfActive', '_onQueryChanged');
onWhitespaceChanged = c(this, '_openIfActive', '_onWhitespaceChanged');
this.input.bind()
.onSync('focused', onFocused, this)
.onSync('blurred', onBlurred, this)
.onSync('enterKeyed', onEnterKeyed, this)
.onSync('tabKeyed', onTabKeyed, this)
.onSync('escKeyed', onEscKeyed, this)
.onSync('upKeyed', onUpKeyed, this)
.onSync('downKeyed', onDownKeyed, this)
.onSync('leftKeyed', onLeftKeyed, this)
.onSync('rightKeyed', onRightKeyed, this)
.onSync('queryChanged', onQueryChanged, this)
.onSync('whitespaceChanged', onWhitespaceChanged, this)
.onSync('langDirChanged', this._onLangDirChanged, this);
}
// instance methods
// ----------------
_.mixin(Typeahead.prototype, {
// here's where hacks get applied and we don't feel bad about it
_hacks: function hacks() {
var $input, $menu;
// these default values are to make testing easier
$input = this.input.$input || $('<div>');
$menu = this.menu.$node || $('<div>');
// #705: if there's scrollable overflow, ie doesn't support
// blur cancellations when the scrollbar is clicked
//
// #351: preventDefault won't cancel blurs in ie <= 8
$input.on('blur.tt', function($e) {
var active, isActive, hasActive;
active = document.activeElement;
isActive = $menu.is(active);
hasActive = $menu.has(active).length > 0;
if (_.isMsie() && (isActive || hasActive)) {
$e.preventDefault();
// stop immediate in order to prevent Input#_onBlur from
// getting exectued
$e.stopImmediatePropagation();
_.defer(function() { $input.focus(); });
}
});
// #351: prevents input blur due to clicks within menu
$menu.on('mousedown.tt', function($e) { $e.preventDefault(); });
},
// ### event handlers
_onSelectableClicked: function onSelectableClicked(type, $el) {
this.select($el);
},
_onDatasetCleared: function onDatasetCleared() {
this._updateHint();
},
_onDatasetRendered: function onDatasetRendered(type, dataset, suggestions, async) {
this._updateHint();
this.eventBus.trigger('render', suggestions, async, dataset);
},
_onAsyncRequested: function onAsyncRequested(type, dataset, query) {
this.eventBus.trigger('asyncrequest', query, dataset);
},
_onAsyncCanceled: function onAsyncCanceled(type, dataset, query) {
this.eventBus.trigger('asynccancel', query, dataset);
},
_onAsyncReceived: function onAsyncReceived(type, dataset, query) {
this.eventBus.trigger('asyncreceive', query, dataset);
},
_onFocused: function onFocused() {
this._minLengthMet() && this.menu.update(this.input.getQuery());
},
_onBlurred: function onBlurred() {
if (this.input.hasQueryChangedSinceLastFocus()) {
this.eventBus.trigger('change', this.input.getQuery());
}
},
_onEnterKeyed: function onEnterKeyed(type, $e) {
var $selectable;
if ($selectable = this.menu.getActiveSelectable()) {
this.select($selectable) && $e.preventDefault();
}
},
_onTabKeyed: function onTabKeyed(type, $e) {
var $selectable;
if ($selectable = this.menu.getActiveSelectable()) {
this.select($selectable) && $e.preventDefault();
}
else if ($selectable = this.menu.getTopSelectable()) {
this.autocomplete($selectable) && $e.preventDefault();
}
},
_onEscKeyed: function onEscKeyed() {
this.close();
},
_onUpKeyed: function onUpKeyed() {
this.moveCursor(-1);
},
_onDownKeyed: function onDownKeyed() {
this.moveCursor(+1);
},
_onLeftKeyed: function onLeftKeyed() {
if (this.dir === 'rtl' && this.input.isCursorAtEnd()) {
this.autocomplete(this.menu.getActiveSelectable() || this.menu.getTopSelectable());
}
},
_onRightKeyed: function onRightKeyed() {
if (this.dir === 'ltr' && this.input.isCursorAtEnd()) {
this.autocomplete(this.menu.getActiveSelectable() || this.menu.getTopSelectable());
}
},
_onQueryChanged: function onQueryChanged(e, query) {
this._minLengthMet(query) ? this.menu.update(query) : this.menu.empty();
},
_onWhitespaceChanged: function onWhitespaceChanged() {
this._updateHint();
},
_onLangDirChanged: function onLangDirChanged(e, dir) {
if (this.dir !== dir) {
this.dir = dir;
this.menu.setLanguageDirection(dir);
}
},
// ### private
_openIfActive: function openIfActive() {
this.isActive() && this.open();
},
_minLengthMet: function minLengthMet(query) {
query = _.isString(query) ? query : (this.input.getQuery() || '');
return query.length >= this.minLength;
},
_updateHint: function updateHint() {
var $selectable, data, val, query, escapedQuery, frontMatchRegEx, match;
$selectable = this.menu.getTopSelectable();
data = this.menu.getSelectableData($selectable);
val = this.input.getInputValue();
if (data && !_.isBlankString(val) && !this.input.hasOverflow()) {
query = Input.normalizeQuery(val);
escapedQuery = _.escapeRegExChars(query);
// match input value, then capture trailing text
frontMatchRegEx = new RegExp('^(?:' + escapedQuery + ')(.+$)', 'i');
match = frontMatchRegEx.exec(data.val);
// clear hint if there's no trailing text
match && this.input.setHint(val + match[1]);
}
else {
this.input.clearHint();
}
},
// ### public
isEnabled: function isEnabled() {
return this.enabled;
},
enable: function enable() {
this.enabled = true;
},
disable: function disable() {
this.enabled = false;
},
isActive: function isActive() {
return this.active;
},
activate: function activate() {
// already active
if (this.isActive()) {
return true;
}
// unable to activate either due to the typeahead being disabled
// or due to the active event being prevented
else if (!this.isEnabled() || this.eventBus.before('active')) {
return false;
}
// activate
else {
this.active = true;
this.eventBus.trigger('active');
return true;
}
},
deactivate: function deactivate() {
// already idle
if (!this.isActive()) {
return true;
}
// unable to deactivate due to the idle event being prevented
else if (this.eventBus.before('idle')) {
return false;
}
// deactivate
else {
this.active = false;
this.close();
this.eventBus.trigger('idle');
return true;
}
},
isOpen: function isOpen() {
return this.menu.isOpen();
},
open: function open() {
if (!this.isOpen() && !this.eventBus.before('open')) {
this.menu.open();
this._updateHint();
this.eventBus.trigger('open');
}
return this.isOpen();
},
close: function close() {
if (this.isOpen() && !this.eventBus.before('close')) {
this.menu.close();
this.input.clearHint();
this.input.resetInputValue();
this.eventBus.trigger('close');
}
return !this.isOpen();
},
setVal: function setVal(val) {
// expect val to be a string, so be safe, and coerce
this.input.setQuery(_.toStr(val));
},
getVal: function getVal() {
return this.input.getQuery();
},
select: function select($selectable) {
var data = this.menu.getSelectableData($selectable);
if (data && !this.eventBus.before('select', data.obj)) {
this.input.setQuery(data.val, true);
this.eventBus.trigger('select', data.obj);
this.close();
// return true if selection succeeded
return true;
}
return false;
},
autocomplete: function autocomplete($selectable) {
var query, data, isValid;
query = this.input.getQuery();
data = this.menu.getSelectableData($selectable);
isValid = data && query !== data.val;
if (isValid && !this.eventBus.before('autocomplete', data.obj)) {
this.input.setQuery(data.val);
this.eventBus.trigger('autocomplete', data.obj);
// return true if autocompletion succeeded
return true;
}
return false;
},
moveCursor: function moveCursor(delta) {
var query, $candidate, data, payload, cancelMove;
query = this.input.getQuery();
$candidate = this.menu.selectableRelativeToCursor(delta);
data = this.menu.getSelectableData($candidate);
payload = data ? data.obj : null;
// update will return true when it's a new query and new suggestions
// need to be fetched in this case we don't want to move the cursor
cancelMove = this._minLengthMet() && this.menu.update(query);
if (!cancelMove && !this.eventBus.before('cursorchange', payload)) {
this.menu.setCursor($candidate);
// cursor moved to different selectable
if (data) {
this.input.setInputValue(data.val);
}
// cursor moved off of selectables, back to input
else {
this.input.resetInputValue();
this._updateHint();
}
this.eventBus.trigger('cursorchange', payload);
// return true if move succeeded
return true;
}
return false;
},
destroy: function destroy() {
this.input.destroy();
this.menu.destroy();
}
});
return Typeahead;
// helper functions
// ----------------
function c(ctx) {
var methods = [].slice.call(arguments, 1);
return function() {
var args = [].slice.call(arguments);
_.each(methods, function(method) {
return ctx[method].apply(ctx, args);
});
};
}
})();