220 lines
5.9 KiB
JavaScript
220 lines
5.9 KiB
JavaScript
/*
|
|
* typeahead.js
|
|
* https://github.com/twitter/typeahead.js
|
|
* Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT
|
|
*/
|
|
|
|
var Menu = (function() {
|
|
'use strict';
|
|
|
|
// constructor
|
|
// -----------
|
|
|
|
function Menu(o, www) {
|
|
var that = this;
|
|
|
|
o = o || {};
|
|
|
|
if (!o.node) {
|
|
$.error('node is required');
|
|
}
|
|
|
|
www.mixin(this);
|
|
|
|
this.$node = $(o.node);
|
|
|
|
// the latest query #update was called with
|
|
this.query = null;
|
|
this.datasets = _.map(o.datasets, initializeDataset);
|
|
|
|
function initializeDataset(oDataset) {
|
|
var node = that.$node.find(oDataset.node).first();
|
|
oDataset.node = node.length ? node : $('<div>').appendTo(that.$node);
|
|
|
|
return new Dataset(oDataset, www);
|
|
}
|
|
}
|
|
|
|
// instance methods
|
|
// ----------------
|
|
|
|
_.mixin(Menu.prototype, EventEmitter, {
|
|
|
|
// ### event handlers
|
|
|
|
_onSelectableClick: function onSelectableClick($e) {
|
|
this.trigger('selectableClicked', $($e.currentTarget));
|
|
},
|
|
|
|
_onRendered: function onRendered(type, dataset, suggestions, async) {
|
|
this.$node.toggleClass(this.classes.empty, this._allDatasetsEmpty());
|
|
this.trigger('datasetRendered', dataset, suggestions, async);
|
|
},
|
|
|
|
_onCleared: function onCleared() {
|
|
this.$node.toggleClass(this.classes.empty, this._allDatasetsEmpty());
|
|
this.trigger('datasetCleared');
|
|
},
|
|
|
|
_propagate: function propagate() {
|
|
this.trigger.apply(this, arguments);
|
|
},
|
|
|
|
// ### private
|
|
|
|
_allDatasetsEmpty: function allDatasetsEmpty() {
|
|
return _.every(this.datasets, isDatasetEmpty);
|
|
|
|
function isDatasetEmpty(dataset) { return dataset.isEmpty(); }
|
|
},
|
|
|
|
_getSelectables: function getSelectables() {
|
|
return this.$node.find(this.selectors.selectable);
|
|
},
|
|
|
|
_removeCursor: function _removeCursor() {
|
|
var $selectable = this.getActiveSelectable();
|
|
$selectable && $selectable.removeClass(this.classes.cursor);
|
|
},
|
|
|
|
_ensureVisible: function ensureVisible($el) {
|
|
var elTop, elBottom, nodeScrollTop, nodeHeight;
|
|
|
|
elTop = $el.position().top;
|
|
elBottom = elTop + $el.outerHeight(true);
|
|
nodeScrollTop = this.$node.scrollTop();
|
|
nodeHeight = this.$node.height() +
|
|
parseInt(this.$node.css('paddingTop'), 10) +
|
|
parseInt(this.$node.css('paddingBottom'), 10);
|
|
|
|
if (elTop < 0) {
|
|
this.$node.scrollTop(nodeScrollTop + elTop);
|
|
}
|
|
|
|
else if (nodeHeight < elBottom) {
|
|
this.$node.scrollTop(nodeScrollTop + (elBottom - nodeHeight));
|
|
}
|
|
},
|
|
|
|
// ### public
|
|
|
|
bind: function() {
|
|
var that = this, onSelectableClick;
|
|
|
|
onSelectableClick = _.bind(this._onSelectableClick, this);
|
|
this.$node.on('click.tt', this.selectors.selectable, onSelectableClick);
|
|
this.$node.on('mouseover', this.selectors.selectable, function(){ that.setCursor($(this)) });
|
|
|
|
_.each(this.datasets, function(dataset) {
|
|
dataset
|
|
.onSync('asyncRequested', that._propagate, that)
|
|
.onSync('asyncCanceled', that._propagate, that)
|
|
.onSync('asyncReceived', that._propagate, that)
|
|
.onSync('rendered', that._onRendered, that)
|
|
.onSync('cleared', that._onCleared, that);
|
|
});
|
|
|
|
return this;
|
|
},
|
|
|
|
isOpen: function isOpen() {
|
|
return this.$node.hasClass(this.classes.open);
|
|
},
|
|
|
|
open: function open() {
|
|
this.$node.scrollTop(0);
|
|
this.$node.addClass(this.classes.open);
|
|
},
|
|
|
|
close: function close() {
|
|
this.$node.removeClass(this.classes.open);
|
|
this._removeCursor();
|
|
},
|
|
|
|
setLanguageDirection: function setLanguageDirection(dir) {
|
|
this.$node.attr('dir', dir);
|
|
},
|
|
|
|
selectableRelativeToCursor: function selectableRelativeToCursor(delta) {
|
|
var $selectables, $oldCursor, oldIndex, newIndex;
|
|
|
|
$oldCursor = this.getActiveSelectable();
|
|
$selectables = this._getSelectables();
|
|
|
|
// shifting before and after modulo to deal with -1 index
|
|
oldIndex = $oldCursor ? $selectables.index($oldCursor) : -1;
|
|
newIndex = oldIndex + delta;
|
|
newIndex = (newIndex + 1) % ($selectables.length + 1) - 1;
|
|
|
|
// wrap new index if less than -1
|
|
newIndex = newIndex < -1 ? $selectables.length - 1 : newIndex;
|
|
|
|
return newIndex === -1 ? null : $selectables.eq(newIndex);
|
|
},
|
|
|
|
setCursor: function setCursor($selectable) {
|
|
this._removeCursor();
|
|
|
|
if ($selectable = $selectable && $selectable.first()) {
|
|
$selectable.addClass(this.classes.cursor);
|
|
|
|
// in the case of scrollable overflow
|
|
// make sure the cursor is visible in the node
|
|
this._ensureVisible($selectable);
|
|
}
|
|
},
|
|
|
|
getSelectableData: function getSelectableData($el) {
|
|
return ($el && $el.length) ? Dataset.extractData($el) : null;
|
|
},
|
|
|
|
getActiveSelectable: function getActiveSelectable() {
|
|
var $selectable = this._getSelectables().filter(this.selectors.cursor).first();
|
|
|
|
return $selectable.length ? $selectable : null;
|
|
},
|
|
|
|
getTopSelectable: function getTopSelectable() {
|
|
var $selectable = this._getSelectables().first();
|
|
|
|
return $selectable.length ? $selectable : null;
|
|
},
|
|
|
|
update: function update(query) {
|
|
var isValidUpdate = query !== this.query;
|
|
|
|
// don't update if the query hasn't changed
|
|
if (isValidUpdate) {
|
|
this.query = query;
|
|
_.each(this.datasets, updateDataset);
|
|
}
|
|
|
|
return isValidUpdate;
|
|
|
|
function updateDataset(dataset) { dataset.update(query); }
|
|
},
|
|
|
|
empty: function empty() {
|
|
_.each(this.datasets, clearDataset);
|
|
|
|
this.query = null;
|
|
this.$node.addClass(this.classes.empty);
|
|
|
|
function clearDataset(dataset) { dataset.clear(); }
|
|
},
|
|
|
|
destroy: function destroy() {
|
|
this.$node.off('.tt');
|
|
|
|
// #970
|
|
this.$node = $('<div>');
|
|
|
|
_.each(this.datasets, destroyDataset);
|
|
|
|
function destroyDataset(dataset) { dataset.destroy(); }
|
|
}
|
|
});
|
|
|
|
return Menu;
|
|
})();
|