').html(html).text();
+ },
+
+ /**
+ Converts value to string (for internal compare). For submitting to server used value2submit().
+
+ @method value2str(value)
+ @param {mixed} value
+ @returns {string}
+ **/
+ value2str: function(value) {
+ return value;
+ },
+
+ /**
+ Converts string received from server into value. Usually from `data-value` attribute.
+
+ @method str2value(str)
+ @param {string} str
+ @returns {mixed}
+ **/
+ str2value: function(str) {
+ return str;
+ },
+
+ /**
+ Converts value for submitting to server. Result can be string or object.
+
+ @method value2submit(value)
+ @param {mixed} value
+ @returns {mixed}
+ **/
+ value2submit: function(value) {
+ return value;
+ },
+
+ /**
+ Sets value of input.
+
+ @method value2input(value)
+ @param {mixed} value
+ **/
+ value2input: function(value) {
+ this.$input.val(value);
+ },
+
+ /**
+ Returns value of input. Value can be object (e.g. datepicker)
+
+ @method input2value()
+ **/
+ input2value: function() {
+ return this.$input.val();
+ },
+
+ /**
+ Activates input. For text it sets focus.
+
+ @method activate()
+ **/
+ activate: function() {
+ if(this.$input.is(':visible')) {
+ this.$input.focus();
+ }
+ },
+
+ /**
+ Creates input.
+
+ @method clear()
+ **/
+ clear: function() {
+ this.$input.val(null);
+ },
+
+ /**
+ method to escape html.
+ **/
+ escape: function(str) {
+ return $('
').text(str).html();
+ },
+
+ /**
+ attach handler to automatically submit form when value changed (useful when buttons not shown)
+ **/
+ autosubmit: function() {
+
+ },
+
+ /**
+ Additional actions when destroying element
+ **/
+ destroy: function() {
+ },
+
+ // -------- helper functions --------
+ setClass: function() {
+ if(this.options.inputclass) {
+ this.$input.addClass(this.options.inputclass);
+ }
+ },
+
+ setAttr: function(attr) {
+ if (this.options[attr] !== undefined && this.options[attr] !== null) {
+ this.$input.attr(attr, this.options[attr]);
+ }
+ },
+
+ option: function(key, value) {
+ this.options[key] = value;
+ }
+
+ };
+
+ AbstractInput.defaults = {
+ /**
+ HTML template of input. Normally you should not change it.
+
+ @property tpl
+ @type string
+ @default ''
+ **/
+ tpl: '',
+ /**
+ CSS class automatically applied to input
+
+ @property inputclass
+ @type string
+ @default null
+ **/
+ inputclass: null,
+
+ /**
+ If `true` - html will be escaped in content of element via $.text() method.
+ If `false` - html will not be escaped, $.html() used.
+ When you use own `display` function, this option obviosly has no effect.
+
+ @property escape
+ @type boolean
+ @since 1.5.0
+ @default true
+ **/
+ escape: true,
+
+ //scope for external methods (e.g. source defined as function)
+ //for internal use only
+ scope: null,
+
+ //need to re-declare showbuttons here to get it's value from common config (passed only options existing in defaults)
+ showbuttons: true
+ };
+
+ $.extend($.fn.editabletypes, {abstractinput: AbstractInput});
+
+}(window.jQuery));
+
+/**
+List - abstract class for inputs that have source option loaded from js array or via ajax
+
+@class list
+@extends abstractinput
+**/
+(function ($) {
+ "use strict";
+
+ var List = function (options) {
+
+ };
+
+ $.fn.editableutils.inherit(List, $.fn.editabletypes.abstractinput);
+
+ $.extend(List.prototype, {
+ render: function () {
+
+ var deferred = $.Deferred();
+
+ this.error = null;
+ this.onSourceReady(function () {
+ this.renderList();
+ deferred.resolve();
+ }, function () {
+ this.error = this.options.sourceError;
+ deferred.resolve();
+ });
+
+ return deferred.promise();
+ },
+
+ html2value: function (html) {
+ return null; //can't set value by text
+ },
+
+ value2html: function (value, element, display, response) {
+ var deferred = $.Deferred(),
+ success = function () {
+ if(typeof display === 'function') {
+ //custom display method
+ display.call(element, value, this.sourceData, response);
+ } else {
+ this.value2htmlFinal(value, element);
+ }
+ deferred.resolve();
+ };
+
+ //for null value just call success without loading source
+ if(value === null) {
+ success.call(this);
+ } else {
+ this.onSourceReady(success, function () { deferred.resolve(); });
+ }
+
+ return deferred.promise();
+ },
+
+ // ------------- additional functions ------------
+
+ onSourceReady: function (success, error) {
+ //run source if it function
+ var source;
+ if (typeof(this.options.source) === 'function') {
+ source = this.options.source.call(this.options.scope);
+ this.sourceData = null;
+ //note: if function returns the same source as URL - sourceData will be taken from cahce and no extra request performed
+ } else {
+ source = this.options.source;
+ }
+
+ //if allready loaded just call success
+ if(this.options.sourceCache && Array.isArray(this.sourceData)) {
+ success.call(this);
+ return;
+ }
+
+ //try parse json in single quotes (for double quotes jquery does automatically)
+ try {
+ source = $.fn.editableutils.tryParseJson(source, false);
+ } catch (e) {
+ error.call(this);
+ return;
+ }
+
+ //loading from url
+ if (typeof source === 'string') {
+ //try to get sourceData from cache
+ if(this.options.sourceCache) {
+ var cacheID = source,
+ cache;
+
+ if (!$(document).data(cacheID)) {
+ $(document).data(cacheID, {});
+ }
+ cache = $(document).data(cacheID);
+
+ //check for cached data
+ if (cache.loading === false && cache.sourceData) { //take source from cache
+ this.sourceData = cache.sourceData;
+ this.doPrepend();
+ success.call(this);
+ return;
+ } else if (cache.loading === true) { //cache is loading, put callback in stack to be called later
+ cache.callbacks.push($.proxy(function () {
+ this.sourceData = cache.sourceData;
+ this.doPrepend();
+ success.call(this);
+ }, this));
+
+ //also collecting error callbacks
+ cache.err_callbacks.push($.proxy(error, this));
+ return;
+ } else { //no cache yet, activate it
+ cache.loading = true;
+ cache.callbacks = [];
+ cache.err_callbacks = [];
+ }
+ }
+
+ //ajaxOptions for source. Can be overwritten bt options.sourceOptions
+ var ajaxOptions = $.extend({
+ url: source,
+ type: 'get',
+ cache: false,
+ dataType: 'json',
+ success: $.proxy(function (data) {
+ if(cache) {
+ cache.loading = false;
+ }
+ this.sourceData = this.makeArray(data);
+ if(Array.isArray(this.sourceData)) {
+ if(cache) {
+ //store result in cache
+ cache.sourceData = this.sourceData;
+ //run success callbacks for other fields waiting for this source
+ $.each(cache.callbacks, function () { this.call(); });
+ }
+ this.doPrepend();
+ success.call(this);
+ } else {
+ error.call(this);
+ if(cache) {
+ //run error callbacks for other fields waiting for this source
+ $.each(cache.err_callbacks, function () { this.call(); });
+ }
+ }
+ }, this),
+ error: $.proxy(function () {
+ error.call(this);
+ if(cache) {
+ cache.loading = false;
+ //run error callbacks for other fields
+ $.each(cache.err_callbacks, function () { this.call(); });
+ }
+ }, this)
+ }, this.options.sourceOptions);
+
+ //loading sourceData from server
+ $.ajax(ajaxOptions);
+
+ } else { //options as json/array
+ this.sourceData = this.makeArray(source);
+
+ if(Array.isArray(this.sourceData)) {
+ this.doPrepend();
+ success.call(this);
+ } else {
+ error.call(this);
+ }
+ }
+ },
+
+ doPrepend: function () {
+ if(this.options.prepend === null || this.options.prepend === undefined) {
+ return;
+ }
+
+ if(!Array.isArray(this.prependData)) {
+ //run prepend if it is function (once)
+ if (typeof (this.options.prepend) === 'function') {
+ this.options.prepend = this.options.prepend.call(this.options.scope);
+ }
+
+ //try parse json in single quotes
+ this.options.prepend = $.fn.editableutils.tryParseJson(this.options.prepend, true);
+
+ //convert prepend from string to object
+ if (typeof this.options.prepend === 'string') {
+ this.options.prepend = {'': this.options.prepend};
+ }
+
+ this.prependData = this.makeArray(this.options.prepend);
+ }
+
+ if(Array.isArray(this.prependData) && Array.isArray(this.sourceData)) {
+ this.sourceData = this.prependData.concat(this.sourceData);
+ }
+ },
+
+ /*
+ renders input list
+ */
+ renderList: function() {
+ // this method should be overwritten in child class
+ },
+
+ /*
+ set element's html by value
+ */
+ value2htmlFinal: function(value, element) {
+ // this method should be overwritten in child class
+ },
+
+ /**
+ * convert data to array suitable for sourceData, e.g. [{value: 1, text: 'abc'}, {...}]
+ */
+ makeArray: function(data) {
+ var count, obj, result = [], item, iterateItem;
+ if(!data || typeof data === 'string') {
+ return null;
+ }
+
+ if(Array.isArray(data)) { //array
+ /*
+ 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 from `each` if item has more than one key.
+ }
+ };
+
+ for(var i = 0; i < data.length; i++) {
+ 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);
+ //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 {
+ //case: ['text1', 'text2' ...]
+ result.push({value: item, text: item});
+ }
+ }
+ } else { //case: {val1: 'text1', val2: 'text2, ...}
+ $.each(data, function (k, v) {
+ result.push({value: k, text: v});
+ });
+ }
+ return result;
+ },
+
+ option: function(key, value) {
+ this.options[key] = value;
+ if(key === 'source') {
+ this.sourceData = null;
+ }
+ if(key === 'prepend') {
+ this.prependData = null;
+ }
+ }
+
+ });
+
+ List.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
+ /**
+ Source data for list.
+ 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
+ **/
+ source: null,
+ /**
+ Data automatically prepended to the beginning of dropdown list.
+
+ @property prepend
+ @type string | array | object | function
+ @default false
+ **/
+ prepend: false,
+ /**
+ Error message when list cannot be loaded (e.g. ajax error)
+
+ @property sourceError
+ @type string
+ @default Error when loading list
+ **/
+ sourceError: 'Error when loading list',
+ /**
+ if
true
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
+ **/
+ sourceCache: true,
+ /**
+ Additional ajax options to be used in $.ajax() when loading list from server.
+ Useful to send extra parameters (`data` key) or change request method (`type` key).
+
+ @property sourceOptions
+ @type object|function
+ @default null
+ @since 1.5.0
+ **/
+ sourceOptions: null
+ });
+
+ $.fn.editabletypes.list = List;
+
+}(window.jQuery));
+
+/**
+Text input
+
+@class text
+@extends abstractinput
+@final
+@example
+
awesome
+
+**/
+(function ($) {
+ "use strict";
+
+ var Text = function (options) {
+ this.init('text', options, Text.defaults);
+ };
+
+ $.fn.editableutils.inherit(Text, $.fn.editabletypes.abstractinput);
+
+ $.extend(Text.prototype, {
+ render: function() {
+ this.renderClear();
+ this.setClass();
+ this.setAttr('placeholder');
+ },
+
+ activate: function() {
+ if(this.$input.is(':visible')) {
+ this.$input.focus();
+ $.fn.editableutils.setCursorPosition(this.$input.get(0), this.$input.val().length);
+ if(this.toggleClear) {
+ this.toggleClear();
+ }
+ }
+ },
+
+ //render clear button
+ renderClear: function() {
+ if (this.options.clear) {
+ this.$clear = $('
');
+ this.$input.after(this.$clear)
+ .css('padding-right', 24)
+ .keyup($.proxy(function(e) {
+ //arrows, enter, tab, etc
+ if(~$.inArray(e.keyCode, [40,38,9,13,27])) {
+ return;
+ }
+
+ clearTimeout(this.t);
+ var that = this;
+ this.t = setTimeout(function() {
+ that.toggleClear(e);
+ }, 100);
+
+ }, this))
+ .parent().css('position', 'relative');
+
+ this.$clear.click($.proxy(this.clear, this));
+ }
+ },
+
+ postrender: function() {
+ /*
+ //now `clear` is positioned via css
+ if(this.$clear) {
+ //can position clear button only here, when form is shown and height can be calculated
+// var h = this.$input.outerHeight(true) || 20,
+ var h = this.$clear.parent().height(),
+ delta = (h - this.$clear.height()) / 2;
+
+ //this.$clear.css({bottom: delta, right: delta});
+ }
+ */
+ },
+
+ //show / hide clear button
+ toggleClear: function(e) {
+ if(!this.$clear) {
+ return;
+ }
+
+ var len = this.$input.val().length,
+ visible = this.$clear.is(':visible');
+
+ if(len && !visible) {
+ this.$clear.show();
+ }
+
+ if(!len && visible) {
+ this.$clear.hide();
+ }
+ },
+
+ clear: function() {
+ this.$clear.hide();
+ this.$input.val('').focus();
+ }
+ });
+
+ Text.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
+ /**
+ @property tpl
+ @default
+ **/
+ tpl: '
',
+ /**
+ Placeholder attribute of input. Shown when input is empty.
+
+ @property placeholder
+ @type string
+ @default null
+ **/
+ placeholder: null,
+
+ /**
+ Whether to show `clear` button
+
+ @property clear
+ @type boolean
+ @default true
+ **/
+ clear: true
+ });
+
+ $.fn.editabletypes.text = Text;
+
+}(window.jQuery));
+
+/**
+Textarea input
+
+@class textarea
+@extends abstractinput
+@final
+@example
+
+
+**/
+(function ($) {
+ "use strict";
+
+ var Textarea = function (options) {
+ this.init('textarea', options, Textarea.defaults);
+ };
+
+ $.fn.editableutils.inherit(Textarea, $.fn.editabletypes.abstractinput);
+
+ $.extend(Textarea.prototype, {
+ render: function () {
+ this.setClass();
+ this.setAttr('placeholder');
+ this.setAttr('rows');
+
+ //ctrl + enter
+ this.$input.keydown(function (e) {
+ if (e.ctrlKey && e.which === 13) {
+ $(this).closest('form').submit();
+ }
+ });
+ },
+
+ //using `white-space: pre-wrap` solves \n <--> BR conversion very elegant!
+ /*
+ value2html: function(value, element) {
+ var html = '', lines;
+ if(value) {
+ lines = value.split("\n");
+ for (var i = 0; i < lines.length; i++) {
+ lines[i] = $('
').text(lines[i]).html();
+ }
+ html = lines.join('
');
+ }
+ $(element).html(html);
+ },
+
+ html2value: function(html) {
+ if(!html) {
+ return '';
+ }
+
+ var regex = new RegExp(String.fromCharCode(10), 'g');
+ var lines = html.split(/
/i);
+ for (var i = 0; i < lines.length; i++) {
+ var text = $('
').html(lines[i]).text();
+
+ // Remove newline characters (\n) to avoid them being converted by value2html() method
+ // thus adding extra
tags
+ text = text.replace(regex, '');
+
+ lines[i] = text;
+ }
+ return lines.join("\n");
+ },
+ */
+ activate: function() {
+ $.fn.editabletypes.text.prototype.activate.call(this);
+ }
+ });
+
+ Textarea.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
+ /**
+ @property tpl
+ @default
+ **/
+ tpl:'
',
+ /**
+ @property inputclass
+ @default input-large
+ **/
+ inputclass: 'input-large',
+ /**
+ Placeholder attribute of input. Shown when input is empty.
+
+ @property placeholder
+ @type string
+ @default null
+ **/
+ placeholder: null,
+ /**
+ Number of rows in textarea
+
+ @property rows
+ @type integer
+ @default 7
+ **/
+ rows: 7
+ });
+
+ $.fn.editabletypes.textarea = Textarea;
+
+}(window.jQuery));
+
+/**
+Select (dropdown)
+
+@class select
+@extends list
+@final
+@example
+
+
+**/
+(function ($) {
+ "use strict";
+
+ var Select = function (options) {
+ this.init('select', options, Select.defaults);
+ };
+
+ $.fn.editableutils.inherit(Select, $.fn.editabletypes.list);
+
+ $.extend(Select.prototype, {
+ renderList: function() {
+ this.$input.empty();
+
+ var fillItems = function($el, data) {
+ var attr;
+ if(Array.isArray(data)) {
+ for(var i=0; i
', attr), data[i].children));
+ } else {
+ attr.value = data[i].value;
+ if(data[i].disabled) {
+ attr.disabled = true;
+ }
+ $el.append($('