Skip to content

Instantly share code, notes, and snippets.

@vitalets
Last active December 17, 2015 23:29
Show Gist options
  • Save vitalets/5689823 to your computer and use it in GitHub Desktop.
Save vitalets/5689823 to your computer and use it in GitHub Desktop.
X-editable combobox: typeahead + dropdown
/**
Typeahead input (bootstrap only). Based on Twitter Bootstrap [typeahead](http://twitter.github.com/bootstrap/javascript.html#typeahead).
Depending on `source` format typeahead operates in two modes:
* **strings**:
When `source` defined as array of strings, e.g. `['text1', 'text2', 'text3' ...]`.
User can submit one of these strings or any text entered in input (even if it is not matching source).
* **objects**:
When `source` defined as array of objects, e.g. `[{value: 1, text: "text1"}, {value: 2, text: "text2"}, ...]`.
User can submit only values that are in source (otherwise `null` is submitted). This is more like *dropdown* behavior.
@class typeahead
@extends list
@since 1.4.1
@final
@example
<a href="#" id="country" data-type="typeahead" data-pk="1" data-url="/post" data-original-title="Input country"></a>
<script>
$(function(){
$('#country').editable({
value: 'ru',
source: [
{value: 'gb', text: 'Great Britain'},
{value: 'us', text: 'United States'},
{value: 'ru', text: 'Russia'}
]
});
});
</script>
**/
(function ($) {
"use strict";
var Constructor = function (options) {
this.init('combobox', options, Constructor.defaults);
//overriding objects in config (as by default jQuery extend() is not recursive)
this.options.combobox = $.extend({}, Constructor.defaults.combobox, {
//set default methods for typeahead to work with objects
matcher: this.matcher,
sorter: this.sorter,
highlighter: this.highlighter,
updater: this.updater
}, options.typeahead);
};
$.fn.editableutils.inherit(Constructor, $.fn.editabletypes.list);
// Переопределение шаблона кнопок
// $.fn.editableform.buttons = '<button type="submit" class="btn btn-primary editable-submit"><i class="icon-ok icon-white"></i></button>';
$.extend(Constructor.prototype, {
renderList: function() {
this.$input = this.$tpl.is('input') ? this.$tpl : this.$tpl.find('input[type="text"]');
// console.log(this.options);
// set source of typeahead
if(!this.options.typeahead) {
this.options.typeahead = {};
}
this.options.typeahead.source = this.sourceData;
// apply typeahead
this.$input.typeahead(this.options.typeahead);
// patch some methods in typeahead
var ta = this.$input.data('typeahead');
ta.render = $.proxy(this.typeaheadRender, ta);
ta.select = $.proxy(this.typeaheadSelect, ta);
ta.move = $.proxy(this.typeaheadMove, ta);
this.renderClear();
this.setClass();
this.setAttr('placeholder');
// #######################################################################
// Генерация стандартных вариантов для выпадающего списка
var fillItems = function($menu, data) {
// console.log('Enter FillItems for '+ data.length);
if($.isArray(data)) {
for(var i=0; i<data.length; i++) {
var li = $('<li/>')
.appendTo($menu);
var a = $('<a/>')
.text(data[i])
.appendTo(li);
}
}
return $menu;
};
// Определяем массив стандартных вариантов и кнопку списка
var dropdownArray = this.options.defaultValues;
var button = this.$tpl.find('button');
// Создание выпадающего списка стандартных вариантов
if ($.isArray(dropdownArray) && dropdownArray.length > 0) {
var menuReady = fillItems($(this.options.menu), dropdownArray);
// Делаем кнопку активной и добавляем после нее список вариантов
button
.removeClass('disabled')
.attr('title','Стандартные варианты')
.after(menuReady);
} else {
// При отсутствии вариантов и нажатии на кнопку переводим фокус на input
button.click(this.$input, function(event) {
event.preventDefault();
event.data.focus();
});
}
// При выборе ссылки передаем как event.data контекст this.$input к текстовому полю
// Устанавливаем в поле новое значение и переводим в него фокус
this.$tpl.find('.dropdown-menu a').click(this.$input, function(event) {
event.preventDefault();
event.data
.val($(this).text())
.focus();
});
// #######################################################################
},
value2htmlFinal: function(value, element) {
if(this.getIsObjects()) {
var items = $.fn.editableutils.itemsByValue(value, this.sourceData);
$(element).text(items.length ? items[0].text : '');
} else {
$(element).text(value);
}
},
html2value: function (html) {
return html ? html : null;
},
value2input: function(value) {
if(this.getIsObjects()) {
var items = $.fn.editableutils.itemsByValue(value, this.sourceData);
this.$input.data('value', value).val(items.length ? items[0].text : '');
} else {
this.$input.val(value);
}
},
input2value: function() {
if(this.getIsObjects()) {
var value = this.$input.data('value'),
items = $.fn.editableutils.itemsByValue(value, this.sourceData);
if(items.length && items[0].text.toLowerCase() === this.$input.val().toLowerCase()) {
return value;
} else {
return null; //entered string not found in source
}
} else {
return this.$input.val();
}
},
/*
if in sourceData values <> texts, typeahead in "objects" mode:
user must pick some value from list, otherwise `null` returned.
if all values == texts put typeahead in "strings" mode:
anything what entered is submited.
*/
getIsObjects: function() {
if(this.isObjects === undefined) {
this.isObjects = false;
for(var i=0; i<this.sourceData.length; i++) {
if(this.sourceData[i].value !== this.sourceData[i].text) {
this.isObjects = true;
break;
}
}
}
return this.isObjects;
},
/*
Methods borrowed from text input
*/
activate: $.fn.editabletypes.text.prototype.activate,
renderClear: $.fn.editabletypes.text.prototype.renderClear,
postrender: $.fn.editabletypes.text.prototype.postrender,
toggleClear: $.fn.editabletypes.text.prototype.toggleClear,
clear: function() {
$.fn.editabletypes.text.prototype.clear.call(this);
this.$input.data('value', '');
},
/*
Typeahead option methods used as defaults
*/
/*jshint eqeqeq:false, curly: false, laxcomma: true, asi: true*/
matcher: function (item) {
return $.fn.typeahead.Constructor.prototype.matcher.call(this, item.text);
},
sorter: function (items) {
var beginswith = []
, caseSensitive = []
, caseInsensitive = []
, item
, text;
while (item = items.shift()) {
text = item.text;
if (!text.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item);
else if (~text.indexOf(this.query)) caseSensitive.push(item);
else caseInsensitive.push(item);
}
return beginswith.concat(caseSensitive, caseInsensitive);
},
highlighter: function (item) {
return $.fn.typeahead.Constructor.prototype.highlighter.call(this, item.text);
},
updater: function (item) {
this.$element.data('value', item.value);
return item.text;
},
/*
Overwrite typeahead's render method to store objects.
There are a lot of disscussion in bootstrap repo on this point and still no result.
See https://github.com/twitter/bootstrap/issues/5967
This function just store item via jQuery data() method instead of attr('data-value')
*/
typeaheadRender: function (items) {
var that = this;
items = $(items).map(function (i, item) {
i = $(that.options.item).data('item', item);
i.find('a').html(that.highlighter(item));
return i[0];
});
// console.log(that.options);
//add option to disable autoselect of first line
//see https://github.com/twitter/bootstrap/pull/4164
if (this.options.autoSelect) {
items.first().addClass('active');
}
this.$menu.html(items);
return this;
},
//add option to disable autoselect of first line
//see https://github.com/twitter/bootstrap/pull/4164
typeaheadSelect: function () {
var val = this.$menu.find('.active').data('item')
if(this.options.autoSelect || val){
this.$element
.val(this.updater(val))
.change()
}
return this.hide()
},
/*
if autoSelect = false and nothing matched we need extra press onEnter that is not convinient.
This patch fixes it.
*/
typeaheadMove: function (e) {
if (!this.shown) return
switch(e.keyCode) {
case 9: // tab
case 13: // enter
case 27: // escape
if (!this.$menu.find('.active').length) return
e.preventDefault()
break
case 38: // up arrow
e.preventDefault()
this.prev()
break
case 40: // down arrow
e.preventDefault()
this.next()
break
}
e.stopPropagation()
}
/*jshint eqeqeq: true, curly: true, laxcomma: false, asi: false*/
});
// Расширение стандартных опций типа и перезапись необходимых
Constructor.defaults = $.extend({}, $.fn.editabletypes.list.defaults, {
/**
@property tpl
@default <input type="text">
**/
tpl:'<div class="input-prepend dropdown combobox">'+
'<input type="text">'+
'<button class="btn disabled" data-toggle="dropdown" title="Стандартных вариантов нет">'+
'<i class="icon-chevron-down"></i></button>'+
'</div>',
menu:'<ul class="dropdown-menu"></ul>',
/**
Configuration of typeahead. [Full list of options](http://twitter.github.com/bootstrap/javascript.html#typeahead).
@property typeahead
@type object
@default null
**/
typeahead: null,
/**
Whether to show `clear` button
@property clear
@type boolean
@default true
**/
clear: true,
/**
Массив для списка стандартных вариантов
@property defaultValues
@type array
@default []
**/
defaultValues: []
});
$.fn.editabletypes.combobox = Constructor;
}(window.jQuery));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment