bootstrap-editable-rails 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  In-place editing with Twitter Bootstrap for Rails
4
4
 
5
- This gem is based on X-editable (v1.3.0) which is the new version of Bootstrap Editable.
5
+ This gem is based on X-editable (v1.4.1) which is the new version of Bootstrap Editable.
6
6
 
7
7
  https://github.com/vitalets/x-editable
8
8
 
@@ -35,15 +35,13 @@ Write the top of `app/assets/javascripts/application.js` like this:
35
35
  //= require_tree .
36
36
  ```
37
37
 
38
- (You can choose `bootstrap-editable-inline`)
39
-
40
38
  and need to load `bootstrap-editable.css` at the place where you like.
41
39
 
42
40
  ### HTML
43
41
 
44
- Follow the documents above.
42
+ Follow the documents of X-editable above.
45
43
 
46
- Additional required attribute(option) is `resource`.
44
+ Additional required attribute is `resource`.
47
45
 
48
46
  ```html
49
47
  <a href="#" id="username" data-type="text" data-resource="post" data-name="username" data-url="/posts/1" data-original-title="Enter username">superuser</a>
@@ -55,6 +53,14 @@ then, sends `PUT /posts/1` request with the body:
55
53
  post[username]=superuser
56
54
  ```
57
55
 
56
+ When using `textarea` type, `textarea_format` helper method for formatting line breaks is available.
57
+
58
+ ```html
59
+ <a href="#" id="body" data-type="textarea" data-resource="post" data-name="body" data-url="/posts/1" data-original-title="Enter body">
60
+ <%= textarea_format(@post.body) %>
61
+ </a>
62
+ ```
63
+
58
64
  ### Controller
59
65
 
60
66
  PostsController receives the parameters
@@ -65,7 +71,7 @@ PostsController receives the parameters
65
71
 
66
72
  and must respond with 2xx (means _success_) status code if successful.
67
73
 
68
- For example, scaffold goes well because default dataType is json.
74
+ For example, scaffold works well by 204 because default dataType is json.
69
75
 
70
76
  ```ruby
71
77
  def update
@@ -1,7 +1,7 @@
1
1
  module Bootstrap
2
2
  module Editable
3
3
  module Rails
4
- VERSION = "0.0.3"
4
+ VERSION = "0.0.4"
5
5
  end
6
6
  end
7
7
  end
Binary file
@@ -10,14 +10,21 @@ jQuery ($) ->
10
10
  # TODO: should not send when create new object
11
11
  if typeof originalUrl == 'function' # user's function
12
12
  originalUrl.call(@options.scope, params)
13
- else # send ajax to server and return deferred object
13
+ else if originalUrl? && @options.send != 'never'
14
+ # send ajax to server and return deferred object
14
15
  obj = {}
15
- data = {}
16
16
  obj[params.name] = params.value
17
- data[resource] = obj
17
+ # support custom inputtypes (eg address)
18
+ if resource
19
+ params[resource] = obj
20
+ else
21
+ params = obj
22
+ delete params.name
23
+ delete params.value
24
+ delete params.pk
18
25
  $.ajax($.extend({
19
26
  url : originalUrl
20
- data : data
27
+ data : params
21
28
  type : 'PUT' # TODO: should be 'POST' when create new object
22
29
  dataType: 'json'
23
30
  }, @options.ajaxOptions))
@@ -1,7 +1,7 @@
1
- /*! X-editable - v1.3.0
1
+ /*! X-editable - v1.4.1
2
2
  * In-place editing with Twitter Bootstrap, jQuery UI or pure jQuery
3
3
  * http://github.com/vitalets/x-editable
4
- * Copyright (c) 2012 Vitaliy Potapov; Licensed MIT */
4
+ * Copyright (c) 2013 Vitaliy Potapov; Licensed MIT */
5
5
 
6
6
  /**
7
7
  Form with single input element, two buttons and two states: normal/loading.
@@ -16,28 +16,21 @@ Editableform is linked with one of input types, e.g. 'text', 'select' etc.
16
16
 
17
17
  var EditableForm = function (div, options) {
18
18
  this.options = $.extend({}, $.fn.editableform.defaults, options);
19
- this.$div = $(div); //div, containing form. Not form tag! Not editable-element.
19
+ this.$div = $(div); //div, containing form. Not form tag. Not editable-element.
20
20
  if(!this.options.scope) {
21
21
  this.options.scope = this;
22
22
  }
23
- this.initInput();
23
+ //nothing shown after init
24
24
  };
25
25
 
26
26
  EditableForm.prototype = {
27
27
  constructor: EditableForm,
28
28
  initInput: function() { //called once
29
- var TypeConstructor, typeOptions;
30
-
31
- //create input of specified type
32
- if(typeof $.fn.editabletypes[this.options.type] === 'function') {
33
- TypeConstructor = $.fn.editabletypes[this.options.type];
34
- typeOptions = $.fn.editableutils.sliceObj(this.options, $.fn.editableutils.objectKeys(TypeConstructor.defaults));
35
- this.input = new TypeConstructor(typeOptions);
36
- } else {
37
- $.error('Unknown type: '+ this.options.type);
38
- return;
39
- }
40
-
29
+ //take input from options (as it is created in editable-element)
30
+ this.input = this.options.input;
31
+
32
+ //set initial value
33
+ //todo: may be add check: typeof str === 'string' ?
41
34
  this.value = this.input.str2value(this.options.value);
42
35
  },
43
36
  initTemplate: function() {
@@ -52,47 +45,49 @@ Editableform is linked with one of input types, e.g. 'text', 'select' etc.
52
45
  @method render
53
46
  **/
54
47
  render: function() {
48
+ //init loader
55
49
  this.$loading = $($.fn.editableform.loading);
56
50
  this.$div.empty().append(this.$loading);
57
- this.showLoading();
58
51
 
59
52
  //init form template and buttons
60
- this.initTemplate();
53
+ this.initTemplate();
61
54
  if(this.options.showbuttons) {
62
55
  this.initButtons();
63
56
  } else {
64
57
  this.$form.find('.editable-buttons').remove();
65
58
  }
66
59
 
60
+ //show loading state
61
+ this.showLoading();
62
+
67
63
  /**
68
64
  Fired when rendering starts
69
65
  @event rendering
70
66
  @param {Object} event event object
71
67
  **/
72
68
  this.$div.triggerHandler('rendering');
69
+
70
+ //init input
71
+ this.initInput();
72
+
73
+ //append input to form
74
+ this.input.prerender();
75
+ this.$form.find('div.editable-input').append(this.input.$tpl);
73
76
 
77
+ //append form to container
78
+ this.$div.append(this.$form);
79
+
74
80
  //render input
75
81
  $.when(this.input.render())
76
82
  .then($.proxy(function () {
77
- //input
78
- this.$form.find('div.editable-input').append(this.input.$input);
79
-
80
- //automatically submit inputs when no buttons shown
83
+ //setup input to submit automatically when no buttons shown
81
84
  if(!this.options.showbuttons) {
82
85
  this.input.autosubmit();
83
86
  }
84
-
85
- //"clear" link
86
- if(this.input.$clear) {
87
- this.$form.find('div.editable-input').append($('<div class="editable-clear">').append(this.input.$clear));
88
- }
89
-
90
- //append form to container
91
- this.$div.append(this.$form);
92
87
 
93
88
  //attach 'cancel' handler
94
89
  this.$form.find('.editable-cancel').click($.proxy(this.cancel, this));
95
-
90
+
96
91
  if(this.input.error) {
97
92
  this.error(this.input.error);
98
93
  this.$form.find('.editable-submit').attr('disabled', true);
@@ -116,6 +111,11 @@ Editableform is linked with one of input types, e.g. 'text', 'select' etc.
116
111
  this.$div.triggerHandler('rendered');
117
112
 
118
113
  this.showForm();
114
+
115
+ //call postrender method to perform actions required visibility of form
116
+ if(this.input.postrender) {
117
+ this.input.postrender();
118
+ }
119
119
  }, this));
120
120
  },
121
121
  cancel: function() {
@@ -127,11 +127,17 @@ Editableform is linked with one of input types, e.g. 'text', 'select' etc.
127
127
  this.$div.triggerHandler('cancel');
128
128
  },
129
129
  showLoading: function() {
130
- var w;
130
+ var w, h;
131
131
  if(this.$form) {
132
- //set loading size equal to form
133
- this.$loading.width(this.$form.outerWidth());
134
- this.$loading.height(this.$form.outerHeight());
132
+ //set loading size equal to form
133
+ w = this.$form.outerWidth();
134
+ h = this.$form.outerHeight();
135
+ if(w) {
136
+ this.$loading.width(w);
137
+ }
138
+ if(h) {
139
+ this.$loading.height(h);
140
+ }
135
141
  this.$form.hide();
136
142
  } else {
137
143
  //stretch loading to fill container width
@@ -159,14 +165,23 @@ Editableform is linked with one of input types, e.g. 'text', 'select' etc.
159
165
 
160
166
  error: function(msg) {
161
167
  var $group = this.$form.find('.control-group'),
162
- $block = this.$form.find('.editable-error-block');
168
+ $block = this.$form.find('.editable-error-block'),
169
+ lines;
163
170
 
164
171
  if(msg === false) {
165
172
  $group.removeClass($.fn.editableform.errorGroupClass);
166
173
  $block.removeClass($.fn.editableform.errorBlockClass).empty().hide();
167
174
  } else {
175
+ //convert newline to <br> for more pretty error display
176
+ if(msg) {
177
+ lines = msg.split("\n");
178
+ for (var i = 0; i < lines.length; i++) {
179
+ lines[i] = $('<div>').text(lines[i]).html();
180
+ }
181
+ msg = lines.join('<br>');
182
+ }
168
183
  $group.addClass($.fn.editableform.errorGroupClass);
169
- $block.addClass($.fn.editableform.errorBlockClass).text(msg).show();
184
+ $block.addClass($.fn.editableform.errorBlockClass).html(msg).show();
170
185
  }
171
186
  },
172
187
 
@@ -218,6 +233,7 @@ Editableform is linked with one of input types, e.g. 'text', 'select' etc.
218
233
  }
219
234
 
220
235
  //if success callback returns object like {newValue: <something>} --> use that value instead of submitted
236
+ //it is usefull if you want to chnage value in url-function
221
237
  if(res && typeof res === 'object' && res.hasOwnProperty('newValue')) {
222
238
  newValue = res.newValue;
223
239
  }
@@ -299,10 +315,15 @@ Editableform is linked with one of input types, e.g. 'text', 'select' etc.
299
315
  },
300
316
 
301
317
  option: function(key, value) {
302
- this.options[key] = value;
318
+ if(key in this.options) {
319
+ this.options[key] = value;
320
+ }
321
+
303
322
  if(key === 'value') {
304
323
  this.setValue(value);
305
324
  }
325
+
326
+ //do not pass option to input as it is passed in editable-element
306
327
  },
307
328
 
308
329
  setValue: function(value, convertStr) {
@@ -311,6 +332,11 @@ Editableform is linked with one of input types, e.g. 'text', 'select' etc.
311
332
  } else {
312
333
  this.value = value;
313
334
  }
335
+
336
+ //if form is visible, update input
337
+ if(this.$form && this.$form.is(':visible')) {
338
+ this.input.value2input(this.value);
339
+ }
314
340
  }
315
341
  };
316
342
 
@@ -363,18 +389,25 @@ Editableform is linked with one of input types, e.g. 'text', 'select' etc.
363
389
  type: 'text',
364
390
  /**
365
391
  Url for submit, e.g. <code>'/post'</code>
366
- If function - it will be called instead of ajax. Function can return deferred object to run fail/done callbacks.
392
+ If function - it will be called instead of ajax. Function should return deferred object to run fail/done callbacks.
367
393
 
368
394
  @property url
369
395
  @type string|function
370
396
  @default null
371
397
  @example
372
398
  url: function(params) {
399
+ var d = new $.Deferred;
373
400
  if(params.value === 'abc') {
374
- var d = new $.Deferred;
375
- return d.reject('field cannot be "abc"'); //returning error via deferred object
401
+ return d.reject('error message'); //returning error via deferred object
376
402
  } else {
377
- someModel.set(params.name, params.value); //save data in some js model
403
+ //async saving data in js model
404
+ someModel.asyncSaveMethod({
405
+ ...,
406
+ success: function(){
407
+ d.resolve();
408
+ }
409
+ });
410
+ return d.promise();
378
411
  }
379
412
  }
380
413
  **/
@@ -463,21 +496,21 @@ Editableform is linked with one of input types, e.g. 'text', 'select' etc.
463
496
  /**
464
497
  Additional options for ajax request.
465
498
  List of values: http://api.jquery.com/jQuery.ajax
466
-
499
+
467
500
  @property ajaxOptions
468
501
  @type object
469
502
  @default null
470
503
  @since 1.1.1
504
+ @example
505
+ ajaxOptions: {
506
+ type: 'put',
507
+ dataType: 'json'
508
+ }
471
509
  **/
472
510
  ajaxOptions: null,
473
511
  /**
474
512
  Whether to show buttons or not.
475
- Form without buttons can be auto-submitted by input or by onblur = 'submit'.
476
- @example
477
- ajaxOptions: {
478
- method: 'PUT',
479
- dataType: 'xml'
480
- }
513
+ Form without buttons is auto-submitted.
481
514
 
482
515
  @property showbuttons
483
516
  @type boolean
@@ -621,19 +654,22 @@ Editableform is linked with one of input types, e.g. 'text', 'select' etc.
621
654
  return newObj;
622
655
  },
623
656
 
624
- /**
625
- * exclude complex objects from $.data() before pass to config
657
+ /*
658
+ exclude complex objects from $.data() before pass to config
626
659
  */
627
660
  getConfigData: function($element) {
628
661
  var data = {};
629
662
  $.each($element.data(), function(k, v) {
630
- if(typeof v !== 'object' || (v && typeof v === 'object' && v.constructor === Object)) {
663
+ if(typeof v !== 'object' || (v && typeof v === 'object' && (v.constructor === Object || v.constructor === Array))) {
631
664
  data[k] = v;
632
665
  }
633
666
  });
634
667
  return data;
635
668
  },
636
669
 
670
+ /*
671
+ returns keys of object
672
+ */
637
673
  objectKeys: function(o) {
638
674
  if (Object.keys) {
639
675
  return Object.keys(o);
@@ -657,9 +693,95 @@ Editableform is linked with one of input types, e.g. 'text', 'select' etc.
657
693
  **/
658
694
  escape: function(str) {
659
695
  return $('<div>').text(str).html();
660
- }
696
+ },
697
+
698
+ /*
699
+ returns array items from sourceData having value property equal or inArray of 'value'
700
+ */
701
+ itemsByValue: function(value, sourceData, valueProp) {
702
+ if(!sourceData || value === null) {
703
+ return [];
704
+ }
705
+
706
+ valueProp = valueProp || 'value';
707
+
708
+ var isValArray = $.isArray(value),
709
+ result = [],
710
+ that = this;
711
+
712
+ $.each(sourceData, function(i, o) {
713
+ if(o.children) {
714
+ result = result.concat(that.itemsByValue(value, o.children));
715
+ } else {
716
+ /*jslint eqeq: true*/
717
+ if(isValArray) {
718
+ if($.grep(value, function(v){ return v == (o && typeof o === 'object' ? o[valueProp] : o); }).length) {
719
+ result.push(o);
720
+ }
721
+ } else {
722
+ if(value == (o && typeof o === 'object' ? o[valueProp] : o)) {
723
+ result.push(o);
724
+ }
725
+ }
726
+ /*jslint eqeq: false*/
727
+ }
728
+ });
729
+
730
+ return result;
731
+ },
732
+
733
+ /*
734
+ Returns input by options: type, mode.
735
+ */
736
+ createInput: function(options) {
737
+ var TypeConstructor, typeOptions, input,
738
+ type = options.type;
739
+
740
+ //`date` is some kind of virtual type that is transformed to one of exact types
741
+ //depending on mode and core lib
742
+ if(type === 'date') {
743
+ //inline
744
+ if(options.mode === 'inline') {
745
+ if($.fn.editabletypes.datefield) {
746
+ type = 'datefield';
747
+ } else if($.fn.editabletypes.dateuifield) {
748
+ type = 'dateuifield';
749
+ }
750
+ //popup
751
+ } else {
752
+ if($.fn.editabletypes.date) {
753
+ type = 'date';
754
+ } else if($.fn.editabletypes.dateui) {
755
+ type = 'dateui';
756
+ }
757
+ }
758
+
759
+ //if type still `date` and not exist in types, replace with `combodate` that is base input
760
+ if(type === 'date' && !$.fn.editabletypes.date) {
761
+ type = 'combodate';
762
+ }
763
+ }
764
+
765
+ //change wysihtml5 to textarea for jquery UI and plain versions
766
+ if(type === 'wysihtml5' && !$.fn.editabletypes[type]) {
767
+ type = 'textarea';
768
+ }
769
+
770
+ //create input of specified type. Input will be used for converting value, not in form
771
+ if(typeof $.fn.editabletypes[type] === 'function') {
772
+ TypeConstructor = $.fn.editabletypes[type];
773
+ typeOptions = this.sliceObj(options, this.objectKeys(TypeConstructor.defaults));
774
+ input = new TypeConstructor(typeOptions);
775
+ return input;
776
+ } else {
777
+ $.error('Unknown type: '+ type);
778
+ return false;
779
+ }
780
+ }
781
+
661
782
  };
662
- }(window.jQuery));
783
+ }(window.jQuery));
784
+
663
785
  /**
664
786
  Attaches stand-alone container with editable-form to HTML element. Element is used only for positioning, value is not stored anywhere.<br>
665
787
  This method applied internally in <code>$().editable()</code>. You should subscribe on it's events (save / cancel) to get profit of it.<br>
@@ -671,19 +793,27 @@ Applied as jQuery method.
671
793
  **/
672
794
  (function ($) {
673
795
 
674
- var EditableContainer = function (element, options) {
796
+ var Popup = function (element, options) {
675
797
  this.init(element, options);
676
798
  };
799
+
800
+ var Inline = function (element, options) {
801
+ this.init(element, options);
802
+ };
677
803
 
678
804
  //methods
679
- EditableContainer.prototype = {
805
+ Popup.prototype = {
680
806
  containerName: null, //tbd in child class
681
807
  innerCss: null, //tbd in child class
682
808
  init: function(element, options) {
683
809
  this.$element = $(element);
684
- //todo: what is in priority: data or js?
685
- this.options = $.extend({}, $.fn.editableContainer.defaults, $.fn.editableutils.getConfigData(this.$element), options);
810
+ //since 1.4.1 container do not use data-* directly as they already merged into options.
811
+ this.options = $.extend({}, $.fn.editableContainer.defaults, options);
686
812
  this.splitOptions();
813
+
814
+ //set scope of form callbacks to element
815
+ this.formOptions.scope = this.$element[0];
816
+
687
817
  this.initContainer();
688
818
 
689
819
  //bind 'destroyed' listener to destroy container when element is removed from dom
@@ -691,7 +821,7 @@ Applied as jQuery method.
691
821
  this.destroy();
692
822
  }, this));
693
823
 
694
- //attach document handlers (once)
824
+ //attach document handler to close containers on click / escape
695
825
  if(!$(document).data('editable-handlers-attached')) {
696
826
  //close all on escape
697
827
  $(document).on('keyup.editable', function (e) {
@@ -703,15 +833,22 @@ Applied as jQuery method.
703
833
 
704
834
  //close containers when click outside
705
835
  $(document).on('click.editable', function(e) {
706
- var $target = $(e.target);
836
+ var $target = $(e.target), i,
837
+ exclude_classes = ['.editable-container',
838
+ '.ui-datepicker-header',
839
+ '.modal-backdrop',
840
+ '.bootstrap-wysihtml5-insert-image-modal',
841
+ '.bootstrap-wysihtml5-insert-link-modal'];
707
842
 
708
- //if click inside some editableContainer --> no nothing
709
- if($target.is('.editable-container') || $target.parents('.editable-container').length || $target.parents('.ui-datepicker-header').length) {
710
- return;
711
- } else {
712
- //close all open containers (except one)
713
- EditableContainer.prototype.closeOthers(e.target);
843
+ //if click inside one of exclude classes --> no nothing
844
+ for(i=0; i<exclude_classes.length; i++) {
845
+ if($target.is(exclude_classes[i]) || $target.parents(exclude_classes[i]).length) {
846
+ return;
847
+ }
714
848
  }
849
+
850
+ //close all open containers (except one - target)
851
+ Popup.prototype.closeOthers(e.target);
715
852
  });
716
853
 
717
854
  $(document).data('editable-handlers-attached', true);
@@ -723,6 +860,7 @@ Applied as jQuery method.
723
860
  this.containerOptions = {};
724
861
  this.formOptions = {};
725
862
  var cDef = $.fn[this.containerName].defaults;
863
+ //keys defined in container defaults go to container, others go to form
726
864
  for(var k in this.options) {
727
865
  if(k in cDef) {
728
866
  this.containerOptions[k] = this.options[k];
@@ -732,20 +870,37 @@ Applied as jQuery method.
732
870
  }
733
871
  },
734
872
 
873
+ /*
874
+ Returns jquery object of container
875
+ @method tip()
876
+ */
877
+ tip: function() {
878
+ return this.container() ? this.container().$tip : null;
879
+ },
880
+
881
+ /* returns container object */
882
+ container: function() {
883
+ return this.$element.data(this.containerName);
884
+ },
885
+
886
+ call: function() {
887
+ this.$element[this.containerName].apply(this.$element, arguments);
888
+ },
889
+
735
890
  initContainer: function(){
736
891
  this.call(this.containerOptions);
737
892
  },
738
893
 
739
- initForm: function() {
740
- this.formOptions.scope = this.$element[0]; //set scope of form callbacks to element
741
- this.$form = $('<div>')
894
+ renderForm: function() {
895
+ this.$form
742
896
  .editableform(this.formOptions)
743
897
  .on({
744
- save: $.proxy(this.save, this),
745
- cancel: $.proxy(function(){ this.hide('cancel'); }, this),
746
- nochange: $.proxy(function(){ this.hide('nochange'); }, this),
898
+ save: $.proxy(this.save, this), //click on submit button (value changed)
899
+ nochange: $.proxy(function(){ this.hide('nochange'); }, this), //click on submit button (value NOT changed)
900
+ cancel: $.proxy(function(){ this.hide('cancel'); }, this), //click on calcel button
747
901
  show: $.proxy(this.setPosition, this), //re-position container every time form is shown (occurs each time after loading state)
748
902
  rendering: $.proxy(this.setPosition, this), //this allows to place container correctly when loading shown
903
+ resize: $.proxy(this.setPosition, this), //this allows to re-position container when form size is changed
749
904
  rendered: $.proxy(function(){
750
905
  /**
751
906
  Fired when container is shown and form is rendered (for select will wait for loading dropdown options)
@@ -760,31 +915,16 @@ Applied as jQuery method.
760
915
  **/
761
916
  this.$element.triggerHandler('shown');
762
917
  }, this)
763
- });
764
- return this.$form;
918
+ })
919
+ .editableform('render');
765
920
  },
766
921
 
767
- /*
768
- Returns jquery object of container
769
- @method tip()
770
- */
771
- tip: function() {
772
- return this.container().$tip;
773
- },
774
-
775
- container: function() {
776
- return this.$element.data(this.containerName);
777
- },
778
-
779
- call: function() {
780
- this.$element[this.containerName].apply(this.$element, arguments);
781
- },
782
-
783
922
  /**
784
923
  Shows container with form
785
924
  @method show()
786
925
  @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true.
787
- **/
926
+ **/
927
+ /* Note: poshytip owerwrites this method totally! */
788
928
  show: function (closeAll) {
789
929
  this.$element.addClass('editable-open');
790
930
  if(closeAll !== false) {
@@ -792,16 +932,37 @@ Applied as jQuery method.
792
932
  this.closeOthers(this.$element[0]);
793
933
  }
794
934
 
935
+ //show container itself
795
936
  this.innerShow();
796
- },
797
-
798
- /* internal show method. To be overwritten in child classes */
799
- innerShow: function () {
800
- this.call('show');
801
937
  this.tip().addClass('editable-container');
802
- this.initForm();
803
- this.tip().find(this.innerCss).empty().append(this.$form);
804
- this.$form.editableform('render');
938
+
939
+ /*
940
+ Currently, form is re-rendered on every show.
941
+ The main reason is that we dont know, what container will do with content when closed:
942
+ remove(), detach() or just hide().
943
+
944
+ Detaching form itself before hide and re-insert before show is good solution,
945
+ but visually it looks ugly, as container changes size before hide.
946
+ */
947
+
948
+ //if form already exist - delete previous data
949
+ if(this.$form) {
950
+ //todo: destroy prev data!
951
+ //this.$form.destroy();
952
+ }
953
+
954
+ this.$form = $('<div>');
955
+
956
+ //insert form into container body
957
+ if(this.tip().is(this.innerCss)) {
958
+ //for inline container
959
+ this.tip().append(this.$form);
960
+ } else {
961
+ this.tip().find(this.innerCss).append(this.$form);
962
+ }
963
+
964
+ //render form
965
+ this.renderForm();
805
966
  },
806
967
 
807
968
  /**
@@ -813,8 +974,10 @@ Applied as jQuery method.
813
974
  if(!this.tip() || !this.tip().is(':visible') || !this.$element.hasClass('editable-open')) {
814
975
  return;
815
976
  }
977
+
816
978
  this.$element.removeClass('editable-open');
817
979
  this.innerHide();
980
+
818
981
  /**
819
982
  Fired when container was hidden. It occurs on both save or cancel.
820
983
 
@@ -832,9 +995,14 @@ Applied as jQuery method.
832
995
  this.$element.triggerHandler('hidden', reason);
833
996
  },
834
997
 
998
+ /* internal show method. To be overwritten in child classes */
999
+ innerShow: function () {
1000
+
1001
+ },
1002
+
835
1003
  /* internal hide method. To be overwritten in child classes */
836
1004
  innerHide: function () {
837
- this.call('hide');
1005
+
838
1006
  },
839
1007
 
840
1008
  /**
@@ -843,7 +1011,7 @@ Applied as jQuery method.
843
1011
  @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true.
844
1012
  **/
845
1013
  toggle: function(closeAll) {
846
- if(this.tip && this.tip().is(':visible')) {
1014
+ if(this.container() && this.tip() && this.tip().is(':visible')) {
847
1015
  this.hide();
848
1016
  } else {
849
1017
  this.show(closeAll);
@@ -859,7 +1027,6 @@ Applied as jQuery method.
859
1027
  },
860
1028
 
861
1029
  save: function(e, params) {
862
- this.hide('save');
863
1030
  /**
864
1031
  Fired when new value was submitted. You can use <code>$(this).data('editableContainer')</code> inside handler to access to editableContainer instance
865
1032
 
@@ -880,6 +1047,9 @@ Applied as jQuery method.
880
1047
  });
881
1048
  **/
882
1049
  this.$element.triggerHandler('save', params);
1050
+
1051
+ //hide must be after trigger, as saving value may require methods od plugin, applied to input
1052
+ this.hide('save');
883
1053
  },
884
1054
 
885
1055
  /**
@@ -911,9 +1081,17 @@ Applied as jQuery method.
911
1081
  @method destroy()
912
1082
  **/
913
1083
  destroy: function() {
914
- this.call('destroy');
1084
+ this.hide();
1085
+ this.innerDestroy();
1086
+ this.$element.off('destroyed');
1087
+ this.$element.removeData('editableContainer');
915
1088
  },
916
1089
 
1090
+ /* to be overwritten in child classes */
1091
+ innerDestroy: function() {
1092
+
1093
+ },
1094
+
917
1095
  /*
918
1096
  Closes other containers except one related to passed element.
919
1097
  Other containers can be cancelled or submitted (depends on onblur option)
@@ -972,11 +1150,12 @@ Applied as jQuery method.
972
1150
  return this.each(function () {
973
1151
  var $this = $(this),
974
1152
  dataKey = 'editableContainer',
975
- data = $this.data(dataKey),
976
- options = typeof option === 'object' && option;
1153
+ data = $this.data(dataKey),
1154
+ options = typeof option === 'object' && option,
1155
+ Constructor = (options.mode === 'inline') ? Inline : Popup;
977
1156
 
978
1157
  if (!data) {
979
- $this.data(dataKey, (data = new EditableContainer(this, options)));
1158
+ $this.data(dataKey, (data = new Constructor(this, options)));
980
1159
  }
981
1160
 
982
1161
  if (typeof option === 'string') { //call method
@@ -985,8 +1164,9 @@ Applied as jQuery method.
985
1164
  });
986
1165
  };
987
1166
 
988
- //store constructor
989
- $.fn.editableContainer.Constructor = EditableContainer;
1167
+ //store constructors
1168
+ $.fn.editableContainer.Popup = Popup;
1169
+ $.fn.editableContainer.Inline = Inline;
990
1170
 
991
1171
  //defaults
992
1172
  $.fn.editableContainer.defaults = {
@@ -1025,7 +1205,25 @@ Applied as jQuery method.
1025
1205
  @default 'cancel'
1026
1206
  @since 1.1.1
1027
1207
  **/
1028
- onblur: 'cancel'
1208
+ onblur: 'cancel',
1209
+
1210
+ /**
1211
+ Animation speed (inline mode)
1212
+ @property anim
1213
+ @type string
1214
+ @default 'fast'
1215
+ **/
1216
+ anim: 'fast',
1217
+
1218
+ /**
1219
+ Mode of editable, can be `popup` or `inline`
1220
+
1221
+ @property mode
1222
+ @type string
1223
+ @default 'popup'
1224
+ @since 1.4.0
1225
+ **/
1226
+ mode: 'popup'
1029
1227
  };
1030
1228
 
1031
1229
  /*
@@ -1042,6 +1240,58 @@ Applied as jQuery method.
1042
1240
 
1043
1241
  }(window.jQuery));
1044
1242
 
1243
+ /**
1244
+ * Editable Inline
1245
+ * ---------------------
1246
+ */
1247
+ (function ($) {
1248
+
1249
+ //copy prototype from EditableContainer
1250
+ //extend methods
1251
+ $.extend($.fn.editableContainer.Inline.prototype, $.fn.editableContainer.Popup.prototype, {
1252
+ containerName: 'editableform',
1253
+ innerCss: '.editable-inline',
1254
+
1255
+ initContainer: function(){
1256
+ //container is <span> element
1257
+ this.$tip = $('<span></span>').addClass('editable-inline');
1258
+
1259
+ //convert anim to miliseconds (int)
1260
+ if(!this.options.anim) {
1261
+ this.options.anim = 0;
1262
+ }
1263
+ },
1264
+
1265
+ splitOptions: function() {
1266
+ //all options are passed to form
1267
+ this.containerOptions = {};
1268
+ this.formOptions = this.options;
1269
+ },
1270
+
1271
+ tip: function() {
1272
+ return this.$tip;
1273
+ },
1274
+
1275
+ innerShow: function () {
1276
+ this.$element.hide();
1277
+ this.tip().insertAfter(this.$element).show();
1278
+ },
1279
+
1280
+ innerHide: function () {
1281
+ this.$tip.hide(this.options.anim, $.proxy(function() {
1282
+ this.$element.show();
1283
+ this.innerDestroy();
1284
+ }, this));
1285
+ },
1286
+
1287
+ innerDestroy: function() {
1288
+ if(this.tip()) {
1289
+ this.tip().empty().remove();
1290
+ }
1291
+ }
1292
+ });
1293
+
1294
+ }(window.jQuery));
1045
1295
  /**
1046
1296
  Makes editable any HTML element on the page. Applied as jQuery method.
1047
1297
 
@@ -1052,34 +1302,27 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1052
1302
 
1053
1303
  var Editable = function (element, options) {
1054
1304
  this.$element = $(element);
1055
- this.options = $.extend({}, $.fn.editable.defaults, $.fn.editableutils.getConfigData(this.$element), options);
1056
- this.init();
1305
+ //data-* has more priority over js options: because dynamically created elements may change data-*
1306
+ this.options = $.extend({}, $.fn.editable.defaults, options, $.fn.editableutils.getConfigData(this.$element));
1307
+ if(this.options.selector) {
1308
+ this.initLive();
1309
+ } else {
1310
+ this.init();
1311
+ }
1057
1312
  };
1058
1313
 
1059
1314
  Editable.prototype = {
1060
1315
  constructor: Editable,
1061
1316
  init: function () {
1062
- var TypeConstructor,
1063
- isValueByText = false,
1064
- doAutotext,
1065
- finalize;
1066
-
1067
- //editableContainer must be defined
1068
- if(!$.fn.editableContainer) {
1069
- $.error('You must define $.fn.editableContainer via including corresponding file (e.g. editable-popover.js)');
1070
- return;
1071
- }
1072
-
1317
+ var isValueByText = false,
1318
+ doAutotext, finalize;
1319
+
1073
1320
  //name
1074
1321
  this.options.name = this.options.name || this.$element.attr('id');
1075
1322
 
1076
1323
  //create input of specified type. Input will be used for converting value, not in form
1077
- if(typeof $.fn.editabletypes[this.options.type] === 'function') {
1078
- TypeConstructor = $.fn.editabletypes[this.options.type];
1079
- this.typeOptions = $.fn.editableutils.sliceObj(this.options, $.fn.editableutils.objectKeys(TypeConstructor.defaults));
1080
- this.input = new TypeConstructor(this.typeOptions);
1081
- } else {
1082
- $.error('Unknown type: '+ this.options.type);
1324
+ this.input = $.fn.editableutils.createInput(this.options);
1325
+ if(!this.input) {
1083
1326
  return;
1084
1327
  }
1085
1328
 
@@ -1108,8 +1351,10 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1108
1351
  if(this.options.toggle !== 'manual') {
1109
1352
  this.$element.addClass('editable-click');
1110
1353
  this.$element.on(this.options.toggle + '.editable', $.proxy(function(e){
1354
+ //prevent following link
1111
1355
  e.preventDefault();
1112
- //stop propagation not required anymore because in document click handler it checks event target
1356
+
1357
+ //stop propagation not required because in document click handler it checks event target
1113
1358
  //e.stopPropagation();
1114
1359
 
1115
1360
  if(this.options.toggle === 'mouseenter') {
@@ -1140,30 +1385,54 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1140
1385
 
1141
1386
  @event init
1142
1387
  @param {Object} event event object
1143
- @param {Object} editable editable instance
1388
+ @param {Object} editable editable instance (as here it cannot accessed via data('editable'))
1144
1389
  @since 1.2.0
1390
+ @example
1391
+ $('#username').on('init', function(e, editable) {
1392
+ alert('initialized ' + editable.options.name);
1393
+ });
1145
1394
  **/
1146
1395
  this.$element.triggerHandler('init', this);
1147
1396
  }, this));
1148
1397
  },
1149
1398
 
1399
+ /*
1400
+ Initializes parent element for live editables
1401
+ */
1402
+ initLive: function() {
1403
+ //store selector
1404
+ var selector = this.options.selector;
1405
+ //modify options for child elements
1406
+ this.options.selector = false;
1407
+ this.options.autotext = 'never';
1408
+ //listen toggle events
1409
+ this.$element.on(this.options.toggle + '.editable', selector, $.proxy(function(e){
1410
+ var $target = $(e.target);
1411
+ if(!$target.data('editable')) {
1412
+ $target.editable(this.options).trigger(e);
1413
+ }
1414
+ }, this));
1415
+ },
1416
+
1150
1417
  /*
1151
1418
  Renders value into element's text.
1152
1419
  Can call custom display method from options.
1153
1420
  Can return deferred object.
1154
1421
  @method render()
1422
+ @param {mixed} response server response (if exist) to pass into display function
1155
1423
  */
1156
- render: function() {
1424
+ render: function(response) {
1157
1425
  //do not display anything
1158
1426
  if(this.options.display === false) {
1159
1427
  return;
1160
1428
  }
1161
- //if it is input with source, we pass callback in third param to be called when source is loaded
1162
- if(this.input.options.hasOwnProperty('source')) {
1163
- return this.input.value2html(this.value, this.$element[0], this.options.display);
1429
+
1430
+ //if input has `value2htmlFinal` method, we pass callback in third param to be called when source is loaded
1431
+ if(this.input.value2htmlFinal) {
1432
+ return this.input.value2html(this.value, this.$element[0], this.options.display, response);
1164
1433
  //if display method defined --> use it
1165
1434
  } else if(typeof this.options.display === 'function') {
1166
- return this.options.display.call(this.$element[0], this.value);
1435
+ return this.options.display.call(this.$element[0], this.value, response);
1167
1436
  //else use input's original value2html() method
1168
1437
  } else {
1169
1438
  return this.input.value2html(this.value, this.$element[0]);
@@ -1177,7 +1446,7 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1177
1446
  enable: function() {
1178
1447
  this.options.disabled = false;
1179
1448
  this.$element.removeClass('editable-disabled');
1180
- this.handleEmpty();
1449
+ this.handleEmpty(this.isEmpty);
1181
1450
  if(this.options.toggle !== 'manual') {
1182
1451
  if(this.$element.attr('tabindex') === '-1') {
1183
1452
  this.$element.removeAttr('tabindex');
@@ -1193,7 +1462,7 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1193
1462
  this.options.disabled = true;
1194
1463
  this.hide();
1195
1464
  this.$element.addClass('editable-disabled');
1196
- this.handleEmpty();
1465
+ this.handleEmpty(this.isEmpty);
1197
1466
  //do not stop focus on this element
1198
1467
  this.$element.attr('tabindex', -1);
1199
1468
  },
@@ -1233,12 +1502,7 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1233
1502
 
1234
1503
  //disabled
1235
1504
  if(key === 'disabled') {
1236
- if(value) {
1237
- this.disable();
1238
- } else {
1239
- this.enable();
1240
- }
1241
- return;
1505
+ return value ? this.disable() : this.enable();
1242
1506
  }
1243
1507
 
1244
1508
  //value
@@ -1250,30 +1514,42 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1250
1514
  if(this.container) {
1251
1515
  this.container.option(key, value);
1252
1516
  }
1517
+
1518
+ //pass option to input directly (as it points to the same in form)
1519
+ if(this.input.option) {
1520
+ this.input.option(key, value);
1521
+ }
1522
+
1253
1523
  },
1254
1524
 
1255
1525
  /*
1256
- * set emptytext if element is empty (reverse: remove emptytext if needed)
1526
+ * set emptytext if element is empty
1257
1527
  */
1258
- handleEmpty: function () {
1528
+ handleEmpty: function (isEmpty) {
1259
1529
  //do not handle empty if we do not display anything
1260
1530
  if(this.options.display === false) {
1261
1531
  return;
1262
1532
  }
1263
1533
 
1264
- var emptyClass = 'editable-empty';
1534
+ this.isEmpty = isEmpty !== undefined ? isEmpty : $.trim(this.$element.text()) === '';
1535
+
1265
1536
  //emptytext shown only for enabled
1266
1537
  if(!this.options.disabled) {
1267
- if ($.trim(this.$element.text()) === '') {
1268
- this.$element.addClass(emptyClass).text(this.options.emptytext);
1269
- } else {
1270
- this.$element.removeClass(emptyClass);
1538
+ if (this.isEmpty) {
1539
+ this.$element.text(this.options.emptytext);
1540
+ if(this.options.emptyclass) {
1541
+ this.$element.addClass(this.options.emptyclass);
1542
+ }
1543
+ } else if(this.options.emptyclass) {
1544
+ this.$element.removeClass(this.options.emptyclass);
1271
1545
  }
1272
1546
  } else {
1273
1547
  //below required if element disable property was changed
1274
- if(this.$element.hasClass(emptyClass)) {
1548
+ if(this.isEmpty) {
1275
1549
  this.$element.empty();
1276
- this.$element.removeClass(emptyClass);
1550
+ if(this.options.emptyclass) {
1551
+ this.$element.removeClass(this.options.emptyclass);
1552
+ }
1277
1553
  }
1278
1554
  }
1279
1555
  },
@@ -1291,9 +1567,11 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1291
1567
  //init editableContainer: popover, tooltip, inline, etc..
1292
1568
  if(!this.container) {
1293
1569
  var containerOptions = $.extend({}, this.options, {
1294
- value: this.value
1570
+ value: this.value,
1571
+ input: this.input //pass input to form (as it is already created)
1295
1572
  });
1296
1573
  this.$element.editableContainer(containerOptions);
1574
+ //listen `save` event
1297
1575
  this.$element.on("save.internal", $.proxy(this.save, this));
1298
1576
  this.container = this.$element.data('editableContainer');
1299
1577
  } else if(this.container.tip().is(':visible')) {
@@ -1331,15 +1609,30 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1331
1609
  * called when form was submitted
1332
1610
  */
1333
1611
  save: function(e, params) {
1334
- //if url is not user's function and value was not sent to server and value changed --> mark element with unsaved css.
1335
- if(typeof this.options.url !== 'function' && this.options.display !== false && params.response === undefined && this.input.value2str(this.value) !== this.input.value2str(params.newValue)) {
1336
- this.$element.addClass('editable-unsaved');
1337
- } else {
1338
- this.$element.removeClass('editable-unsaved');
1612
+ //mark element with unsaved class if needed
1613
+ if(this.options.unsavedclass) {
1614
+ /*
1615
+ Add unsaved css to element if:
1616
+ - url is not user's function
1617
+ - value was not sent to server
1618
+ - params.response === undefined, that means data was not sent
1619
+ - value changed
1620
+ */
1621
+ var sent = false;
1622
+ sent = sent || typeof this.options.url === 'function';
1623
+ sent = sent || this.options.display === false;
1624
+ sent = sent || params.response !== undefined;
1625
+ sent = sent || (this.options.savenochange && this.input.value2str(this.value) !== this.input.value2str(params.newValue));
1626
+
1627
+ if(sent) {
1628
+ this.$element.removeClass(this.options.unsavedclass);
1629
+ } else {
1630
+ this.$element.addClass(this.options.unsavedclass);
1631
+ }
1339
1632
  }
1340
1633
 
1341
- // this.hide();
1342
- this.setValue(params.newValue);
1634
+ //set new value
1635
+ this.setValue(params.newValue, false, params.response);
1343
1636
 
1344
1637
  /**
1345
1638
  Fired when new value was submitted. You can use <code>$(this).data('editable')</code> to access to editable instance
@@ -1351,13 +1644,7 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1351
1644
  @param {Object} params.response ajax response
1352
1645
  @example
1353
1646
  $('#username').on('save', function(e, params) {
1354
- //assuming server response: '{success: true}'
1355
- var pk = $(this).data('editable').options.pk;
1356
- if(params.response && params.response.success) {
1357
- alert('value: ' + params.newValue + ' with pk: ' + pk + ' saved!');
1358
- } else {
1359
- alert('error!');
1360
- }
1647
+ alert('Saved value: ' + params.newValue);
1361
1648
  });
1362
1649
  **/
1363
1650
  //event itself is triggered by editableContainer. Description here is only for documentation
@@ -1375,7 +1662,7 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1375
1662
  @param {mixed} value new value
1376
1663
  @param {boolean} convertStr whether to convert value from string to internal format
1377
1664
  **/
1378
- setValue: function(value, convertStr) {
1665
+ setValue: function(value, convertStr, response) {
1379
1666
  if(convertStr) {
1380
1667
  this.value = this.input.str2value(value);
1381
1668
  } else {
@@ -1384,7 +1671,7 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1384
1671
  if(this.container) {
1385
1672
  this.container.option('value', this.value);
1386
1673
  }
1387
- $.when(this.render())
1674
+ $.when(this.render(response))
1388
1675
  .then($.proxy(function() {
1389
1676
  this.handleEmpty();
1390
1677
  }, this));
@@ -1398,7 +1685,28 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1398
1685
  if(this.container) {
1399
1686
  this.container.activate();
1400
1687
  }
1401
- }
1688
+ },
1689
+
1690
+ /**
1691
+ Removes editable feature from element
1692
+ @method destroy()
1693
+ **/
1694
+ destroy: function() {
1695
+ if(this.container) {
1696
+ this.container.destroy();
1697
+ }
1698
+
1699
+ if(this.options.toggle !== 'manual') {
1700
+ this.$element.removeClass('editable-click');
1701
+ this.$element.off(this.options.toggle + '.editable');
1702
+ }
1703
+
1704
+ this.$element.off("save.internal");
1705
+
1706
+ this.$element.removeClass('editable');
1707
+ this.$element.removeClass('editable-open');
1708
+ this.$element.removeData('editable');
1709
+ }
1402
1710
  };
1403
1711
 
1404
1712
  /* EDITABLE PLUGIN DEFINITION
@@ -1415,7 +1723,7 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1415
1723
  url: '/post',
1416
1724
  pk: 1
1417
1725
  });
1418
- **/
1726
+ **/
1419
1727
  $.fn.editable = function (option) {
1420
1728
  //special API methods returning non-jquery object
1421
1729
  var result = {}, args = arguments, datakey = 'editable';
@@ -1432,7 +1740,7 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1432
1740
  username: "username is required",
1433
1741
  fullname: "fullname should be minimum 3 letters length"
1434
1742
  }
1435
- **/
1743
+ **/
1436
1744
  case 'validate':
1437
1745
  this.each(function () {
1438
1746
  var $this = $(this), data = $this.data(datakey), error;
@@ -1453,7 +1761,7 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1453
1761
  username: "superuser",
1454
1762
  fullname: "John"
1455
1763
  }
1456
- **/
1764
+ **/
1457
1765
  case 'getValue':
1458
1766
  this.each(function () {
1459
1767
  var $this = $(this), data = $this.data(datakey);
@@ -1472,11 +1780,11 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1472
1780
  @param {object} options
1473
1781
  @param {object} options.url url to submit data
1474
1782
  @param {object} options.data additional data to submit
1475
- @param {object} options.ajaxOptions additional ajax options
1783
+ @param {object} options.ajaxOptions additional ajax options
1476
1784
  @param {function} options.error(obj) error handler
1477
1785
  @param {function} options.success(obj,config) success handler
1478
1786
  @returns {Object} jQuery object
1479
- **/
1787
+ **/
1480
1788
  case 'submit': //collects value, validate and submit to server for creating new record
1481
1789
  var config = arguments[1] || {},
1482
1790
  $elems = this,
@@ -1584,7 +1892,7 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1584
1892
  **/
1585
1893
  autotext: 'auto',
1586
1894
  /**
1587
- Initial value of input. Taken from <code>data-value</code> or element's text.
1895
+ Initial value of input. If not set, taken from element's text.
1588
1896
 
1589
1897
  @property value
1590
1898
  @type mixed
@@ -1593,10 +1901,21 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1593
1901
  value: null,
1594
1902
  /**
1595
1903
  Callback to perform custom displaying of value in element's text.
1596
- If <code>null</code>, default input's value2html() will be called.
1597
- If <code>false</code>, no displaying methods will be called, element's text will no change.
1904
+ If `null`, default input's display used.
1905
+ If `false`, no displaying methods will be called, element's text will never change.
1598
1906
  Runs under element's scope.
1599
- Second parameter __sourceData__ is passed for inputs with source (select, checklist).
1907
+ _Parameters:_
1908
+
1909
+ * `value` current value to be displayed
1910
+ * `response` server response (if display called after ajax submit), since 1.4.0
1911
+
1912
+ For **inputs with source** (select, checklist) parameters are different:
1913
+
1914
+ * `value` current value to be displayed
1915
+ * `sourceData` array of items for current input (e.g. dropdown items)
1916
+ * `response` server response (if display called after ajax submit), since 1.4.0
1917
+
1918
+ To get currently selected items use `$.fn.editableutils.itemsByValue(value, sourceData)`.
1600
1919
 
1601
1920
  @property display
1602
1921
  @type function|boolean
@@ -1604,11 +1923,63 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1604
1923
  @since 1.2.0
1605
1924
  @example
1606
1925
  display: function(value, sourceData) {
1607
- var escapedValue = $('<div>').text(value).html();
1608
- $(this).html('<b>'+escapedValue+'</b>');
1926
+ //display checklist as comma-separated values
1927
+ var html = [],
1928
+ checked = $.fn.editableutils.itemsByValue(value, sourceData);
1929
+
1930
+ if(checked.length) {
1931
+ $.each(checked, function(i, v) { html.push($.fn.editableutils.escape(v.text)); });
1932
+ $(this).html(html.join(', '));
1933
+ } else {
1934
+ $(this).empty();
1935
+ }
1609
1936
  }
1610
1937
  **/
1611
- display: null
1938
+ display: null,
1939
+ /**
1940
+ Css class applied when editable text is empty.
1941
+
1942
+ @property emptyclass
1943
+ @type string
1944
+ @since 1.4.1
1945
+ @default editable-empty
1946
+ **/
1947
+ emptyclass: 'editable-empty',
1948
+ /**
1949
+ Css class applied when value was stored but not sent to server (`pk` is empty or `send = 'never'`).
1950
+ You may set it to `null` if you work with editables locally and submit them together.
1951
+
1952
+ @property unsavedclass
1953
+ @type string
1954
+ @since 1.4.1
1955
+ @default editable-unsaved
1956
+ **/
1957
+ unsavedclass: 'editable-unsaved',
1958
+ /**
1959
+ If a css selector is provided, editable will be delegated to the specified targets.
1960
+ Usefull for dynamically generated DOM elements.
1961
+ **Please note**, that delegated targets can't use `emptytext` and `autotext` options,
1962
+ as they are initialized after first click.
1963
+
1964
+ @property selector
1965
+ @type string
1966
+ @since 1.4.1
1967
+ @default null
1968
+ @example
1969
+ <div id="user">
1970
+ <a href="#" data-name="username" data-type="text" title="Username">awesome</a>
1971
+ <a href="#" data-name="group" data-type="select" data-source="/groups" data-value="1" title="Group">Operator</a>
1972
+ </div>
1973
+
1974
+ <script>
1975
+ $('#user').editable({
1976
+ selector: 'a',
1977
+ url: '/post',
1978
+ pk: 1
1979
+ });
1980
+ </script>
1981
+ **/
1982
+ selector: null
1612
1983
  };
1613
1984
 
1614
1985
  }(window.jQuery));
@@ -1635,26 +2006,27 @@ To create your own input you can inherit from this class.
1635
2006
  **/
1636
2007
  init: function(type, options, defaults) {
1637
2008
  this.type = type;
1638
- this.options = $.extend({}, defaults, options);
1639
- this.$input = null;
1640
- this.$clear = null;
1641
- this.error = null;
2009
+ this.options = $.extend({}, defaults, options);
2010
+ },
2011
+
2012
+ /*
2013
+ this method called before render to init $tpl that is inserted in DOM
2014
+ */
2015
+ prerender: function() {
2016
+ this.$tpl = $(this.options.tpl); //whole tpl as jquery object
2017
+ this.$input = this.$tpl; //control itself, can be changed in render method
2018
+ this.$clear = null; //clear button
2019
+ this.error = null; //error message, if input cannot be rendered
1642
2020
  },
1643
2021
 
1644
2022
  /**
1645
2023
  Renders input from tpl. Can return jQuery deferred object.
2024
+ Can be overwritten in child objects
1646
2025
 
1647
2026
  @method render()
1648
2027
  **/
1649
2028
  render: function() {
1650
- this.$input = $(this.options.tpl);
1651
- if(this.options.inputclass) {
1652
- this.$input.addClass(this.options.inputclass);
1653
- }
1654
-
1655
- if (this.options.placeholder) {
1656
- this.$input.attr('placeholder', this.options.placeholder);
1657
- }
2029
+
1658
2030
  },
1659
2031
 
1660
2032
  /**
@@ -1691,7 +2063,7 @@ To create your own input you can inherit from this class.
1691
2063
  },
1692
2064
 
1693
2065
  /**
1694
- Converts string received from server into value.
2066
+ Converts string received from server into value. Usually from `data-value` attribute.
1695
2067
 
1696
2068
  @method str2value(str)
1697
2069
  @param {string} str
@@ -1702,7 +2074,7 @@ To create your own input you can inherit from this class.
1702
2074
  },
1703
2075
 
1704
2076
  /**
1705
- Converts value for submitting to server
2077
+ Converts value for submitting to server. Result can be string or object.
1706
2078
 
1707
2079
  @method value2submit(value)
1708
2080
  @param {mixed} value
@@ -1763,7 +2135,25 @@ To create your own input you can inherit from this class.
1763
2135
  **/
1764
2136
  autosubmit: function() {
1765
2137
 
2138
+ },
2139
+
2140
+ // -------- helper functions --------
2141
+ setClass: function() {
2142
+ if(this.options.inputclass) {
2143
+ this.$input.addClass(this.options.inputclass);
2144
+ }
2145
+ },
2146
+
2147
+ setAttr: function(attr) {
2148
+ if (this.options[attr]) {
2149
+ this.$input.attr(attr, this.options[attr]);
2150
+ }
2151
+ },
2152
+
2153
+ option: function(key, value) {
2154
+ this.options[key] = value;
1766
2155
  }
2156
+
1767
2157
  };
1768
2158
 
1769
2159
  AbstractInput.defaults = {
@@ -1782,15 +2172,7 @@ To create your own input you can inherit from this class.
1782
2172
  @type string
1783
2173
  @default input-medium
1784
2174
  **/
1785
- inputclass: 'input-medium',
1786
- /**
1787
- Name attribute of input
1788
-
1789
- @property name
1790
- @type string
1791
- @default null
1792
- **/
1793
- name: null
2175
+ inputclass: 'input-medium'
1794
2176
  };
1795
2177
 
1796
2178
  $.extend($.fn.editabletypes, {abstractinput: AbstractInput});
@@ -1813,11 +2195,9 @@ List - abstract class for inputs that have source option loaded from js array or
1813
2195
 
1814
2196
  $.extend(List.prototype, {
1815
2197
  render: function () {
1816
- List.superclass.render.call(this);
1817
2198
  var deferred = $.Deferred();
2199
+
1818
2200
  this.error = null;
1819
- this.sourceData = null;
1820
- this.prependData = null;
1821
2201
  this.onSourceReady(function () {
1822
2202
  this.renderList();
1823
2203
  deferred.resolve();
@@ -1833,20 +2213,24 @@ List - abstract class for inputs that have source option loaded from js array or
1833
2213
  return null; //can't set value by text
1834
2214
  },
1835
2215
 
1836
- value2html: function (value, element, display) {
1837
- var deferred = $.Deferred();
1838
- this.onSourceReady(function () {
1839
- if(typeof display === 'function') {
1840
- //custom display method
1841
- display.call(element, value, this.sourceData);
1842
- } else {
1843
- this.value2htmlFinal(value, element);
1844
- }
1845
- deferred.resolve();
1846
- }, function () {
1847
- //do nothing with element
1848
- deferred.resolve();
1849
- });
2216
+ value2html: function (value, element, display, response) {
2217
+ var deferred = $.Deferred(),
2218
+ success = function () {
2219
+ if(typeof display === 'function') {
2220
+ //custom display method
2221
+ display.call(element, value, this.sourceData, response);
2222
+ } else {
2223
+ this.value2htmlFinal(value, element);
2224
+ }
2225
+ deferred.resolve();
2226
+ };
2227
+
2228
+ //for null value just call success without loading source
2229
+ if(value === null) {
2230
+ success.call(this);
2231
+ } else {
2232
+ this.onSourceReady(success, function () { deferred.resolve(); });
2233
+ }
1850
2234
 
1851
2235
  return deferred.promise();
1852
2236
  },
@@ -1872,7 +2256,7 @@ List - abstract class for inputs that have source option loaded from js array or
1872
2256
  if (typeof this.options.source === 'string') {
1873
2257
  //try to get from cache
1874
2258
  if(this.options.sourceCache) {
1875
- var cacheID = this.options.source + (this.options.name ? '-' + this.options.name : ''),
2259
+ var cacheID = this.options.source,
1876
2260
  cache;
1877
2261
 
1878
2262
  if (!$(document).data(cacheID)) {
@@ -1883,11 +2267,13 @@ List - abstract class for inputs that have source option loaded from js array or
1883
2267
  //check for cached data
1884
2268
  if (cache.loading === false && cache.sourceData) { //take source from cache
1885
2269
  this.sourceData = cache.sourceData;
2270
+ this.doPrepend();
1886
2271
  success.call(this);
1887
2272
  return;
1888
2273
  } else if (cache.loading === true) { //cache is loading, put callback in stack to be called later
1889
2274
  cache.callbacks.push($.proxy(function () {
1890
2275
  this.sourceData = cache.sourceData;
2276
+ this.doPrepend();
1891
2277
  success.call(this);
1892
2278
  }, this));
1893
2279
 
@@ -1906,7 +2292,6 @@ List - abstract class for inputs that have source option loaded from js array or
1906
2292
  url: this.options.source,
1907
2293
  type: 'get',
1908
2294
  cache: false,
1909
- data: this.options.name ? {name: this.options.name} : {},
1910
2295
  dataType: 'json',
1911
2296
  success: $.proxy(function (data) {
1912
2297
  if(cache) {
@@ -1914,17 +2299,19 @@ List - abstract class for inputs that have source option loaded from js array or
1914
2299
  }
1915
2300
  this.sourceData = this.makeArray(data);
1916
2301
  if($.isArray(this.sourceData)) {
1917
- this.doPrepend();
1918
- success.call(this);
1919
2302
  if(cache) {
1920
2303
  //store result in cache
1921
2304
  cache.sourceData = this.sourceData;
1922
- $.each(cache.callbacks, function () { this.call(); }); //run success callbacks for other fields
2305
+ //run success callbacks for other fields waiting for this source
2306
+ $.each(cache.callbacks, function () { this.call(); });
1923
2307
  }
2308
+ this.doPrepend();
2309
+ success.call(this);
1924
2310
  } else {
1925
2311
  error.call(this);
1926
2312
  if(cache) {
1927
- $.each(cache.err_callbacks, function () { this.call(); }); //run error callbacks for other fields
2313
+ //run error callbacks for other fields waiting for this source
2314
+ $.each(cache.err_callbacks, function () { this.call(); });
1928
2315
  }
1929
2316
  }
1930
2317
  }, this),
@@ -1937,8 +2324,13 @@ List - abstract class for inputs that have source option loaded from js array or
1937
2324
  }
1938
2325
  }, this)
1939
2326
  });
1940
- } else { //options as json/array
1941
- this.sourceData = this.makeArray(this.options.source);
2327
+ } else { //options as json/array/function
2328
+ if ($.isFunction(this.options.source)) {
2329
+ this.sourceData = this.makeArray(this.options.source());
2330
+ } else {
2331
+ this.sourceData = this.makeArray(this.options.source);
2332
+ }
2333
+
1942
2334
  if($.isArray(this.sourceData)) {
1943
2335
  this.doPrepend();
1944
2336
  success.call(this);
@@ -1959,7 +2351,11 @@ List - abstract class for inputs that have source option loaded from js array or
1959
2351
  if (typeof this.options.prepend === 'string') {
1960
2352
  this.options.prepend = {'': this.options.prepend};
1961
2353
  }
1962
- this.prependData = this.makeArray(this.options.prepend);
2354
+ if (typeof this.options.prepend === 'function') {
2355
+ this.prependData = this.makeArray(this.options.prepend());
2356
+ } else {
2357
+ this.prependData = this.makeArray(this.options.prepend);
2358
+ }
1963
2359
  }
1964
2360
 
1965
2361
  if($.isArray(this.prependData) && $.isArray(this.sourceData)) {
@@ -1985,35 +2381,45 @@ List - abstract class for inputs that have source option loaded from js array or
1985
2381
  * convert data to array suitable for sourceData, e.g. [{value: 1, text: 'abc'}, {...}]
1986
2382
  */
1987
2383
  makeArray: function(data) {
1988
- var count, obj, result = [], iterateEl;
2384
+ var count, obj, result = [], item, iterateItem;
1989
2385
  if(!data || typeof data === 'string') {
1990
2386
  return null;
1991
2387
  }
1992
2388
 
1993
2389
  if($.isArray(data)) { //array
1994
- iterateEl = function (k, v) {
2390
+ /*
2391
+ function to iterate inside item of array if item is object.
2392
+ Caclulates count of keys in item and store in obj.
2393
+ */
2394
+ iterateItem = function (k, v) {
1995
2395
  obj = {value: k, text: v};
1996
2396
  if(count++ >= 2) {
1997
- return false;// exit each if object has more than one value
2397
+ return false;// exit from `each` if item has more than one key.
1998
2398
  }
1999
2399
  };
2000
2400
 
2001
2401
  for(var i = 0; i < data.length; i++) {
2002
- if(typeof data[i] === 'object') {
2003
- count = 0;
2004
- $.each(data[i], iterateEl);
2005
- if(count === 1) {
2402
+ item = data[i];
2403
+ if(typeof item === 'object') {
2404
+ count = 0; //count of keys inside item
2405
+ $.each(item, iterateItem);
2406
+ //case: [{val1: 'text1'}, {val2: 'text2} ...]
2407
+ if(count === 1) {
2006
2408
  result.push(obj);
2007
- } else if(count > 1 && data[i].hasOwnProperty('value') && data[i].hasOwnProperty('text')) {
2008
- result.push(data[i]);
2009
- } else {
2010
- //data contains incorrect objects
2409
+ //case: [{value: 1, text: 'text1'}, {value: 2, text: 'text2'}, ...]
2410
+ } else if(count > 1) {
2411
+ //removed check of existance: item.hasOwnProperty('value') && item.hasOwnProperty('text')
2412
+ if(item.children) {
2413
+ item.children = this.makeArray(item.children);
2414
+ }
2415
+ result.push(item);
2011
2416
  }
2012
2417
  } else {
2013
- result.push({value: data[i], text: data[i]});
2418
+ //case: ['text1', 'text2' ...]
2419
+ result.push({value: item, text: item});
2014
2420
  }
2015
2421
  }
2016
- } else { //object
2422
+ } else { //case: {val1: 'text1', val2: 'text2, ...}
2017
2423
  $.each(data, function (k, v) {
2018
2424
  result.push({value: k, text: v});
2019
2425
  });
@@ -2021,41 +2427,45 @@ List - abstract class for inputs that have source option loaded from js array or
2021
2427
  return result;
2022
2428
  },
2023
2429
 
2024
- //search for item by particular value
2025
- itemByVal: function(val) {
2026
- if($.isArray(this.sourceData)) {
2027
- for(var i=0; i<this.sourceData.length; i++){
2028
- /*jshint eqeqeq: false*/
2029
- if(this.sourceData[i].value == val) {
2030
- /*jshint eqeqeq: true*/
2031
- return this.sourceData[i];
2032
- }
2033
- }
2430
+ option: function(key, value) {
2431
+ this.options[key] = value;
2432
+ if(key === 'source') {
2433
+ this.sourceData = null;
2034
2434
  }
2435
+ if(key === 'prepend') {
2436
+ this.prependData = null;
2437
+ }
2035
2438
  }
2036
2439
 
2037
2440
  });
2038
2441
 
2039
2442
  List.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
2040
2443
  /**
2041
- Source data for list. If string - considered ajax url to load items. Otherwise should be an array.
2042
- Array format is: <code>[{value: 1, text: "text"}, {...}]</code><br>
2043
- For compability it also supports format <code>{value1: "text1", value2: "text2" ...}</code> but it does not guarantee elements order.
2044
- If source is **string**, results will be cached for fields with the same source and name. See also <code>sourceCache</code> option.
2444
+ Source data for list.
2445
+ If **array** - it should be in format: `[{value: 1, text: "text1"}, {value: 2, text: "text2"}, ...]`
2446
+ For compability, object format is also supported: `{"1": "text1", "2": "text2" ...}` but it does not guarantee elements order.
2447
+
2448
+ 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.
2449
+
2450
+ If **function**, it should return data in format above (since 1.4.0).
2045
2451
 
2452
+ Since 1.4.1 key `children` supported to render OPTGROUP (for **select** input only).
2453
+ `[{text: "group1", children: [{value: 1, text: "text1"}, {value: 2, text: "text2"}]}, ...]`
2454
+
2455
+
2046
2456
  @property source
2047
- @type string|array|object
2457
+ @type string | array | object | function
2048
2458
  @default null
2049
2459
  **/
2050
- source:null,
2460
+ source: null,
2051
2461
  /**
2052
2462
  Data automatically prepended to the beginning of dropdown list.
2053
2463
 
2054
2464
  @property prepend
2055
- @type string|array|object
2465
+ @type string | array | object | function
2056
2466
  @default false
2057
2467
  **/
2058
- prepend:false,
2468
+ prepend: false,
2059
2469
  /**
2060
2470
  Error message when list cannot be loaded (e.g. ajax error)
2061
2471
 
@@ -2065,8 +2475,8 @@ List - abstract class for inputs that have source option loaded from js array or
2065
2475
  **/
2066
2476
  sourceError: 'Error when loading list',
2067
2477
  /**
2068
- if <code>true</code> and source is **string url** - results will be cached for fields with the same source and name.
2069
- Usefull for editable grids.
2478
+ if <code>true</code> and source is **string url** - results will be cached for fields with the same source.
2479
+ Usefull for editable column in grid to prevent extra requests.
2070
2480
 
2071
2481
  @property sourceCache
2072
2482
  @type boolean
@@ -2078,7 +2488,8 @@ List - abstract class for inputs that have source option loaded from js array or
2078
2488
 
2079
2489
  $.fn.editabletypes.list = List;
2080
2490
 
2081
- }(window.jQuery));
2491
+ }(window.jQuery));
2492
+
2082
2493
  /**
2083
2494
  Text input
2084
2495
 
@@ -2104,12 +2515,67 @@ $(function(){
2104
2515
  $.fn.editableutils.inherit(Text, $.fn.editabletypes.abstractinput);
2105
2516
 
2106
2517
  $.extend(Text.prototype, {
2518
+ render: function() {
2519
+ this.renderClear();
2520
+ this.setClass();
2521
+ this.setAttr('placeholder');
2522
+ },
2523
+
2107
2524
  activate: function() {
2108
2525
  if(this.$input.is(':visible')) {
2109
2526
  this.$input.focus();
2110
2527
  $.fn.editableutils.setCursorPosition(this.$input.get(0), this.$input.val().length);
2528
+ if(this.toggleClear) {
2529
+ this.toggleClear();
2530
+ }
2111
2531
  }
2112
- }
2532
+ },
2533
+
2534
+ //render clear button
2535
+ renderClear: function() {
2536
+ if (this.options.clear) {
2537
+ this.$clear = $('<span class="editable-clear-x"></span>');
2538
+ this.$input.after(this.$clear)
2539
+ .css('padding-right', 20)
2540
+ .keyup($.proxy(this.toggleClear, this))
2541
+ .parent().css('position', 'relative');
2542
+
2543
+ this.$clear.click($.proxy(this.clear, this));
2544
+ }
2545
+ },
2546
+
2547
+ postrender: function() {
2548
+ if(this.$clear) {
2549
+ //can position clear button only here, when form is shown and height can be calculated
2550
+ var h = this.$input.outerHeight() || 20,
2551
+ delta = (h - this.$clear.height()) / 2;
2552
+
2553
+ //workaround for plain-popup
2554
+ if(delta < 3) {
2555
+ delta = 3;
2556
+ }
2557
+
2558
+ this.$clear.css({top: delta, right: delta});
2559
+ }
2560
+ },
2561
+
2562
+ //show / hide clear button
2563
+ toggleClear: function() {
2564
+ if(!this.$clear) {
2565
+ return;
2566
+ }
2567
+
2568
+ if(this.$input.val().length) {
2569
+ this.$clear.show();
2570
+ } else {
2571
+ this.$clear.hide();
2572
+ }
2573
+ },
2574
+
2575
+ clear: function() {
2576
+ this.$clear.hide();
2577
+ this.$input.val('').focus();
2578
+ }
2113
2579
  });
2114
2580
 
2115
2581
  Text.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
@@ -2125,7 +2591,16 @@ $(function(){
2125
2591
  @type string
2126
2592
  @default null
2127
2593
  **/
2128
- placeholder: null
2594
+ placeholder: null,
2595
+
2596
+ /**
2597
+ Whether to show `clear` button
2598
+
2599
+ @property clear
2600
+ @type boolean
2601
+ @default true
2602
+ **/
2603
+ clear: true
2129
2604
  });
2130
2605
 
2131
2606
  $.fn.editabletypes.text = Text;
@@ -2144,7 +2619,8 @@ Textarea input
2144
2619
  $(function(){
2145
2620
  $('#comments').editable({
2146
2621
  url: '/post',
2147
- title: 'Enter comments'
2622
+ title: 'Enter comments',
2623
+ rows: 10
2148
2624
  });
2149
2625
  });
2150
2626
  </script>
@@ -2159,8 +2635,10 @@ $(function(){
2159
2635
 
2160
2636
  $.extend(Textarea.prototype, {
2161
2637
  render: function () {
2162
- Textarea.superclass.render.call(this);
2163
-
2638
+ this.setClass();
2639
+ this.setAttr('placeholder');
2640
+ this.setAttr('rows');
2641
+
2164
2642
  //ctrl + enter
2165
2643
  this.$input.keydown(function (e) {
2166
2644
  if (e.ctrlKey && e.which === 13) {
@@ -2185,43 +2663,56 @@ $(function(){
2185
2663
  if(!html) {
2186
2664
  return '';
2187
2665
  }
2666
+
2667
+ var regex = new RegExp(String.fromCharCode(10), 'g');
2188
2668
  var lines = html.split(/<br\s*\/?>/i);
2189
2669
  for (var i = 0; i < lines.length; i++) {
2190
- lines[i] = $('<div>').html(lines[i]).text();
2670
+ var text = $('<div>').html(lines[i]).text();
2671
+
2672
+ // Remove newline characters (\n) to avoid them being converted by value2html() method
2673
+ // thus adding extra <br> tags
2674
+ text = text.replace(regex, '');
2675
+
2676
+ lines[i] = text;
2191
2677
  }
2192
- return lines.join("\n");
2193
- },
2678
+ return lines.join("\n");
2679
+ },
2194
2680
 
2195
2681
  activate: function() {
2196
- if(this.$input.is(':visible')) {
2197
- $.fn.editableutils.setCursorPosition(this.$input.get(0), this.$input.val().length);
2198
- this.$input.focus();
2199
- }
2200
- }
2682
+ $.fn.editabletypes.text.prototype.activate.call(this);
2683
+ }
2201
2684
  });
2202
2685
 
2203
2686
  Textarea.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
2204
2687
  /**
2205
- @property tpl
2688
+ @property tpl
2206
2689
  @default <textarea></textarea>
2207
- **/
2690
+ **/
2208
2691
  tpl:'<textarea></textarea>',
2209
2692
  /**
2210
- @property inputclass
2693
+ @property inputclass
2211
2694
  @default input-large
2212
- **/
2695
+ **/
2213
2696
  inputclass: 'input-large',
2214
2697
  /**
2215
2698
  Placeholder attribute of input. Shown when input is empty.
2216
2699
 
2217
- @property placeholder
2700
+ @property placeholder
2218
2701
  @type string
2219
2702
  @default null
2220
- **/
2221
- placeholder: null
2703
+ **/
2704
+ placeholder: null,
2705
+ /**
2706
+ Number of rows in textarea
2707
+
2708
+ @property rows
2709
+ @type integer
2710
+ @default 7
2711
+ **/
2712
+ rows: 7
2222
2713
  });
2223
2714
 
2224
- $.fn.editabletypes.textarea = Textarea;
2715
+ $.fn.editabletypes.textarea = Textarea;
2225
2716
 
2226
2717
  }(window.jQuery));
2227
2718
 
@@ -2257,13 +2748,24 @@ $(function(){
2257
2748
 
2258
2749
  $.extend(Select.prototype, {
2259
2750
  renderList: function() {
2260
- if(!$.isArray(this.sourceData)) {
2261
- return;
2262
- }
2751
+ this.$input.empty();
2263
2752
 
2264
- for(var i=0; i<this.sourceData.length; i++) {
2265
- this.$input.append($('<option>', {value: this.sourceData[i].value}).text(this.sourceData[i].text));
2266
- }
2753
+ var fillItems = function($el, data) {
2754
+ if($.isArray(data)) {
2755
+ for(var i=0; i<data.length; i++) {
2756
+ if(data[i].children) {
2757
+ $el.append(fillItems($('<optgroup>', {label: data[i].text}), data[i].children));
2758
+ } else {
2759
+ $el.append($('<option>', {value: data[i].value}).text(data[i].text));
2760
+ }
2761
+ }
2762
+ }
2763
+ return $el;
2764
+ };
2765
+
2766
+ fillItems(this.$input, this.sourceData);
2767
+
2768
+ this.setClass();
2267
2769
 
2268
2770
  //enter submit
2269
2771
  this.$input.on('keydown.editable', function (e) {
@@ -2274,11 +2776,14 @@ $(function(){
2274
2776
  },
2275
2777
 
2276
2778
  value2htmlFinal: function(value, element) {
2277
- var text = '', item = this.itemByVal(value);
2278
- if(item) {
2279
- text = item.text;
2779
+ var text = '',
2780
+ items = $.fn.editableutils.itemsByValue(value, this.sourceData);
2781
+
2782
+ if(items.length) {
2783
+ text = items[0].text;
2280
2784
  }
2281
- Select.superclass.constructor.superclass.value2html(text, element);
2785
+
2786
+ $(element).text(text);
2282
2787
  },
2283
2788
 
2284
2789
  autosubmit: function() {
@@ -2333,6 +2838,9 @@ $(function(){
2333
2838
  $.extend(Checklist.prototype, {
2334
2839
  renderList: function() {
2335
2840
  var $label, $div;
2841
+
2842
+ this.$tpl.empty();
2843
+
2336
2844
  if(!$.isArray(this.sourceData)) {
2337
2845
  return;
2338
2846
  }
@@ -2340,13 +2848,15 @@ $(function(){
2340
2848
  for(var i=0; i<this.sourceData.length; i++) {
2341
2849
  $label = $('<label>').append($('<input>', {
2342
2850
  type: 'checkbox',
2343
- value: this.sourceData[i].value,
2344
- name: this.options.name
2851
+ value: this.sourceData[i].value
2345
2852
  }))
2346
2853
  .append($('<span>').text(' '+this.sourceData[i].text));
2347
2854
 
2348
- $('<div>').append($label).appendTo(this.$input);
2855
+ $('<div>').append($label).appendTo(this.$tpl);
2349
2856
  }
2857
+
2858
+ this.$input = this.$tpl.find('input[type="checkbox"]');
2859
+ this.setClass();
2350
2860
  },
2351
2861
 
2352
2862
  value2str: function(value) {
@@ -2367,17 +2877,16 @@ $(function(){
2367
2877
 
2368
2878
  //set checked on required checkboxes
2369
2879
  value2input: function(value) {
2370
- var $checks = this.$input.find('input[type="checkbox"]');
2371
- $checks.removeAttr('checked');
2880
+ this.$input.prop('checked', false);
2372
2881
  if($.isArray(value) && value.length) {
2373
- $checks.each(function(i, el) {
2882
+ this.$input.each(function(i, el) {
2374
2883
  var $el = $(el);
2375
2884
  // cannot use $.inArray as it performs strict comparison
2376
2885
  $.each(value, function(j, val){
2377
2886
  /*jslint eqeq: true*/
2378
2887
  if($el.val() == val) {
2379
2888
  /*jslint eqeq: false*/
2380
- $el.attr('checked', 'checked');
2889
+ $el.prop('checked', true);
2381
2890
  }
2382
2891
  });
2383
2892
  });
@@ -2386,7 +2895,7 @@ $(function(){
2386
2895
 
2387
2896
  input2value: function() {
2388
2897
  var checked = [];
2389
- this.$input.find('input:checked').each(function(i, el) {
2898
+ this.$input.filter(':checked').each(function(i, el) {
2390
2899
  checked.push($(el).val());
2391
2900
  });
2392
2901
  return checked;
@@ -2395,11 +2904,8 @@ $(function(){
2395
2904
  //collect text of checked boxes
2396
2905
  value2htmlFinal: function(value, element) {
2397
2906
  var html = [],
2398
- /*jslint eqeq: true*/
2399
- checked = $.grep(this.sourceData, function(o){
2400
- return $.grep(value, function(v){ return v == o.value; }).length;
2401
- });
2402
- /*jslint eqeq: false*/
2907
+ checked = $.fn.editableutils.itemsByValue(value, this.sourceData);
2908
+
2403
2909
  if(checked.length) {
2404
2910
  $.each(checked, function(i, v) { html.push($.fn.editableutils.escape(v.text)); });
2405
2911
  $(element).html(html.join('<br>'));
@@ -2409,11 +2915,11 @@ $(function(){
2409
2915
  },
2410
2916
 
2411
2917
  activate: function() {
2412
- this.$input.find('input[type="checkbox"]').first().focus();
2918
+ this.$input.first().focus();
2413
2919
  },
2414
2920
 
2415
2921
  autosubmit: function() {
2416
- this.$input.find('input[type="checkbox"]').on('keydown', function(e){
2922
+ this.$input.on('keydown', function(e){
2417
2923
  if (e.which === 13) {
2418
2924
  $(this).closest('form').submit();
2419
2925
  }
@@ -2426,21 +2932,21 @@ $(function(){
2426
2932
  @property tpl
2427
2933
  @default <div></div>
2428
2934
  **/
2429
- tpl:'<div></div>',
2935
+ tpl:'<div class="editable-checklist"></div>',
2430
2936
 
2431
2937
  /**
2432
2938
  @property inputclass
2433
2939
  @type string
2434
- @default editable-checklist
2940
+ @default null
2435
2941
  **/
2436
- inputclass: 'editable-checklist',
2942
+ inputclass: null,
2437
2943
 
2438
2944
  /**
2439
- Separator of values when reading from 'data-value' string
2945
+ Separator of values when reading from `data-value` attribute
2440
2946
 
2441
2947
  @property separator
2442
2948
  @type string
2443
- @default ', '
2949
+ @default ','
2444
2950
  **/
2445
2951
  separator: ','
2446
2952
  });
@@ -2571,18 +3077,9 @@ Number
2571
3077
  $.extend(NumberInput.prototype, {
2572
3078
  render: function () {
2573
3079
  NumberInput.superclass.render.call(this);
2574
-
2575
- if (this.options.min !== null) {
2576
- this.$input.attr('min', this.options.min);
2577
- }
2578
-
2579
- if (this.options.max !== null) {
2580
- this.$input.attr('max', this.options.max);
2581
- }
2582
-
2583
- if (this.options.step !== null) {
2584
- this.$input.attr('step', this.options.step);
2585
- }
3080
+ this.setAttr('min');
3081
+ this.setAttr('max');
3082
+ this.setAttr('step');
2586
3083
  }
2587
3084
  });
2588
3085
  NumberInput.defaults = $.extend({}, $.fn.editabletypes.text.defaults, {
@@ -2606,29 +3103,19 @@ Range (inherit from number)
2606
3103
  $.fn.editableutils.inherit(Range, $.fn.editabletypes.number);
2607
3104
  $.extend(Range.prototype, {
2608
3105
  render: function () {
2609
- this.$input = $(this.options.tpl);
2610
- var $slider = this.$input.filter('input');
2611
- if(this.options.inputclass) {
2612
- $slider.addClass(this.options.inputclass);
2613
- }
2614
- if (this.options.min !== null) {
2615
- $slider.attr('min', this.options.min);
2616
- }
2617
-
2618
- if (this.options.max !== null) {
2619
- $slider.attr('max', this.options.max);
2620
- }
3106
+ this.$input = this.$tpl.filter('input');
2621
3107
 
2622
- if (this.options.step !== null) {
2623
- $slider.attr('step', this.options.step);
2624
- }
3108
+ this.setClass();
3109
+ this.setAttr('min');
3110
+ this.setAttr('max');
3111
+ this.setAttr('step');
2625
3112
 
2626
- $slider.on('input', function(){
3113
+ this.$input.on('input', function(){
2627
3114
  $(this).siblings('output').text($(this).val());
2628
3115
  });
2629
3116
  },
2630
3117
  activate: function() {
2631
- this.$input.filter('input').focus();
3118
+ this.$input.focus();
2632
3119
  }
2633
3120
  });
2634
3121
  Range.defaults = $.extend({}, $.fn.editabletypes.number.defaults, {
@@ -2637,6 +3124,783 @@ Range (inherit from number)
2637
3124
  });
2638
3125
  $.fn.editabletypes.range = Range;
2639
3126
  }(window.jQuery));
3127
+ /**
3128
+ Select2 input. Based on amazing work of Igor Vaynberg https://github.com/ivaynberg/select2.
3129
+ Please see [original docs](http://ivaynberg.github.com/select2) for detailed description and options.
3130
+ You should manually include select2 distributive:
3131
+
3132
+ <link href="select2/select2.css" rel="stylesheet" type="text/css"></link>
3133
+ <script src="select2/select2.js"></script>
3134
+
3135
+ @class select2
3136
+ @extends abstractinput
3137
+ @since 1.4.1
3138
+ @final
3139
+ @example
3140
+ <a href="#" id="country" data-type="select2" data-pk="1" data-value="ru" data-url="/post" data-original-title="Select country"></a>
3141
+ <script>
3142
+ $(function(){
3143
+ $('#country').editable({
3144
+ source: [
3145
+ {id: 'gb', text: 'Great Britain'},
3146
+ {id: 'us', text: 'United States'},
3147
+ {id: 'ru', text: 'Russia'}
3148
+ ],
3149
+ select2: {
3150
+ multiple: true
3151
+ }
3152
+ });
3153
+ });
3154
+ </script>
3155
+ **/
3156
+ (function ($) {
3157
+
3158
+ var Constructor = function (options) {
3159
+ this.init('select2', options, Constructor.defaults);
3160
+
3161
+ options.select2 = options.select2 || {};
3162
+
3163
+ var that = this,
3164
+ mixin = {
3165
+ placeholder: options.placeholder
3166
+ };
3167
+
3168
+ //detect whether it is multi-valued
3169
+ this.isMultiple = options.select2.tags || options.select2.multiple;
3170
+
3171
+ //if not `tags` mode, we need define init set data from source
3172
+ if(!options.select2.tags) {
3173
+ if(options.source) {
3174
+ mixin.data = options.source;
3175
+ }
3176
+
3177
+ //this function can be defaulted in seletc2. See https://github.com/ivaynberg/select2/issues/710
3178
+ mixin.initSelection = function (element, callback) {
3179
+ var val = that.str2value(element.val()),
3180
+ data = $.fn.editableutils.itemsByValue(val, mixin.data, 'id');
3181
+
3182
+ //for single-valued mode should not use array. Take first element instead.
3183
+ if($.isArray(data) && data.length && !that.isMultiple) {
3184
+ data = data[0];
3185
+ }
3186
+
3187
+ callback(data);
3188
+ };
3189
+ }
3190
+
3191
+ //overriding objects in config (as by default jQuery extend() is not recursive)
3192
+ this.options.select2 = $.extend({}, Constructor.defaults.select2, mixin, options.select2);
3193
+ };
3194
+
3195
+ $.fn.editableutils.inherit(Constructor, $.fn.editabletypes.abstractinput);
3196
+
3197
+ $.extend(Constructor.prototype, {
3198
+ render: function() {
3199
+ this.setClass();
3200
+ //apply select2
3201
+ this.$input.select2(this.options.select2);
3202
+
3203
+ //trigger resize of editableform to re-position container in multi-valued mode
3204
+ if(this.isMultiple) {
3205
+ this.$input.on('change', function() {
3206
+ $(this).closest('form').parent().triggerHandler('resize');
3207
+ });
3208
+ }
3209
+
3210
+ },
3211
+
3212
+ value2html: function(value, element) {
3213
+ var text = '', data;
3214
+ if(this.$input) { //when submitting form
3215
+ data = this.$input.select2('data');
3216
+ } else { //on init (autotext)
3217
+ //here select2 instance not created yet and data may be even not loaded.
3218
+ //we can check data/tags property of select config and if exist lookup text
3219
+ if(this.options.select2.tags) {
3220
+ data = value;
3221
+ } else if(this.options.select2.data) {
3222
+ data = $.fn.editableutils.itemsByValue(value, this.options.select2.data, 'id');
3223
+ }
3224
+ }
3225
+
3226
+ if($.isArray(data)) {
3227
+ //collect selected data and show with separator
3228
+ text = [];
3229
+ $.each(data, function(k, v){
3230
+ text.push(v && typeof v === 'object' ? v.text : v);
3231
+ });
3232
+ } else if(data) {
3233
+ text = data.text;
3234
+ }
3235
+
3236
+ text = $.isArray(text) ? text.join(this.options.viewseparator) : text;
3237
+
3238
+ $(element).text(text);
3239
+ },
3240
+
3241
+ html2value: function(html) {
3242
+ return this.options.select2.tags ? this.str2value(html, this.options.viewseparator) : null;
3243
+ },
3244
+
3245
+ value2input: function(value) {
3246
+ this.$input.val(value).trigger('change');
3247
+ },
3248
+
3249
+ input2value: function() {
3250
+ return this.$input.select2('val');
3251
+ },
3252
+
3253
+ str2value: function(str, separator) {
3254
+ if(typeof str !== 'string' || !this.isMultiple) {
3255
+ return str;
3256
+ }
3257
+
3258
+ separator = separator || this.options.select2.separator || $.fn.select2.defaults.separator;
3259
+
3260
+ var val, i, l;
3261
+
3262
+ if (str === null || str.length < 1) {
3263
+ return null;
3264
+ }
3265
+ val = str.split(separator);
3266
+ for (i = 0, l = val.length; i < l; i = i + 1) {
3267
+ val[i] = $.trim(val[i]);
3268
+ }
3269
+
3270
+ return val;
3271
+ }
3272
+
3273
+ });
3274
+
3275
+ Constructor.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
3276
+ /**
3277
+ @property tpl
3278
+ @default <input type="hidden">
3279
+ **/
3280
+ tpl:'<input type="hidden">',
3281
+ /**
3282
+ Configuration of select2. [Full list of options](http://ivaynberg.github.com/select2).
3283
+
3284
+ @property select2
3285
+ @type object
3286
+ @default null
3287
+ **/
3288
+ select2: null,
3289
+ /**
3290
+ Placeholder attribute of select
3291
+
3292
+ @property placeholder
3293
+ @type string
3294
+ @default null
3295
+ **/
3296
+ placeholder: null,
3297
+ /**
3298
+ Source data for select. It will be assigned to select2 `data` property and kept here just for convenience.
3299
+ Please note, that format is different from simple `select` input: use 'id' instead of 'value'.
3300
+ E.g. `[{id: 1, text: "text1"}, {id: 2, text: "text2"}, ...]`.
3301
+
3302
+ @property source
3303
+ @type array
3304
+ @default null
3305
+ **/
3306
+ source: null,
3307
+ /**
3308
+ Separator used to display tags.
3309
+
3310
+ @property viewseparator
3311
+ @type string
3312
+ @default ', '
3313
+ **/
3314
+ viewseparator: ', '
3315
+ });
3316
+
3317
+ $.fn.editabletypes.select2 = Constructor;
3318
+
3319
+ }(window.jQuery));
3320
+ /**
3321
+ * Combodate - 1.0.1
3322
+ * Dropdown date and time picker.
3323
+ * Converts text input into dropdowns to pick day, month, year, hour, minute and second.
3324
+ * Uses momentjs as datetime library http://momentjs.com.
3325
+ * For i18n include corresponding file from https://github.com/timrwood/moment/tree/master/lang
3326
+ *
3327
+ * Author: Vitaliy Potapov
3328
+ * Project page: http://github.com/vitalets/combodate
3329
+ * Copyright (c) 2012 Vitaliy Potapov. Released under MIT License.
3330
+ **/
3331
+ (function ($) {
3332
+
3333
+ var Combodate = function (element, options) {
3334
+ this.$element = $(element);
3335
+ if(!this.$element.is('input')) {
3336
+ $.error('Combodate should be applied to INPUT element');
3337
+ return;
3338
+ }
3339
+ this.options = $.extend({}, $.fn.combodate.defaults, options, this.$element.data());
3340
+ this.init();
3341
+ };
3342
+
3343
+ Combodate.prototype = {
3344
+ constructor: Combodate,
3345
+ init: function () {
3346
+ this.map = {
3347
+ //key regexp moment.method
3348
+ day: ['D', 'date'],
3349
+ month: ['M', 'month'],
3350
+ year: ['Y', 'year'],
3351
+ hour: ['[Hh]', 'hours'],
3352
+ minute: ['m', 'minutes'],
3353
+ second: ['s', 'seconds'],
3354
+ ampm: ['[Aa]', '']
3355
+ };
3356
+
3357
+ this.$widget = $('<span class="combodate"></span>').html(this.getTemplate());
3358
+
3359
+ this.initCombos();
3360
+
3361
+ //update original input on change
3362
+ this.$widget.on('change', 'select', $.proxy(function(){
3363
+ this.$element.val(this.getValue());
3364
+ }, this));
3365
+
3366
+ this.$widget.find('select').css('width', 'auto');
3367
+
3368
+ //hide original input and insert widget
3369
+ this.$element.hide().after(this.$widget);
3370
+
3371
+ //set initial value
3372
+ this.setValue(this.$element.val() || this.options.value);
3373
+ },
3374
+
3375
+ /*
3376
+ Replace tokens in template with <select> elements
3377
+ */
3378
+ getTemplate: function() {
3379
+ var tpl = this.options.template;
3380
+
3381
+ //first pass
3382
+ $.each(this.map, function(k, v) {
3383
+ v = v[0];
3384
+ var r = new RegExp(v+'+'),
3385
+ token = v.length > 1 ? v.substring(1, 2) : v;
3386
+
3387
+ tpl = tpl.replace(r, '{'+token+'}');
3388
+ });
3389
+
3390
+ //replace spaces with &nbsp;
3391
+ tpl = tpl.replace(/ /g, '&nbsp;');
3392
+
3393
+ //second pass
3394
+ $.each(this.map, function(k, v) {
3395
+ v = v[0];
3396
+ var token = v.length > 1 ? v.substring(1, 2) : v;
3397
+
3398
+ tpl = tpl.replace('{'+token+'}', '<select class="'+k+'"></select>');
3399
+ });
3400
+
3401
+ return tpl;
3402
+ },
3403
+
3404
+ /*
3405
+ Initialize combos that presents in template
3406
+ */
3407
+ initCombos: function() {
3408
+ var that = this;
3409
+ $.each(this.map, function(k, v) {
3410
+ var $c = that.$widget.find('.'+k), f, items;
3411
+ if($c.length) {
3412
+ that['$'+k] = $c; //set properties like this.$day, this.$month etc.
3413
+ f = 'fill' + k.charAt(0).toUpperCase() + k.slice(1); //define method name to fill items, e.g `fillDays`
3414
+ items = that[f]();
3415
+ that['$'+k].html(that.renderItems(items));
3416
+ }
3417
+ });
3418
+ },
3419
+
3420
+ /*
3421
+ Initialize items of combos. Handles `firstItem` option
3422
+ */
3423
+ initItems: function(key) {
3424
+ var values = [];
3425
+ if(this.options.firstItem === 'name') {
3426
+ var header = typeof moment.relativeTime[key] === 'function' ? moment.relativeTime[key](1, true, key, false) : moment.relativeTime[key];
3427
+ //take last entry (see momentjs lang files structure)
3428
+ header = header.split(' ').reverse()[0];
3429
+ values.push(['', header]);
3430
+ } else if(this.options.firstItem === 'empty') {
3431
+ values.push(['', '']);
3432
+ }
3433
+ return values;
3434
+ },
3435
+
3436
+ /*
3437
+ render items to string of <option> tags
3438
+ */
3439
+ renderItems: function(items) {
3440
+ var str = [];
3441
+ for(var i=0; i<items.length; i++) {
3442
+ str.push('<option value="'+items[i][0]+'">'+items[i][1]+'</option>');
3443
+ }
3444
+ return str.join("\n");
3445
+ },
3446
+
3447
+ /*
3448
+ fill day
3449
+ */
3450
+ fillDay: function() {
3451
+ var items = this.initItems('d'), name, i,
3452
+ twoDigit = this.options.template.indexOf('DD') !== -1;
3453
+
3454
+ for(i=1; i<=31; i++) {
3455
+ name = twoDigit ? this.leadZero(i) : i;
3456
+ items.push([i, name]);
3457
+ }
3458
+ return items;
3459
+ },
3460
+
3461
+ /*
3462
+ fill month
3463
+ */
3464
+ fillMonth: function() {
3465
+ var items = this.initItems('M'), name, i,
3466
+ longNames = this.options.template.indexOf('MMMM') !== -1,
3467
+ shortNames = this.options.template.indexOf('MMM') !== -1,
3468
+ twoDigit = this.options.template.indexOf('MM') !== -1;
3469
+
3470
+ for(i=0; i<=11; i++) {
3471
+ if(longNames) {
3472
+ name = moment.months[i];
3473
+ } else if(shortNames) {
3474
+ name = moment.monthsShort[i];
3475
+ } else if(twoDigit) {
3476
+ name = this.leadZero(i+1);
3477
+ } else {
3478
+ name = i+1;
3479
+ }
3480
+ items.push([i, name]);
3481
+ }
3482
+ return items;
3483
+ },
3484
+
3485
+ /*
3486
+ fill year
3487
+ */
3488
+ fillYear: function() {
3489
+ var items = this.initItems('y'), name, i,
3490
+ longNames = this.options.template.indexOf('YYYY') !== -1;
3491
+
3492
+ for(i=this.options.maxYear; i>=this.options.minYear; i--) {
3493
+ name = longNames ? i : (i+'').substring(2);
3494
+ items.push([i, name]);
3495
+ }
3496
+ return items;
3497
+ },
3498
+
3499
+ /*
3500
+ fill hour
3501
+ */
3502
+ fillHour: function() {
3503
+ var items = this.initItems('h'), name, i,
3504
+ h12 = this.options.template.indexOf('h') !== -1,
3505
+ h24 = this.options.template.indexOf('H') !== -1,
3506
+ twoDigit = this.options.template.toLowerCase().indexOf('hh') !== -1,
3507
+ max = h12 ? 12 : 23;
3508
+
3509
+ for(i=0; i<=max; i++) {
3510
+ name = twoDigit ? this.leadZero(i) : i;
3511
+ items.push([i, name]);
3512
+ }
3513
+ return items;
3514
+ },
3515
+
3516
+ /*
3517
+ fill minute
3518
+ */
3519
+ fillMinute: function() {
3520
+ var items = this.initItems('m'), name, i,
3521
+ twoDigit = this.options.template.indexOf('mm') !== -1;
3522
+
3523
+ for(i=0; i<=59; i+= this.options.minuteStep) {
3524
+ name = twoDigit ? this.leadZero(i) : i;
3525
+ items.push([i, name]);
3526
+ }
3527
+ return items;
3528
+ },
3529
+
3530
+ /*
3531
+ fill second
3532
+ */
3533
+ fillSecond: function() {
3534
+ var items = this.initItems('s'), name, i,
3535
+ twoDigit = this.options.template.indexOf('ss') !== -1;
3536
+
3537
+ for(i=0; i<=59; i+= this.options.secondStep) {
3538
+ name = twoDigit ? this.leadZero(i) : i;
3539
+ items.push([i, name]);
3540
+ }
3541
+ return items;
3542
+ },
3543
+
3544
+ /*
3545
+ fill ampm
3546
+ */
3547
+ fillAmpm: function() {
3548
+ var ampmL = this.options.template.indexOf('a') !== -1,
3549
+ ampmU = this.options.template.indexOf('A') !== -1,
3550
+ items = [
3551
+ ['am', ampmL ? 'am' : 'AM'],
3552
+ ['pm', ampmL ? 'pm' : 'PM']
3553
+ ];
3554
+ return items;
3555
+ },
3556
+
3557
+ /*
3558
+ Returns current date value.
3559
+ If format not specified - `options.format` used.
3560
+ If format = `null` - Moment object returned.
3561
+ */
3562
+ getValue: function(format) {
3563
+ var dt, values = {},
3564
+ that = this,
3565
+ notSelected = false;
3566
+
3567
+ //getting selected values
3568
+ $.each(this.map, function(k, v) {
3569
+ if(k === 'ampm') {
3570
+ return;
3571
+ }
3572
+ var def = k === 'day' ? 1 : 0;
3573
+
3574
+ values[k] = that['$'+k] ? parseInt(that['$'+k].val(), 10) : def;
3575
+
3576
+ if(isNaN(values[k])) {
3577
+ notSelected = true;
3578
+ return false;
3579
+ }
3580
+ });
3581
+
3582
+ //if at least one visible combo not selected - return empty string
3583
+ if(notSelected) {
3584
+ return '';
3585
+ }
3586
+
3587
+ //convert hours if 12h format
3588
+ if(this.$ampm) {
3589
+ values.hour = this.$ampm.val() === 'am' ? values.hour : values.hour+12;
3590
+ if(values.hour === 24) {
3591
+ values.hour = 0;
3592
+ }
3593
+ }
3594
+
3595
+ dt = moment([values.year, values.month, values.day, values.hour, values.minute, values.second]);
3596
+
3597
+ //highlight invalid date
3598
+ this.highlight(dt);
3599
+
3600
+ format = format === undefined ? this.options.format : format;
3601
+ if(format === null) {
3602
+ return dt.isValid() ? dt : null;
3603
+ } else {
3604
+ return dt.isValid() ? dt.format(format) : '';
3605
+ }
3606
+ },
3607
+
3608
+ setValue: function(value) {
3609
+ if(!value) {
3610
+ return;
3611
+ }
3612
+
3613
+ var dt = typeof value === 'string' ? moment(value, this.options.format) : moment(value),
3614
+ that = this,
3615
+ values = {};
3616
+
3617
+ if(dt.isValid()) {
3618
+ //read values from date object
3619
+ $.each(this.map, function(k, v) {
3620
+ if(k === 'ampm') {
3621
+ return;
3622
+ }
3623
+ values[k] = dt[v[1]]();
3624
+ });
3625
+
3626
+ if(this.$ampm) {
3627
+ if(values.hour > 12) {
3628
+ values.hour -= 12;
3629
+ values.ampm = 'pm';
3630
+ } else {
3631
+ values.ampm = 'am';
3632
+ }
3633
+ }
3634
+
3635
+ $.each(values, function(k, v) {
3636
+ if(that['$'+k]) {
3637
+ that['$'+k].val(v);
3638
+ }
3639
+ });
3640
+
3641
+ this.$element.val(dt.format(this.options.format));
3642
+ }
3643
+ },
3644
+
3645
+ /*
3646
+ highlight combos if date is invalid
3647
+ */
3648
+ highlight: function(dt) {
3649
+ if(!dt.isValid()) {
3650
+ if(this.options.errorClass) {
3651
+ this.$widget.addClass(this.options.errorClass);
3652
+ } else {
3653
+ //store original border color
3654
+ if(!this.borderColor) {
3655
+ this.borderColor = this.$widget.find('select').css('border-color');
3656
+ }
3657
+ this.$widget.find('select').css('border-color', 'red');
3658
+ }
3659
+ } else {
3660
+ if(this.options.errorClass) {
3661
+ this.$widget.removeClass(this.options.errorClass);
3662
+ } else {
3663
+ this.$widget.find('select').css('border-color', this.borderColor);
3664
+ }
3665
+ }
3666
+ },
3667
+
3668
+ leadZero: function(v) {
3669
+ return v <= 9 ? '0' + v : v;
3670
+ },
3671
+
3672
+ destroy: function() {
3673
+ this.$widget.remove();
3674
+ this.$element.removeData('combodate').show();
3675
+ }
3676
+
3677
+ //todo: clear method
3678
+ };
3679
+
3680
+ $.fn.combodate = function ( option ) {
3681
+ var d, args = Array.apply(null, arguments);
3682
+ args.shift();
3683
+
3684
+ //getValue returns date as string / object (not jQuery object)
3685
+ if(option === 'getValue' && this.length && (d = this.eq(0).data('combodate'))) {
3686
+ return d.getValue.apply(d, args);
3687
+ }
3688
+
3689
+ return this.each(function () {
3690
+ var $this = $(this),
3691
+ data = $this.data('combodate'),
3692
+ options = typeof option == 'object' && option;
3693
+ if (!data) {
3694
+ $this.data('combodate', (data = new Combodate(this, options)));
3695
+ }
3696
+ if (typeof option == 'string' && typeof data[option] == 'function') {
3697
+ data[option].apply(data, args);
3698
+ }
3699
+ });
3700
+ };
3701
+
3702
+ $.fn.combodate.defaults = {
3703
+ //in this format value stored in original input
3704
+ format: 'DD-MM-YYYY HH:mm',
3705
+ //in this format items in dropdowns are displayed
3706
+ template: 'D / MMM / YYYY H : mm',
3707
+ //initial value, can be `new Date()`
3708
+ value: null,
3709
+ minYear: 1970,
3710
+ maxYear: 2015,
3711
+ minuteStep: 5,
3712
+ secondStep: 1,
3713
+ firstItem: 'empty', //'name', 'empty', 'none'
3714
+ errorClass: null
3715
+ };
3716
+
3717
+ }(window.jQuery));
3718
+ /**
3719
+ Combodate input - dropdown date and time picker.
3720
+ Based on [combodate](http://vitalets.github.com/combodate) plugin. To use it you should manually include [momentjs](http://momentjs.com).
3721
+
3722
+ <script src="js/moment.min.js"></script>
3723
+
3724
+ Allows to input:
3725
+
3726
+ * only date
3727
+ * only time
3728
+ * both date and time
3729
+
3730
+ Please note, that format is taken from momentjs and **not compatible** with bootstrap-datepicker / jquery UI datepicker.
3731
+ Internally value stored as `momentjs` object.
3732
+
3733
+ @class combodate
3734
+ @extends abstractinput
3735
+ @final
3736
+ @since 1.4.0
3737
+ @example
3738
+ <a href="#" id="dob" data-type="combodate" data-pk="1" data-url="/post" data-value="1984-05-15" data-original-title="Select date"></a>
3739
+ <script>
3740
+ $(function(){
3741
+ $('#dob').editable({
3742
+ format: 'YYYY-MM-DD',
3743
+ viewformat: 'DD.MM.YYYY',
3744
+ template: 'D / MMMM / YYYY',
3745
+ combodate: {
3746
+ minYear: 2000,
3747
+ maxYear: 2015,
3748
+ minuteStep: 1
3749
+ }
3750
+ }
3751
+ });
3752
+ });
3753
+ </script>
3754
+ **/
3755
+
3756
+ /*global moment*/
3757
+
3758
+ (function ($) {
3759
+
3760
+ var Constructor = function (options) {
3761
+ this.init('combodate', options, Constructor.defaults);
3762
+
3763
+ //by default viewformat equals to format
3764
+ if(!this.options.viewformat) {
3765
+ this.options.viewformat = this.options.format;
3766
+ }
3767
+
3768
+ //overriding combodate config (as by default jQuery extend() is not recursive)
3769
+ this.options.combodate = $.extend({}, Constructor.defaults.combodate, options.combodate, {
3770
+ format: this.options.format,
3771
+ template: this.options.template
3772
+ });
3773
+ };
3774
+
3775
+ $.fn.editableutils.inherit(Constructor, $.fn.editabletypes.abstractinput);
3776
+
3777
+ $.extend(Constructor.prototype, {
3778
+ render: function () {
3779
+ this.$input.combodate(this.options.combodate);
3780
+
3781
+ //"clear" link
3782
+ /*
3783
+ if(this.options.clear) {
3784
+ this.$clear = $('<a href="#"></a>').html(this.options.clear).click($.proxy(function(e){
3785
+ e.preventDefault();
3786
+ e.stopPropagation();
3787
+ this.clear();
3788
+ }, this));
3789
+
3790
+ this.$tpl.parent().append($('<div class="editable-clear">').append(this.$clear));
3791
+ }
3792
+ */
3793
+ },
3794
+
3795
+ value2html: function(value, element) {
3796
+ var text = value ? value.format(this.options.viewformat) : '';
3797
+ $(element).text(text);
3798
+ },
3799
+
3800
+ html2value: function(html) {
3801
+ return html ? moment(html, this.options.viewformat) : null;
3802
+ },
3803
+
3804
+ value2str: function(value) {
3805
+ return value ? value.format(this.options.format) : '';
3806
+ },
3807
+
3808
+ str2value: function(str) {
3809
+ return str ? moment(str, this.options.format) : null;
3810
+ },
3811
+
3812
+ value2submit: function(value) {
3813
+ return this.value2str(value);
3814
+ },
3815
+
3816
+ value2input: function(value) {
3817
+ this.$input.combodate('setValue', value);
3818
+ },
3819
+
3820
+ input2value: function() {
3821
+ return this.$input.combodate('getValue', null);
3822
+ },
3823
+
3824
+ activate: function() {
3825
+ this.$input.siblings('.combodate').find('select').eq(0).focus();
3826
+ },
3827
+
3828
+ /*
3829
+ clear: function() {
3830
+ this.$input.data('datepicker').date = null;
3831
+ this.$input.find('.active').removeClass('active');
3832
+ },
3833
+ */
3834
+
3835
+ autosubmit: function() {
3836
+
3837
+ }
3838
+
3839
+ });
3840
+
3841
+ Constructor.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
3842
+ /**
3843
+ @property tpl
3844
+ @default <input type="text">
3845
+ **/
3846
+ tpl:'<input type="text">',
3847
+ /**
3848
+ @property inputclass
3849
+ @default null
3850
+ **/
3851
+ inputclass: null,
3852
+ /**
3853
+ Format used for sending value to server. Also applied when converting date from <code>data-value</code> attribute.<br>
3854
+ See list of tokens in [momentjs docs](http://momentjs.com/docs/#/parsing/string-format)
3855
+
3856
+ @property format
3857
+ @type string
3858
+ @default YYYY-MM-DD
3859
+ **/
3860
+ format:'YYYY-MM-DD',
3861
+ /**
3862
+ Format used for displaying date. Also applied when converting date from element's text on init.
3863
+ If not specified equals to `format`.
3864
+
3865
+ @property viewformat
3866
+ @type string
3867
+ @default null
3868
+ **/
3869
+ viewformat: null,
3870
+ /**
3871
+ Template used for displaying dropdowns.
3872
+
3873
+ @property template
3874
+ @type string
3875
+ @default D / MMM / YYYY
3876
+ **/
3877
+ template: 'D / MMM / YYYY',
3878
+ /**
3879
+ Configuration of combodate.
3880
+ Full list of options: http://vitalets.github.com/combodate/#docs
3881
+
3882
+ @property combodate
3883
+ @type object
3884
+ @default null
3885
+ **/
3886
+ combodate: null
3887
+
3888
+ /*
3889
+ (not implemented yet)
3890
+ Text shown as clear date button.
3891
+ If <code>false</code> clear button will not be rendered.
3892
+
3893
+ @property clear
3894
+ @type boolean|string
3895
+ @default 'x clear'
3896
+ */
3897
+ //clear: '&times; clear'
3898
+ });
3899
+
3900
+ $.fn.editabletypes.combodate = Constructor;
3901
+
3902
+ }(window.jQuery));
3903
+
2640
3904
  /*
2641
3905
  Editableform based on Twitter Bootstrap
2642
3906
  */
@@ -2666,7 +3930,7 @@ Editableform based on Twitter Bootstrap
2666
3930
  (function ($) {
2667
3931
 
2668
3932
  //extend methods
2669
- $.extend($.fn.editableContainer.Constructor.prototype, {
3933
+ $.extend($.fn.editableContainer.Popup.prototype, {
2670
3934
  containerName: 'popover',
2671
3935
  //for compatibility with bootstrap <= 2.2.1 (content inserted into <p> instead of directly .popover-content)
2672
3936
  innerCss: $($.fn.popover.defaults.template).find('p').length ? '.popover-content p' : '.popover-content',
@@ -2675,10 +3939,39 @@ Editableform based on Twitter Bootstrap
2675
3939
  $.extend(this.containerOptions, {
2676
3940
  trigger: 'manual',
2677
3941
  selector: false,
2678
- content: ' '
3942
+ content: ' ',
3943
+ template: $.fn.popover.defaults.template
2679
3944
  });
3945
+
3946
+ //as template property is used in inputs, hide it from popover
3947
+ var t;
3948
+ if(this.$element.data('template')) {
3949
+ t = this.$element.data('template');
3950
+ this.$element.removeData('template');
3951
+ }
3952
+
2680
3953
  this.call(this.containerOptions);
2681
- },
3954
+
3955
+ if(t) {
3956
+ //restore data('template')
3957
+ this.$element.data('template', t);
3958
+ }
3959
+ },
3960
+
3961
+ /* show */
3962
+ innerShow: function () {
3963
+ this.call('show');
3964
+ },
3965
+
3966
+ /* hide */
3967
+ innerHide: function () {
3968
+ this.call('hide');
3969
+ },
3970
+
3971
+ /* destroy */
3972
+ innerDestroy: function() {
3973
+ this.call('destroy');
3974
+ },
2682
3975
 
2683
3976
  setContainerOption: function(key, value) {
2684
3977
  this.container().options[key] = value;
@@ -2742,18 +4035,13 @@ Editableform based on Twitter Bootstrap
2742
4035
  }
2743
4036
  });
2744
4037
 
2745
- //defaults
2746
- /*
2747
- $.fn.editableContainer.defaults = $.extend({}, $.fn.popover.defaults, $.fn.editableContainer.defaults, {
2748
-
2749
- });
2750
- */
2751
-
2752
4038
  }(window.jQuery));
2753
4039
  /**
2754
4040
  Bootstrap-datepicker.
2755
- Description and examples: http://vitalets.github.com/bootstrap-datepicker.
2756
- For localization you can include js file from here: https://github.com/eternicode/bootstrap-datepicker/tree/master/js/locales
4041
+ Description and examples: https://github.com/eternicode/bootstrap-datepicker.
4042
+ For **i18n** you should include js file from here: https://github.com/eternicode/bootstrap-datepicker/tree/master/js/locales
4043
+ and set `language` option.
4044
+ Since 1.4.0 date has different appearance in **popup** and **inline** modes.
2757
4045
 
2758
4046
  @class date
2759
4047
  @extends abstractinput
@@ -2777,45 +4065,52 @@ $(function(){
2777
4065
 
2778
4066
  var Date = function (options) {
2779
4067
  this.init('date', options, Date.defaults);
2780
-
2781
- //set popular options directly from settings or data-* attributes
2782
- var directOptions = $.fn.editableutils.sliceObj(this.options, ['format']);
2783
-
2784
- //overriding datepicker config (as by default jQuery extend() is not recursive)
2785
- this.options.datepicker = $.extend({}, Date.defaults.datepicker, directOptions, options.datepicker);
2786
-
2787
- //by default viewformat equals to format
2788
- if(!this.options.viewformat) {
2789
- this.options.viewformat = this.options.datepicker.format;
2790
- }
2791
-
2792
- //language
2793
- this.options.datepicker.language = this.options.datepicker.language || 'en';
2794
-
2795
- //store DPglobal
2796
- this.dpg = $.fn.datepicker.DPGlobal;
2797
-
2798
- //store parsed formats
2799
- this.parsedFormat = this.dpg.parseFormat(this.options.datepicker.format);
2800
- this.parsedViewFormat = this.dpg.parseFormat(this.options.viewformat);
4068
+ this.initPicker(options, Date.defaults);
2801
4069
  };
2802
4070
 
2803
4071
  $.fn.editableutils.inherit(Date, $.fn.editabletypes.abstractinput);
2804
4072
 
2805
4073
  $.extend(Date.prototype, {
4074
+ initPicker: function(options, defaults) {
4075
+ //'format' is set directly from settings or data-* attributes
4076
+
4077
+ //by default viewformat equals to format
4078
+ if(!this.options.viewformat) {
4079
+ this.options.viewformat = this.options.format;
4080
+ }
4081
+
4082
+ //overriding datepicker config (as by default jQuery extend() is not recursive)
4083
+ //since 1.4 datepicker internally uses viewformat instead of format. Format is for submit only
4084
+ this.options.datepicker = $.extend({}, defaults.datepicker, options.datepicker, {
4085
+ format: this.options.viewformat
4086
+ });
4087
+
4088
+ //language
4089
+ this.options.datepicker.language = this.options.datepicker.language || 'en';
4090
+
4091
+ //store DPglobal
4092
+ this.dpg = $.fn.datepicker.DPGlobal;
4093
+
4094
+ //store parsed formats
4095
+ this.parsedFormat = this.dpg.parseFormat(this.options.format);
4096
+ this.parsedViewFormat = this.dpg.parseFormat(this.options.viewformat);
4097
+ },
4098
+
2806
4099
  render: function () {
2807
- Date.superclass.render.call(this);
2808
4100
  this.$input.datepicker(this.options.datepicker);
2809
-
4101
+
4102
+ //"clear" link
2810
4103
  if(this.options.clear) {
2811
4104
  this.$clear = $('<a href="#"></a>').html(this.options.clear).click($.proxy(function(e){
2812
4105
  e.preventDefault();
2813
4106
  e.stopPropagation();
2814
4107
  this.clear();
2815
4108
  }, this));
2816
- }
4109
+
4110
+ this.$tpl.parent().append($('<div class="editable-clear">').append(this.$clear));
4111
+ }
2817
4112
  },
2818
-
4113
+
2819
4114
  value2html: function(value, element) {
2820
4115
  var text = value ? this.dpg.formatDate(value, this.parsedViewFormat, this.options.datepicker.language) : '';
2821
4116
  Date.superclass.value2html(text, element);
@@ -2869,12 +4164,12 @@ $(function(){
2869
4164
  @property tpl
2870
4165
  @default <div></div>
2871
4166
  **/
2872
- tpl:'<div></div>',
4167
+ tpl:'<div class="editable-date well"></div>',
2873
4168
  /**
2874
4169
  @property inputclass
2875
- @default editable-date well
4170
+ @default null
2876
4171
  **/
2877
- inputclass: 'editable-date well',
4172
+ inputclass: null,
2878
4173
  /**
2879
4174
  Format used for sending value to server. Also applied when converting date from <code>data-value</code> attribute.<br>
2880
4175
  Possible tokens are: <code>d, dd, m, mm, yy, yyyy</code>
@@ -2925,6 +4220,84 @@ $(function(){
2925
4220
 
2926
4221
  }(window.jQuery));
2927
4222
 
4223
+ /**
4224
+ Bootstrap datefield input - modification for inline mode.
4225
+ Shows normal <input type="text"> and binds popup datepicker.
4226
+ Automatically shown in inline mode.
4227
+
4228
+ @class datefield
4229
+ @extends date
4230
+
4231
+ @since 1.4.0
4232
+ **/
4233
+ (function ($) {
4234
+
4235
+ var DateField = function (options) {
4236
+ this.init('datefield', options, DateField.defaults);
4237
+ this.initPicker(options, DateField.defaults);
4238
+ };
4239
+
4240
+ $.fn.editableutils.inherit(DateField, $.fn.editabletypes.date);
4241
+
4242
+ $.extend(DateField.prototype, {
4243
+ render: function () {
4244
+ this.$input = this.$tpl.find('input');
4245
+ this.setClass();
4246
+ this.setAttr('placeholder');
4247
+
4248
+ this.$tpl.datepicker(this.options.datepicker);
4249
+
4250
+ //need to disable original event handlers
4251
+ this.$input.off('focus keydown');
4252
+
4253
+ //update value of datepicker
4254
+ this.$input.keyup($.proxy(function(){
4255
+ this.$tpl.removeData('date');
4256
+ this.$tpl.datepicker('update');
4257
+ }, this));
4258
+
4259
+ },
4260
+
4261
+ value2input: function(value) {
4262
+ this.$input.val(value ? this.dpg.formatDate(value, this.parsedViewFormat, this.options.datepicker.language) : '');
4263
+ this.$tpl.datepicker('update');
4264
+ },
4265
+
4266
+ input2value: function() {
4267
+ return this.html2value(this.$input.val());
4268
+ },
4269
+
4270
+ activate: function() {
4271
+ $.fn.editabletypes.text.prototype.activate.call(this);
4272
+ },
4273
+
4274
+ autosubmit: function() {
4275
+ //reset autosubmit to empty
4276
+ }
4277
+ });
4278
+
4279
+ DateField.defaults = $.extend({}, $.fn.editabletypes.date.defaults, {
4280
+ /**
4281
+ @property tpl
4282
+ **/
4283
+ tpl:'<div class="input-append date"><input type="text"/><span class="add-on"><i class="icon-th"></i></span></div>',
4284
+ /**
4285
+ @property inputclass
4286
+ @default 'input-small'
4287
+ **/
4288
+ inputclass: 'input-small',
4289
+
4290
+ /* datepicker config */
4291
+ datepicker: {
4292
+ weekStart: 0,
4293
+ startView: 0,
4294
+ autoclose: true
4295
+ }
4296
+ });
4297
+
4298
+ $.fn.editabletypes.datefield = DateField;
4299
+
4300
+ }(window.jQuery));
2928
4301
  /* =========================================================
2929
4302
  * bootstrap-datepicker.js
2930
4303
  * http://www.eyecon.ro/bootstrap-datepicker
@@ -2963,51 +4336,45 @@ $(function(){
2963
4336
  this.element = $(element);
2964
4337
  this.language = options.language||this.element.data('date-language')||"en";
2965
4338
  this.language = this.language in dates ? this.language : "en";
4339
+ this.isRTL = dates[this.language].rtl||false;
2966
4340
  this.format = DPGlobal.parseFormat(options.format||this.element.data('date-format')||'mm/dd/yyyy');
2967
- this.isInline = false;
4341
+ this.isInline = false;
2968
4342
  this.isInput = this.element.is('input');
2969
4343
  this.component = this.element.is('.date') ? this.element.find('.add-on') : false;
2970
4344
  this.hasInput = this.component && this.element.find('input').length;
2971
4345
  if(this.component && this.component.length === 0)
2972
4346
  this.component = false;
2973
4347
 
2974
- if (this.isInput) { //single input
2975
- this.element.on({
2976
- focus: $.proxy(this.show, this),
2977
- keyup: $.proxy(this.update, this),
2978
- keydown: $.proxy(this.keydown, this)
2979
- });
2980
- } else if(this.component && this.hasInput) { //component: input + button
2981
- // For components that are not readonly, allow keyboard nav
2982
- this.element.find('input').on({
2983
- focus: $.proxy(this.show, this),
2984
- keyup: $.proxy(this.update, this),
2985
- keydown: $.proxy(this.keydown, this)
2986
- });
2987
-
2988
- this.component.on('click', $.proxy(this.show, this));
2989
- } else if(this.element.is('div')) { //inline datepicker
2990
- this.isInline = true;
2991
- } else {
2992
- this.element.on('click', $.proxy(this.show, this));
2993
- }
2994
-
2995
- this.picker = $(DPGlobal.template)
2996
- .appendTo(this.isInline ? this.element : 'body')
2997
- .on({
2998
- click: $.proxy(this.click, this),
2999
- mousedown: $.proxy(this.mousedown, this)
3000
- });
3001
-
3002
- if(this.isInline) {
3003
- this.picker.addClass('datepicker-inline');
3004
- } else {
3005
- this.picker.addClass('dropdown-menu');
3006
- }
4348
+ this._attachEvents();
3007
4349
 
4350
+ this.forceParse = true;
4351
+ if ('forceParse' in options) {
4352
+ this.forceParse = options.forceParse;
4353
+ } else if ('dateForceParse' in this.element.data()) {
4354
+ this.forceParse = this.element.data('date-force-parse');
4355
+ }
4356
+
4357
+
4358
+ this.picker = $(DPGlobal.template)
4359
+ .appendTo(this.isInline ? this.element : 'body')
4360
+ .on({
4361
+ click: $.proxy(this.click, this),
4362
+ mousedown: $.proxy(this.mousedown, this)
4363
+ });
4364
+
4365
+ if(this.isInline) {
4366
+ this.picker.addClass('datepicker-inline');
4367
+ } else {
4368
+ this.picker.addClass('datepicker-dropdown dropdown-menu');
4369
+ }
4370
+ if (this.isRTL){
4371
+ this.picker.addClass('datepicker-rtl');
4372
+ this.picker.find('.prev i, .next i')
4373
+ .toggleClass('icon-arrow-left icon-arrow-right');
4374
+ }
3008
4375
  $(document).on('mousedown', function (e) {
3009
4376
  // Clicked outside the datepicker, hide it
3010
- if ($(e.target).closest('.datepicker').length == 0) {
4377
+ if ($(e.target).closest('.datepicker').length === 0) {
3011
4378
  that.hide();
3012
4379
  }
3013
4380
  });
@@ -3026,6 +4393,7 @@ $(function(){
3026
4393
  this.keyboardNavigation = this.element.data('date-keyboard-navigation');
3027
4394
  }
3028
4395
 
4396
+ this.viewMode = this.startViewMode = 0;
3029
4397
  switch(options.startView || this.element.data('date-start-view')){
3030
4398
  case 2:
3031
4399
  case 'decade':
@@ -3035,11 +4403,6 @@ $(function(){
3035
4403
  case 'year':
3036
4404
  this.viewMode = this.startViewMode = 1;
3037
4405
  break;
3038
- case 0:
3039
- case 'month':
3040
- default:
3041
- this.viewMode = this.startViewMode = 0;
3042
- break;
3043
4406
  }
3044
4407
 
3045
4408
  this.todayBtn = (options.todayBtn||this.element.data('date-today-btn')||false);
@@ -3049,21 +4412,73 @@ $(function(){
3049
4412
  this.weekEnd = ((this.weekStart + 6) % 7);
3050
4413
  this.startDate = -Infinity;
3051
4414
  this.endDate = Infinity;
4415
+ this.daysOfWeekDisabled = [];
3052
4416
  this.setStartDate(options.startDate||this.element.data('date-startdate'));
3053
4417
  this.setEndDate(options.endDate||this.element.data('date-enddate'));
4418
+ this.setDaysOfWeekDisabled(options.daysOfWeekDisabled||this.element.data('date-days-of-week-disabled'));
3054
4419
  this.fillDow();
3055
4420
  this.fillMonths();
3056
4421
  this.update();
3057
4422
  this.showMode();
3058
4423
 
3059
- if(this.isInline) {
3060
- this.show();
3061
- }
4424
+ if(this.isInline) {
4425
+ this.show();
4426
+ }
3062
4427
  };
3063
4428
 
3064
4429
  Datepicker.prototype = {
3065
4430
  constructor: Datepicker,
3066
4431
 
4432
+ _events: [],
4433
+ _attachEvents: function(){
4434
+ this._detachEvents();
4435
+ if (this.isInput) { // single input
4436
+ this._events = [
4437
+ [this.element, {
4438
+ focus: $.proxy(this.show, this),
4439
+ keyup: $.proxy(this.update, this),
4440
+ keydown: $.proxy(this.keydown, this)
4441
+ }]
4442
+ ];
4443
+ }
4444
+ else if (this.component && this.hasInput){ // component: input + button
4445
+ this._events = [
4446
+ // For components that are not readonly, allow keyboard nav
4447
+ [this.element.find('input'), {
4448
+ focus: $.proxy(this.show, this),
4449
+ keyup: $.proxy(this.update, this),
4450
+ keydown: $.proxy(this.keydown, this)
4451
+ }],
4452
+ [this.component, {
4453
+ click: $.proxy(this.show, this)
4454
+ }]
4455
+ ];
4456
+ }
4457
+ else if (this.element.is('div')) { // inline datepicker
4458
+ this.isInline = true;
4459
+ }
4460
+ else {
4461
+ this._events = [
4462
+ [this.element, {
4463
+ click: $.proxy(this.show, this)
4464
+ }]
4465
+ ];
4466
+ }
4467
+ for (var i=0, el, ev; i<this._events.length; i++){
4468
+ el = this._events[i][0];
4469
+ ev = this._events[i][1];
4470
+ el.on(ev);
4471
+ }
4472
+ },
4473
+ _detachEvents: function(){
4474
+ for (var i=0, el, ev; i<this._events.length; i++){
4475
+ el = this._events[i][0];
4476
+ ev = this._events[i][1];
4477
+ el.off(ev);
4478
+ }
4479
+ this._events = [];
4480
+ },
4481
+
3067
4482
  show: function(e) {
3068
4483
  this.picker.show();
3069
4484
  this.height = this.component ? this.component.outerHeight() : this.element.outerHeight();
@@ -3081,7 +4496,7 @@ $(function(){
3081
4496
  },
3082
4497
 
3083
4498
  hide: function(e){
3084
- if(this.isInline) return;
4499
+ if(this.isInline) return;
3085
4500
  this.picker.hide();
3086
4501
  $(window).off('resize', this.place);
3087
4502
  this.viewMode = this.startViewMode;
@@ -3089,7 +4504,14 @@ $(function(){
3089
4504
  if (!this.isInput) {
3090
4505
  $(document).off('mousedown', this.hide);
3091
4506
  }
3092
- if (e && e.currentTarget.value)
4507
+
4508
+ if (
4509
+ this.forceParse &&
4510
+ (
4511
+ this.isInput && this.element.val() ||
4512
+ this.hasInput && this.element.find('input').val()
4513
+ )
4514
+ )
3093
4515
  this.setValue();
3094
4516
  this.element.trigger({
3095
4517
  type: 'hide',
@@ -3097,9 +4519,15 @@ $(function(){
3097
4519
  });
3098
4520
  },
3099
4521
 
4522
+ remove: function() {
4523
+ this._detachEvents();
4524
+ this.picker.remove();
4525
+ delete this.element.data().datepicker;
4526
+ },
4527
+
3100
4528
  getDate: function() {
3101
4529
  var d = this.getUTCDate();
3102
- return new Date(d.getTime() + (d.getTimezoneOffset()*60000))
4530
+ return new Date(d.getTime() + (d.getTimezoneOffset()*60000));
3103
4531
  },
3104
4532
 
3105
4533
  getUTCDate: function() {
@@ -3119,18 +4547,19 @@ $(function(){
3119
4547
  var formatted = this.getFormattedDate();
3120
4548
  if (!this.isInput) {
3121
4549
  if (this.component){
3122
- this.element.find('input').prop('value', formatted);
4550
+ this.element.find('input').val(formatted);
3123
4551
  }
3124
4552
  this.element.data('date', formatted);
3125
4553
  } else {
3126
- this.element.prop('value', formatted);
4554
+ this.element.val(formatted);
3127
4555
  }
3128
4556
  },
3129
4557
 
3130
- getFormattedDate: function(format) {
3131
- if(format == undefined) format = this.format;
3132
- return DPGlobal.formatDate(this.date, format, this.language);
3133
- },
4558
+ getFormattedDate: function(format) {
4559
+ if (format === undefined)
4560
+ format = this.format;
4561
+ return DPGlobal.formatDate(this.date, format, this.language);
4562
+ },
3134
4563
 
3135
4564
  setStartDate: function(startDate){
3136
4565
  this.startDate = startDate||-Infinity;
@@ -3150,32 +4579,46 @@ $(function(){
3150
4579
  this.updateNavArrows();
3151
4580
  },
3152
4581
 
4582
+ setDaysOfWeekDisabled: function(daysOfWeekDisabled){
4583
+ this.daysOfWeekDisabled = daysOfWeekDisabled||[];
4584
+ if (!$.isArray(this.daysOfWeekDisabled)) {
4585
+ this.daysOfWeekDisabled = this.daysOfWeekDisabled.split(/,\s*/);
4586
+ }
4587
+ this.daysOfWeekDisabled = $.map(this.daysOfWeekDisabled, function (d) {
4588
+ return parseInt(d, 10);
4589
+ });
4590
+ this.update();
4591
+ this.updateNavArrows();
4592
+ },
4593
+
3153
4594
  place: function(){
3154
- if(this.isInline) return;
4595
+ if(this.isInline) return;
3155
4596
  var zIndex = parseInt(this.element.parents().filter(function() {
3156
4597
  return $(this).css('z-index') != 'auto';
3157
4598
  }).first().css('z-index'))+10;
3158
4599
  var offset = this.component ? this.component.offset() : this.element.offset();
4600
+ var height = this.component ? this.component.outerHeight(true) : this.element.outerHeight(true);
3159
4601
  this.picker.css({
3160
- top: offset.top + this.height,
4602
+ top: offset.top + height,
3161
4603
  left: offset.left,
3162
4604
  zIndex: zIndex
3163
4605
  });
3164
4606
  },
3165
4607
 
3166
4608
  update: function(){
3167
- var date, fromArgs = false;
3168
- if(arguments && arguments.length && (typeof arguments[0] === 'string' || arguments[0] instanceof Date)) {
3169
- date = arguments[0];
3170
- fromArgs = true;
3171
- } else {
3172
- date = this.isInput ? this.element.prop('value') : this.element.data('date') || this.element.find('input').prop('value');
3173
- }
4609
+ var date, fromArgs = false;
4610
+ if(arguments && arguments.length && (typeof arguments[0] === 'string' || arguments[0] instanceof Date)) {
4611
+ date = arguments[0];
4612
+ fromArgs = true;
4613
+ } else {
4614
+ date = this.isInput ? this.element.val() : this.element.data('date') || this.element.find('input').val();
4615
+ }
3174
4616
 
3175
4617
  this.date = DPGlobal.parseDate(date, this.format, this.language);
3176
4618
 
3177
- if(fromArgs) this.setValue();
4619
+ if(fromArgs) this.setValue();
3178
4620
 
4621
+ var oldViewDate = this.viewDate;
3179
4622
  if (this.date < this.startDate) {
3180
4623
  this.viewDate = new Date(this.startDate);
3181
4624
  } else if (this.date > this.endDate) {
@@ -3183,12 +4626,19 @@ $(function(){
3183
4626
  } else {
3184
4627
  this.viewDate = new Date(this.date);
3185
4628
  }
4629
+
4630
+ if (oldViewDate && oldViewDate.getTime() != this.viewDate.getTime()){
4631
+ this.element.trigger({
4632
+ type: 'changeDate',
4633
+ date: this.viewDate
4634
+ });
4635
+ }
3186
4636
  this.fill();
3187
4637
  },
3188
4638
 
3189
4639
  fillDow: function(){
3190
- var dowCnt = this.weekStart;
3191
- var html = '<tr>';
4640
+ var dowCnt = this.weekStart,
4641
+ html = '<tr>';
3192
4642
  while (dowCnt < this.weekStart + 7) {
3193
4643
  html += '<th class="dow">'+dates[this.language].daysMin[(dowCnt++)%7]+'</th>';
3194
4644
  }
@@ -3197,8 +4647,8 @@ $(function(){
3197
4647
  },
3198
4648
 
3199
4649
  fillMonths: function(){
3200
- var html = '';
3201
- var i = 0
4650
+ var html = '',
4651
+ i = 0;
3202
4652
  while (i < 12) {
3203
4653
  html += '<span class="month">'+dates[this.language].monthsShort[i++]+'</span>';
3204
4654
  }
@@ -3251,7 +4701,8 @@ $(function(){
3251
4701
  if (currentDate && prevMonth.valueOf() == currentDate) {
3252
4702
  clsName += ' active';
3253
4703
  }
3254
- if (prevMonth.valueOf() < this.startDate || prevMonth.valueOf() > this.endDate) {
4704
+ if (prevMonth.valueOf() < this.startDate || prevMonth.valueOf() > this.endDate ||
4705
+ $.inArray(prevMonth.getUTCDay(), this.daysOfWeekDisabled) !== -1) {
3255
4706
  clsName += ' disabled';
3256
4707
  }
3257
4708
  html.push('<td class="day'+clsName+'">'+prevMonth.getUTCDate() + '</td>');
@@ -3392,7 +4843,7 @@ $(function(){
3392
4843
  var year = this.viewDate.getUTCFullYear(),
3393
4844
  month = this.viewDate.getUTCMonth();
3394
4845
  if (target.is('.old')) {
3395
- if (month == 0) {
4846
+ if (month === 0) {
3396
4847
  month = 11;
3397
4848
  year -= 1;
3398
4849
  } else {
@@ -3432,8 +4883,8 @@ $(function(){
3432
4883
  }
3433
4884
  if (element) {
3434
4885
  element.change();
3435
- if (this.autoclose) {
3436
- this.hide();
4886
+ if (this.autoclose && (!which || which == 'date')) {
4887
+ this.hide();
3437
4888
  }
3438
4889
  }
3439
4890
  },
@@ -3579,16 +5030,16 @@ $(function(){
3579
5030
  if (dir) {
3580
5031
  this.viewMode = Math.max(0, Math.min(2, this.viewMode + dir));
3581
5032
  }
3582
- /*
3583
- vitalets: fixing bug of very special conditions:
3584
- jquery 1.7.1 + webkit + show inline datepicker in bootstrap popover.
3585
- Method show() does not set display css correctly and datepicker is not shown.
3586
- Changed to .css('display', 'block') solve the problem.
3587
- See https://github.com/vitalets/x-editable/issues/37
3588
-
3589
- In jquery 1.7.2+ everything works fine.
3590
- */
3591
- //this.picker.find('>div').hide().filter('.datepicker-'+DPGlobal.modes[this.viewMode].clsName).show();
5033
+ /*
5034
+ vitalets: fixing bug of very special conditions:
5035
+ jquery 1.7.1 + webkit + show inline datepicker in bootstrap popover.
5036
+ Method show() does not set display css correctly and datepicker is not shown.
5037
+ Changed to .css('display', 'block') solve the problem.
5038
+ See https://github.com/vitalets/x-editable/issues/37
5039
+
5040
+ In jquery 1.7.2+ everything works fine.
5041
+ */
5042
+ //this.picker.find('>div').hide().filter('.datepicker-'+DPGlobal.modes[this.viewMode].clsName).show();
3592
5043
  this.picker.find('>div').hide().filter('.datepicker-'+DPGlobal.modes[this.viewMode].clsName).css('display', 'block');
3593
5044
  this.updateNavArrows();
3594
5045
  }
@@ -3622,7 +5073,7 @@ $(function(){
3622
5073
  monthsShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
3623
5074
  today: "Today"
3624
5075
  }
3625
- }
5076
+ };
3626
5077
 
3627
5078
  var DPGlobal = {
3628
5079
  modes: [
@@ -3642,28 +5093,28 @@ $(function(){
3642
5093
  navStep: 10
3643
5094
  }],
3644
5095
  isLeapYear: function (year) {
3645
- return (((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0))
5096
+ return (((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0));
3646
5097
  },
3647
5098
  getDaysInMonth: function (year, month) {
3648
- return [31, (DPGlobal.isLeapYear(year) ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month]
5099
+ return [31, (DPGlobal.isLeapYear(year) ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month];
3649
5100
  },
3650
- validParts: /dd?|mm?|MM?|yy(?:yy)?/g,
3651
- nonpunctuation: /[^ -\/:-@\[-`{-~\t\n\r]+/g,
5101
+ validParts: /dd?|DD?|mm?|MM?|yy(?:yy)?/g,
5102
+ nonpunctuation: /[^ -\/:-@\[\u3400-\u9fff-`{-~\t\n\r]+/g,
3652
5103
  parseFormat: function(format){
3653
5104
  // IE treats \0 as a string end in inputs (truncating the value),
3654
5105
  // so it's a bad format delimiter, anyway
3655
5106
  var separators = format.replace(this.validParts, '\0').split('\0'),
3656
5107
  parts = format.match(this.validParts);
3657
- if (!separators || !separators.length || !parts || parts.length == 0){
5108
+ if (!separators || !separators.length || !parts || parts.length === 0){
3658
5109
  throw new Error("Invalid date format.");
3659
5110
  }
3660
5111
  return {separators: separators, parts: parts};
3661
5112
  },
3662
5113
  parseDate: function(date, format, language) {
3663
5114
  if (date instanceof Date) return date;
3664
- if (/^[-+]\d+[dmwy]([\s,]+[-+]\d+[dmwy])*$/.test(date)) {
3665
- var part_re = /([-+]\d+)([dmwy])/,
3666
- parts = date.match(/([-+]\d+)([dmwy])/g),
5115
+ if (/^[\-+]\d+[dmwy]([\s,]+[\-+]\d+[dmwy])*$/.test(date)) {
5116
+ var part_re = /([\-+]\d+)([dmwy])/,
5117
+ parts = date.match(/([\-+]\d+)([dmwy])/g),
3667
5118
  part, dir;
3668
5119
  date = new Date();
3669
5120
  for (var i=0; i<parts.length; i++) {
@@ -3708,10 +5159,18 @@ $(function(){
3708
5159
  setters_map['M'] = setters_map['MM'] = setters_map['mm'] = setters_map['m'];
3709
5160
  setters_map['dd'] = setters_map['d'];
3710
5161
  date = UTCDate(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0);
3711
- if (parts.length == format.parts.length) {
3712
- for (var i=0, cnt = format.parts.length; i < cnt; i++) {
5162
+ var fparts = format.parts.slice();
5163
+ // Remove noop parts
5164
+ if (parts.length != fparts.length) {
5165
+ fparts = $(fparts).filter(function(i,p){
5166
+ return $.inArray(p, setters_order) !== -1;
5167
+ }).toArray();
5168
+ }
5169
+ // Process remainder
5170
+ if (parts.length == fparts.length) {
5171
+ for (var i=0, cnt = fparts.length; i < cnt; i++) {
3713
5172
  val = parseInt(parts[i], 10);
3714
- part = format.parts[i];
5173
+ part = fparts[i];
3715
5174
  if (isNaN(val)) {
3716
5175
  switch(part) {
3717
5176
  case 'MM':
@@ -3736,8 +5195,8 @@ $(function(){
3736
5195
  }
3737
5196
  for (var i=0, s; i<setters_order.length; i++){
3738
5197
  s = setters_order[i];
3739
- if (s in parsed)
3740
- setters_map[s](date, parsed[s])
5198
+ if (s in parsed && !isNaN(parsed[s]))
5199
+ setters_map[s](date, parsed[s]);
3741
5200
  }
3742
5201
  }
3743
5202
  return date;
@@ -3745,6 +5204,8 @@ $(function(){
3745
5204
  formatDate: function(date, format, language){
3746
5205
  var val = {
3747
5206
  d: date.getUTCDate(),
5207
+ D: dates[language].daysShort[date.getUTCDay()],
5208
+ DD: dates[language].days[date.getUTCDay()],
3748
5209
  m: date.getUTCMonth() + 1,
3749
5210
  M: dates[language].monthsShort[date.getUTCMonth()],
3750
5211
  MM: dates[language].months[date.getUTCMonth()],
@@ -3757,7 +5218,7 @@ $(function(){
3757
5218
  seps = $.extend([], format.separators);
3758
5219
  for (var i=0, cnt = format.parts.length; i < cnt; i++) {
3759
5220
  if (seps.length)
3760
- date.push(seps.shift())
5221
+ date.push(seps.shift());
3761
5222
  date.push(val[format.parts[i]]);
3762
5223
  }
3763
5224
  return date.join('');
@@ -3795,7 +5256,229 @@ $(function(){
3795
5256
  '</table>'+
3796
5257
  '</div>'+
3797
5258
  '</div>';
3798
-
3799
- $.fn.datepicker.DPGlobal = DPGlobal;
3800
-
5259
+
5260
+ $.fn.datepicker.DPGlobal = DPGlobal;
5261
+
3801
5262
  }( window.jQuery );
5263
+
5264
+ /**
5265
+ Typeahead input (bootstrap only). Based on Twitter Bootstrap [typeahead](http://twitter.github.com/bootstrap/javascript.html#typeahead).
5266
+ Depending on `source` format typeahead operates in two modes:
5267
+
5268
+ * **strings**:
5269
+ When `source` defined as array of strings, e.g. `['text1', 'text2', 'text3' ...]`.
5270
+ User can submit one of these strings or any text entered in input (even if it is not matching source).
5271
+
5272
+ * **objects**:
5273
+ When `source` defined as array of objects, e.g. `[{value: 1, text: "text1"}, {value: 2, text: "text2"}, ...]`.
5274
+ User can submit only values that are in source (otherwise `null` is submitted). This is more like *dropdown* behavior.
5275
+
5276
+ @class typeahead
5277
+ @extends list
5278
+ @since 1.4.1
5279
+ @final
5280
+ @example
5281
+ <a href="#" id="country" data-type="typeahead" data-pk="1" data-url="/post" data-original-title="Input country"></a>
5282
+ <script>
5283
+ $(function(){
5284
+ $('#country').editable({
5285
+ value: 'ru',
5286
+ source: [
5287
+ {value: 'gb', text: 'Great Britain'},
5288
+ {value: 'us', text: 'United States'},
5289
+ {value: 'ru', text: 'Russia'}
5290
+ ]
5291
+ }
5292
+ });
5293
+ });
5294
+ </script>
5295
+ **/
5296
+ (function ($) {
5297
+
5298
+ var Constructor = function (options) {
5299
+ this.init('typeahead', options, Constructor.defaults);
5300
+
5301
+ //overriding objects in config (as by default jQuery extend() is not recursive)
5302
+ this.options.typeahead = $.extend({}, Constructor.defaults.typeahead, {
5303
+ //set default methods for typeahead to work with objects
5304
+ matcher: this.matcher,
5305
+ sorter: this.sorter,
5306
+ highlighter: this.highlighter,
5307
+ updater: this.updater
5308
+ }, options.typeahead);
5309
+ };
5310
+
5311
+ $.fn.editableutils.inherit(Constructor, $.fn.editabletypes.list);
5312
+
5313
+ $.extend(Constructor.prototype, {
5314
+ renderList: function() {
5315
+ this.$input = this.$tpl.is('input') ? this.$tpl : this.$tpl.find('input[type="text"]');
5316
+
5317
+ //set source of typeahead
5318
+ this.options.typeahead.source = this.sourceData;
5319
+
5320
+ //apply typeahead
5321
+ this.$input.typeahead(this.options.typeahead);
5322
+
5323
+ //attach own render method
5324
+ this.$input.data('typeahead').render = $.proxy(this.typeaheadRender, this.$input.data('typeahead'));
5325
+
5326
+ this.renderClear();
5327
+ this.setClass();
5328
+ this.setAttr('placeholder');
5329
+ },
5330
+
5331
+ value2htmlFinal: function(value, element) {
5332
+ if(this.getIsObjects()) {
5333
+ var items = $.fn.editableutils.itemsByValue(value, this.sourceData);
5334
+ $(element).text(items.length ? items[0].text : '');
5335
+ } else {
5336
+ $(element).text(value);
5337
+ }
5338
+ },
5339
+
5340
+ html2value: function (html) {
5341
+ return html ? html : null;
5342
+ },
5343
+
5344
+ value2input: function(value) {
5345
+ if(this.getIsObjects()) {
5346
+ var items = $.fn.editableutils.itemsByValue(value, this.sourceData);
5347
+ this.$input.data('value', value).val(items.length ? items[0].text : '');
5348
+ } else {
5349
+ this.$input.val(value);
5350
+ }
5351
+ },
5352
+
5353
+ input2value: function() {
5354
+ if(this.getIsObjects()) {
5355
+ var value = this.$input.data('value'),
5356
+ items = $.fn.editableutils.itemsByValue(value, this.sourceData);
5357
+
5358
+ if(items.length && items[0].text.toLowerCase() === this.$input.val().toLowerCase()) {
5359
+ return value;
5360
+ } else {
5361
+ return null; //entered string not found in source
5362
+ }
5363
+ } else {
5364
+ return this.$input.val();
5365
+ }
5366
+ },
5367
+
5368
+ /*
5369
+ if in sourceData values <> texts, typeahead in "objects" mode:
5370
+ user must pick some value from list, otherwise `null` returned.
5371
+ if all values == texts put typeahead in "strings" mode:
5372
+ anything what entered is submited.
5373
+ */
5374
+ getIsObjects: function() {
5375
+ if(this.isObjects === undefined) {
5376
+ this.isObjects = false;
5377
+ for(var i=0; i<this.sourceData.length; i++) {
5378
+ if(this.sourceData[i].value !== this.sourceData[i].text) {
5379
+ this.isObjects = true;
5380
+ break;
5381
+ }
5382
+ }
5383
+ }
5384
+ return this.isObjects;
5385
+ },
5386
+
5387
+ /*
5388
+ Methods borrowed from text input
5389
+ */
5390
+ activate: $.fn.editabletypes.text.prototype.activate,
5391
+ renderClear: $.fn.editabletypes.text.prototype.renderClear,
5392
+ postrender: $.fn.editabletypes.text.prototype.postrender,
5393
+ toggleClear: $.fn.editabletypes.text.prototype.toggleClear,
5394
+ clear: function() {
5395
+ $.fn.editabletypes.text.prototype.clear.call(this);
5396
+ this.$input.data('value', '');
5397
+ },
5398
+
5399
+
5400
+ /*
5401
+ Typeahead option methods used as defaults
5402
+ */
5403
+ /*jshint eqeqeq:false, curly: false, laxcomma: true*/
5404
+ matcher: function (item) {
5405
+ return $.fn.typeahead.Constructor.prototype.matcher.call(this, item.text);
5406
+ },
5407
+ sorter: function (items) {
5408
+ var beginswith = []
5409
+ , caseSensitive = []
5410
+ , caseInsensitive = []
5411
+ , item
5412
+ , text;
5413
+
5414
+ while (item = items.shift()) {
5415
+ text = item.text;
5416
+ if (!text.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item);
5417
+ else if (~text.indexOf(this.query)) caseSensitive.push(item);
5418
+ else caseInsensitive.push(item);
5419
+ }
5420
+
5421
+ return beginswith.concat(caseSensitive, caseInsensitive);
5422
+ },
5423
+ highlighter: function (item) {
5424
+ return $.fn.typeahead.Constructor.prototype.highlighter.call(this, item.text);
5425
+ },
5426
+ updater: function (item) {
5427
+ item = this.$menu.find('.active').data('item');
5428
+ this.$element.data('value', item.value);
5429
+ return item.text;
5430
+ },
5431
+
5432
+
5433
+ /*
5434
+ Overwrite typeahead's render method to store objects.
5435
+ There are a lot of disscussion in bootstrap repo on this point and still no result.
5436
+ See https://github.com/twitter/bootstrap/issues/5967
5437
+
5438
+ This function just store item in via jQuery data() method instead of attr('data-value')
5439
+ */
5440
+ typeaheadRender: function (items) {
5441
+ var that = this;
5442
+
5443
+ items = $(items).map(function (i, item) {
5444
+ // i = $(that.options.item).attr('data-value', item)
5445
+ i = $(that.options.item).data('item', item);
5446
+ i.find('a').html(that.highlighter(item));
5447
+ return i[0];
5448
+ });
5449
+
5450
+ items.first().addClass('active');
5451
+ this.$menu.html(items);
5452
+ return this;
5453
+ }
5454
+ /*jshint eqeqeq: true, curly: true, laxcomma: false*/
5455
+
5456
+ });
5457
+
5458
+ Constructor.defaults = $.extend({}, $.fn.editabletypes.list.defaults, {
5459
+ /**
5460
+ @property tpl
5461
+ @default <input type="text">
5462
+ **/
5463
+ tpl:'<input type="text">',
5464
+ /**
5465
+ Configuration of typeahead. [Full list of options](http://twitter.github.com/bootstrap/javascript.html#typeahead).
5466
+
5467
+ @property typeahead
5468
+ @type object
5469
+ @default null
5470
+ **/
5471
+ typeahead: null,
5472
+ /**
5473
+ Whether to show `clear` button
5474
+
5475
+ @property clear
5476
+ @type boolean
5477
+ @default true
5478
+ **/
5479
+ clear: true
5480
+ });
5481
+
5482
+ $.fn.editabletypes.typeahead = Constructor;
5483
+
5484
+ }(window.jQuery));