alpaca
Version:
Alpaca provides the easiest and fastest way to generate interactive forms for the web and mobile devices. It runs simply as HTML5 or more elaborately using Bootstrap, jQuery Mobile or jQuery UI. Alpaca uses Handlebars to process JSON schema and provide
246 lines (177 loc) • 6.15 kB
JavaScript
/*
* typeahead.js
* https://github.com/twitter/typeahead.js
* Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT
*/
var Dropdown = (function() {
'use strict';
// constructor
// -----------
function Dropdown(o) {
var that = this, onSuggestionClick, onSuggestionMouseEnter,
onSuggestionMouseLeave;
o = o || {};
if (!o.menu) {
$.error('menu is required');
}
this.isOpen = false;
this.isEmpty = true;
this.datasets = _.map(o.datasets, initializeDataset);
// bound functions
onSuggestionClick = _.bind(this._onSuggestionClick, this);
onSuggestionMouseEnter = _.bind(this._onSuggestionMouseEnter, this);
onSuggestionMouseLeave = _.bind(this._onSuggestionMouseLeave, this);
this.$menu = $(o.menu)
.on('click.tt', '.tt-suggestion', onSuggestionClick)
.on('mouseenter.tt', '.tt-suggestion', onSuggestionMouseEnter)
.on('mouseleave.tt', '.tt-suggestion', onSuggestionMouseLeave);
_.each(this.datasets, function(dataset) {
that.$menu.append(dataset.getRoot());
dataset.onSync('rendered', that._onRendered, that);
});
}
// instance methods
// ----------------
_.mixin(Dropdown.prototype, EventEmitter, {
// ### private
_onSuggestionClick: function onSuggestionClick($e) {
this.trigger('suggestionClicked', $($e.currentTarget));
},
_onSuggestionMouseEnter: function onSuggestionMouseEnter($e) {
this._removeCursor();
this._setCursor($($e.currentTarget), true);
},
_onSuggestionMouseLeave: function onSuggestionMouseLeave() {
this._removeCursor();
},
_onRendered: function onRendered() {
this.isEmpty = _.every(this.datasets, isDatasetEmpty);
this.isEmpty ? this._hide() : (this.isOpen && this._show());
this.trigger('datasetRendered');
function isDatasetEmpty(dataset) { return dataset.isEmpty(); }
},
_hide: function() {
this.$menu.hide();
},
_show: function() {
// can't use jQuery#show because $menu is a span element we want
// display: block; not dislay: inline;
this.$menu.css('display', 'block');
},
_getSuggestions: function getSuggestions() {
return this.$menu.find('.tt-suggestion');
},
_getCursor: function getCursor() {
return this.$menu.find('.tt-cursor').first();
},
_setCursor: function setCursor($el, silent) {
$el.first().addClass('tt-cursor');
!silent && this.trigger('cursorMoved');
},
_removeCursor: function removeCursor() {
this._getCursor().removeClass('tt-cursor');
},
_moveCursor: function moveCursor(increment) {
var $suggestions, $oldCursor, newCursorIndex, $newCursor;
if (!this.isOpen) { return; }
$oldCursor = this._getCursor();
$suggestions = this._getSuggestions();
this._removeCursor();
// shifting before and after modulo to deal with -1 index
newCursorIndex = $suggestions.index($oldCursor) + increment;
newCursorIndex = (newCursorIndex + 1) % ($suggestions.length + 1) - 1;
if (newCursorIndex === -1) {
this.trigger('cursorRemoved');
return;
}
else if (newCursorIndex < -1) {
newCursorIndex = $suggestions.length - 1;
}
this._setCursor($newCursor = $suggestions.eq(newCursorIndex));
// in the case of scrollable overflow
// make sure the cursor is visible in the menu
this._ensureVisible($newCursor);
},
_ensureVisible: function ensureVisible($el) {
var elTop, elBottom, menuScrollTop, menuHeight;
elTop = $el.position().top;
elBottom = elTop + $el.outerHeight(true);
menuScrollTop = this.$menu.scrollTop();
menuHeight = this.$menu.height() +
parseInt(this.$menu.css('paddingTop'), 10) +
parseInt(this.$menu.css('paddingBottom'), 10);
if (elTop < 0) {
this.$menu.scrollTop(menuScrollTop + elTop);
}
else if (menuHeight < elBottom) {
this.$menu.scrollTop(menuScrollTop + (elBottom - menuHeight));
}
},
// ### public
close: function close() {
if (this.isOpen) {
this.isOpen = false;
this._removeCursor();
this._hide();
this.trigger('closed');
}
},
open: function open() {
if (!this.isOpen) {
this.isOpen = true;
!this.isEmpty && this._show();
this.trigger('opened');
}
},
setLanguageDirection: function setLanguageDirection(dir) {
this.$menu.css(dir === 'ltr' ? css.ltr : css.rtl);
},
moveCursorUp: function moveCursorUp() {
this._moveCursor(-1);
},
moveCursorDown: function moveCursorDown() {
this._moveCursor(+1);
},
getDatumForSuggestion: function getDatumForSuggestion($el) {
var datum = null;
if ($el.length) {
datum = {
raw: Dataset.extractDatum($el),
value: Dataset.extractValue($el),
datasetName: Dataset.extractDatasetName($el)
};
}
return datum;
},
getDatumForCursor: function getDatumForCursor() {
return this.getDatumForSuggestion(this._getCursor().first());
},
getDatumForTopSuggestion: function getDatumForTopSuggestion() {
return this.getDatumForSuggestion(this._getSuggestions().first());
},
update: function update(query) {
_.each(this.datasets, updateDataset);
function updateDataset(dataset) { dataset.update(query); }
},
empty: function empty() {
_.each(this.datasets, clearDataset);
this.isEmpty = true;
function clearDataset(dataset) { dataset.clear(); }
},
isVisible: function isVisible() {
return this.isOpen && !this.isEmpty;
},
destroy: function destroy() {
this.$menu.off('.tt');
this.$menu = null;
_.each(this.datasets, destroyDataset);
function destroyDataset(dataset) { dataset.destroy(); }
}
});
return Dropdown;
// helper functions
// ----------------
function initializeDataset(oDataset) {
return new Dataset(oDataset);
}
})();