bootstrap-editable-rails 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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));