vendor/assets/javascripts/bootstrap-editable.js in bootstrap-x-editable-rails-1.4.0 vs vendor/assets/javascripts/bootstrap-editable.js in bootstrap-x-editable-rails-1.4.1
- old
+ new
@@ -1,6 +1,6 @@
-/*! X-editable - v1.4.0
+/*! X-editable - v1.4.1
* In-place editing with Twitter Bootstrap, jQuery UI or pure jQuery
* http://github.com/vitalets/x-editable
* Copyright (c) 2013 Vitaliy Potapov; Licensed MIT */
/**
@@ -14,11 +14,11 @@
**/
(function ($) {
var EditableForm = function (div, options) {
this.options = $.extend({}, $.fn.editableform.defaults, options);
- this.$div = $(div); //div, containing form. Not form tag! Not editable-element.
+ this.$div = $(div); //div, containing form. Not form tag. Not editable-element.
if(!this.options.scope) {
this.options.scope = this;
}
//nothing shown after init
};
@@ -28,10 +28,11 @@
initInput: function() { //called once
//take input from options (as it is created in editable-element)
this.input = this.options.input;
//set initial value
+ //todo: may be add check: typeof str === 'string' ?
this.value = this.input.str2value(this.options.value);
},
initTemplate: function() {
this.$form = $($.fn.editableform.template);
},
@@ -230,10 +231,11 @@
this.showForm();
return;
}
//if success callback returns object like {newValue: <something>} --> use that value instead of submitted
+ //it is usefull if you want to chnage value in url-function
if(res && typeof res === 'object' && res.hasOwnProperty('newValue')) {
newValue = res.newValue;
}
//clear error message
@@ -385,22 +387,29 @@
@default 'text'
**/
type: 'text',
/**
Url for submit, e.g. <code>'/post'</code>
- If function - it will be called instead of ajax. Function can return deferred object to run fail/done callbacks.
+ If function - it will be called instead of ajax. Function should return deferred object to run fail/done callbacks.
@property url
@type string|function
@default null
@example
url: function(params) {
+ var d = new $.Deferred;
if(params.value === 'abc') {
- var d = new $.Deferred;
return d.reject('error message'); //returning error via deferred object
} else {
- someModel.set(params.name, params.value); //save data in some js model
+ //async saving data in js model
+ someModel.asyncSaveMethod({
+ ...,
+ success: function(){
+ d.resolve();
+ }
+ });
+ return d.promise();
}
}
**/
url:null,
/**
@@ -687,25 +696,38 @@
},
/*
returns array items from sourceData having value property equal or inArray of 'value'
*/
- itemsByValue: function(value, sourceData) {
+ itemsByValue: function(value, sourceData, valueProp) {
if(!sourceData || value === null) {
return [];
}
- //convert to array
- if(!$.isArray(value)) {
- value = [value];
- }
+ valueProp = valueProp || 'value';
- /*jslint eqeq: true*/
- var result = $.grep(sourceData, function(o){
- return $.grep(value, function(v){ return v == o.value; }).length;
+ var isValArray = $.isArray(value),
+ result = [],
+ that = this;
+
+ $.each(sourceData, function(i, o) {
+ if(o.children) {
+ result = result.concat(that.itemsByValue(value, o.children));
+ } else {
+ /*jslint eqeq: true*/
+ if(isValArray) {
+ if($.grep(value, function(v){ return v == (o && typeof o === 'object' ? o[valueProp] : o); }).length) {
+ result.push(o);
+ }
+ } else {
+ if(value == (o && typeof o === 'object' ? o[valueProp] : o)) {
+ result.push(o);
+ }
+ }
+ /*jslint eqeq: false*/
+ }
});
- /*jslint eqeq: false*/
return result;
},
/*
@@ -783,12 +805,12 @@
Popup.prototype = {
containerName: null, //tbd in child class
innerCss: null, //tbd in child class
init: function(element, options) {
this.$element = $(element);
- //todo: what is in priority: data or js?
- this.options = $.extend({}, $.fn.editableContainer.defaults, $.fn.editableutils.getConfigData(this.$element), options);
+ //since 1.4.1 container do not use data-* directly as they already merged into options.
+ this.options = $.extend({}, $.fn.editableContainer.defaults, options);
this.splitOptions();
//set scope of form callbacks to element
this.formOptions.scope = this.$element[0];
@@ -876,10 +898,11 @@
save: $.proxy(this.save, this), //click on submit button (value changed)
nochange: $.proxy(function(){ this.hide('nochange'); }, this), //click on submit button (value NOT changed)
cancel: $.proxy(function(){ this.hide('cancel'); }, this), //click on calcel button
show: $.proxy(this.setPosition, this), //re-position container every time form is shown (occurs each time after loading state)
rendering: $.proxy(this.setPosition, this), //this allows to place container correctly when loading shown
+ resize: $.proxy(this.setPosition, this), //this allows to re-position container when form size is changed
rendered: $.proxy(function(){
/**
Fired when container is shown and form is rendered (for select will wait for loading dropdown options)
@event shown
@@ -1002,11 +1025,10 @@
setPosition: function() {
//tbd in child class
},
save: function(e, params) {
- this.hide('save');
/**
Fired when new value was submitted. You can use <code>$(this).data('editableContainer')</code> inside handler to access to editableContainer instance
@event save
@param {Object} event event object
@@ -1023,10 +1045,13 @@
alert('error!');
}
});
**/
this.$element.triggerHandler('save', params);
+
+ //hide must be after trigger, as saving value may require methods od plugin, applied to input
+ this.hide('save');
},
/**
Sets new option
@@ -1253,16 +1278,18 @@
},
innerHide: function () {
this.$tip.hide(this.options.anim, $.proxy(function() {
this.$element.show();
- this.tip().empty().remove();
+ this.innerDestroy();
}, this));
},
innerDestroy: function() {
- this.tip().remove();
+ if(this.tip()) {
+ this.tip().empty().remove();
+ }
}
});
}(window.jQuery));
/**
@@ -1273,12 +1300,17 @@
**/
(function ($) {
var Editable = function (element, options) {
this.$element = $(element);
- this.options = $.extend({}, $.fn.editable.defaults, $.fn.editableutils.getConfigData(this.$element), options);
- this.init();
+ //data-* has more priority over js options: because dynamically created elements may change data-*
+ this.options = $.extend({}, $.fn.editable.defaults, options, $.fn.editableutils.getConfigData(this.$element));
+ if(this.options.selector) {
+ this.initLive();
+ } else {
+ this.init();
+ }
};
Editable.prototype = {
constructor: Editable,
init: function () {
@@ -1317,12 +1349,14 @@
//attach handler activating editable. In disabled mode it just prevent default action (useful for links)
if(this.options.toggle !== 'manual') {
this.$element.addClass('editable-click');
this.$element.on(this.options.toggle + '.editable', $.proxy(function(e){
+ //prevent following link
e.preventDefault();
- //stop propagation not required anymore because in document click handler it checks event target
+
+ //stop propagation not required because in document click handler it checks event target
//e.stopPropagation();
if(this.options.toggle === 'mouseenter') {
//for hover only show container
this.show();
@@ -1361,10 +1395,28 @@
this.$element.triggerHandler('init', this);
}, this));
},
/*
+ Initializes parent element for live editables
+ */
+ initLive: function() {
+ //store selector
+ var selector = this.options.selector;
+ //modify options for child elements
+ this.options.selector = false;
+ this.options.autotext = 'never';
+ //listen toggle events
+ this.$element.on(this.options.toggle + '.editable', selector, $.proxy(function(e){
+ var $target = $(e.target);
+ if(!$target.data('editable')) {
+ $target.editable(this.options).trigger(e);
+ }
+ }, this));
+ },
+
+ /*
Renders value into element's text.
Can call custom display method from options.
Can return deferred object.
@method render()
@param {mixed} response server response (if exist) to pass into display function
@@ -1373,12 +1425,12 @@
//do not display anything
if(this.options.display === false) {
return;
}
- //if it is input with source, we pass callback in third param to be called when source is loaded
- if(this.input.options.hasOwnProperty('source')) {
+ //if input has `value2htmlFinal` method, we pass callback in third param to be called when source is loaded
+ if(this.input.value2htmlFinal) {
return this.input.value2html(this.value, this.$element[0], this.options.display, response);
//if display method defined --> use it
} else if(typeof this.options.display === 'function') {
return this.options.display.call(this.$element[0], this.value, response);
//else use input's original value2html() method
@@ -1392,11 +1444,11 @@
@method enable()
**/
enable: function() {
this.options.disabled = false;
this.$element.removeClass('editable-disabled');
- this.handleEmpty();
+ this.handleEmpty(this.isEmpty);
if(this.options.toggle !== 'manual') {
if(this.$element.attr('tabindex') === '-1') {
this.$element.removeAttr('tabindex');
}
}
@@ -1408,11 +1460,11 @@
**/
disable: function() {
this.options.disabled = true;
this.hide();
this.$element.addClass('editable-disabled');
- this.handleEmpty();
+ this.handleEmpty(this.isEmpty);
//do not stop focus on this element
this.$element.attr('tabindex', -1);
},
/**
@@ -1469,31 +1521,37 @@
}
},
/*
- * set emptytext if element is empty (reverse: remove emptytext if needed)
+ * set emptytext if element is empty
*/
- handleEmpty: function () {
+ handleEmpty: function (isEmpty) {
//do not handle empty if we do not display anything
if(this.options.display === false) {
return;
}
- var emptyClass = 'editable-empty';
+ this.isEmpty = isEmpty !== undefined ? isEmpty : $.trim(this.$element.text()) === '';
+
//emptytext shown only for enabled
if(!this.options.disabled) {
- if ($.trim(this.$element.text()) === '') {
- this.$element.addClass(emptyClass).text(this.options.emptytext);
- } else {
- this.$element.removeClass(emptyClass);
+ if (this.isEmpty) {
+ this.$element.text(this.options.emptytext);
+ if(this.options.emptyclass) {
+ this.$element.addClass(this.options.emptyclass);
+ }
+ } else if(this.options.emptyclass) {
+ this.$element.removeClass(this.options.emptyclass);
}
} else {
//below required if element disable property was changed
- if(this.$element.hasClass(emptyClass)) {
+ if(this.isEmpty) {
this.$element.empty();
- this.$element.removeClass(emptyClass);
+ if(this.options.emptyclass) {
+ this.$element.removeClass(this.options.emptyclass);
+ }
}
}
},
/**
@@ -1511,10 +1569,11 @@
var containerOptions = $.extend({}, this.options, {
value: this.value,
input: this.input //pass input to form (as it is already created)
});
this.$element.editableContainer(containerOptions);
+ //listen `save` event
this.$element.on("save.internal", $.proxy(this.save, this));
this.container = this.$element.data('editableContainer');
} else if(this.container.tip().is(':visible')) {
return;
}
@@ -1548,17 +1607,33 @@
/*
* called when form was submitted
*/
save: function(e, params) {
- //if url is not user's function and value was not sent to server and value changed --> mark element with unsaved css.
- if(typeof this.options.url !== 'function' && this.options.display !== false && params.response === undefined && this.input.value2str(this.value) !== this.input.value2str(params.newValue)) {
- this.$element.addClass('editable-unsaved');
- } else {
- this.$element.removeClass('editable-unsaved');
+ //mark element with unsaved class if needed
+ if(this.options.unsavedclass) {
+ /*
+ Add unsaved css to element if:
+ - url is not user's function
+ - value was not sent to server
+ - params.response === undefined, that means data was not sent
+ - value changed
+ */
+ var sent = false;
+ sent = sent || typeof this.options.url === 'function';
+ sent = sent || this.options.display === false;
+ sent = sent || params.response !== undefined;
+ sent = sent || (this.options.savenochange && this.input.value2str(this.value) !== this.input.value2str(params.newValue));
+
+ if(sent) {
+ this.$element.removeClass(this.options.unsavedclass);
+ } else {
+ this.$element.addClass(this.options.unsavedclass);
+ }
}
+ //set new value
this.setValue(params.newValue, false, params.response);
/**
Fired when new value was submitted. You can use <code>$(this).data('editable')</code> to access to editable instance
@@ -1646,11 +1721,11 @@
$('#username').editable({
type: 'text',
url: '/post',
pk: 1
});
- **/
+ **/
$.fn.editable = function (option) {
//special API methods returning non-jquery object
var result = {}, args = arguments, datakey = 'editable';
switch (option) {
/**
@@ -1663,11 +1738,11 @@
// possible result:
{
username: "username is required",
fullname: "fullname should be minimum 3 letters length"
}
- **/
+ **/
case 'validate':
this.each(function () {
var $this = $(this), data = $this.data(datakey), error;
if (data && (error = data.validate())) {
result[data.options.name] = error;
@@ -1684,11 +1759,11 @@
// possible result:
{
username: "superuser",
fullname: "John"
}
- **/
+ **/
case 'getValue':
this.each(function () {
var $this = $(this), data = $this.data(datakey);
if (data && data.value !== undefined && data.value !== null) {
result[data.options.name] = data.input.value2submit(data.value);
@@ -1703,15 +1778,15 @@
@method submit(options)
@param {object} options
@param {object} options.url url to submit data
@param {object} options.data additional data to submit
- @param {object} options.ajaxOptions additional ajax options
+ @param {object} options.ajaxOptions additional ajax options
@param {function} options.error(obj) error handler
@param {function} options.success(obj,config) success handler
@returns {Object} jQuery object
- **/
+ **/
case 'submit': //collects value, validate and submit to server for creating new record
var config = arguments[1] || {},
$elems = this,
errors = this.editable('validate'),
values;
@@ -1858,11 +1933,55 @@
} else {
$(this).empty();
}
}
**/
- display: null
+ display: null,
+ /**
+ Css class applied when editable text is empty.
+
+ @property emptyclass
+ @type string
+ @since 1.4.1
+ @default editable-empty
+ **/
+ emptyclass: 'editable-empty',
+ /**
+ Css class applied when value was stored but not sent to server (`pk` is empty or `send = 'never'`).
+ You may set it to `null` if you work with editables locally and submit them together.
+
+ @property unsavedclass
+ @type string
+ @since 1.4.1
+ @default editable-unsaved
+ **/
+ unsavedclass: 'editable-unsaved',
+ /**
+ If a css selector is provided, editable will be delegated to the specified targets.
+ Usefull for dynamically generated DOM elements.
+ **Please note**, that delegated targets can't use `emptytext` and `autotext` options,
+ as they are initialized after first click.
+
+ @property selector
+ @type string
+ @since 1.4.1
+ @default null
+ @example
+ <div id="user">
+ <a href="#" data-name="username" data-type="text" title="Username">awesome</a>
+ <a href="#" data-name="group" data-type="select" data-source="/groups" data-value="1" title="Group">Operator</a>
+ </div>
+
+ <script>
+ $('#user').editable({
+ selector: 'a',
+ url: '/post',
+ pk: 1
+ });
+ </script>
+ **/
+ selector: null
};
}(window.jQuery));
/**
@@ -2051,19 +2170,11 @@
@property inputclass
@type string
@default input-medium
**/
- inputclass: 'input-medium',
- /**
- Name attribute of input
-
- @property name
- @type string
- @default null
- **/
- name: null
+ inputclass: 'input-medium'
};
$.extend($.fn.editabletypes, {abstractinput: AbstractInput});
}(window.jQuery));
@@ -2212,11 +2323,11 @@
$.each(cache.err_callbacks, function () { this.call(); });
}
}, this)
});
} else { //options as json/array/function
- if (typeof this.options.source === 'function') {
+ if ($.isFunction(this.options.source)) {
this.sourceData = this.makeArray(this.options.source());
} else {
this.sourceData = this.makeArray(this.options.source);
}
@@ -2268,39 +2379,49 @@
/**
* convert data to array suitable for sourceData, e.g. [{value: 1, text: 'abc'}, {...}]
*/
makeArray: function(data) {
- var count, obj, result = [], iterateEl;
+ var count, obj, result = [], item, iterateItem;
if(!data || typeof data === 'string') {
return null;
}
if($.isArray(data)) { //array
- iterateEl = function (k, v) {
+ /*
+ function to iterate inside item of array if item is object.
+ Caclulates count of keys in item and store in obj.
+ */
+ iterateItem = function (k, v) {
obj = {value: k, text: v};
if(count++ >= 2) {
- return false;// exit each if object has more than one value
+ return false;// exit from `each` if item has more than one key.
}
};
for(var i = 0; i < data.length; i++) {
- if(typeof data[i] === 'object') {
- count = 0;
- $.each(data[i], iterateEl);
- if(count === 1) {
+ item = data[i];
+ if(typeof item === 'object') {
+ count = 0; //count of keys inside item
+ $.each(item, iterateItem);
+ //case: [{val1: 'text1'}, {val2: 'text2} ...]
+ if(count === 1) {
result.push(obj);
- } else if(count > 1 && data[i].hasOwnProperty('value') && data[i].hasOwnProperty('text')) {
- result.push(data[i]);
- } else {
- //data contains incorrect objects
+ //case: [{value: 1, text: 'text1'}, {value: 2, text: 'text2'}, ...]
+ } else if(count > 1) {
+ //removed check of existance: item.hasOwnProperty('value') && item.hasOwnProperty('text')
+ if(item.children) {
+ item.children = this.makeArray(item.children);
+ }
+ result.push(item);
}
} else {
- result.push({value: data[i], text: data[i]});
+ //case: ['text1', 'text2' ...]
+ result.push({value: item, text: item});
}
}
- } else { //object
+ } else { //case: {val1: 'text1', val2: 'text2, ...}
$.each(data, function (k, v) {
result.push({value: k, text: v});
});
}
return result;
@@ -2319,16 +2440,20 @@
});
List.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
/**
Source data for list.
- If **array** - it should be in format: `[{value: 1, text: "text1"}, {...}]`
+ If **array** - it should be in format: `[{value: 1, text: "text1"}, {value: 2, text: "text2"}, ...]`
For compability, object format is also supported: `{"1": "text1", "2": "text2" ...}` but it does not guarantee elements order.
If **string** - considered ajax url to load items. In that case results will be cached for fields with the same source and name. See also `sourceCache` option.
If **function**, it should return data in format above (since 1.4.0).
+
+ Since 1.4.1 key `children` supported to render OPTGROUP (for **select** input only).
+ `[{text: "group1", children: [{value: 1, text: "text1"}, {value: 2, text: "text2"}]}, ...]`
+
@property source
@type string | array | object | function
@default null
**/
@@ -2348,12 +2473,12 @@
@type string
@default Error when loading list
**/
sourceError: 'Error when loading list',
/**
- if <code>true</code> and source is **string url** - results will be cached for fields with the same source and name.
- Usefull for editable grids.
+ if <code>true</code> and source is **string url** - results will be cached for fields with the same source.
+ Usefull for editable column in grid to prevent extra requests.
@property sourceCache
@type boolean
@default true
@since 1.2.0
@@ -2413,14 +2538,11 @@
this.$input.after(this.$clear)
.css('padding-right', 20)
.keyup($.proxy(this.toggleClear, this))
.parent().css('position', 'relative');
- this.$clear.click($.proxy(function(){
- this.$clear.hide();
- this.$input.val('').focus();
- }, this));
+ this.$clear.click($.proxy(this.clear, this));
}
},
postrender: function() {
if(this.$clear) {
@@ -2441,16 +2563,21 @@
toggleClear: function() {
if(!this.$clear) {
return;
}
- if(this.$input.val()) {
+ if(this.$input.val().length) {
this.$clear.show();
} else {
this.$clear.hide();
}
- }
+ },
+
+ clear: function() {
+ this.$clear.hide();
+ this.$input.val('').focus();
+ }
});
Text.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
/**
@property tpl
@@ -2620,18 +2747,25 @@
$.fn.editableutils.inherit(Select, $.fn.editabletypes.list);
$.extend(Select.prototype, {
renderList: function() {
this.$input.empty();
-
- if(!$.isArray(this.sourceData)) {
- return;
- }
- for(var i=0; i<this.sourceData.length; i++) {
- this.$input.append($('<option>', {value: this.sourceData[i].value}).text(this.sourceData[i].text));
- }
+ var fillItems = function($el, data) {
+ if($.isArray(data)) {
+ for(var i=0; i<data.length; i++) {
+ if(data[i].children) {
+ $el.append(fillItems($('<optgroup>', {label: data[i].text}), data[i].children));
+ } else {
+ $el.append($('<option>', {value: data[i].value}).text(data[i].text));
+ }
+ }
+ }
+ return $el;
+ };
+
+ fillItems(this.$input, this.sourceData);
this.setClass();
//enter submit
this.$input.on('keydown.editable', function (e) {
@@ -2712,12 +2846,11 @@
}
for(var i=0; i<this.sourceData.length; i++) {
$label = $('<label>').append($('<input>', {
type: 'checkbox',
- value: this.sourceData[i].value,
- name: this.options.name
+ value: this.sourceData[i].value
}))
.append($('<span>').text(' '+this.sourceData[i].text));
$('<div>').append($label).appendTo(this.$tpl);
}
@@ -2742,20 +2875,20 @@
return value;
},
//set checked on required checkboxes
value2input: function(value) {
- this.$input.removeAttr('checked');
+ this.$input.prop('checked', false);
if($.isArray(value) && value.length) {
this.$input.each(function(i, el) {
var $el = $(el);
// cannot use $.inArray as it performs strict comparison
$.each(value, function(j, val){
/*jslint eqeq: true*/
if($el.val() == val) {
/*jslint eqeq: false*/
- $el.attr('checked', 'checked');
+ $el.prop('checked', true);
}
});
});
}
},
@@ -2990,10 +3123,203 @@
inputclass: 'input-medium'
});
$.fn.editabletypes.range = Range;
}(window.jQuery));
/**
+Select2 input. Based on amazing work of Igor Vaynberg https://github.com/ivaynberg/select2.
+Please see [original docs](http://ivaynberg.github.com/select2) for detailed description and options.
+You should manually include select2 distributive:
+
+ <link href="select2/select2.css" rel="stylesheet" type="text/css"></link>
+ <script src="select2/select2.js"></script>
+
+@class select2
+@extends abstractinput
+@since 1.4.1
+@final
+@example
+<a href="#" id="country" data-type="select2" data-pk="1" data-value="ru" data-url="/post" data-original-title="Select country"></a>
+<script>
+$(function(){
+ $('#country').editable({
+ source: [
+ {id: 'gb', text: 'Great Britain'},
+ {id: 'us', text: 'United States'},
+ {id: 'ru', text: 'Russia'}
+ ],
+ select2: {
+ multiple: true
+ }
+ });
+});
+</script>
+**/
+(function ($) {
+
+ var Constructor = function (options) {
+ this.init('select2', options, Constructor.defaults);
+
+ options.select2 = options.select2 || {};
+
+ var that = this,
+ mixin = {
+ placeholder: options.placeholder
+ };
+
+ //detect whether it is multi-valued
+ this.isMultiple = options.select2.tags || options.select2.multiple;
+
+ //if not `tags` mode, we need define init set data from source
+ if(!options.select2.tags) {
+ if(options.source) {
+ mixin.data = options.source;
+ }
+
+ //this function can be defaulted in seletc2. See https://github.com/ivaynberg/select2/issues/710
+ mixin.initSelection = function (element, callback) {
+ var val = that.str2value(element.val()),
+ data = $.fn.editableutils.itemsByValue(val, mixin.data, 'id');
+
+ //for single-valued mode should not use array. Take first element instead.
+ if($.isArray(data) && data.length && !that.isMultiple) {
+ data = data[0];
+ }
+
+ callback(data);
+ };
+ }
+
+ //overriding objects in config (as by default jQuery extend() is not recursive)
+ this.options.select2 = $.extend({}, Constructor.defaults.select2, mixin, options.select2);
+ };
+
+ $.fn.editableutils.inherit(Constructor, $.fn.editabletypes.abstractinput);
+
+ $.extend(Constructor.prototype, {
+ render: function() {
+ this.setClass();
+ //apply select2
+ this.$input.select2(this.options.select2);
+
+ //trigger resize of editableform to re-position container in multi-valued mode
+ if(this.isMultiple) {
+ this.$input.on('change', function() {
+ $(this).closest('form').parent().triggerHandler('resize');
+ });
+ }
+
+ },
+
+ value2html: function(value, element) {
+ var text = '', data;
+ if(this.$input) { //when submitting form
+ data = this.$input.select2('data');
+ } else { //on init (autotext)
+ //here select2 instance not created yet and data may be even not loaded.
+ //we can check data/tags property of select config and if exist lookup text
+ if(this.options.select2.tags) {
+ data = value;
+ } else if(this.options.select2.data) {
+ data = $.fn.editableutils.itemsByValue(value, this.options.select2.data, 'id');
+ }
+ }
+
+ if($.isArray(data)) {
+ //collect selected data and show with separator
+ text = [];
+ $.each(data, function(k, v){
+ text.push(v && typeof v === 'object' ? v.text : v);
+ });
+ } else if(data) {
+ text = data.text;
+ }
+
+ text = $.isArray(text) ? text.join(this.options.viewseparator) : text;
+
+ $(element).text(text);
+ },
+
+ html2value: function(html) {
+ return this.options.select2.tags ? this.str2value(html, this.options.viewseparator) : null;
+ },
+
+ value2input: function(value) {
+ this.$input.val(value).trigger('change');
+ },
+
+ input2value: function() {
+ return this.$input.select2('val');
+ },
+
+ str2value: function(str, separator) {
+ if(typeof str !== 'string' || !this.isMultiple) {
+ return str;
+ }
+
+ separator = separator || this.options.select2.separator || $.fn.select2.defaults.separator;
+
+ var val, i, l;
+
+ if (str === null || str.length < 1) {
+ return null;
+ }
+ val = str.split(separator);
+ for (i = 0, l = val.length; i < l; i = i + 1) {
+ val[i] = $.trim(val[i]);
+ }
+
+ return val;
+ }
+
+ });
+
+ Constructor.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
+ /**
+ @property tpl
+ @default <input type="hidden">
+ **/
+ tpl:'<input type="hidden">',
+ /**
+ Configuration of select2. [Full list of options](http://ivaynberg.github.com/select2).
+
+ @property select2
+ @type object
+ @default null
+ **/
+ select2: null,
+ /**
+ Placeholder attribute of select
+
+ @property placeholder
+ @type string
+ @default null
+ **/
+ placeholder: null,
+ /**
+ Source data for select. It will be assigned to select2 `data` property and kept here just for convenience.
+ Please note, that format is different from simple `select` input: use 'id' instead of 'value'.
+ E.g. `[{id: 1, text: "text1"}, {id: 2, text: "text2"}, ...]`.
+
+ @property source
+ @type array
+ @default null
+ **/
+ source: null,
+ /**
+ Separator used to display tags.
+
+ @property viewseparator
+ @type string
+ @default ', '
+ **/
+ viewseparator: ', '
+ });
+
+ $.fn.editabletypes.select2 = Constructor;
+
+}(window.jQuery));
+/**
* Combodate - 1.0.1
* Dropdown date and time picker.
* Converts text input into dropdowns to pick day, month, year, hour, minute and second.
* Uses momentjs as datetime library http://momentjs.com.
* For i18n include corresponding file from https://github.com/timrwood/moment/tree/master/lang
@@ -4932,5 +5258,227 @@
'</div>';
$.fn.datepicker.DPGlobal = DPGlobal;
}( window.jQuery );
+
+/**
+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 ($) {
+
+ var Constructor = function (options) {
+ this.init('typeahead', options, Constructor.defaults);
+
+ //overriding objects in config (as by default jQuery extend() is not recursive)
+ this.options.typeahead = $.extend({}, Constructor.defaults.typeahead, {
+ //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);
+
+ $.extend(Constructor.prototype, {
+ renderList: function() {
+ this.$input = this.$tpl.is('input') ? this.$tpl : this.$tpl.find('input[type="text"]');
+
+ //set source of typeahead
+ this.options.typeahead.source = this.sourceData;
+
+ //apply typeahead
+ this.$input.typeahead(this.options.typeahead);
+
+ //attach own render method
+ this.$input.data('typeahead').render = $.proxy(this.typeaheadRender, this.$input.data('typeahead'));
+
+ this.renderClear();
+ this.setClass();
+ this.setAttr('placeholder');
+ },
+
+ 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*/
+ 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) {
+ item = this.$menu.find('.active').data('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 in 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).attr('data-value', item)
+ i = $(that.options.item).data('item', item);
+ i.find('a').html(that.highlighter(item));
+ return i[0];
+ });
+
+ items.first().addClass('active');
+ this.$menu.html(items);
+ return this;
+ }
+ /*jshint eqeqeq: true, curly: true, laxcomma: false*/
+
+ });
+
+ Constructor.defaults = $.extend({}, $.fn.editabletypes.list.defaults, {
+ /**
+ @property tpl
+ @default <input type="text">
+ **/
+ tpl:'<input type="text">',
+ /**
+ 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
+ });
+
+ $.fn.editabletypes.typeahead = Constructor;
+
+}(window.jQuery));
\ No newline at end of file